Reputation: 470
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
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
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
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
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
Reputation: 2547
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
)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:
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.Upvotes: 3