Как реализовать корзину на django?
1. Оформи код соответственно
2. Модель херня. Научись О2М связям
3. Стоит ли хранить корзины на бэкенде? Их хранят на бэкенде, когда любят спамить людей, бросивших корзины
4. В коде 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()
Ошибка
Вот пример одной корзины, которую я делал несколько лет назад.basket.pyfrom 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.pyfrom 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 responsesettings.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)
Смысл в том, что все корзины хранятся только в куках (из-за SESSION_ENGINE) у самих пользователей. Это означает, что хоть миллиард юзеров зайдёт и добавят по миллиону товаров — они не прибавят ни байта занимаемого места на HDD, пока не сделают заказ. К тому же, куки — очень быстрое хранилище и изменения в корзине происходят моментально. К примеру, AJAX запросы в ChangeBasketOption в среднем занимают 30мс в браузере.
Можете посмотреть код моего интернет-магазина. Там хорошо реализована корзина.
https://github.com/UlugbekMuslitdinov/exizmat