V
V
Vadim2019-05-11 13:53:01
Django
Vadim, 2019-05-11 13:53:01

How to implement a shopping cart in django?

Just started learning django, don't judge too harshly. There is a book model and a basket model where you need to throw this book.
I created a separate application for the shopping cart. On the book's detailed description page, I created links to add it to the cart.

a href="{% url 'basket:add' book.pk %}" Добавить книгу в корзину

The link goes, everything works, but the book object does not add to the basket.
What's wrong here?
Basket template:
{% extends "master.html" %}
{% block title %}<title>Basket</title>{% endblock %}
{% block content %}
    <h1>Basket</h1>
    {% if basket %}
    
    <ul>
        {% for book in basket %}
        <li>
            <a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})
        </li>
        {% endfor %}
    </ul>
    <hr>
    {% else %}
        <p>There are no books in the basket.</p>
    {% endif %}
{% endblock %}

book model:
class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
    summary = models.TextField(
        max_length=1000,
        null=True, blank=True,
        help_text="Enter a brief description of the book"
        )
    isbn = models.CharField(
        max_length=13,
        blank=True,
        help_text='13 Character <a href="https://www.isbn-international.org/content/what-isbn">ISBN number</a>')
    genre = models.ManyToManyField(
        Genre,
        help_text="Select a genre for this book", blank=True)

    def display_genre(self):
        return ','.join(genre.name for genre in self.genre.all()[:3])
    display_genre.short_description = 'Genre'

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("book_detail", args=[str(self.id)])

Basket model:
class Basket(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='basket')
    book = models.ForeignKey(
        Book, on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField(
        verbose_name='количество', default=0)
    add_datetime = models.DateTimeField(
        verbose_name='время', auto_now_add=True)

Cart View:
def basket(request):
    content = {}
    return render(request, 'basketapp/basket.html', content)


def basket_add(request, pk):
    book = get_object_or_404(Book, pk=pk)
    basket = Basket.objects.filter(user=request.user, book=book).first()
    if not basket:
        basket = Basket(user=request.user, book=book)
        basket.quantity += 1
        basket.save()
    return HttpResponseRedirect(reverse('basketapp:basket'))


def basket_remove(request, pk):
    content = {}
    return render(request, 'basketapp/basket.html', content)

Answer the question

In order to leave comments, you need to log in

2 answer(s)
R
Roman Kitaev, 2019-05-11
@deliro

1. Issue the code accordingly
2. The model is garbage. Learn O2M Relationships
3. Should you store carts on the backend? They are stored on the backend when they like to spam cart abandoners
4. In the code

basket = Basket.objects.filter(user=request.user, book=book).first()
    if not basket:
        basket = Basket(user=request.user, book=book)
        basket.quantity += 1
        basket.save()

Error
Here is an example of one basket I made a few years ago.
basket.py
from collections import UserDict

from core.models import ProductOption, Product
from .models import Item


class Basket(UserDict):
    changed = False

    def add(self, quantity=0, option=None, set_=False):
        self.changed = True
        id_ = str(option.product.id)
        option = str(option.id)
        self.setdefault(id_, {})
        self[id_].setdefault(option, 0)

        if set_:
            self[id_][option] = quantity
        else:
            self[id_][option] += quantity

        if self[id_][option] <= 0:
            del self[id_][option]
            if not self[id_]:
                del self[id_]
            return 0
        else:
            return self[id_][option]

    @property
    def total_count(self):
        return sum(x for product, options in self.items() for _, x in options.items())

    @property
    def total_price(self):
        prices = {str(id_): price for id_, price in
                  Product.objects.filter(id__in=self.keys()).values_list('id', 'price')}
        return sum(x * prices[product] for product, options in self.items() for _, x in options.items())

    def cost(self, option):
        price = option.product.price
        return self.count_option(option) * price

    def count_option(self, option):
        product_id = str(option.product.id)
        option_id = str(option.id)
        return self.get(product_id, {}).get(option_id, 0)

    def flush(self):
        self.changed = True
        for key in list(self):
            del self[key]

    def build_order(self, order):
        items = []
        for product_id, data in self.items():
            product = Product.objects.get(id=product_id)

            for option_id, quantity in data.items():
                if quantity == 0:
                    continue
                option = None
                if option_id != '0':
                    option = ProductOption.objects.get(id=option_id)
                items.append(
                    Item(order=order, option=option, quantity=quantity, product=product)
                )
        order.items.bulk_create(items)
        self.flush()
        return order

    def fix(self):
        """Фиксит корзину на случай, если опции удалили, а они находятся в корзине"""
        ids = self.keys()
        exist_in_db = (Product.objects
                       .filter(id__in=ids, options__in_stock=True, options__show=True)
                       .values_list('id', flat=True))
        to_remove = set(ids) - set(str(x) for x in exist_in_db)
        for id_ in to_remove:
            del self[id_]
        if to_remove:
            self.changed = True

    def to_dict(self):
        return dict(self)

middleware.py
from django.utils.deprecation import MiddlewareMixin
from .basket import Basket


class BasketMiddleware(MiddlewareMixin):
    def process_request(self, request):
        request.basket = Basket(request.session.get('basket', {}))

    def process_response(self, request, response):
        if getattr(request, 'basket', None) is None:
            return response
        if request.basket.changed:
            request.session['basket'] = request.basket.to_dict()
            request.session.save()
        return response

settings.py
# ...
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
MIDDLEWARE_CLASSES = [
    # ...
    'orders.middleware.BasketMiddleware',
]
# ...

views.py
@method_decorator(csrf_exempt, name='dispatch')
class ChangeBasketOption(View):
    def post(self, request):
        change = int(request.POST.get('change'))
        pk = int(request.POST.get('id'))
        set_ = bool(request.POST.get('set', 0))
        option = get_object_or_404(ProductOption, pk=pk)
        value = request.basket.add(option=option, quantity=change, set_=set_)
        cost = request.basket.cost(option)
        return JsonResponse({
            'value': value,
            'cost': cost,
            'total': request.basket.total_price
        })


class Basket(FormView):
    template_name = 'orders/basket.html'
    form_class = OrderForm
    success_url = reverse_lazy('ordered')

    def get(self, request, *args, **kwargs):
        request.basket.fix()
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        kwargs['products'] = Product.objects.filter(id__in=self.request.basket.keys())
        kwargs['can_order'] = self.request.basket.total_price >= min_order_cost()
        return super().get_context_data(**kwargs)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs

    def form_valid(self, form):
        order = form.save()
        order = self.request.basket.build_order(order)
        mail_new_order(order)
        return super().form_valid(form)

The point is that all baskets are stored only in cookies (due to SESSION_ENGINE) from the users themselves. This means that even a billion users will come in and add a million products each - they will not add a single byte of HDD space until they place an order. In addition, cookies are very fast storage and changes in the shopping cart happen instantly. For example, AJAX requests to ChangeBasketOption take an average of 30ms in the browser.

U
UlugbekMuslitdinov, 2021-01-15
@UlugbekMuslitdinov

You can see the code of my online store. There is a well-implemented shopping cart.
https://github.com/UlugbekMuslitdinov/exizmat

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question