Hey there and welcome to yet another article where I will walk you through an exciting concept which is an integral piece of most, if not all, e-commerce websites and applications.
The shopping cart or basket is very crucial in that it allows customers to pick the products they want to purchase akin to real life where we use a trolley or a basket to hold the various items we would like to purchase. Similarly, we accomplish the same task virtually using a shopping cart.
This tutorial aims to walk you through a step by step process of building such shopping cart logic while leveraging the power of Django and Django Rest Framework. Without further do let's dive in, shall we?
PROJECT SET UP
We will start by navigating to a folder on our workspace where we will create the project folder. For me, that is the Desktop folder, so open a terminal window and navigate to that folder:
cd desktop
mkdir drf_shopping_cart && cd drf_shopping_cart
Above, we are navigating into the desktop folder on our machine and then creating a folder called drf_shopping_cart
and then navigating into it as well. Next let's issue the following commands which I explain shortly:
virtualenv env
source env/bin/activate
pip install django djangorestframework pillow
Here, we are creating a virtual environment to isolate our project's dependencies from the system wide configurations and settings and then we activate it and finally install our two main dependencies that is Django and DRF and the third dependency Pillow that is used for image processing in Python.
With those out of the way, issue the following commands which I explain in a bit:
django-admin startproject mysite .
python manage.py startapp cart
So the first command is used to create a new project and then the startapp
command is used to create a new app inside our project. The cart
app is where we shall spend a lot of our time writing code. The next vital step is to add our newly created cart
app alongside DRF to the list of installed apps in the settings.py
so go ahead and add it like so:
INSTALLED_APPS = [
....
'cart',
'rest_framework',
]
SETTING UP THE MODELS
Now if you think about it, we need a database table that will contain product information like name, price, image e.t.c. We can achieve that by using a Django model such that we will write Python code as a class and this will be translated to an SQL table thanks to the built-in Django ORM.
Head over to cart/models.py
and add the following code:
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=150)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True)
is_available = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
def __str__(self) -> str:
return str(self.name)
There we have defined a few table fields but feel free to add more as you see fit. To make sure that our database has this captured, we need to run the following commands to sync apply the changes:
python makemigrations && python manage.py migrate
At this point our database is ready.
Now, because we will handle product images we need to add a few more settings so that we do not run into errors and frustrations. First up open mysite/settings.py
file and at the bottom add the following:
import os
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
We are setting the MEDIA_ROOT
, that is the absolute path to the directory where our application will serve the images from
and then we are then setting MEDIA_URL
which is the url that will handle the images served from media root.
Next open mysite/urls.py
and add the following code:
#...
from django.conf.urls.static import static
from django.conf import settings
urlpatterns = [
#...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
At this point we are ready to move to the next chapter.
POPULATING THE DATABASE WITH PRODUCTS
Now we need to populate the database table for products with some data that we can use to test the shopping cart functionality.
We will need to create an endpoint that allows us to create and list products and to achieve this, we will create a serializer class to convert complex data types to native Python equivalents that can then be rendered as JSON.
Next we will create a view that will allow us to write into and read from the database.
Inside the cart folder create a file called serializers.py
and add the following code:
from rest_framework import serializers
from .models import Product
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = "__all__"
Next open up cart/views.py
and add the following code:
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from .serializers import ProductSerializer
from .models import Product
class ProductAPI(APIView):
"""
Single API to handle product operations
"""
serializer_class = ProductSerializer
def get(self, request, format=None):
qs = Product.objects.all()
return Response(
{"data": self.serializer_class(qs, many=True).data},
status=status.HTTP_200_OK
)
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
serializer.data,
status=status.HTTP_201_CREATED
)
We have created a single endpoint that will allow us to perform a GET
request to retrieve all products in the database and a POST
request to save new products into the database.
Next open mysite/urls.py
and add the following code:
#...
from cart.views import ProductAPI
urlpatterns = [
#...
path('products', ProductAPI.as_view(), name='products'),
]
#...
We are ready to add new products to the database so spin up the server by:
python manage.py runserver
Visit http://localhost:8000/products and add a couple of products.
BUILDING THE SHOPPING CART
At this point we already have a product catalog and now we need to implement a shopping cart that allows customers to add any product they want while browsing the online shop.
The shopping cart should store whatever products it has temporarily while the customers browse the site until they place an order.
For this, we will make use of Django's session framework to persist the cart and keep it in session till an order is placed or the session expires.
Django's session framework supports both anonymous and user sessions and the data stored is unique to each site visitor. When we created the project, Django added a session middleware django.contrib.sessions.middleware.SessionMiddleware
to manage all sessions in the project. This middleware avails the current session in the request object as a dictionary thus we can access it by calling request.session
and it is JSON serializable.
Enough literature, lets's write some code :). Here is what we are about to do:
- Create a cart service to handle cart operations
- Create an API endpoint that calls the cart service
- Test every cart operation
As mentioned above we need to create a service to put the logic for the cart. Inside the cart folder create a file called service.py
and add the following code:
from decimal import Decimal
from django.conf import settings
from .serializers import ProductSerializer
from .models import Product
class Cart:
def __init__(self, request):
"""
initialize the cart
"""
self.session = request.session
cart = self.session.get(settings.CART_SESSION_ID)
if not cart:
# save an empty cart in session
cart = self.session[settings.CART_SESSION_ID] = {}
self.cart = cart
def save(self):
self.session.modified = True
def add(self, product, quantity=1, overide_quantity=False):
"""
Add product to the cart or update its quantity
"""
product_id = str(product["id"])
if product_id not in self.cart:
self.cart[product_id] = {
"quantity": 0,
"price": str(product["price"])
}
if overide_quantity:
self.cart[product_id]["quantity"] = quantity
else:
self.cart[product_id]["quantity"] += quantity
self.save()
def remove(self, product):
"""
Remove a product from the cart
"""
product_id = str(product["id"])
if product_id in self.cart:
del self.cart[product_id]
self.save()
def __iter__(self):
"""
Loop through cart items and fetch the products from the database
"""
product_ids = self.cart.keys()
products = Product.objects.filter(id__in=product_ids)
cart = self.cart.copy()
for product in products:
cart[str(product.id)]["product"] = ProductSerializer(product).data
for item in cart.values():
item["price"] = Decimal(item["price"])
item["total_price"] = item["price"] * item["quantity"]
yield item
def __len__(self):
"""
Count all items in the cart
"""
return sum(item["quantity"] for item in self.cart.values())
def get_total_price(self):
return sum(Decimal(item["price"]) * item["quantity"] for item in self.cart.values())
def clear(self):
# remove cart from session
del self.session[settings.CART_SESSION_ID]
self.save()
Now let's go through the code above since a lot is happening therein.
We defined a Cart
class that allows us to manage the shopping cart.
The __init__
method is our constructor and requires a request object to initialise the cart. We also store the current session making it accessible to the other methods of the cart class. As seen, we try to get the cart from the current session and if it is missing we create an empty cart by setting an empty dictionary in the session.
The add
method is our next one and this is used to add products to the cart or update their quantity. It takes product, quantity, override_quantity
as parameteres. The product ID acts as the key to the dictionary while the quantity and price figures become the value of the dictionary.
The save
method simply marks the session as modified and thus Django knows that the session changed and needs to be saved
The remove
method does exactly that, removes a given product from the cart. It also calls the save method to make sure everything is saved.
We then defined an __iter__
method which allows us to loop over cart's items and access the related product objects. Here we fetch product instances present in the cart. We will make use of this in our view.
The __len__
method returns the number of total items in the cart.
The get_total_price
method calculates the total cost of items in the cart.
Last but not least, the clear
method removes the cart object from the session as it deletes the entire dictionary before calling the save
method.
Finally we add the following setting to the mysite/settings.py
file.
CART_SESSION_ID = 'cart
With all that in place, our Cart
class is ready to manage shopping carts.
CREATING SHOPPING CART API VIEWS
Well, we have a Cart
class to manage the cart, we now need to create API endpoints to list, add, update, remove items and clear the cart.
With this in mind, let's create the views. Open cart/views.py
and add the following code:
class CartAPI(APIView):
"""
Single API to handle cart operations
"""
def get(self, request, format=None):
cart = Cart(request)
return Response(
{"data": list(cart.__iter__()),
"cart_total_price": cart.get_total_price()},
status=status.HTTP_200_OK
)
def post(self, request, **kwargs):
cart = Cart(request)
if "remove" in request.data:
product = request.data["product"]
cart.remove(product)
elif "clear" in request.data:
cart.clear()
else:
product = request.data
cart.add(
product=product["product"],
quantity=product["quantity"],
overide_quantity=product["overide_quantity"] if "overide_quantity" in product else False
)
return Response(
{"message": "cart updated"},
status=status.HTTP_202_ACCEPTED)
A lot is going on there so let's break it down. We are sub classing Rest framework's APIView and defining two http
methods.
In the get
method we send a GET
request and initialise a cart object then we call its __iter__
method that will give us back a generator object. We then call the built in list
to get a list that we then serialize as part of our response. Also we are returning the cart's total by calling get_total_price
.
In the post
method we send a POST
request and then initialise an instance of the cart passing the request and then we check for the presence of a few things in the request payload.
We check whether we have a key named remove
in the payload and if it exists then we call the remove()
of the cart passing in the product from the payload.
We then check whether we have a key named clear
in the payload and if it exists then we call the clear()
of the cart to remove it from the current session.
If we don't have those two keys then we call the add()
of the cart passing in the product, quantity and whether or not to override quantity.
At the end of the day we send back a response with a message and status but we can do more than that, be creative. We just have one last step before we test our logic.
Open mysite/urls.py
and add the following code to it:
#...
from cart.views import CartAPI
urlpatterns = [
#...
path('cart', CartAPI.as_view(), name='cart'),
#...
]
#...
LET'S TEST THE CART
Ensure that the server is still running. Head over to the cart's url that is http://localhost:8000/cart. Let's make a post call with the follwoing payload:
{
"product": {
"id": 1,
"name": "Macbook Pro",
"description": "Our most powerful laptops, MacBook Pros are supercharged by M1 and M2 chips. Featuring Magic Keyboard, Touch ID, and brilliant Retina display.",
"price": "1800.00",
"image": "/products/2023/08/30/sp809-mbp16touch-silver-2019.jpeg",
"is_available": true,
"created_at": "2023-08-30T11:58:54.476688Z",
"modified_at": "2023-08-30T11:58:54.476720Z"
},
"quantity": 5
}
Now on get request we should see the following screen to show that the addition worked.
To remove a specific product from the cart the payload to send would look like:
{
"product": {
"id": 1,
"name": "Macbook Pro",
"description": "Our most powerful laptops, MacBook Pros are supercharged by M1 and M2 chips. Featuring Magic Keyboard, Touch ID, and brilliant Retina display.",
"price": "1800.00",
"image": "/products/2023/08/30/sp809-mbp16touch-silver-2019.jpeg",
"is_available": true,
"created_at": "2023-08-30T11:58:54.476688Z",
"modified_at": "2023-08-30T11:58:54.476720Z"
},
"remove": true
}
And to clear the cart the payload would be simply:
{
"clear": true
}
Go ahead and test them out and see what you end up :)
THAT"S A WRAP
Yes that's a wrap! If you made it to the end of the tutorial then a big hugops to you. Thank you for taking time to read and follow along and I hope you learnt a thing or two that will help progress your coding journey.
If you have any thoughts, questions etc then leave them down below.
I am also looking for my next role so if your team is hiring or know anyone who is, then I'll be much obliged if I get a referral.
I am available via Twitter or my linkedin or my website and my email is nicksonlangat95@gmail.com.
The code used in this tutorial can be found here.
Bye for now and see you on the next one. Cheers 🥂
Top comments (5)
Hi Nick - thanks for the post. it greatly helps.
Great content !
💚💛
Is this approach ensure that any customer has a special cart, I mean i want to a specific customer to create own cart ,and other customer has a different account can create a cart for his account?
Thank you, very helpful!