Building E-Commerce Product Module with Django, Celery and Redis

Product Homepage View

The product homepage displays categories, promotional banners, advertisements, and category-specific product listings. The backend must provide the following data to the frontend:

  • All product category information
  • Banner carousel data
  • Promotional advertisement details
  • Categorry display titles and images
  • User shopping cart information

IndexView Implementation

from django.shortcuts import render
from django.views.generic import View
from .models import GoodsCategory, IndexGoodsBanner, IndexPromotionBanner
from .models import IndexCategoryGoodsBanner

class IndexView(View):
    def get(self, request):
        # Fetch all product categories
        goods_cate = GoodsCategory.objects.all()
        
        # Retrieve banner information
        index_banner = IndexGoodsBanner.objects.all().order_by("index")
        
        # Retrieve promotional advertisements
        promotion_banners = IndexPromotionBanner.objects.all().order_by("index")

        for category in goods_cate:
            # Category titles for homepage display
            category_title = IndexCategoryGoodsBanner.objects.filter(
                category=category, display_type=0
            )[:4]
            category.title = category_title
            
            # Category images for homepage display
            category_image = IndexCategoryGoodsBanner.objects.filter(
                category=category, display_type=1
            )[:4]
            category.img = category_image

        # Initialize cart count
        cart_num = 0
        context = {
            "goods_cate": goods_cate,
            "index_banners": index_banner,
            "promotion_banners": promotion_banners,
            "cart_num": cart_num
        }

        return render(request, 'index.html', context)

Frontend Template: Product Categories

<ul class="subnav fl">
    {% for cate in goods_cate %}
       <li><a href="#model0{{ forloop.counter }}" class="{{ cate.logo }}">{{ cate.name }}</a></li>
    {% endfor %}
</ul>

Frontend Template: Banner Carousel

<ul class="slide_pics">
    {% for slide in index_banners %}
    <li><a href="#"><img src="{{ slide.image.url }}" alt="Slide"></a></li>
    {% endfor %}
</ul>

Frontend Template: Promotional Advertisements

<div class="adv fl">
   {% for ad in promotion_banners %}
      <a href="{{ ad.url }}"><img src="{{ ad.image.url }}"></a>
    {% endfor %}
</div>

Frontend Template: Category Product Display

{% for cate in goods_cate %}
    <div class="list_model">
        <div class="list_title clearfix">
            <h3 class="fl" id="model01">{{ cate.name }}</h3>
            <div class="subtitle fl">
                <span>|</span>
                {% for title in cate.title %}
                    <a href="#">{{ title.sku.title }}</a>
                {% endfor %}
            </div>
            <a href="#" class="goods_more fr" id="fruit_more">View More ></a>
        </div>

        <div class="goods_con clearfix">
            <div class="goods_banner fl"><img src="{{ cate.image.url }}"></div>
            <ul class="goods_list fl">
                {% for img in cate.img %}
                    <li>
                        <h4><a href="#">{{ img.sku.name }}</a></h4>
                        <a href="#"><img src="{{ img.sku.default_image.url }}"></a>
                        <div class="prize">{{ img.sku.price }}</div>
                    </li>
                {% endfor %}
            </ul>
        </div>
    </div>
{% endfor %}

User Authentication Display

{% if user.is_authenticated %}
   <div class="login_btn fl">
         Welcome: <em>{{ user.username }}</em>
         <span>|</span>
         <a href="{% url 'users:logout' %}">Logout</a>
   </div>
{% else %}
    <div class="login_btn fl">
        <a href="{% url 'users:login' %}">Login</a>
        <span>|</span>
        <a href="{% url 'users:register' %}">Register</a>
   </div>
{% endif %}

Access the homepage at:

http://127.0.0.1:8000

Page Staticization

Page staticization involves rendering a dynamic template once and saving the result as a static HTML file. This approach is suitable for pages that don't change frequently. The staticization process is triggered when administrators modify the homepage data, with the generation handled asynchronously via Celery to avoid impacting other operations.

