Reputation: 1673
I am working on a Django cart application. I have two models Cart and Item. I am trying to get the quantity to update when an Item is added to the basket but cant get the views to work properly. I am having problems getting item_obj assignment to work - do I need to do anything with the model manager here? Any help is really appreciated.
Models.py
class Cart(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
products = models.ManyToManyField(Product, blank=True)
total = models.DecimalField(default=0.00, max_digits=10, decimal_places=2)
updated = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(auto_now_add=True)
objects = CartManager()
def __str__(self):
return str(self.id)
class Item(models.Model):
item = models.ForeignKey(Product, null=True)
cart = models.ForeignKey(Cart, null=True)
quantity = models.PositiveIntegerField()
Views.py extract
def cart_update(request):
product_id = request.POST.get('product_id')
product_obj = Item.objects.get(id=product_id)
print(item_id)
item_obj = Item.objects.get(id=product_id)
cart_obj, new_obj = Cart.objects.new_or_get(request)
if item_obj in cart_obj.products.all():
cart_obj.products.add(product_obj)
item_obj.quantity += 1
item_obj.save()
else:
cart_obj.products.add(product_obj)
return redirect("cart:home")
EDIT
Views.py update (12/02/2018)
def cart_update(request):
# Based on the user who is making the request, grab the cart object
cart_obj, new_obj = Cart.objects.new_or_get(request)
# Get entries in the cart
my_carts_current_entries = Entry.objects.filter(cart=cart_obj)
# Get a list of your products
products = Product.objects.all()
if request.POST:
# Get the product's ID from the POST request.
product_id = request.POST.get('product_id')
# Get the quantity of the product desired.
product_quantity = request.POST.get('product_quantity')
# Create the new Entry...this will update the cart on creation
Entry.objects.create(cart=cart_obj, product=product_id, quantity=product_quantity)
return HttpResponse('carts/carts.html')
return render(request, 'carts/carts.html', {'cart_obj': cart_obj, 'my_carts_current_entries': my_carts_current_entries,
'products': products})
Also I have a seperate model manager for the cart that creates a new cart or assigns one to the user, see below:
class CartManager(models.Manager):
def new_or_get(self, request):
cart_id = request.session.get("cart_id", None)
qs = self.get_queryset().filter(id=cart_id)
if qs.count() == 1:
new_obj = False
cart_obj = qs.first()
if request.user.is_authenticated() and cart_obj.user is None:
cart_obj.user = request.user
cart_obj.save()
else:
cart_obj = Cart.objects.new(user=request.user)
new_obj = True
request.session['cart_id'] = cart_obj.id
return cart_obj, new_obj
def new(self, user=None):
user_obj = None
if user is not None:
if user.is_authenticated():
user_obj = user
return self.model.objects.create(user=user_obj)
The error from the debug log is available here:
Thanks in advance
Update 2:
This is how my template file reads at the moment.
{% for product in cat_appetizers %}
<table>
<tr>
<td><h5>{{ product.name }}</h5>
<td><p><strong>£ {{ product.price }}</strong></p></td>
<td>
<form class='form-product-ajax' method="POST" action='{% url "cart:update" %}' data-endpoint='{% url "cart:update" %}' class="form"> {% csrf_token %}
<input type="hidden" name='product_id' value='{{ product.id }}'>
<span class='submit-span'>
{% if product in cart.products.all %}
<button>Remove</button>
{% else %}
<button>Add to Basket</button>
</span>
{% endif %}
</form>
</td>
</tr>
</table>
{% endfor %}
Upvotes: 1
Views: 13792
Reputation: 196
We can update the Cart model by using a post-save signal in the models. The following post-save receiver will update the Cart model any time an Entry is created.
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.datetime_safe import datetime
class Product(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=64, unique=True)
description = models.TextField(default='')
cost = models.DecimalField(default=0.00, max_digits=10, decimal_places=2)
class Cart(models.Model):
user = models.ForeignKey(User, null=True, blank=True, on_delete='CASCADE')
count = models.PositiveIntegerField(default=0)
total = models.DecimalField(default=0.00, max_digits=10, decimal_places=2)
updated = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return "User: {} has {} items in their cart. Their total is ${}".format(self.user, self.count, self.total)
class Entry(models.Model):
product = models.ForeignKey(Product, null=True, on_delete='CASCADE')
cart = models.ForeignKey(Cart, null=True, on_delete='CASCADE')
quantity = models.PositiveIntegerField()
def __str__(self):
return "This entry contains {} {}(s).".format(self.quantity, self.product.name)
@receiver(post_save, sender=Entry)
def update_cart(sender, instance, **kwargs):
line_cost = instance.quantity * instance.product.cost
instance.cart.total += line_cost
instance.cart.count += instance.quantity
instance.cart.updated = datetime.now()
def test_my_cart():
apple, created = Product.objects.get_or_create(name='apple', cost=0.25)
my_cart, created = Cart.objects.get_or_create(user=None)
print(my_cart)
# STDOUT --> User: None has 0 items in their cart. Their total is $0.00
entry1 = Entry.objects.create(product=apple, cart=my_cart, quantity=3)
print(entry1)
# STDOUT --> This entry contains 3 apple(s)
print(my_cart)
# STDOUT --> User: None has 3 items in their cart. Their total is $0.75
If you would like to remove the line entry from the cart at a later time there is also a post-delete signal in Django.
Best of luck
To use this in the admin panel, try the following:
# admin.py
from django.contrib import admin
from django.utils.datetime_safe import datetime
from app.models import Product, Cart, Entry
class EntryAdmin(admin.ModelAdmin):
# Overide of the save model
def save_model(self, request, obj, form, change):
obj.cart.total += obj.quantity * obj.product.cost
obj.cart.count += obj.quantity
obj.cart.updated = datetime.now()
obj.cart.save()
super().save_model(request, obj, form, change)
# Register your models here.
admin.site.register(Product)
admin.site.register(Cart)
admin.site.register(Entry, EntryAdmin)
I would also like to note that since there are "multiple ways to skin a cat" we could override the save method in the model instead of using the signal. I think this is dependent on application and personal preference though.
This is an example view that will:
from django.shortcuts import render
from scratchwork.models import Cart, Entry
from django.contrib.auth.models import User
def test_view(request):
""" This view displays what is in a user's cart. """
# Based on the user who is making the request, grab the cart object
my_cart = Cart.objects.get_or_create(user=User)
# Get a queryset of entries that correspond to "my_cart"
list_of_entries = Entry.objects.filter(cart=my_cart)
# Make a list of the product's names
list_of_products = list(list_of_entries.values_list('product__name', flat=True))
# Remove redundant product names
list_of_products = list(set(list_of_products))
return render(request, 'something.html', {'list_of_products': list_of_products})
Before I talk about code, I'm going to do a quick comparison so we are on the same page concerning the model classes.
Imagine you are shopping on Amazon. You as a Amazon customer have a Cart. The cart will show you any Entry that you add to it while you are shopping on the site.
As you go browsing the wondrous wares of Amazon you stumble across a pair of shoes you want to buy. These shoes are a Product and have a name, a price, and a description of the them.
You decide to purchase these shoes so you click "Add to cart". At this time the Amazon web service generates an Entry linked to the Cart containing the information about the Product.
So you are clicking a button to send POST data back to the view. Assuming you are either using a form or AJAX data to get the information back, this is how I would go about designing the view.
from django.http import HttpResponse
from django.shortcuts import render
from scratchwork.models import Cart, Entry, Product
from django.contrib.auth.models import User
def test_view(request):
# Based on the user who is making the request, grab the cart object
my_cart = Cart.objects.get_or_create(user=User)
# Get entries in the cart
my_carts_current_entries = Entry.objects.filter(cart=my_cart)
# Get a list of your products
products = Product.objects.all()
if request.POST:
# Get the product's ID from the POST request.
product_id = request.POST.get('product_id')
# Get the object using our unique primary key
product_obj = Product.objects.get(id=product_id)
# Get the quantity of the product desired.
product_quantity = request.POST.get('product_quantity')
# Create the new Entry...this will update the cart on creation
Entry.objects.create(cart=my_cart, product=product_obj, quantity=product_quantity)
return HttpResponse('somewhereelse.html')
return render(request, 'something.html', {'my_cart': my_cart, 'my_carts_current_entries': my_carts_current_entries,
'products': products})
Upvotes: 7