Jack
Jack

Reputation: 470

Adding products to cart not working properly

I'm facing a problem with the code, a bug to be more specific, that is when I'm adding an item to the cart for the first time(with variations let's say Green and Medium), it's being added nicely. Then when I'm adding another item(let's say Blue and Small), it's also working. But when, I'm increasing the item quantity from the order_summary.html, it's increasing the quantity of the other item not the one I clicked(if I clicked Red and Medium, Blue and Large's quantity is increased) and says : Please specify the required variations. Why is this happening? It's also worth noting that when I'm adding the same item with same variations from my single product page, then it's working fine. I think this bug is occurring because of the way my views is written. I tried to solve it myself, but I'm getting lost. Can anyone please help me out? Thanks in advance!

My models.py:

class Item(models.Model):
   title = models.CharField(max_length=120)
   price = models.FloatField()

class Variation(models.Model):
   item = models.ForeignKey(Item, on_delete=models.CASCADE)
   name = models.CharField(max_length=50) # size, color

class ItemVariation(models.Model):
   variation  = models.ForeignKey(Variation, on_delete=models.CASCADE)
   value = models.CharField(max_length=50) # small, medium large etc

class OrderItem(models.Model):
   user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
   item  = models.ForeignKey(Item, on_delete=models.CASCADE)
   item_variations = models.ManyToManyField(ItemVariation)
   quantity = models.IntegerField(default=1)
   ordered = models.BooleanField(default=False)

class Order(models.Model):
   user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
   ref_code = models.CharField(max_length=20)
   ordered = models.BooleanField(default=False)
   items = models.ManyToManyField(OrderItem)
   start_date = models.DateTimeField(auto_now_add= True)
   ordered_date = models.DateTimeField()

My views.py:

@login_required
def add_to_cart(request, slug):
    item = get_object_or_404(Item, slug=slug)
    variations = request.POST.getlist('variations', [])
    print(variations)
    print(request.POST)
    minimum_variation_count = Variation.objects.filter(item=item).count()
    print(minimum_variation_count)
    print(len(variations))
    if len(variations) < minimum_variation_count:
        messages.info(request, "Please specify the required variations.")


    order_item_qs = OrderItem.objects.filter(
        item=item,
        user= request.user,
        ordered=False,
    )

    for v in variations:
        print(v)
        order_item_qs = order_item_qs.filter(
            item_variations__value=v
        )

    if order_item_qs.exists():
        order_item = order_item_qs.first()
        order_item.quantity += 1
        # messages.success(request, "Product quantity was updated.")
        order_item.save()
    else:
        order_item = OrderItem.objects.create(
            item=item,
            user= request.user,
            ordered=False,
        )

        # order_item.item_variations.add(*variations)
        item_variations_to_add = ItemVariation.objects.filter(
                variation__item=item,
                value__in=variations
            ).values_list('id', flat=True)
        order_item.item_variations.add(*item_variations_to_add)
        order_item.save()

    order_qs = Order.objects.filter(user=request.user, ordered=False)
    if order_qs.exists():
        order = order_qs[0]
        if not order.items.filter(item__id=order_item.id).exists():
            order.items.add(order_item)
            # print(request.POST.getlist('variations', None)) 
            messages.success(request, "Product added to cart.")
            return redirect("order-summary")
    else:
        ordered_date = timezone.now()
        order = Order.objects.create(user=request.user, ordered_date=ordered_date)
        order.items.add(order_item)
        # print(request.POST.getlist('variations', None)) 
        messages.success(request, "Product added to cart.")
        return redirect("order-summary")
    return redirect("order-summary")


