pythondjangodjango-sessions

KeyError at /cart/ 'product_obj' Django


In my cart i can add same product with different size and color. but randomly when i want to to add same product with different size and color i have this error in my cart iter function. i've tracked the process in debugger and found that it somehow traped in my Product model str method and finally this error raise.

models:

from django.db import models
from django.urls import reverse
from django.conf import settings
from colorfield.fields import ColorField


class Product(models.Model):
    GENDER_MALE = 'm'
    GENDER_FEMALE = 'f'
    GENDER_BOTH = 'b'
    GENDER_CHOICE = [
        (GENDER_MALE, 'Male'),
        (GENDER_FEMALE, 'Female'),
        (GENDER_BOTH, 'Both')
    ]

    name = models.CharField(max_length=200)
    category = models.CharField(max_length=200)
    gender = models.CharField(choices=GENDER_CHOICE, max_length=1)
    sizes = models.ManyToManyField(to="store.Size", related_name="sizes")
    slug = models.SlugField(unique=True, allow_unicode=True, db_collation='utf8_persian_ci')
    price = models.PositiveIntegerField()
    description = models.TextField()
    inventory = models.IntegerField()
    datetime_created = models.DateTimeField(auto_now_add=True)
    datetime_modified = models.DateTimeField(auto_now=True)
    discounts = models.IntegerField(default=0)
    available_colors = models.ManyToManyField(to="store.Color", related_name="colors")
    status = models.BooleanField(default=True)

    def __str__(self):
        return self.name
    
    def get_absolute_url(self):
        return reverse("product_detail", kwargs={"slug": self.slug})


class Size(models.Model):
    size = models.CharField(max_length=2)

    def __str__(self):
        return self.size


class Color(models.Model):
    color = ColorField()
    name = models.CharField(max_length=200)

    def __str__(self):
        return self.name

class Picture(models.Model):
    picture = models.ImageField(upload_to=f'static/store/images/')
    product = models.ForeignKey(Product, default=None, related_name='images', on_delete=models.PROTECT)

    def __str__(self):
        return self.picture.url

views:

def cart_detail_view(request):
    cart = Cart(request)
    colors = Color.objects.all()
    for item in cart:
        item['product_update_quantity_form'] = AddProductToCartForm(initial={
            'inplace': True,
        })
    return render(request, 'products/cart_detail.html', {'cart': cart, 'colors': colors})


def add_to_cart_view(request, pk):
    cart = Cart(request)
    product = get_object_or_404(Product, pk=pk)
    form = AddProductToCartForm(request.POST)
    print(request.POST)
    if form.is_valid():
        cleaned_data = form.cleaned_data
        size = cleaned_data['size']
        color = cleaned_data['color']
        quantity = (cleaned_data['quantity'])
        replace_current_quantity = cleaned_data['inplace']
        cart.add(product, size, color, quantity, replace_current_quantity)
    return redirect('cart_detail')

def remove_item_from_cart(request, cart_item):
    cart = Cart(request)
    cart.remove(cart_item)
    return redirect('cart_detail')

@require_POST
def clear_cart(request):
    cart = Cart(request)
    if len(cart) != 0:
        cart.clear()

    return redirect('product_list')

cart.py:

from .models import Product

class Cart:
    def __init__(self, request):

        """
        Initialize the cart
        """

        self.request = request
        self.session = request.session

        cart = self.session.get('cart')

        if not cart:
            cart = self.session['cart'] = {}

        self.cart = cart

    def add(self, product, size, color, quantity=1, replace_current_quantity=False):

        """
        Add a product to the cart
        """
        cart_item = '1'
        flag = True
        for item in self.cart.items():
            if item[1]['product_id'] == product.id and item[1]['size'] == size and item[1]['color'] == color:
                if replace_current_quantity:
                     item[1]['quantity'] = quantity
                else:
                    item[1]['quantity'] += quantity
                flag = False

        keys = list(self.cart.keys())
        if flag:
            while flag:
                if cart_item in keys:
                    cart_item = str(int(cart_item) + 1)
                    continue
                flag = False
                
            self.cart[cart_item] = {'product_id': product.id}
            self.cart[cart_item]['size'] = size
            self.cart[cart_item]['color'] = color
            self.cart[cart_item]['quantity'] = quantity
        self.save()

    def remove(self, cart_item):
        cart_item = str(cart_item)
                
        """
        Remove a product from the cart
        """
        if cart_item in self.cart.keys():
            del self.cart[cart_item]
            self.save()
        
    def save(self):

        """
        Mark session as modified to save changes
        """
        self.session.modified = True

    def __iter__(self):
        product_ids = [value['product_id'] for value in self.cart.values()]
        products = Product.objects.filter(id__in=product_ids)

        cart = self.cart.copy()
        keys = list(cart.keys())
        for index,product in enumerate(products):
            cart[str(keys[index])]['product_obj'] = product
            cart[str(keys[index])]['cart_item'] = keys[index]


        
        for item in cart.values():
            item['total_price'] = item['product_obj'].price * item['quantity']
            yield item

    def __len__(self):
        return len(self.cart)
    
    def clear(self):
        del self.session['cart']
        self.save()

    def get_total_price(self):
        return sum(item['product_obj'].price * item['quantity'] for item in self.cart.values())

cart_detail.html:

<form action="{% url "cart_add" item.product_obj.pk %}" method="post">
    {% csrf_token %}

    {% render_field item.product_update_quantity_form.color  value="{{item.color.color}}" type="hidden" %}
    {% render_field item.product_update_quantity_form.size  value="{{item.size.size}}" type="hidden" %}

    <td>
        <div class="numbers-row">
            <input type="text" value="{{item.quantity}}" id="quantity_1" class="qty2" name="quantity">
        <div class="inc button_inc">+</div><div class="dec button_inc">-</div></div>
    </td>
    <td>
        {{item.product_update_quantity_form.inplace}}
        <button style="color:blue; background-color: transparent; border:none;" type="submit"><i class="ti-reload"></i></button>
    </td>
</form>

forms:

class AddProductToCartForm(forms.Form):
    QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 31)]
    quantity = forms.TypedChoiceField(choices=QUANTITY_CHOICES, coerce=int,)
    color = forms.CharField(max_length=20)
    size = forms.CharField(max_length=20)
    inplace = forms.BooleanField(required=False, widget=forms.HiddenInput)

Traceback:

Environment:


Request Method: GET
Request URL: http://127.0.0.1:8000/cart/

Django Version: 5.0.1
Python Version: 3.12.1
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'allauth',
 'allauth.account',
 'allauth.socialaccount',
 'allauth.socialaccount.providers.google',
 'widget_tweaks',
 'colorfield',
 'store',
 'core']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'allauth.account.middleware.AccountMiddleware']



Traceback (most recent call last):
  File "D:\Django\shoe_store\venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "D:\Django\shoe_store\venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Django\shoe_store\store\views.py", line 67, in cart_detail_view
    for item in cart:
    ^^^^^^
  File "D:\Django\shoe_store\store\cart.py", line 81, in __iter__
    item['total_price'] = item['product_obj'].price * item['quantity']
                          ^^^^^^^^^^^^^^^^^^^

Exception Type: KeyError at /cart/
Exception Value: 'product_obj'

i've tracked the process in debugger and found that it somehow traped in my Product model str method and finally this error raise.


Solution

  • It is too much code. I try to include only relevant sections.

    Basically, on the first for loop you are populating cart dictionary's item objects with product as a value and 'product_obj' as a key.

    for index,product in enumerate(products):
                cart[str(keys[index])]['product_obj'] = product
                cart[str(keys[index])]['cart_item'] = keys[index]
    
    for item in cart.values():
        item['total_price'] = item['product_obj'].price * item['quantity']
        yield item            
    

    On the second for loop you are trying to retrieve the product (and its price) using 'product_obj' key but python is throwing a KeyError:

    File "D:\Django\shoe_store\store\cart.py", line 81, in __iter__
     item['total_price'] = item['product_obj'].price * item['quantity']
                              ^^^^^^^^^^^^^^^^^^^
    
    Exception Type: KeyError at /cart/
    Exception Value: 'product_obj'
    

    In other words, either your all or certain item objects don't have product_obj and product key value pairs. It might be happening because your cart has more items than products, so on the 1st loop product objects are being added to only a few cart items.

    Solution:

    Make sure the products and cart have the same length, if they must have the same length, then you should raise error or take some other appropriate actions when there is a mismatch.

    or iterate only over cart items that have product object.