Celery Task for Static HTML Generation

from django.core.mail import send_mail
from django.conf import settings
from django.template import loader
from goods.models import GoodsCategory, IndexGoodsBanner, IndexPromotionBanner
from goods.models import IndexCategoryGoodsBanner

@app.task
def generate_static_index_html():
    """Generate static HTML file for homepage"""
    # Fetch all product categories
    goods_cate = GoodsCategory.objects.all()
    
    # Retrieve banner information
    index_banner = IndexGoodsBanner.objects.all().order_by("index")
    
    # Retrieve promotional advertisements
    promotion_banners = IndexPromotionBanner.objects.all().order_by("index")

    for category in goods_cate:
        category_title = IndexCategoryGoodsBanner.objects.filter(
            category=category, display_type=0
        )[:4]
        category.title = category_title
        
        category_image = IndexCategoryGoodsBanner.objects.filter(
            category=category, display_type=1
        )[:4]
        category.img = category_image

    cart_num = 0
    context = {
        "goods_cate": goods_cate,
        "index_banners": index_banner,
        "promotion_banners": promotion_banners,
        "cart_num": cart_num
    }

    # Load template
    template = loader.get_template("static_index.html")
    
    # Render template to generate HTML data
    html_data = template.render(context)
    
    # Save static HTML data
    file_path = os.path.join(settings.STATICFILES_DIRS[0], "index.html")
    with open(file_path, "w") as f:
        f.write(html_data)

The template rendering process loads the tempalte, generates HTML data with context, and writes it to index.html. When browsers access the homepage, they receive the static file, reducing server load.

For unauthenticated users, copy index.html to static_index.html in the templates directory and remove authentication-related code:

<div class="login_btn fl">
    <a href="{% url 'users:login' %}">Login</a>
    <span>|</span>
    <a href="{% url 'users:register' %}">Register</a>
</div>

Nginx Configuration

sudo vim /usr/local/nginx/conf/nginx.conf
sudo /usr/local/nginx/sbin/nginx -s reload

Starting Celery Server

cd /path/to/project
celery -A celery_tasks.tasks worker --loglevel=info

Testing Static Generation

from celery_tasks.tasks import generate_static_index_html
generate_static_index_html.delay()

Access via IP address to use the static file:

http://192.168.228.135/

To separate Django dynamic pages from Nginx static files, update the product page URL:

url(r'^index$', views.IndexView.as_view(), name='index')

Triggering Static Generation via Admin

Overide save_model and delete_model methods in the admin to trigger Celery tasks when administrators add or modify models:

from django.contrib import admin
from goods.models import IndexGoodsBanner, IndexCategoryGoodsBanner, IndexPromotionBanner
from goods.models import GoodsCategory
from celery_tasks.tasks import generate_static_index_html