@login_required
def remove_single_item_from_cart(request, slug):
    item = get_object_or_404(Item, slug=slug)
    variations = request.POST.getlist('variations', [])
    order_qs = Order.objects.filter(
        user=request.user,
        ordered=False
    )
    if order_qs.exists():
        order = order_qs[0]
        # check if the order item is in the order
        if order.items.filter(item__slug=item.slug).exists():
            order_item = OrderItem.objects.filter(
                item=item,
                user=request.user,
                ordered=False
            )[0]
            if order_item.quantity > 1:
                order_item.quantity -= 1
                order_item.save()
                messages.info(request, "Product quantity was updated.")
            else:
                order.items.remove(order_item)
                order_item.delete()
                messages.info(request, "Product was removed from cart.")
            return redirect("order-summary")
        else:
            messages.info(request, "Product was not in your cart")
            return redirect("product", slug=slug)
    else:
        messages.info(request, "You do not have an active order")
        return redirect("product", slug=slug)
    return redirect("order-summary")

My order_summary.html (where I can increase quantity):


<th scope="row" class="border-0">
  <div class="p-2">
    <img src="{{ order_item.item.image_url }}" alt="" width="70" class="img-fluid rounded shadow-sm">
      <div class="ml-3 d-inline-block align-middle">
        <h5 class="mb-1"> <a href="{{ order_item.item.get_absolute_url }}" class="text-dark
        -inline-block align-middle">{{ order_item.item.title }}</a>
        {% for var in order_item.item_variations.all %}
          <!-- <h6>{{ order_item.item_variations.all }}</h6> -->
          <!-- <ul> -->
            <!-- <li><h6>{{ var.variation.name }}: {{ var.value }}</h6></li> -->
          <!-- </ul> -->
          <h6>{{ var.variation.name }}: {{ var.value }}</h6>
        {% endfor %}
      </div>
  </div>
</th>
{% if order_item.item.discount_price %}
  <td class="border-0 align-middle"><strong>${{ order_item.get_total_discount_item_price }} <span class="badge badge-warning">Saving ${{ order_item.get_amount_saved }}</span></strong></td>
{% else %}
  <td class="border-0 align-middle"><strong>${{ order_item.get_total_item_price }}</strong></td>
{% endif %}
    <td class="border-0 align-middle"><strong>
      <div class="pull-center">
          <a href="{% url 'remove-single-item-from-cart' order_item.item.slug %}" class="btn mr-2"><i class="fa fa-minus"></i></a>
          {{ order_item.quantity }}<a href="{% url 'add-to-cart' order_item.item.slug %}" class="btn ml-2"><i class="fa fa-plus"></i></a>
        </div>      
      </div>
      </strong></td>
    <td class="border-0 align-middle"><a href="{% url 'remove-from-cart' order_item.item.slug %}" class="text-dark"><i class="fa fa-trash"></i></a></td>
  </tr>

My single_product.html (I can increase my product quantity from here, and it is working correctly) :

<form class="form" method="POST" action="{{ object.get_add_to_cart_url }}">
                    {% csrf_token %}
                      {% for var in object.variation_set.all %}
                        <h5>Choose {{ var.name }}</h5>
                        <select class="form-control mb-4 col-md-4" name="variations">
                          {% for item in var.itemvariation_set.all %}
                            <option value="{{ item.value }}">{{ item.value|capfirst }}</option>
                          {% endfor %}
                        </select> 
                      {% endfor %}


                    <div class="action">
                      <button class="btn btn-success">Add to Cart</button>
                      <button class="like btn btn-danger" type="button"><span class="fa fa-heart"></span></button>
                    </div>
                  </form>

My admin.py:

from django.contrib import admin
from .models import Item, Variation, ItemVariation


class ItemVariationAdmin(admin.ModelAdmin):
    list_display = ['variation',
                    'value']

    list_filter = ['variation', 'variation__item']
    search_fields = ['value']


class ItemVariationInLineAdmin(admin.TabularInline):
    model = ItemVariation
    extra = 1


class VariationAdmin(admin.ModelAdmin):
    list_display = ['item',
                    'name']
    list_filter = ['item']
    search_fields = ['name']
    inlines = [ItemVariationInLineAdmin]

admin.site.register(Item)
admin.site.register(ItemVariation, ItemVariationAdmin)
admin.site.register(Variation, VariationAdmin)

My urls.py:

path('add_to_cart/<slug>/', orders_views.add_to_cart, name='add-to-cart'),
path('remove_from_cart/<slug>/', orders_views.remove_from_cart, name='remove-from-cart'),

Upvotes: 5

Views: 1095

Answers (5)

MattRowbum
MattRowbum

Reputation: 2192

You have mentioned in the comments that you now need solutions to both increase and decrease the OrderItem quantities. It's time to take a different approach.

This solution provides a view that takes care of changes to the quantity of ordered items (plus and minus). It uses the OrderItem primary key, which is much more reliable than a combination of Item.pk and variations.

Add this to views.py:

@login_required
def update_qty(request):
    if request.method == 'POST':
        item_slug = request.POST.get('item_slug', None)
        # Check for an order_item
        order_item_pk = request.POST.get('order_item', None)
        order_item = OrderItem.objects.filter(pk=order_item_pk).first()
        if not order_item:
            messages.info(request, "Product was not in your cart")
            return redirect("product", slug=item_slug)
        # Check for an active order
        order = Order.objects.filter(user=request.user, ordered=False).first()
        if not order:
            messages.info(request, "You do not have an active order")
            return redirect("product", slug=item_slug)
        # Check that order_item is in active order
        if not order_item.order == order:
            messages.info(request, "Product was not in your cart")
            return redirect("product", slug=slug)
        # Update quantities 
        action = request.POST.get('action', None)
        if action == "plus":
            order_item.quantity += 1
            order_item.save()
            messages.info(request, "Product quantity was updated.")
        elif action == "minus":
            order_item.quantity -= 1
            if order_item.quantity < 1:
                order_item.delete()
                messages.info(request, "Product was removed from cart.")
            else
                order_item.save()
                messages.info(request, "Product quantity was updated.")
    return redirect("order-summary")

Add this to your urls.py:

path('update-qty', orders_views.update_qty, name='update-qty'),

In order_summary.html, near the bottom, replace this:

<div class="pull-center">
    <a href="{% url 'remove-single-item-from-cart' order_item.item.slug %}" class="btn mr-2"><i class="fa fa-minus"></i></a>
    {{ order_item.quantity }}<a href="{% url 'add-to-cart' order_item.item.slug %}" class="btn ml-2"><i class="fa fa-plus"></i></a>
    </div>
</div>

With something like this:

<div class="pull-center">
    <form method="POST" action="{% url 'update-qty' %}">
        {% csrf_token %}
        <button type="submit" name="action" value="minus" class="btn mr-2"><i class="fa fa-minus"></i></button>
        {{ order_item.quantity }}<button type="submit" name="action" value="plus" class="btn ml-2"><i class="fa fa-plus"></i></button>
        <input type="hidden" name="item_slug" value="{{ order_item.item.slug }}">
        <input type="hidden" name="order_item" value="{{ order_item.pk }}">
    </form>
</div>

Upvotes: 2

MattRowbum
MattRowbum

Reputation: 2192

If you don't want to change your views, your order_summary.html needs a form for each OrderItem to POST the relevant variations when changing quantity.

In order_summary.html, near the bottom, replace this:

<div class="pull-center">
    <a href="{% url 'remove-single-item-from-cart' order_item.item.slug %}" class="btn mr-2"><i class="fa fa-minus"></i></a>
    {{ order_item.quantity }}<a href="{% url 'add-to-cart' order_item.item.slug %}" class="btn ml-2"><i class="fa fa-plus"></i></a>
    </div>
</div>

With something like this:

<div class="pull-center">
    <form method="POST" action="{% url 'add-to-cart' order_item.item.slug %}">
        {% csrf_token %}
        <a href="{% url 'remove-single-item-from-cart' order_item.item.slug %}" class="btn mr-2"><i class="fa fa-minus"></i></a>
        {{ order_item.quantity }}<button type="submit" class="btn ml-2"><i class="fa fa-plus"></i></button>
        {% for iv in order_item.item_variations.all %}
        <input type="hidden" name="variations" value="{{ iv.value }}">
        {% endfor %}
    </form>
</div>

You will need to make sure that this section of your template isn't already wrapped in a form.

Upvotes: 2

auvipy
auvipy

Reputation: 1198

I would rather suggest using either class-based-views [generic] / Class-based-vanilla views for CRUD views in any application code. function-based views are too much for simple CRUD based views.

Upvotes: 0

A_K
A_K

Reputation: 912

I think you issue is in the view

@login_required
def add_to_cart(request, slug):
    item = get_object_or_404(Item, slug=slug)

    order_item_qs = OrderItem.objects.filter(
        item=item,
        user=request.user,
        ordered=False
    )

    item_var = []  # item variation
    if request.method == 'POST':
        for items in request.POST:
            key = items
            val = request.POST[key]
            try:
                v = Variation.objects.get(
                    item=item,
                    category__iexact=key,
                    title__iexact=val
                )
                item_var.append(v)
            except:
                pass

        if len(item_var) > 0:
            for items in item_var:
                order_item_qs = order_item_qs.filter(
                    variation__exact=items,
                )

    if order_item_qs.exists():
        order_item = order_item_qs.first()
        order_item.quantity += 1
        order_item.save()
    else:
        order_item = OrderItem.objects.create(
            item=item,
            user=request.user,
            ordered=False
        )
        order_item.variation.add(*item_var)
        order_item.save()

    order_qs = Order.objects.filter(user=request.user, ordered=False)
    if order_qs.exists():
        order = order_qs[0]
        # check if the order item is in the order
        if not order.items.filter(item__id=order_item.id).exists():
            order.items.add(order_item)
            messages.info(request, "This item quantity was updated.")
            return redirect("order-summary")
    else:
        ordered_date = timezone.now()
        order = Order.objects.create(
            user=request.user, ordered_date=ordered_date)
        order.items.add(order_item)
        messages.info(request, "This item was added to cart.")
        return redirect("order-summary")

Instead of Item variation I would go for a Variation Manager

class VariationManager(models.Manager):
    def all(self):
        return super(VariationManager, self).filter(active=True)

    def sizes(self):
        return self.all().filter(category='size')

    def colors(self):
        return self.all().filter(category='color')


VAR_CATEGORIES = (
    ('size', 'size',),
    ('color', 'color',),
)


class Variation(models.Model):
    item = models.ForeignKey(Item, on_delete=models.CASCADE)
    category = models.CharField(
        max_length=120, choices=VAR_CATEGORIES, default='size')
    title = models.CharField(max_length=120)

    objects = VariationManager()
    active = models.BooleanField(default=True)

    def __str__(self):
        return self.title

for the templates

                {% if order_item.variation.all %}
                {% for variation in order_item.variation.all %}
                {{ variation.title|capfirst }}
                {% endfor %}
                {% endif %}

This should work

Upvotes: 1

Ben
Ben

Reputation: 2547

  1. I would expect variations to be empty, since you aren't sending via POST request, and also aren't sending any variations input from your html form. This is why you get the "Please specify the required variations." message. (len(variations) == 0)
  2. Because variations is empty, for v in variations isn't doing any filtering work. So when you get to order_item_qs.first(), you're not necessarily getting the OrderItem you expect, and may be +=1 on the wrong OrderItem.

To fix:

  1. Easiest would be to add a increase_quantity url endpoint, where you pass the OrderItem id into the view. Then you know exactly which OrderItem to increase the quantity and won't need to do any filtering to find it.
  2. Otherwise you need to figure out how you want to send the variations to the view. If using POST, then you want to use a input of some sort. If GET, then you could append the url, or something of that nature. (I'd strongly recommend sending as a POST request.)

Upvotes: 3

Related Questions