I am trying to build an e-commerce website where I want users to be able to see a product quickview
a product without going to the product_detail page. So I want to dynamically load all product details on a quickview
modal. Can anyone help me with these errors? Thanks in advance!
Okay so my models.py:
class Business(models.Model):
BUSINESS_TYPE_CHOICES = [
('product', 'Product Business'),
('service', 'Service Business'),
]
seller = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name='business')
business_name = models.CharField(max_length=100)
description = models.TextField()
business_slug = models.SlugField(unique=True, blank=True)
business_type = models.CharField(max_length=20, choices=BUSINESS_TYPE_CHOICES)
countries = models.ManyToManyField(Country)
states = models.ManyToManyField(State) # Add this line
address = models.CharField(max_length=200)
phone = models.CharField(max_length=20)
website = models.URLField(blank=True, null=True)
email = models.EmailField(blank=True, null=True)
profile_picture = models.ImageField(upload_to='business_profiles/', blank=True, null=True)
banner_image = models.ImageField(upload_to='business_banners/', blank=True, null=True)
is_featured = models.BooleanField(default=False)
def __str__(self):
return self.business_name
def save(self, *args, **kwargs):
if not self.business_slug:
self.business_slug = slugify(self.business_name)
super().save(*args, **kwargs)
class OpeningHour(models.Model):
DAY_CHOICES = [
('monday', 'Monday'),
('tuesday', 'Tuesday'),
('wednesday', 'Wednesday'),
('thursday', 'Thursday'),
('friday', 'Friday'),
('saturday', 'Saturday'),
('sunday', 'Sunday'),
('public_holiday', 'Public Holiday'),
]
business = models.ForeignKey(Business, on_delete=models.CASCADE, related_name='opening_hours')
day = models.CharField(max_length=20, choices=DAY_CHOICES)
is_closed = models.BooleanField(default=False)
opening_time = models.TimeField(null=True, blank=True)
closing_time = models.TimeField(null=True, blank=True)
def __str__(self):
if self.is_closed:
return f"{self.business.business_name} - {self.get_day_display()} (Closed)"
else:
return f"{self.business.business_name} - {self.get_day_display()} ({self.opening_time} - {self.closing_time})"
class Product(models.Model):
name = models.CharField(max_length=100)
product_slug = models.SlugField(unique=True, blank=True)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
image = models.ImageField(upload_to='products/')
image2 = models.ImageField(upload_to='products/', null=True, blank=True)
business = models.ForeignKey(Business, on_delete=models.CASCADE, related_name='products')
in_stock = models.BooleanField(default=True)
is_popular = models.BooleanField(default=False)
is_best_seller = models.BooleanField(default=False)
min_delivery_time = models.PositiveIntegerField(null=True, help_text="Minimum estimated delivery time in business days")
max_delivery_time = models.PositiveIntegerField(null=True, help_text="Maximum estimated delivery time in business days")
has_variations = models.BooleanField(default=False)
def __str__(self):
return f'{self.business.business_name}, {self.name}'
def get_json_data(self):
data = {
'id': self.id,
'name': self.name,
'price': float(self.price),
'description': self.description,
'images': [self.image.url, self.image2.url] if self.image and self.image2 else [],
'min_delivery_time': self.min_delivery_time,
'max_delivery_time': self.max_delivery_time,
}
return json.dumps(data)
def save(self, *args, **kwargs):
if not self.product_slug:
self.product_slug = slugify(self.name)
super().save(*args, **kwargs)
VAR_CATEGORIES = (
('size', 'Size'),
('color', 'Color'),
)
class Variation(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='variations')
name = models.CharField(max_length=50, choices=VAR_CATEGORIES, null=True, blank=True)
def __str__(self):
return f"{self.product.name} - {self.get_name_display()}"
class ProductVariation(models.Model):
variation = models.ForeignKey(Variation, on_delete=models.CASCADE, related_name='values', null=True)
value = models.CharField(max_length=50, null=True)
image = models.ImageField(upload_to='product_variations/', null=True, blank=True)
class Meta:
unique_together = (('variation', 'value'),)
def __str__(self):
return f"{self.variation.product.name} - {self.variation.get_name_display()} - {self.value}"
class Cart(models.Model):
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='cart')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
variation_key = models.CharField(max_length=255, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.username} - {self.product.name}"
class CartItemVariation(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='variations')
product_variation = models.ForeignKey(ProductVariation, on_delete=models.CASCADE)
def __str__(self):
return f"{self.cart} - {self.product_variation}"
My views.py:
class BusinessDetailView(View):
def get(self, request, business_slug):
business = get_object_or_404(Business, business_slug=business_slug)
products = business.products.all()
services = business.services.all()
opening_hours = business.opening_hours.all()
return render(request, 'business/business_detail.html', {'business': business, 'products': products, 'opening_hours': opening_hours, 'services': services})
class ProductDetailView(View):
def get(self, request, business_slug=None, product_slug=None):
if request.is_ajax():
return self.ajax_get(request, business_slug, product_slug)
business = get_object_or_404(Business, business_slug=business_slug)
product = get_object_or_404(Product, product_slug=product_slug, business=business)
color_variations = Variation.objects.filter(product=product, name='color')
size_variations = Variation.objects.filter(product=product, name='size')
context = {
'product': product,
'business': business,
'color_variations': color_variations,
'size_variations': size_variations,
}
return render(request, 'business/product_detail.html', context)
def ajax_get(self, request, business_slug, product_slug):
business = get_object_or_404(Business, business_slug=business_slug)
product = get_object_or_404(Product, product_slug=product_slug, business=business)
color_variations = Variation.objects.filter(product=product, name='color')
size_variations = Variation.objects.filter(product=product, name='size')
product_data = {
'id': product.id,
'name': product.name,
'price': float(product.price),
'description': product.description,
'images': [product.image.url, product.image2.url] if product.image and product.image2 else [],
'color_variations': list(color_variations.values('id', 'values__id', 'values__value', 'values__image')),
'size_variations': list(size_variations.values('id', 'values__id', 'values__value')),
'sku': product.product_slug,
'categories': [product.business.business_name],
'tags': [tag.name for tag in product.tags.all()] if hasattr(product, 'tags') else []
}
return JsonResponse(product_data)
My urls.py:
path('business/<slug:business_slug>/', views.BusinessDetailView.as_view(), name='business_detail'),
path('business/<slug:business_slug>/edit/', views.edit_business, name='edit_business'),
path('business/<slug:business_slug>/product/create/', views.ProductCreateView.as_view(), name='product_create'),
path('business/<slug:business_slug>/product/<slug:product_slug>/', views.ProductDetailView.as_view(), name='product_detail'),
path('ajax/product/<slug:business_slug>/<slug:product_slug>/', views.ProductDetailView.as_view(), name='ajax_product_detail'),
My main.js:
// Modal QuickView
document.addEventListener("DOMContentLoaded", function() {
const quickViewButtons = document.querySelectorAll(".quick-view-btn");
const modalQuickViewBlock = document.querySelector(".modal-quickview-block");
const modalQuickViewMain = document.querySelector(".modal-quickview-main");
const closeQuickViewButton = document.querySelector(".modal-quickview-block .close-btn");
quickViewButtons.forEach(button => {
button.addEventListener("click", function() {
const productSlug = button.dataset.productSlug;
const businessSlug = button.dataset.businessSlug;
console.log('Quick View Button:', button);
console.log('Product Slug:', productSlug);
console.log('Business Slug:', businessSlug);
if (!productSlug || !businessSlug) {
console.error('Product Slug or Business Slug is missing');
return;
}
fetch(`/ajax/product/${businessSlug}/${productSlug}/`)
.then(response => {
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('Data:', data);
updateQuickViewModal(data);
modalQuickViewBlock.style.display = "block";
})
.catch(error => {
console.error('Error:', error);
});
});
});
closeQuickViewButton.addEventListener("click", function() {
modalQuickViewBlock.style.display = "none";
});
modalQuickViewBlock.addEventListener("click", function(event) {
if (event.target === modalQuickViewBlock) {
modalQuickViewBlock.style.display = "none";
}
});
function updateQuickViewModal(data) {
document.getElementById("quickview-name").innerText = data.name;
document.getElementById("quickview-price").innerText = `$${data.price}`;
document.getElementById("quickview-description").innerText = data.description;
document.getElementById("quickview-sku").innerText = data.sku;
document.getElementById("quickview-categories").innerText = data.categories.join(", ");
document.getElementById("quickview-tags").innerText = data.tags.join(", ");
const imagesContainer = document.getElementById("quickview-images");
imagesContainer.innerHTML = "";
data.images.forEach(imageUrl => {
const imgElement = document.createElement("div");
imgElement.className = "bg-img w-full aspect-[3/4] max-md:w-[150px] max-md:flex-shrink-0 rounded-[20px] overflow-hidden md:mt-6";
imgElement.innerHTML = `<img src="${imageUrl}" alt="product image" class="w-full h-full object-cover">`;
imagesContainer.appendChild(imgElement);
});
const colorsContainer = document.getElementById("quickview-colors");
colorsContainer.innerHTML = "";
data.color_variations.forEach(variation => {
const colorItem = document.createElement("div");
colorItem.classList.add("color-item", "w-12", "h-12", "rounded-xl", "duration-300", "relative");
colorItem.dataset.variationId = variation.values__id;
colorItem.dataset.color = variation.values__value;
if (variation.values__image) {
const imgElement = document.createElement("img");
imgElement.src = variation.values__image;
imgElement.alt = "color";
imgElement.classList.add("rounded-xl");
colorItem.appendChild(imgElement);
}
const tagAction = document.createElement("div");
tagAction.classList.add("tag-action", "bg-black", "text-white", "caption2", "capitalize", "px-1.5", "py-0.5", "rounded-sm");
tagAction.innerText = variation.values__value;
colorItem.appendChild(tagAction);
colorItem.addEventListener("click", function() {
colorsContainer.querySelectorAll('.color-item').forEach(i => i.classList.remove('active'));
colorItem.classList.add('active');
document.querySelector('#selected-color').innerText = `Color: ${variation.values__value}`;
});
colorsContainer.appendChild(colorItem);
});
const sizesContainer = document.getElementById("quickview-sizes");
sizesContainer.innerHTML = "";
data.size_variations.forEach(variation => {
const sizeItem = document.createElement("div");
sizeItem.classList.add("size-item", "w-12", "h-12", "flex", "items-center", "justify-center", "text-button", "rounded-full", "bg-white", "border", "border-line");
sizeItem.dataset.variationId = variation.values__id;
sizeItem.dataset.size = variation.values__value;
sizeItem.innerText = variation.values__value;
sizeItem.addEventListener("click", function() {
sizesContainer.querySelectorAll('.size-item').forEach(i => i.classList.remove('active'));
sizeItem.classList.add('active');
document.querySelector('#selected-size').innerText = `Size: ${variation.values__value}`;
});
sizesContainer.appendChild(sizeItem);
});
}
});
My business_detail.html:
<div class="list-action grid grid-cols-2 gap-3 px-5 absolute w-full bottom-5 max-lg:hidden">
<div class="quick-view-btn w-full text-button-uppercase py-2 text-center rounded-full duration-300 bg-white hover:bg-black hover:text-white"
data-product-slug="{{ product.product_slug }}"
data-business-slug="{{ business.business_slug }}">
Quick View
</div>
<script>
console.log('Product ID from template:', {{ product.id|safe }});
const quickViewButtons = document.querySelectorAll('.quick-view-btn');
quickViewButtons.forEach(button => {
button.addEventListener('click', () => {
const productSlug = button.dataset.productSlug;
const businessSlug = button.dataset.businessSlug;
console.log('Quick View Button:', button);
console.log('Product Slug:', productSlug);
console.log('Business Slug:', businessSlug);
if (!productSlug || !businessSlug) {
console.error('Product Slug or Business Slug is missing');
return;
}
fetch(`/ajax/product/${productSlug}/?business_slug=${businessSlug}`)
.then(response => {
console.log('Response status:', response.status);
return response.json();
})
.then(data => {
console.log('Data:', data);
updateQuickViewModal(data);
modalQuickViewBlock.style.display = "block";
})
.catch(error => {
console.error('Error:', error);
});
});
});
</script>
Yet when I click on the quickview button, it says on my console:
Quick View Button: <div class="quick-view-btn w-full text-button-uppercase py-2 text-center rounded-full duration-300 bg-white hover:bg-black hover:text-white" data-product-slug="jacket" data-business-slug="lululemon">…</div>
lululemon/:3551 Product Slug: jacket
lululemon/:3552 Business Slug: lululemon
main.js:1377 Quick View Button: <div class="quick-view-btn w-full text-button-uppercase py-2 text-center rounded-full duration-300 bg-white hover:bg-black hover:text-white" data-product-slug="jacket" data-business-slug="lululemon">…</div>
main.js:1378 Product Slug: jacket
main.js:1379 Business Slug: lululemon
lululemon/:3559
GET http://127.0.0.1:8000/ajax/product/jacket/?business_slug=lululemon 404 (Not Found)
(anonymous) @ lululemon/:3559Understand this error
lululemon/:3561 Response status: 404
lululemon/:3570 Error: SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
(anonymous) @ lululemon/:3570
Promise.catch (async)
(anonymous) @ lululemon/:3569Understand this error
main.js:1388 Response status: 200
main.js:1400 Error: SyntaxError: Unexpected token '<', "
<!DOCTYPE "... is not valid JSON
On my terminal it says:
[02/Jun/2024 07:25:39] "GET /business/lululemon/assets/data/Product.json HTTP/1.1" 404 8444
Not Found: /ajax/product/jacket/
[02/Jun/2024 07:25:41] "GET /ajax/product/jacket/?business_slug=lululemon HTTP/1.1" 404 8375
[02/Jun/2024 07:25:41] "GET /ajax/product/lululemon/jacket/ HTTP/1.1" 200 271499
3
Answers
Change your main.js to:
Your
fetch("/ajax/product/•••")
invocation makes a GET request to the URLconf"ajax_product_detail"
. The view at that URL isProductDetailView
. It is a class based view so a GET request to this view will invoke theget
subroutine in this class. This subroutine renders the template"business/product_detail.html"
.Do you see the problem now? You probably think the subroutine
ajax_get
will get invoked. Not it won’t. You are better off extracting this subroutine into its own view:Now the GET request will be handled properly by the right view.
I offered this solution because I realized your view is not specific to any model.
In your views.py, the ajax_get method of the ProductDetailView class is returning a JsonResponse with the product data. However, the url
/ajax/product/jacket/?business_slug=lululemon
is not being properly handled by Django which results in the 404 error.Update your main.js: