skip to Main Content

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


  1. Change your main.js to:

    // 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);
            });
        }
    });
    
    Login or Signup to reply.
  2. Your fetch("/ajax/product/•••") invocation makes a GET request to the URLconf "ajax_product_detail". The view at that URL is ProductDetailView. It is a class based view so a GET request to this view will invoke the get 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:

    class AjaxProductDetailView(View):
        def 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)
    path('ajax/product/<slug:business_slug>/<slug:product_slug>/', views.AjaxProductDetailView.as_view(), name='ajax_product_detail')

    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.

    Login or Signup to reply.
  3. 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:

    fetch(`/ajax/product/${businessSlug}/${productSlug}/`)
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search