class BaseModel(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        obj.save()
        generate_static_index_html.delay()
        
    def delete_model(self, request, obj):
        """Called when admin deletes model data"""
        obj.delete()
        generate_static_index_html.delay()

class IndexPromotionBannerAdmin(BaseModel):
    pass

class GoodsCategoryAdmin(BaseModel):
    pass

admin.site.register(IndexPromotionBanner, IndexPromotionBannerAdmin)
admin.site.register(GoodsCategory, GoodsCategoryAdmin)

Caching Implementation

Caching stores backup data in memory. Subsequent requests check memory first—if data exists, it's returned immediately; otherwise, Django computes the data dynamically and caches it for future use.

Django's cache interface:

from django.core.cache import cache
>>> cache.set('my_key', 'hello, world!', 30)
>>> cache.get('my_key')
'hello, world!'

IndexView with Caching

from django.core.cache import cache

class IndexView(View):
    def get(self, request):
        # Attempt to retrieve data from cache
        context = cache.get("index_page_data")
        
        if context is None:
            print("Cache miss - querying database")
            
            goods_cate = GoodsCategory.objects.all()
            index_banner = IndexGoodsBanner.objects.all().order_by("index")
            promotion_banners = IndexPromotionBanner.objects.all().order_by("index")

            for category in goods_cate:
                category_title = IndexCategoryGoodsBanner.objects.filter(
                    category=category, display_type=0
                )[:4]
                category.title = category_title
                
                category_image = IndexCategoryGoodsBanner.objects.filter(
                    category=category, display_type=1
                )[:4]
                category.img = category_image

            context = {
                "goods_cate": goods_cate,
                "index_banners": index_banner,
                "promotion_banners": promotion_banners,
            }
            
            # Store data in cache with 1-hour TTL
            cache.set('index_page_data', context, 3600)

        # Initialize cart count
        cart_num = 0
        context.update(cart_num=0)

        return render(request, 'index.html', context)

Cache Management and Admin Integration

Delete cache when updating database:

cache.delete("index_page_data")

Set cache expiration:

cache.set("index_page_data", context, 3600)

Admin integration for cache invalidation:

from django.core.cache import cache

class BaseAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        """Called when admin saves model data"""
        obj.save()
        generate_static_index_html.delay()
        cache.delete("index_page_data")

    def delete_model(self, request, obj):
        """Called when admin deletes model data"""
        obj.delete()
        generate_static_index_html.delay()
        cache.delete("index_page_data")

Shopping Cart Data for Authenticated Users

User shopping cart is stored in Redis as a hash structure with key format "cart_user_id" containing product IDs as fields and quantities as values:

from django_redis import get_redis_connection

# For authenticated users, retrieve cart count from Redis
if request.user.is_authenticated():
    redis_con = get_redis_connection("default")
    cart = redis_con.hgetall('cart_%s' % request.user.id)
    
    # Sum up all product quantities
    for count in cart.values():
        cart_num += int(count)

context.update(cart_num=cart_num)

Complete IndexView with Cart Integration

class IndexView(View):
    def get(self, request):
        # Try cache first
        context = cache.get("index_page_data")
        
        if context is None:
            print("Cache miss - querying database")
            
            goods_cate = GoodsCategory.objects.all()
            index_banner = IndexGoodsBanner.objects.all().order_by("index")
            promotion_banners = IndexPromotionBanner.objects.all().order_by("index")

            for category in goods_cate:
                category_title = IndexCategoryGoodsBanner.objects.filter(
                    category=category, display_type=0
                )[:4]
                category.title = category_title
                
                category_image = IndexCategoryGoodsBanner.objects.filter(
                    category=category, display_type=1
                )[:4]
                category.img = category_image

            context = {
                "goods_cate": goods_cate,
                "index_banners": index_banner,
                "promotion_banners": promotion_banners,
            }
            cache.set('index_page_data', context, 3600)

        # Cart count initialization
        cart_num = 0
        
        # Retrieve cart from Redis for authenticated users
        if request.user.is_authenticated():
            redis_con = get_redis_connection("default")
            cart = redis_con.hgetall('cart_%s' % request.user.id)
            for count in cart.values():
                cart_num += int(count)

        context.update(cart_num=cart_num)

        return render(request, 'index.html', context)

Product Detail Page

The product detail page requires the following backend logic:

  • All product category information
  • Product SKU details
  • Two latest recommended products from same category
  • Other specifications of the product
  • Order review comments
  • Shopping cart count
  • User browsing history

DetailView Implementation

class DetailView(View):
    def get(self, request, sku_id):
        # Fetch all product categories
        goods_cate = GoodsCategory.objects.all()
        
        # Fetch product information
        try:
            sku = GoodsSKU.objects.get(id=sku_id)
        except GoodsSKU.DoesNotExist:
            return redirect(reverse("goods:index"))

        # Get latest 2 recommended products from same category
        new_goods = GoodsSKU.objects.filter(
            category=sku.category
        ).order_by("-create_time")[:2]
        
        # Get other specifications
        goods_skus = sku.goods.goodssku_set.exclude(id=sku_id)

        # Retrieve order review comments
        sku_orders = sku.ordergoods_set.all().order_by("-create_time")[:30]
        if sku_orders:
            for sku_order in sku_orders:
                sku_order.create_time = sku_order.create_time.strftime("%Y-%m-%d %H-%M-%S")
                sku_order.username = sku_order.order.user.username
        else:
            sku_orders = []

        context = {
            "goods_cate": goods_cate,
            "sku": sku,
            "new_goods": new_goods,
            "goods_skus": goods_skus,
            "orders": sku_orders
        }

        # Cart count
        cart_num = 0
        
        if request.user.is_authenticated():
            user_id = request.user.id
            redis_conn = get_redis_connection("default")
            
            # Get cart data from Redis
            cart = redis_conn.hgetall("cart_%s" % user_id)
            for val in cart.values():
                cart_num += int(val)

            # Browsing history management
            # Remove existing record of this product
            redis_conn.lrem("history_%s" % user_id, 0, sku_id)
            # Add new browsing record
            redis_conn.lpush("history_%s" % user_id, sku_id)
            # Keep only latest 5 records
            redis_conn.ltrim("history_%s" % user_id, 0, 4)

        context.update({"cart_num": cart_num})

        return render(request, 'detail.html', context)

Browsing History Logic

The browsing history implementation:

  1. Removes all existing records of the current product ID using lrem with 0 (which removes all occurrences)
  2. Adds the new product ID to the front of the list using lpush
  3. Trims the list to keep only the latest 5 records using ltrim
# Browsing history management
redis_conn.lrem("history_%s" % user_id, 0, sku_id)  # Remove existing
redis_conn.lpush("history_%s" % user_id, sku_id)   # Add new
redis_conn.ltrim("history_%s" % user_id, 0, 4)      # Keep 5 records

Product Detail Page with Caching

class DetailView(View):
    def get(self, request, sku_id):
        context = cache.get("detail_data_%s" % request.user.id)
        
        if context is None:
            goods_cate = GoodsCategory.objects.all()
            
            try:
                sku = GoodsSKU.objects.get(id=sku_id)
            except GoodsSKU.DoesNotExist:
                return redirect(reverse("goods:index"))

            new_goods = GoodsSKU.objects.filter(
                category=sku.category
            ).order_by("-create_time")[:2]
            
            goods_skus = sku.goods.goodssku_set.exclude(id=sku_id)

            sku_orders = sku.ordergoods_set.all().order_by("-create_time")[:30]
            if sku_orders:
                for sku_order in sku_orders:
                    sku_order.create_time = sku_order.create_time.strftime("%Y-%m-%d %H-%M-%S")
                    sku_order.username = sku_order.order.user.username
            else:
                sku_orders = []

            context = {
                "goods_cate": goods_cate,
                "sku": sku,
                "new_goods": new_goods,
                "goods_skus": goods_skus,
                "orders": sku_orders
            }

            # Set cache with 1-hour TTL
            cache.set("detail_data_%s" % request.user.id, context, 3600)

        # Cart count
        cart_num = 0
        
        if request.user.is_authenticated():
            user_id = request.user.id
            redis_conn = get_redis_connection("default")
            
            cart = redis_conn.hgetall("cart_%s" % user_id)
            for val in cart.values():
                cart_num += int(val)

            # Browsing history
            redis_conn.lrem("history_%s" % user_id, 0, sku_id)
            redis_conn.lpush("history_%s" % user_id, sku_id)
            redis_conn.ltrim("history_%s" % user_id, 0, 4)

        context.update({"cart_num": cart_num})

        return render(request, 'detail.html', context)

Update all detail page links to use the proper URL pattern:

{% url "goods:detail" sku.id %}

Verify browsing history in the user profile page or by checking Redis directly.

Tags: Django python E-commerce Celery Redis

Posted on Sun, 05 Jul 2026 16:49:27 +0000 by steveclondon