Search code examples
pythondjangodatabaseshopping-cart

Implementing "Save For Later" functionality in a Django Shopping Cart App?


I'm trying to teach myself Django by using it to make an e-commerce site. I'm working on the shopping cart right now; it's implemented using Django sessions and it's currently working fine, but I'm having trouble implementing the "save for later" functionality that you can find on lots of online stores (i.e. Amazon or whatever) that allows users to remove items from their shopping cart and instead put them on a list that allows them to easily see it from their shopping cart page. Before I go on, here's the views.py and cart.py for my current shopping cart:

cart.py:

from decimal import Decimal
from django.conf import settings
from bookDetails.models import Book




# This is the cart class.
class Cart(object):
    # Constructor method for the class - includes a request parameter
    def __init__(self, request):
        # Start by creating a session for the new cart
        self.session = request.session

        userCart = self.session.get(settings.CART_SESSION_ID)

        if not userCart:
            userCart = self.session[settings.CART_SESSION_ID] = {}

        self.userCart = userCart

    def save(self):
        self.session.modified = True

    def add(self, book, amount=1, change_amount=False):
        book_id = str(book.id)

        if book_id not in self.userCart:
            self.userCart[book_id] = {'amount': 0,
                                      'author': book.book_author,
                                      'author_bio': book.author_bio,
                                      'description': book.book_description,
                                      'genre': book.book_genre,
                                      'publishing_info': book.publishing_info,
                                      'avg_rating': str(book.avg_rating),
                                      'price': str(book.price)}

        if change_amount:
            self.userCart[book_id]['amount'] = amount
        else:
            self.userCart[book_id]['amount'] += amount

        self.save()


    def remove(self, book):
        book_id = str(book.id)

        if book_id in self.userCart:
            del self.userCart[book_id]
            self.save()


    def __iter__(self):

        book_ids = self.userCart.keys()

        books = Book.objects.filter(id__in=book_ids)

        cart = self.userCart.copy()

        for book in books:
            cart[str(book.id)]['book'] = book

        for book in cart.values():
            book['price'] = Decimal(book['price'])

            book['total_price'] = book['price'] * book['amount']

            yield book

    def __len__(self):
        return sum(book['amount'] for book in self.userCart.values())

    def get_total_price(self):
        return sum((book['price'] * book['amount']) for book in self.userCart.values())

    def clear(self):
        del self.session[settings.CART_SESSION_ID]
        self.save()

Views.py:

from django.shortcuts import render, redirect, get_object_or_404

from django.views.decorators.http import require_POST

# This is the Book model from the bookDetails package I made.
from bookDetails.models import Book
# These are the cart and cart forms.
from .cart import Cart
from .forms import AddToCartForm

@require_POST
def addToCart(request, book_id):
    userCart = Cart(request)
    book = get_object_or_404(Book, id=book_id)


    form = AddToCartForm(request.POST)

    if form.is_valid():
        data = form.cleaned_data
        userCart.add(book=book,
                     amount=data['amount'],
                     change_amount=data['change_amount'])


    return redirect('cart:cart_info')

def removeFromCart(request, book_id):
    userCart = Cart(request)

    book = get_object_or_404(Book, id=book_id)

    userCart.remove(book)

    return redirect('cart:cart_info')

def cart_info(request):
    userCart = Cart(request)


    for current in userCart:
        current['update_amount_form'] = AddToCartForm(
            initial={'amount': current['amount'],
                     'change_amount': True}
        )

    return render(request, 'cart/info.html', {'userCart': userCart})


# This view displays the checkout page

def checkout(request):
    userCart = Cart(request)

    userCart.clear()

    return render(request, 'cart/checkout.html', {'userCart': userCart})

So given the way I have the cart set up, what would be the simplest/most efficient way to set up a "save for later" functionality? What I originally tried to do is create another class just like the Cart class, except it was called SFLList (Saved For Later List), then just copy-pasted most of the code from the cart class right onto it and adjusted them for a simple list, something like this

class SFLList(object):

        def __init__(self, request):

            self.session = request.session

            SFL = self.session.get(settings.SFL_SESSION_ID)

            if not SFL:
                SFL = self.session[settings.SFL_SESSION_ID] = {}

            self.SFL = SFL
# Below this would go functions like addSFL, removeSFL, 
# and the __iter__ function, all redefined to work with SFLList

...But this ended up giving me a TypeError because "the Decimal type is not JSON serializable" or something to that effect, which may look like it has something to do with the way I converted the price attribute to str so it could be serializeable (in the add function of the cart class), but the code and site work perfectly fine as they are. It only broke down and gave me that error when I added the SFLList code and tried to integrate it.

I spent all day yesterday trying to get my new SFLList class to work, but to no avail. I ended up just discarding the changes and reverting to my latest commit (before I made SFLList and the changes related to it). As expected, no TypeError because of Decimal when it's just the Cart class, defined exactly as I've got it here.

I feel like there has to be an easier way to do this. All "saved for later" has to do is just the exact same thing my cart already does, but without having the book inside the cart. Ordinarily what I'd do in a language like Java or C++ or whatever is just create an array and move the "Book" instances into it, then just iterate through the array and print all of each book's attributes in order. This is my first time working with something like Django where things are seemingly mostly done through database queries. It doesn't seem I like I can really use an array or list to store things inside models - the closest thing I found was something called an ArrayField, but apparently it requires that your database be "Postgres", which I don't think my project is using (settings.py has the DB set up as sqlite3).

What's the best way to do this?


Solution

  • I think you can simplify the problem just setting a boolean variable in your cart item dictionary like this

    self.userCart[book_id] = {
      'amount': 0,
      'author': book.book_author,
      'author_bio': book.author_bio,
      'description': book.book_description,
      'genre': book.book_genre,
      'publishing_info': book.publishing_info,
      'avg_rating': str(book.avg_rating),
      'price': str(book.price),
      'for_later': False  # saved for later if True
    }
    

    If you want only the books in the cart you can:

    def get_books_ids(self):
       books_ids = []
       for key, val in self.userCart.items():
           if not val['for_later']:
               books_ids.append(key)
       return books_ids
    
    def __iter__(self):
      book_ids = self.get_books_ids()
    
      books = Book.objects.filter(id__in=book_ids)
    
      cart = self.userCart.copy()
    
      for book in books:
          cart[str(book.id)]['book'] = book
    
      for book in cart.values():
          book['price'] = Decimal(book['price'])
    
          book['total_price'] = book['price'] * book['amount']
    
          yield book