Building Shopping Cart and Order Management APIs with Django REST Framework
Shopping Cart Implementation
Serializer Design for Cart Operations
To manage shopping cart data, we define a serializer that handles the addition of items. Since we need custom logic for merging quantities if an item already exists, inheriting from serializers.Serializer is appropriate rather than ModelSerializer.
from rest_framework import serializers
from .models import Cart
from goods.models import Product
class CartItemSerializer(serializers.Serializer):
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
quantity = serializers.IntegerField(
required=True,
label="Quantity",
min_value=1,
error_messages={"min_value": "Quantity cannot be less than 1.", "required": "Please select a quantity."}
)
product = serializers.PrimaryKeyRelatedField(
required=True,
queryset=Product.objects.all()
)
def create(self, validated_data):
current_user = self.context["request"].user
quantity = validated_data["quantity"]
product = validated_data["product"]
existing_item = Cart.objects.filter(user=current_user, product=product).first()
if existing_item:
existing_item.quantity += quantity
existing_item.save()
return existing_item
return Cart.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.quantity = validated_data.get("quantity", instance.quantity)
instance.save()
return instance
ViewSet Configuration
The view handles CRUD operations for the cart. We restrict access to authenticated users and ensure they can only modify their own items. We also configure the lookup field to use the product ID for updates.
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.authentication import SessionAuthentication
from .serializers import CartItemSerializer, CartDetailSerializer
from .models import Cart
from utils.permissions import IsOwnerOrReadOnly
class CartViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
authentication_classes = [JSONWebTokenAuthentication, SessionAuthentication]
serializer_class = CartItemSerializer
lookup_field = "product_id"
def get_queryset(self):
return Cart.objects.filter(user=self.request.user)
def get_serializer_class(self):
if self.action == 'list':
return CartDetailSerializer
return CartItemSerializer
Dynamic Serializer for Product Details
When listing cart items, clients often need detailed product information (like name or image) rather than just the product ID. We use a dynamic serializer to handle this.
class CartDetailSerializer(serializers.ModelSerializer):
product = ProductSerializer(read_only=True)
class Meta:
model = Cart
fields = ("product", "quantity")
Inventory Management Signals
It is crucial to maintain accurate inventory levels when items are added to or removed from the cart. We override the perform methods in the ViewSet to adjust the product stock accordingly.
def perform_create(self, serializer):
cart_item = serializer.save()
product = cart_item.product
product.stock -= cart_item.quantity
product.save()
def perform_destroy(self, instance):
instance.product.stock += instance.quantity
instance.product.save()
instance.delete()
def perform_update(self, serializer):
original_quantity = serializer.instance.quantity
updated_item = serializer.save()
quantity_delta = updated_item.quantity - original_quantity
updated_item.product.stock -= quantity_delta
updated_item.product.save()
Order Management System
Order Serializer Logic
When a user creates an order, the system must generate a unique order number and populate read-only fields such as payment status. The validate method is used to inject the generated order number before saving.
import time
import random
from .models import OrderInfo, OrderGoods
class OrderSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
# Read-only fields generated by the system
order_number = serializers.CharField(read_only=True)
trade_no = serializers.CharField(read_only=True)
pay_status = serializers.CharField(read_only=True)
pay_time = serializers.DateTimeField(read_only=True)
def generate_order_sn(self):
# Format: Timestamp + User ID + Random integer
time_str = time.strftime("%Y%m%d%H%M%S")
user_id = self.context["request"].user.id
random_suffix = random.randint(10, 99)
return f"{time_str}{user_id}{random_suffix}"
def validate(self, attrs):
attrs["order_number"] = self.generate_order_sn()
return attrs
class Meta:
model = OrderInfo
fields = "__all__"
Order Detail Nested Serializer
To display order details, we nest the goods information within the order serializer.
class OrderGoodsSerializer(serializers.ModelSerializer):
product = ProductSerializer(read_only=True)
class Meta:
model = OrderGoods
fields = "__all__"
class OrderDetailSerializer(serializers.ModelSerializer):
items = OrderGoodsSerializer(many=True, read_only=True)
class Meta:
model = OrderInfo
fields = "__all__"
Order ViewSet and Cart Clearance
The order creation process involves validating the data, saving the order, transferring items from the shopping cart to the order record, and finally clearing the cart.
from rest_framework import mixins
class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin,
mixins.CreateModelMixin, mixins.DestroyModelMixin,
viewsets.GenericViewSet):
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
authentication_classes = [JSONWebTokenAuthentication, SessionAuthentication]
serializer_class = OrderSerializer
def get_serializer_class(self):
if self.action == "retrieve":
return OrderDetailSerializer
return OrderSerializer
def get_queryset(self):
return OrderInfo.objects.filter(user=self.request.user)
def perform_create(self, serializer):
new_order = serializer.save()
cart_items = Cart.objects.filter(user=self.request.user)
for item in cart_items:
OrderGoods.objects.create(
order=new_order,
product=item.product,
quantity=item.quantity
)
item.delete()
return new_order
URL Configuration
Finally, register the ViewSets with the router.
from django.conf.urls import url, include
from rest_framework.routers import DefaultRouter
from trade.views import CartViewSet, OrderViewSet
router = DefaultRouter()
router.register(r'carts', CartViewSet, base_name="carts")
router.register(r'orders', OrderViewSet, base_name="orders")
urlpatterns = [
url(r'^', include(router.urls)),
]