Rutnet
Rutnet

Reputation: 1673

Django Cart and Item Model - getting quantity to update

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:

http://dpaste.com/1NX02JW

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

Answers (1)

wrabbit
wrabbit

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.

models.py

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()

Example Function

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


Edit #1

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.


Edit #2

This is an example view that will:

  1. Get the User's cart
  2. Grab the list of entries from that cart
  3. Get a list of all unique names of products in those entries

example_view.py

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})

Edit #3

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.

Implementation

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

Related Questions