Product Catalog View Implementation
The following class-based view handles the display of product listings within a specific category. It manages sorting options, pagination logic, and retrieves the current cart count for authenticated users.
class ProductCatalogView(View):
def get(self, request, category_id, page_num):
# Retrieve sorting preference from query parameters
ordering = request.GET.get('ordering', 'default')
# Validate sorting option
if ordering not in ('price', 'sales'):
ordering = 'default'
# Fetch the specific category or redirect if missing
try:
current_category = GoodsCategory.objects.get(id=category_id)
except GoodsCategory.DoesNotExist:
return redirect(reverse('goods:index'))
# Retrieve all categories for navigation
all_categories = GoodsCategory.objects.all()
# Fetch newest arrivals for the sidebar recommendation
featured_items = GoodsSKU.objects.filter(
category=current_category
).order_by('-create_time')[:2]
# Apply sorting logic to the product queryset
if ordering == 'price':
product_list = GoodsSKU.objects.filter(
category=current_category
).order_by('price')
elif ordering == 'sales':
product_list = GoodsSKU.objects.filter(
category=current_category
).order_by('sales')
else:
product_list = GoodsSKU.objects.filter(
category=current_category
)
# Calculate cart item count for authenticated users
cart_count = 0
redis_conn = get_redis_connection('default')
if request.user.is_authenticated:
user_cart = redis_conn.hgetall('cart_%s' % request.user.id)
for quantity in user_cart.values():
cart_count += int(quantity)
# Configure pagination
items_per_page = 10
paginator = Paginator(product_list, items_per_page)
page_num = int(page_num)
try:
current_page_items = paginator.page(page_num)
except EmptyPage:
current_page_items = paginator.page(1)
# Determine pagination range display logic
total_pages = paginator.num_pages
page_range = []
if total_pages <= 5:
page_range = range(1, total_pages + 1)
elif page_num <= 3:
page_range = range(1, 6)
elif total_pages - page_num < 3:
page_range = range(total_pages - 4, total_pages + 1)
else:
page_range = range(page_num - 2, page_num + 3)
context = {
'category': current_category,
'categories': all_categories,
'page_items': current_page_items,
'featured_items': featured_items,
'page_range': page_range,
'ordering': ordering,
'cart_count': cart_count
}
return render(request, 'catalog.html', context)
URL Configuration for Catalog
Map the view to a URL pattern that accepts category and page parameters.
path('catalog/<int:category_id>/<int:page_num>/',
ProductCatalogView.as_view(),
name='catalog'),
Catalog Template Structure
The template extends the base layout and renders the product grid, sorting options, and pagination controls.
{% extends "base.html" %}
{% load static %}
{% block title %}Product Catalog{% endblock %}
{% block body %}
<div class="nav-container">
<div class="nav-bar clearfix">
<div class="category-menu fl">
<h1>All Categories</h1>
<ul class="submenu">
{% for cat in categories %}
<li>
<a href="{% url 'goods:catalog' cat.id 1 %}"
class="{{ cat.logo }}">
{{ cat.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="breadcrumb-trail">
<a href="{% url 'goods:index' %}">Home</a>
<span>></span>
<a href="{% url 'goods:catalog' category.id 1 %}">{{ category.name }}</a>
</div>
<div class="content-wrap clearfix">
<div class="sidebar fl clearfix">
<div class="new-arrivals">
<h3>New Arrivals</h3>
<ul>
{% for item in featured_items %}
<li>
<a href="{% url 'goods:detail' item.id %}">
<img src="{{ item.default_image.url }}">
</a>
<h4>
<a href="{% url 'goods:detail' item.id %}">
{{ item.name }}
</a>
</h4>
<div class="price-tag">${{ item.price }}</div>
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="main-content fr clearfix">
<div class="sorting-bar">
<a href="{% url 'goods:catalog' category.id 1 %}?ordering=default"
{% if ordering == "default" %}class="active"{% endif %}>Default</a>
<a href="{% url 'goods:catalog' category.id 1 %}?ordering=price"
{% if ordering == "price" %}class="active"{% endif %}>Price</a>
<a href="{% url 'goods:catalog' category.id 1 %}?ordering=sales"
{% if ordering == "sales" %}class="active"{% endif %}>Popularity</a>
</div>
<ul class="product-grid clearfix">
{% for item in page_items %}
<li>
<a href="{% url 'goods:detail' item.id %}">
<img src="{{ item.default_image.url }}">
</a>
<h4>
<a href="{% url 'goods:detail' item.id %}">
{{ item.name }}
</a>
</h4>
<div class="actions">
<span class="price">${{ item.price }}</span>
<span class="unit">{{ item.price }}/{{ item.unit }}</span>
<a href="#" class="btn-add-cart" title="Add to Cart"></a>
</div>
</li>
{% endfor %}
</ul>
<div class="pagination">
{% if page_items.has_previous %}
<a href="?page={{ page_items.previous_page_number }}&ordering={{ ordering }}">Prev</a>
{% endif %}
{% for p in page_range %}
<a href="?page={{ p }}&ordering={{ ordering }}"
{% if p == page_items.number %}class="active"{% endif %}>
{{ p }}
</a>
{% endfor %}
{% if page_items.has_next %}
<a href="?page={{ page_items.next_page_number }}&ordering={{ ordering }}">Next</a>
{% endif %}
</div>
</div>
</div>
{% endblock %}
Full-Text Search Engine Setup
Full-text search provides better efficiency than standard database fuzzy matching and supports tokenization for languages like Chinese. The implementation utilizes Haystack as the framework, Whoosh as the backend engine, and Jieba for Chinese segmentation.
Install the required packages within the virtual environment:
pip install django-haystack
pip install whoosh
pip install jieba
Configuration Settings
Add haystack to the installed applications in settings.py.
INSTALLED_APPS = [
# ...
'haystack',
]
Configure the search engine connection and signal processor to ensure indexes update automatically when data changes.
# Haystack configuration
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
'PATH': os.path.join(BASE_DIR, 'search_index'),
}
}
# Enable real-time indexing
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Control results per page
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10
Define Search Indexes
Create a search_indexes.py file within the application directory (e.g., goods). This defines how models map to the search index.
from haystack import indexes
from goods.models import GoodsSKU
class ProductSKUIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
return GoodsSKU
def index_queryset(self, using=None):
return self.get_model().objects.all()
Create the template file used by Haystack to determine which fields are indexed. The path must match the app name and model name: templates/search/indexes/goods/goodssku_text.txt.
{{ object.name }}
{{ object.title }}
{{ object.desc }}
Generate the initial index files using the management command:
python manage.py rebuild_index
Search Interface Implementation
Include the Haystack URLs in the projects' main URL configuration.
import haystack.urls
urlpatterns = [
# ...
path('search/', include(haystack.urls)),
]
Update the search form in the base template to submit queries to the search endpoint.
<div class="search-box fl">
<form action="/search/" method="get">
<input type="text" class="search-input fl" name="q" placeholder="Search products">
<input type="submit" class="search-btn fr" value="Go">
</form>
</div>
Search Results Template
Create templates/search/search.html to render the query results. Haystack passes a page object containing the results.
{% extends 'base.html' %}
{% load static %}
{% block title %}Search Results{% endblock %}
{% block search_bar %}
<div class="search-header clearfix">
<a href="{% url 'goods:index' %}" class="logo fl">
<img src="{% static 'images/logo.png' %}">
</a>
<div class="page-title fl">| Search Results</div>
<div class="search-box fr">
<form action="/search/" method="get">
<input type="text" class="search-input fl" name="q" placeholder="Search products">
<input type="submit" class="search-btn fr" value="Go">
</form>
</div>
</div>
{% endblock %}
{% block body %}
<div class="main-wrap clearfix">
<ul class="product-grid clearfix">
{% for result in page %}
<li>
<a href="{% url 'goods:detail' result.object.id %}">
<img src="{{ result.object.default_image.url }}">
</a>
<h4>
<a href="{% url 'goods:detail' result.object.id %}">
{{ result.object.name }}
</a>
</h4>
<div class="actions">
<span class="price">${{ result.object.price }}</span>
<span class="unit">{{ result.object.price }}/{{ result.object.unit }}</span>
</div>
</li>
{% empty %}
<p>No products found matching your query.</p>
{% endfor %}
</ul>
{% if page.has_previous or page.has_next %}
<div class="pagination">
{% if page.has_previous %}
<a href="/search/?q={{ query }}&page={{ page.previous_page_number }}">Prev</a>
{% endif %}
|
{% if page.has_next %}
<a href="/search/?q={{ query }}&page={{ page.next_page_number }}">Next</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
Chinese Tokenization Configuration
The default Whoosh engine does not handle Chinese characters effectively. A custom enalyzer using Jieba must be configured.
Create a file named JiebaTokenizer.py in the haystack backend directory (or within you're project if configuring a custom backend path).
import jieba
from whoosh.analysis import Tokenizer, Token
class JiebaSegmentTokenizer(Tokenizer):
def __call__(self, value, positions=False, chars=False,
keeporiginal=False, removestops=True,
start_pos=0, start_char=0, mode='', **kwargs):
t = Token(positions, chars, removestops=removestops, mode=mode, **kwargs)
segments = jieba.cut(value, cut_all=True)
for word in segments:
t.original = t.text = word
t.boost = 1.0
if positions:
t.pos = start_pos + value.find(word)
if chars:
t.startchar = start_char + value.find(word)
t.endchar = start_char + value.find(word) + len(word)
yield t
def JiebaAnalyzer():
return JiebaSegmentTokenizer()
Copy the original whoosh_backend.py to a new file named custom_whoosh_backend.py within the same directory. Modify the import and analyzer usage.
# Inside custom_whoosh_backend.py
from .JiebaTokenizer import JiebaAnalyzer
# Locate the schema definition
# Change:
# analyzer=StemmingAnalyzer()
# To:
analyzer=JiebaAnalyzer()
Update settings.py to point to the custom backend.
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'path.to.custom_whoosh_backend.WhooshEngine',
'PATH': os.path.join(BASE_DIR, 'search_index'),
}
}
Rebuild the index to apply the new tokenization rules.
python manage.py rebuild_index