【终极攻略】Django基于类的视图:12个高级技巧打造完美Web应用
前言:为什么资深开发者都在使用基于类的视图?
Django框架提供了两种编写视图的方式:基于函数的视图(FBVs)和基于类的视图(CBVs)。虽然函数视图简单直观,但随着项目复杂度增加,代码重复和维护难度也随之增长。基于类的视图通过面向对象的设计模式,为开发者提供了更强大的代码复用机制和更清晰的代码组织方式。根据Stack Overflow的调查,超过70%的Django高级开发者在复杂项目中选择使用CBVs,因为它们能将重复代码减少高达40%。本文将深入探讨Django基于类的视图,带你掌握从基础到高级的12个核心技巧,让你的Django代码更加优雅、高效且易于维护。
1. 基于类的视图基础
1.1 从函数视图到类视图的转换
首先,让我们看看同一个视图的两种实现方式:
函数视图实现:
# views.py
from django.shortcuts import render
from django.http import HttpResponse
from .models import Article
def article_list(request):
articles = Article.objects.all()
return render(request, 'articles/article_list.html', {'articles': articles})
def article_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
return render(request, 'articles/article_detail.html', {'article': article})
类视图实现:
# views.py
from django.views.generic import ListView, DetailView
from .models import Article
class ArticleListView(ListView):
model = Article
template_name = 'articles/article_list.html'
context_object_name = 'articles'
class ArticleDetailView(DetailView):
model = Article
template_name = 'articles/article_detail.html'
context_object_name = 'article'
URL配置:
# urls.py
from django.urls import path
from . import views
urlpatterns = [
# 函数视图
path('articles/', views.article_list, name='article-list'),
path('articles/<int:pk>/', views.article_detail, name='article-detail'),
# 类视图
path('articles/', views.ArticleListView.as_view(), name='article-list'),
path('articles/<int:pk>/', views.ArticleDetailView.as_view(), name='article-detail'),
]
1.2 Django内置通用视图概览
Django提供了丰富的通用视图基类,用于常见的任务:
from django.views.generic import (
View, # 基础视图
TemplateView, # 渲染模板
RedirectView, # 重定向
DetailView, # 详情页
ListView, # 列表页
CreateView, # 创建表单
UpdateView, # 更新表单
DeleteView, # 删除确认
FormView, # 通用表单
ArchiveIndexView, # 存档索引
YearArchiveView, # 年度存档
MonthArchiveView, # 月度存档
DayArchiveView, # 日存档
DateDetailView # 日期详情
)
1.3 视图处理流程
基于类的视图处理请求的标准流程:
dispatch()
: 确定使用哪个HTTP方法处理函数http_method_not_allowed()
: 处理不允许的方法get()
,post()
,put()
,delete()
: 处理不同HTTP方法的请求
from django.views import View
from django.http import HttpResponse
class MyView(View):
def dispatch(self, request, *args, **kwargs):
print("处理请求前执行")
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
return HttpResponse("GET请求处理")
def post(self, request, *args, **kwargs):
return HttpResponse("POST请求处理")
def http_method_not_allowed(self, request, *args, **kwargs):
print("方法不被允许")
return super().http_method_not_allowed(request, *args, **kwargs)
2. 常用通用视图详解
2.1 TemplateView - 静态页面渲染
适用于简单的静态页面,如"关于我们"、"联系方式"等:
from django.views.generic import TemplateView
class AboutView(TemplateView):
template_name = "about.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['company_name'] = "Django公司"
context['founded_year'] = 2005
context['team_members'] = ["Alice", "Bob", "Charlie"]
return context
URL配置:
path('about/', views.AboutView.as_view(), name='about'),
2.2 ListView - 对象列表展示
用于显示模型对象的列表页:
from django.views.generic import ListView
from .models import Product
class ProductListView(ListView):
model = Product
template_name = "products/product_list.html"
context_object_name = "products"
paginate_by = 10 # 每页显示10条
ordering = ['-created_at'] # 按创建时间倒序
def get_queryset(self):
queryset = super().get_queryset()
# 添加过滤功能
category = self.request.GET.get('category')
if category:
queryset = queryset.filter(category=category)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
return context
模板使用:
<!-- products/product_list.html -->
<h1>产品列表</h1>
<div class="filters">
<form method="get">
<select name="category" onchange="this.form.submit()">
<option value="">所有分类</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if request.GET.category == category.id|stringformat:"i" %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</form>
</div>
<div class="products">
{% for product in products %}
<div class="product">
<h2>{{ product.name }}</h2>
<p>{{ product.description }}</p>
<p>价格: ${{ product.price }}</p>
</div>
{% empty %}
<p>没有找到符合条件的产品</p>
{% endfor %}
</div>
<!-- 分页 -->
{% if is_paginated %}
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">« 首页</a>
<a href="?page={{ page_obj.previous_page_number }}">上一页</a>
{% endif %}
<span class="current">
第 {{ page_obj.number }} 页,共 {{ page_obj.paginator.num_pages }} 页
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">下一页</a>
<a href="?page={{ page_obj.paginator.num_pages }}">末页 »</a>
{% endif %}
</span>
</div>
{% endif %}
2.3 DetailView - 对象详情展示
用于显示单个对象的详细信息:
from django.views.generic import DetailView
from .models import Product
class ProductDetailView(DetailView):
model = Product
template_name = "products/product_detail.html"
context_object_name = "product"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 添加相关产品
context['related_products'] = Product.objects.filter(
category=self.object.category
).exclude(id=self.object.id)[:5]
# 添加评论
context['comments'] = self.object.comments.all()
return context
2.4 FormView - 表单处理
用于处理不与模型直接关联的表单:
from django.views.generic import FormView
from django.urls import reverse_lazy
from .forms import ContactForm
class ContactFormView(FormView):
template_name = "contact.html"
form_class = ContactForm
success_url = reverse_lazy('contact_success')
def form_valid(self, form):
# 处理表单数据
name = form.cleaned_data['name']
email = form.cleaned_data['email']
message = form.cleaned_data['message']
# 发送电子邮件
send_mail(
f'来自{name}的联系表单',
message,
email,
['admin@example.com'],
fail_silently=False,
)
return super().form_valid(form)
def form_invalid(self, form):
# 可以在这里添加额外的验证或日志记录
return super().form_invalid(form)
2.5 CreateView, UpdateView, DeleteView - 模型CRUD操作
用于模型的创建、更新和删除:
from django.views.generic import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Product
from .forms import ProductForm
class ProductCreateView(CreateView):
model = Product
form_class = ProductForm
template_name = "products/product_form.html"
success_url = reverse_lazy('product-list')
def form_valid(self, form):
form.instance.created_by = self.request.user
return super().form_valid(form)
class ProductUpdateView(UpdateView):
model = Product
form_class = ProductForm
template_name = "products/product_form.html"
def get_success_url(self):
return reverse_lazy('product-detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
form.instance.updated_by = self.request.user
return super().form_valid(form)
class ProductDeleteView(DeleteView):
model = Product
template_name = "products/product_confirm_delete.html"
success_url = reverse_lazy('product-list')
def delete(self, request, *args, **kwargs):
product = self.get_object()
# 记录删除操作日志
ActivityLog.objects.create(
user=request.user,
action=f"删除了产品: {product.name}"
)
return super().delete(request, *args, **kwargs)
3. Mixin与视图组合
3.1 Django内置Mixin概览
Django提供了多种Mixin类,可以组合使用实现复杂功能:
from django.contrib.auth.mixins import (
LoginRequiredMixin, # 需要登录
PermissionRequiredMixin, # 需要权限
UserPassesTestMixin, # 自定义测试
)
from django.views.generic.detail import (
SingleObjectMixin, # 处理单个对象
SingleObjectTemplateResponseMixin, # 单对象模板响应
)
from django.views.generic.list import (
MultipleObjectMixin, # 处理多个对象
MultipleObjectTemplateResponseMixin, # 多对象模板响应
)
from django.views.generic.edit import (
FormMixin, # 表单处理
ModelFormMixin, # 模型表单处理
ContextMixin, # 上下文处理
)
3.2 组合多个Mixin创建复杂视图
通过组合多个Mixin,可以创建功能丰富的视图:
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import ListView
from django.db.models import Q
class StaffProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = Product
template_name = "products/staff_product_list.html"
context_object_name = "products"
permission_required = "products.view_product"
login_url = "/login/"
paginate_by = 20
def get_queryset(self):
queryset = super().get_queryset()
# 搜索功能
search_query = self.request.GET.get('q')
if search_query:
queryset = queryset.filter(
Q(name__icontains=search_query) |
Q(description__icontains=search_query) |
Q(sku__icontains=search_query)
)
# 过滤功能
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_query'] = self.request.GET.get('q', '')
context['status_choices'] = Product.STATUS_CHOICES
context['selected_status'] = self.request.GET.get('status', '')
return context
3.3 自定义Mixin
创建自定义Mixin实现特定功能:
class SetHeadingMixin:
"""为模板添加页面标题"""
heading = ""
def get_heading(self):
return self.heading
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['heading'] = self.get_heading()
return context
class PageTitleMixin:
"""设置页面标题"""
page_title = ""
def get_page_title(self):
return self.page_title
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = self.get_page_title()
return context
class StaffRequiredMixin:
"""确保用户是员工"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.is_staff:
return redirect('login')
return super().dispatch(request, *args, **kwargs)
class AjaxResponseMixin:
"""处理AJAX请求"""
def dispatch(self, request, *args, **kwargs):
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
return self.ajax_dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
def ajax_dispatch(self, request, *args, **kwargs):
handler = getattr(self, f'ajax_{request.method.lower()}', None)
if handler:
return handler(request, *args, **kwargs)
return JsonResponse({'error': 'AJAX method not allowed'}, status=405)
综合使用:
class ProductDetailView(LoginRequiredMixin, SetHeadingMixin, PageTitleMixin, AjaxResponseMixin, DetailView):
model = Product
template_name = "products/product_detail.html"
context_object_name = "product"
heading = "产品详情"
def get_page_title(self):
return f"产品: {self.object.name}"
def ajax_get(self, request, *args, **kwargs):
self.object = self.get_object()
return JsonResponse({
'id': self.object.id,
'name': self.object.name,
'price': str(self.object.price),
'stock': self.object.stock,
'status': self.object.get_status_display()
})
4. 处理请求与响应
4.1 请求处理与重定向
处理不同的HTTP请求方法:
from django.views.generic import View
from django.shortcuts import redirect
from django.http import JsonResponse
class CartView(View):
def get(self, request, *args, **kwargs):
"""显示购物车"""
cart_items = request.session.get('cart', [])
return render(request, 'cart.html', {'cart_items': cart_items})
def post(self, request, *args, **kwargs):
"""添加商品到购物车"""
product_id = request.POST.get('product_id')
quantity = int(request.POST.get('quantity', 1))
# 获取当前购物车
cart = request.session.get('cart', [])
# 添加商品
cart.append({
'product_id': product_id,
'quantity': quantity
})
# 更新会话
request.session['cart'] = cart
return redirect('cart')
def delete(self, request, *args, **kwargs):
"""删除购物车项目"""
if not request.headers.get('x-requested-with') == 'XMLHttpRequest':
return JsonResponse({'error': '仅支持AJAX请求'}, status=400)
data = json.loads(request.body)
item_index = data.get('item_index')
cart = request.session.get('cart', [])
if 0 <= item_index < len(cart):
del cart[item_index]
request.session['cart'] = cart
return JsonResponse({'success': True})
return JsonResponse({'error': '项目不存在'}, status=404)
4.2 动态添加响应头
自定义响应头的处理:
from django.views.generic import TemplateView
class PDFReportView(TemplateView):
template_name = "reports/pdf_template.html"
def render_to_response(self, context, **response_kwargs):
response = super().render_to_response(context, **response_kwargs)
# 添加文件下载头
response['Content-Disposition'] = 'attachment; filename="report.pdf"'
response['Content-Type'] = 'application/pdf'
# 添加缓存控制
response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response['Pragma'] = 'no-cache'
response['Expires'] = '0'
return response
4.3 内容协商与多格式响应
根据请求格式返回不同类型的响应:
from django.views.generic import DetailView
from django.http import JsonResponse, HttpResponse
import csv
class ProductExportView(DetailView):
model = Product
def render_to_response(self, context, **response_kwargs):
product = context['object']
# 获取请求的格式
format_type = self.request.GET.get('format', 'html')
if format_type == 'json':
# 返回JSON格式
return JsonResponse({
'id': product.id,
'name': product.name,
'price': str(product.price),
'description': product.description,
'category': product.category.name if product.category else '',
'stock': product.stock
})
elif format_type == 'csv':
# 返回CSV格式
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="{product.id}.csv"'
writer = csv.writer(response)
writer.writerow(['ID', 'Name', 'Price', 'Description', 'Category', 'Stock'])
writer.writerow([
product.id,
product.name,
product.price,
product.description,
product.category.name if product.category else '',
product.stock
])
return response
# 默认返回HTML模板
return super().render_to_response(context, **response_kwargs)
5. 上下文处理与模板渲染
5.1 自定义context_data
扩展模板上下文数据:
from django.views.generic import ListView
from .models import Product, Category
class HomePageView(ListView):
model = Product
template_name = 'home.html'
context_object_name = 'featured_products'
def get_queryset(self):
return Product.objects.filter(is_featured=True)[:6]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 添加热门分类
context['popular_categories'] = Category.objects.annotate(
product_count=Count('product')
).order_by('-product_count')[:5]
# 添加最新产品
context['new_arrivals'] = Product.objects.order_by('-created_at')[:8]
# 添加促销产品
context['on_sale'] = Product.objects.filter(on_sale=True)[:4]
# 添加站点统计
context['stats'] = {
'product_count': Product.objects.count(),
'category_count': Category.objects.count(),
'user_count': User.objects.count()
}
# 添加随机推荐
context['recommended'] = Product.objects.order_by('?')[:4]
return context
5.2 动态模板选择
根据条件选择不同的模板:
from django.views.generic import DetailView
class ProductView(DetailView):
model = Product
context_object_name = 'product'
def get_template_names(self):
"""根据产品类型或用户选择不同的模板"""
product = self.object
# 检查是否为移动设备
user_agent = self.request.META.get('HTTP_USER_AGENT', '').lower()
is_mobile = 'mobile' in user_agent or 'android' in user_agent
# 根据产品类型选择模板
if product.product_type == 'digital':
template = 'products/digital_product.html'
elif product.product_type == 'physical':
template = 'products/physical_product.html'
else:
template = 'products/product_detail.html'
# 移动设备使用移动版模板
if is_mobile:
return [f'mobile/{template}', template]
return [template]
5.3 动态处理表单上下文
动态处理表单上下文:
from django.views.generic import UpdateView
from .models import Product
from .forms import ProductForm, ProductImageFormSet
class ProductEditView(UpdateView):
model = Product
form_class = ProductForm
template_name = 'products/product_edit.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 添加内联表单集
if self.request.POST:
context['image_formset'] = ProductImageFormSet(
self.request.POST,
self.request.FILES,
instance=self.object
)
else:
context['image_formset'] = ProductImageFormSet(instance=self.object)
# 添加分类下拉选项
context['categories'] = Category.objects.all()
# 添加最近编辑记录
context['recent_edits'] = ProductEditLog.objects.filter(
product=self.object
).order_by('-timestamp')[:5]
return context
def form_valid(self, form):
context = self.get_context_data()
image_formset = context['image_formset']
if image_formset.is_valid():
self.object = form.save()
image_formset.instance = self.object
image_formset.save()
# 记录编辑日志
ProductEditLog.objects.create(
product=self.object,
user=self.request.user,
action="更新产品及图片"
)
return redirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(form=form))
6. 内置通用视图高级定制
6.1 ListView高级定制
自定义列表视图的高级功能:
from django.views.generic import ListView
from django.db.models import Count, Avg, Q
class AdvancedProductListView(ListView):
model = Product
template_name = 'products/advanced_list.html'
context_object_name = 'products'
paginate_by = 15
paginate_orphans = 3 # 避免最后一页只有少量商品
def get_queryset(self):
queryset = super().get_queryset()
# 添加注解
queryset = queryset.annotate(
review_count=Count('reviews'),
avg_rating=Avg('reviews__rating')
)
# 搜索
search = self.request.GET.get('search', '')
if search:
queryset = queryset.filter(
Q(name__icontains=search) |
Q(description__icontains=search) |
Q(category__name__icontains=search)
)
# 类别过滤
category = self.request.GET.get('category')
if category:
queryset = queryset.filter(category__slug=category)
# 价格范围过滤
min_price = self.request.GET.get('min_price')
if min_price:
queryset = queryset.filter(price__gte=min_price)
max_price = self.request.GET.get('max_price')
if max_price:
queryset = queryset.filter(price__lte=max_price)
# 处理排序
sort = self.request.GET.get('sort', 'name')
direction = '-' if self.request.GET.get('dir') == 'desc' else ''
# 特殊排序字段处理
if sort == 'price':
queryset = queryset.order_by(f'{direction}price')
elif sort == 'rating':
queryset = queryset.order_by(f'{direction}avg_rating')
elif sort == 'popular':
queryset = queryset.order_by(f'{direction}review_count')
else: # 默认按名称排序
queryset = queryset.order_by(f'{direction}name')
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 添加过滤数据
context['categories'] = Category.objects.all()
context['selected_category'] = self.request.GET.get('category', '')
context['search'] = self.request.GET.get('search', '')
context['min_price'] = self.request.GET.get('min_price', '')
context['max_price'] = self.request.GET.get('max_price', '')
context['sort'] = self.request.GET.get('sort', 'name')
context['dir'] = self.request.GET.get('dir', 'asc')
# 保持过滤条件的分页链接
context['page_query'] = '&'.join([
f"{key}={value}"
for key, value in self.request.GET.items()
if key != 'page'
])
# 添加价格统计信息
context['price_stats'] = {
'min': Product.objects.aggregate(Min('price'))['price__min'],
'max': Product.objects.aggregate(Max('price'))['price__max'],
'avg': Product.objects.aggregate(Avg('price'))['price__avg']
}
return context
6.2 DetailView自定义
定制详情视图的高级功能:
from django.views.generic import DetailView
from django.db.models import Prefetch
class ProductDetailView(DetailView):
model = Product
template_name = 'products/product_detail.html'
context_object_name = 'product'
query_pk_and_slug = True # 同时使用pk和slug查询
pk_url_kwarg = 'id'
slug_url_kwarg = 'slug'
def get_queryset(self):
# 优化查询,减少数据库访问
return Product.objects.select_related(
'category', 'brand', 'manufacturer'
).prefetch_related(
Prefetch(
'reviews',
queryset=Review.objects.select_related('user').order_by('-created_at')
),
'tags',
'images'
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
product = self.object
# 添加相关商品
context['related_products'] = Product.objects.filter(
category=product.category
).exclude(
id=product.id
).order_by('?')[:4]
# 添加最近浏览记录
recent_views = self.request.session.get('recent_products', [])
if product.id not in recent_views:
recent_views.insert(0, product.id)
recent_views = recent_views[:5] # 保留最近5个
self.request.session['recent_products'] = recent_views
if recent_views:
context['recently_viewed'] = Product.objects.filter(
id__in=recent_views
).exclude(id=product.id)
# 添加评论表单
context['review_form'] = ReviewForm()
# 处理产品统计
product.view_count = F('view_count') + 1
product.save(update_fields=['view_count'])
return context
def post(self, request, *args, **kwargs):
"""处理评论提交"""
self.object = self.get_object()
form = ReviewForm(request.POST)
if form.is_valid() and request.user.is_authenticated:
review = form.save(commit=False)
review.product = self.object
review.user = request.user
review.save()
return redirect('product-detail', id=self.object.id, slug=self.object.slug)
context = self.get_context_data(review_form=form)
return self.render_to_response(context)
6.3 CreateView和UpdateView高级应用
定制创建和更新视图:
from django.views.generic import CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.db import transaction
class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
model = Product
form_class = ProductForm
template_name = 'products/product_form.html'
permission_required = 'products.add_product'
def get_initial(self):
"""设置初始值"""
initial = super().get_initial()
initial['created_by'] = self.request.user
# 复制现有产品数据
product_id = self.request.GET.get('clone')
if product_id:
try:
product = Product.objects.get(id=product_id)
initial['name'] = f"Copy of {product.name}"
initial['description'] = product.description
initial['price'] = product.price
initial['category'] = product.category
initial['brand'] = product.brand
except Product.DoesNotExist:
pass
return initial
def form_valid(self, form):
"""保存表单前处理"""
with transaction.atomic():
# 设置创建者
form.instance.created_by = self.request.user
# 保存产品
self.object = form.save()
# 处理标签
tags = self.request.POST.get('tags', '').split(',')
for tag_name in tags:
tag_name = tag_name.strip()
if tag_name:
tag, created = Tag.objects.get_or_create(name=tag_name)
self.object.tags.add(tag)
# 处理SKU生成
if not self.object.sku:
self.object.sku = f"P{self.object.id:06d}"
self.object.save(update_fields=['sku'])
# 记录活动日志
ActivityLog.objects.create(
user=self.request.user,
action=f"创建产品: {self.object.name}"
)
return super().form_valid(form)
class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
model = Product
form_class = ProductForm
template_name = 'products/product_form.html'
permission_required = 'products.change_product'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
# 传递额外参数给表单
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
with transaction.atomic():
# 添加修改者信息
form.instance.updated_by = self.request.user
# 处理标签
tags = self.request.POST.get('tags', '').split(',')
self.object = form.save()
# 清除现有标签
self.object.tags.clear()
# 添加新标签
for tag_name in tags:
tag_name = tag_name.strip()
if tag_name:
tag, created = Tag.objects.get_or_create(name=tag_name)
self.object.tags.add(tag)
# 记录版本历史
ProductHistory.objects.create(
product=self.object,
name=self.object.name,
description=self.object.description,
price=self.object.price,
category=self.object.category,
changed_by=self.request.user
)
# 记录活动日志
ActivityLog.objects.create(
user=self.request.user,
action=f"更新产品: {self.object.name}"
)
return super().form_valid(form)
7. 类视图的访问控制
7.1 用户认证和权限控制
控制视图的访问权限:
from django.contrib.auth.mixins import (
LoginRequiredMixin,
PermissionRequiredMixin,
UserPassesTestMixin
)
from django.views.generic import ListView, DetailView
class StaffProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = Product
template_name = 'products/staff_list.html'
context_object_name = 'products'
permission_required = 'products.view_product'
login_url = '/login/'
redirect_field_name = 'next'
# 可以要求多个权限
# permission_required = ('products.view_product', 'products.add_product')
# 可以设置检查方法
permission_required = 'products.view_product'
raise_exception = True # 无权限时引发403而不是重定向
def has_permission(self):
"""重写权限检查"""
user = self.request.user
if user.is_superuser:
return True
return super().has_permission()
class OwnerProductDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
model = Product
template_name = 'products/owner_detail.html'
def test_func(self):
"""确保用户是产品所有者"""
product = self.get_object()
return self.request.user == product.created_by
def handle_no_permission(self):
"""自定义无权限处理"""
if self.request.user.is_authenticated:
messages.error(self.request, "您无权查看此产品详情")
return redirect('product-list')
return super().handle_no_permission()
7.2 会话与Cookie处理
在类视图中处理会话和Cookie:
from django.views.generic import View
from django.http import HttpResponse
class LastVisitedView(View):
def get(self, request, *args, **kwargs):
# 获取会话数据
last_visit = request.session.get('last_visit', 'First Visit')
visit_count = request.session.get('visit_count', 0)
# 更新会话数据
request.session['last_visit'] = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
request.session['visit_count'] = visit_count + 1
# 设置有效期
request.session.set_expiry(3600) # 1小时后过期
# 获取Cookie
theme = request.COOKIES.get('theme', 'light')
# 准备响应
response = HttpResponse(
f"Last visit: {last_visit}<br>"
f"Visit count: {visit_count}<br>"
f"Current theme: {theme}"
)
# 设置Cookie
if 'set_theme' in request.GET:
new_theme = request.GET['set_theme']
response.set_cookie('theme', new_theme, max_age=30*24*60*60) # 30天
return response
7.3 视图访问限流
使用Django Rest Framework风格的限流装饰器:
from django.core.cache import cache
from django.http import HttpResponse
from functools import wraps
from django.views.generic import View
def rate_limit(limit, period):
"""限流装饰器,limit为请求次数,period为时间段(秒)"""
def decorator(view_func):
@wraps(view_func)
def wrapped_view(self, request, *args, **kwargs):
# 使用IP作为键
key = f"ratelimit:{request.META['REMOTE_ADDR']}:{view_func.__name__}"
# 获取当前计数
requests = cache.get(key, 0)
if requests >= limit:
return HttpResponse("请求过于频繁,请稍后再试", status=429)
# 更新计数
cache.set(key, requests + 1, period)
return view_func(self, request, *args, **kwargs)
return wrapped_view
return decorator
class APIView(View):
@rate_limit(limit=5, period=60)
def get(self, request, *args, **kwargs):
# 每分钟最多5个请求
return JsonResponse({'message': 'API response'})
@rate_limit(limit=3, period=60)
def post(self, request, *args, **kwargs):
# 每分钟最多3个请求
return JsonResponse({'message': 'Data received'})
8. 高级路由与URL模式
8.1 视图类装饰器应用
为类视图应用装饰器:
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import ListView
# 方法1:使用method_decorator
@method_decorator(cache_page(60 * 15), name='dispatch') # 缓存15分钟
class CachedProductListView(ListView):
model = Product
template_name = 'products/cached_list.html'
# 方法2:在类内部使用method_decorator
class ApiProductView(View):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
products = Product.objects.all()
data = [{'id': p.id, 'name': p.name} for p in products]
return JsonResponse(data, safe=False)
def post(self, request, *args, **kwargs):
# 处理POST请求...
pass
# 方法3:使用多个装饰器
@method_decorator(cache_page(60 * 5), name='get') # 只缓存GET请求
@method_decorator(csrf_exempt, name='dispatch')
class ProductApiView(View):
def get(self, request, *args, **kwargs):
# GET请求会被缓存
return JsonResponse(...)
def post(self, request, *args, **kwargs):
# POST请求不会被缓存
return JsonResponse(...)
8.2 URL参数处理
处理URL中的查询参数:
from django.views.generic import ListView
from django.http import Http404
class CategoryProductView(ListView):
model = Product
template_name = 'products/category_list.html'
context_object_name = 'products'
paginate_by = 20
def get_queryset(self):
queryset = super().get_queryset()
# 获取URL参数
self.category_slug = self.kwargs.get('category_slug')
# 验证分类存在
try:
self.category = Category.objects.get(slug=self.category_slug)
except Category.DoesNotExist:
raise Http404("分类不存在")
# 根据分类过滤
return queryset.filter(category=self.category)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['category'] = self.category
# 添加子分类
context['subcategories'] = Category.objects.filter(parent=self.category)
# 添加过滤选项
context['min_price'] = self.request.GET.get('min_price', '')
context['max_price'] = self.request.GET.get('max_price', '')
# 构建查询字符串
query_dict = self.request.GET.copy()
if 'page' in query_dict:
del query_dict['page']
context['query_string'] = query_dict.urlencode()
return context
8.3 路径转换器与正则表达式
使用高级URL模式匹配:
# urls.py
from django.urls import path, re_path, register_converter
from . import views, converters
# 注册自定义路径转换器
register_converter(converters.YearConverter, 'year')
register_converter(converters.MonthConverter, 'month')
urlpatterns = [
# 基本路径转换器
path('products/<int:pk>/', views.ProductDetailView.as_view(), name='product-detail'),
path('products/<slug:slug>/', views.ProductBySlugView.as_view(), name='product-by-slug'),
# 自定义路径转换器
path('archive/<year:year>/<month:month>/', views.ProductArchiveView.as_view(), name='product-archive'),
# 正则表达式路径
re_path(r'^products/(?P<sku>[A-Z]{2}\d{6})/$', views.ProductBySKUView.as_view(), name='product-by-sku'),
# 可选参数正则表达式
re_path(r'^search/(?:page-(?P<page>\d+)/)?$', views.ProductSearchView.as_view(), name='product-search'),
]
# converters.py
class YearConverter:
regex = '[0-9]{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return f'{value:04d}'
class MonthConverter:
regex = '(0?[1-9]|1[0-2])'
def to_python(self, value):
return int(value)
def to_url(self, value):
return f'{value:02d}'
# views.py
class ProductArchiveView(ListView):
model = Product
template_name = 'products/archive.html'
def get_queryset(self):
year = self.kwargs['year']
month = self.kwargs['month']
# 根据年月筛选产品
return Product.objects.filter(
created_at__year=year,
created_at__month=month
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['year'] = self.kwargs['year']
context['month'] = self.kwargs['month']
return context
class ProductBySKUView(DetailView):
model = Product
template_name = 'products/detail.html'
def get_object(self):
sku = self.kwargs['sku']
return get_object_or_404(Product, sku=sku)
9. 视图组合与逻辑复用
9.1 组合多个类视图
将多个视图逻辑组合在一起:
from django.views.generic import View, DetailView, FormView
from django.views.generic.detail import SingleObjectMixin
class ProductDisplay(DetailView):
model = Product
template_name = 'products/product_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = ReviewForm()
return context
class ProductReview(SingleObjectMixin, FormView):
model = Product
form_class = ReviewForm
template_name = 'products/product_detail.html'
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def form_valid(self, form):
review = form.save(commit=False)
review.product = self.object
review.user = self.request.user
review.save()
return super().form_valid(form)
def get_success_url(self):
return reverse('product-detail', kwargs={'pk': self.object.pk})
class ProductDetailView(View):
"""组合显示和表单提交功能的视图"""
def get(self, request, *args, **kwargs):
view = ProductDisplay.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = ProductReview.as_view()
return view(request, *args, **kwargs)
9.2 创建可复用视图组件
创建可在多个视图间共享的组件:
class FilterableListMixin:
"""添加过滤功能的通用Mixin"""
def get_queryset(self):
queryset = super().get_queryset()
# 处理过滤参数
category = self.request.GET.get('category')
if category:
queryset = queryset.filter(category__slug=category)
min_price = self.request.GET.get('min_price')
if min_price:
queryset = queryset.filter(price__gte=min_price)
max_price = self.request.GET.get('max_price')
if max_price:
queryset = queryset.filter(price__lte=max_price)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
context['filter_params'] = {
'category': self.request.GET.get('category', ''),
'min_price': self.request.GET.get('min_price', ''),
'max_price': self.request.GET.get('max_price', '')
}
return context
class OrderableListMixin:
"""添加排序功能的通用Mixin"""
def get_queryset(self):
queryset = super().get_queryset()
# 处理排序参数
sort = self.request.GET.get('sort')
if sort:
direction = '-' if self.request.GET.get('dir') == 'desc' else ''
queryset = queryset.order_by(f'{direction}{sort}')
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['sort'] = self.request.GET.get('sort', '')
context['direction'] = self.request.GET.get('dir', 'asc')
return context
class SearchableListMixin:
"""添加搜索功能的通用Mixin"""
search_fields = ['name', 'description'] # 默认搜索字段
def get_queryset(self):
queryset = super().get_queryset()
# 处理搜索
search = self.request.GET.get('search')
if search:
search_filter = Q()
for field in self.search_fields:
search_filter |= Q(**{f'{field}__icontains': search})
queryset = queryset.filter(search_filter)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search'] = self.request.GET.get('search', '')
return context
# 使用组合的Mixin创建完整视图
class ProductListView(
LoginRequiredMixin,
FilterableListMixin,
OrderableListMixin,
SearchableListMixin,
ListView
):
model = Product
template_name = 'products/list.html'
paginate_by = 20
search_fields = ['name', 'description', 'sku', 'category__name']
# 其他自定义逻辑...
9.3 通过继承构建视图层次结构
创建视图层次结构:
# 基础视图
class BaseView(View):
"""所有视图的基类"""
def dispatch(self, request, *args, **kwargs):
# 请求开始计时
self.request_start_time = time.time()
# 添加通用跟踪ID
self.request_id = str(uuid.uuid4())
# 执行请求处理
response = super().dispatch(request, *args, **kwargs)
# 请求结束计时
request_time = time.time() - self.request_start_time
# 添加性能指标
response['X-Request-Id'] = self.request_id
response['X-Request-Time'] = f"{request_time:.4f}s"
return response
# 基本模板视图
class BaseTemplateView(BaseView, TemplateView):
"""提供基本模板功能"""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['site_name'] = 'Django Shop'
context['current_year'] = datetime.now().year
return context
# 列表基础视图
class BaseListView(BaseTemplateView, ListView):
"""所有列表视图的基类"""
paginate_by = 20
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_range'] = self.get_page_range()
return context
def get_page_range(self):
"""生成分页器使用的页码范围"""
page_obj = context.get('page_obj')
if not page_obj:
return []
current = page_obj.number
total = page_obj.paginator.num_pages
# 显示当前页附近的5个页码
return range(max(1, current - 2), min(total + 1, current + 3))
# 具体页面视图
class ProductListView(FilterableListMixin, OrderableListMixin, BaseListView):
"""产品列表页"""
model = Product
template_name = 'products/list.html'
context_object_name = 'products'
class CategoryProductListView(ProductListView):
"""分类产品列表页"""
def get_queryset(self):
queryset = super().get_queryset()
self.category = get_object_or_404(Category, slug=self.kwargs['slug'])
return queryset.filter(category=self.category)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['category'] = self.category
return context
10. AJAX与JSON响应处理
10.1 基于类的AJAX视图
创建处理AJAX请求的视图:
import json
from django.http import JsonResponse
from django.views.generic import View
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
@method_decorator(csrf_exempt, name='dispatch')
class AjaxProductView(View):
"""处理产品AJAX操作的基本视图"""
def json_response(self, data, status=200):
"""创建JSON响应"""
return JsonResponse(data, status=status, safe=False)
def get_json_data(self):
"""从请求获取JSON数据"""
try:
return json.loads(self.request.body)
except json.JSONDecodeError:
return {}
def get(self, request, *args, **kwargs):
"""获取产品列表"""
product_id = kwargs.get('pk')
if product_id:
# 获取单个产品
try:
product = Product.objects.get(pk=product_id)
data = {
'id': product.id,
'name': product.name,
'price': str(product.price),
'stock': product.stock,
'category': product.category.name if product.category else None
}
return self.json_response(data)
except Product.DoesNotExist:
return self.json_response({'error': '产品不存在'}, status=404)
else:
# 获取产品列表
products = Product.objects.all()[:20] # 限制返回数量
data = [{
'id': p.id,
'name': p.name,
'price': str(p.price)
} for p in products]
return self.json_response(data)
def post(self, request, *args, **kwargs):
"""创建产品"""
if not request.user.has_perm('products.add_product'):
return self.json_response({'error': '没有权限'}, status=403)
data = self.get_json_data()
form = ProductForm(data)
if form.is_valid():
product = form.save()
return self.json_response({
'id': product.id,
'name': product.name,
'message': '产品已创建'
}, status=201)
else:
return self.json_response({'errors': form.errors}, status=400)
def put(self, request, *args, **kwargs):
"""更新产品"""
if not request.user.has_perm('products.change_product'):
return self.json_response({'error': '没有权限'}, status=403)
product_id = kwargs.get('pk')
if not product_id:
return self.json_response({'error': '产品ID必须提供'}, status=400)
try:
product = Product.objects.get(pk=product_id)
except Product.DoesNotExist:
return self.json_response({'error': '产品不存在'}, status=404)
data = self.get_json_data()
form = ProductForm(data, instance=product)
if form.is_valid():
product = form.save()
return self.json_response({
'id': product.id,
'name': product.name,
'message': '产品已更新'
})
else:
return self.json_response({'errors': form.errors}, status=400)
def delete(self, request, *args, **kwargs):
"""删除产品"""
if not request.user.has_perm('products.delete_product'):
return self.json_response({'error': '没有权限'}, status=403)
product_id = kwargs.get('pk')
if not product_id:
return self.json_response({'error': '产品ID必须提供'}, status=400)
try:
product = Product.objects.get(pk=product_id)
product_name = product.name
product.delete()
return self.json_response({
'message': f'产品 {product_name} 已删除'
})
except Product.DoesNotExist:
return self.json_response({'error': '产品不存在'}, status=404)
10.2 处理JSON数据提交
处理JSON数据提交:
from django.views.generic import View
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
class CartApiView(View):
"""处理购物车的JSON API"""
@method_decorator(ensure_csrf_cookie)
def get(self, request, *args, **kwargs):
"""获取购物车内容"""
cart = request.session.get('cart', [])
# 加载购物车产品详情
cart_items = []
for item in cart:
try:
product = Product.objects.get(id=item['product_id'])
cart_items.append({
'id': item['id'],
'product_id': product.id,
'name': product.name,
'price': str(product.price),
'quantity': item['quantity'],
'total': str(product.price * item['quantity'])
})
except Product.DoesNotExist:
pass
cart_total = sum(float(item['total']) for item in cart_items)
return JsonResponse({
'items': cart_items,
'total': str(cart_total),
'count': len(cart_items)
})
@method_decorator(csrf_protect)
def post(self, request, *args, **kwargs):
"""添加商品到购物车"""
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': '无效的JSON数据'}, status=400)
product_id = data.get('product_id')
quantity = int(data.get('quantity', 1))
if not product_id:
return JsonResponse({'error': '必须提供product_id'}, status=400)
try:
product = Product.objects.get(id=product_id)
except Product.DoesNotExist:
return JsonResponse({'error': '产品不存在'}, status=404)
# 获取当前购物车
cart = request.session.get('cart', [])
# 生成唯一项目ID
item_id = str(uuid.uuid4())
# 添加商品
cart.append({
'id': item_id,
'product_id': product_id,
'quantity': quantity
})
# 更新会话
request.session['cart'] = cart
return JsonResponse({
'message': f'已添加 {product.name} 到购物车',
'item_id': item_id,
'cart_count': len(cart)
}, status=201)
@method_decorator(csrf_protect)
def put(self, request, *args, **kwargs):
"""更新购物车项目"""
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': '无效的JSON数据'}, status=400)
item_id = data.get('item_id')
quantity = int(data.get('quantity', 1))
if not item_id:
return JsonResponse({'error': '必须提供item_id'}, status=400)
# 获取当前购物车
cart = request.session.get('cart', [])
# 查找并更新项目
item_updated = False
for item in cart:
if item['id'] == item_id:
item['quantity'] = quantity
item_updated = True
break
if not item_updated:
return JsonResponse({'error': '购物车项目不存在'}, status=404)
# 更新会话
request.session['cart'] = cart
return JsonResponse({
'message': '购物车已更新',
'cart_count': len(cart)
})
@method_decorator(csrf_protect)
def delete(self, request, *args, **kwargs):
"""删除购物车项目"""
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': '无效的JSON数据'}, status=400)
item_id = data.get('item_id')
if not item_id:
return JsonResponse({'error': '必须提供item_id'}, status=400)
# 获取当前购物车
cart = request.session.get('cart', [])
# 查找并删除项目
original_length = len(cart)
cart = [item for item in cart if item['id'] != item_id]
if len(cart) == original_length:
return JsonResponse({'error': '购物车项目不存在'}, status=404)
# 更新会话
request.session['cart'] = cart
return JsonResponse({
'message': '购物车项目已删除',
'cart_count': len(cart)
})
10.3 SPA后端API视图
创建单页应用的后端API视图:
from django.views.generic import View
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from django.contrib.auth import authenticate, login, logout
@method_decorator(ensure_csrf_cookie, name='dispatch')
class ApiBaseView(View):
"""SPA应用的API基础视图"""
def get_json_data(self):
"""从请求获取JSON数据"""
try:
return json.loads(self.request.body)
except json.JSONDecodeError:
return {}
def json_response(self, data, status=200):
"""返回标准格式的JSON响应"""
return JsonResponse(data, status=status)
class ApiAuthView(ApiBaseView):
"""认证API视图"""
def post(self, request, *args, **kwargs):
"""用户登录"""
data = self.get_json_data()
username = data.get('username')
password = data.get('password')
if not username or not password:
return self.json_response({
'success': False,
'errors': {'__all__': ['请提供用户名和密码']}
}, status=400)
user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
return self.json_response({
'success': True,
'user': {
'id': user.id,
'username': user.username,
'email': user.email,
'is_staff': user.is_staff
}
})
else:
return self.json_response({
'success': False,
'errors': {'__all__': ['用户名或密码错误']}
}, status=400)
def delete(self, request, *args, **kwargs):
"""用户登出"""
logout(request)
return self.json_response({
'success': True,
'message': '已成功退出登录'
})
class ApiProductView(ApiBaseView):
"""产品API视图"""
def get(self, request, *args, **kwargs):
"""获取产品列表或详情"""
product_id = kwargs.get('pk')
if product_id:
try:
product = Product.objects.get(pk=product_id)
return self.json_response({
'success': True,
'product': {
'id': product.id,
'name': product.name,
'price': str(product.price),
'description': product.description,
'category': {
'id': product.category.id,
'name': product.category.name
} if product.category else None,
'image_url': product.image.url if product.image else None
}
})
except Product.DoesNotExist:
return self.json_response({
'success': False,
'error': '产品不存在'
}, status=404)
# 处理分页和过滤
page = int(request.GET.get('page', 1))
page_size = int(request.GET.get('page_size', 10))
category = request.GET.get('category')
query = Product.objects.all()
if category:
query = query.filter(category__slug=category)
# 计算分页
total = query.count()
offset = (page - 1) * page_size
products = query[offset:offset + page_size]
return self.json_response({
'success': True,
'products': [{
'id': p.id,
'name': p.name,
'price': str(p.price),
'category': p.category.name if p.category else None,
'image_url': p.image.url if p.image else None
} for p in products],
'pagination': {
'page': page,
'page_size': page_size,
'total': total,
'pages': (total + page_size - 1) // page_size
}
})
11. 测试基于类的视图
11.1 单元测试CBV
为类视图编写单元测试:
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Product, Category
class ProductListViewTest(TestCase):
"""测试ProductListView"""
def setUp(self):
"""测试前创建数据"""
self.client = Client()
# 创建用户
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpassword'
)
# 创建分类
self.category = Category.objects.create(
name='Test Category',
slug='test-category'
)
# 创建产品
for i in range(15):
Product.objects.create(
name=f'Test Product {i}',
description=f'Description for product {i}',
price=10.00 + i,
category=self.category,
stock=100
)
def test_view_url_exists(self):
"""测试URL是否存在"""
response = self.client.get('/products/')
self.assertEqual(response.status_code, 200)
def test_view_url_accessible_by_name(self):
"""测试URL是否可通过名称访问"""
response = self.client.get(reverse('product-list'))
self.assertEqual(response.status_code, 200)
def test_view_uses_correct_template(self):
"""测试是否使用正确的模板"""
response = self.client.get(reverse('product-list'))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'products/product_list.html')
def test_pagination_is_ten(self):
"""测试分页功能"""
response = self.client.get(reverse('product-list'))
self.assertEqual(response.status_code, 200)
self.assertTrue('products' in response.context)
self.assertTrue('is_paginated' in response.context)
self.assertTrue(response.context['is_paginated'])
self.assertEqual(len(response.context['products']), 10)
def test_second_page(self):
"""测试第二页内容"""
response = self.client.get(reverse('product-list') + '?page=2')
self.assertEqual(response.status_code, 200)
self.assertTrue('products' in response.context)
self.assertEqual(len(response.context['products']), 5)
def test_category_filter(self):
"""测试分类过滤功能"""
url = reverse('product-list') + f'?category={self.category.slug}'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context['products']), 10)
for product in response.context['products']:
self.assertEqual(product.category, self.category)
class ProductDetailViewTest(TestCase):
"""测试ProductDetailView"""
def setUp(self):
# 类似的设置...
pass
def test_view_detail(self):
"""测试产品详情页"""
product = Product.objects.first()
response = self.client.get(reverse('product-detail', args=[product.id]))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['product'], product)
11.2 使用RequestFactory测试
使用RequestFactory进行更精确的测试:
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User, AnonymousUser
from django.contrib.messages.storage.fallback import FallbackStorage
from .views import ProductCreateView, ProductUpdateView, ProductDeleteView
class ProductViewsTest(TestCase):
"""测试产品管理视图"""
def setUp(self):
self.factory = RequestFactory()
# 创建用户
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpassword'
)
# 创建管理员用户
self.admin_user = User.objects.create_user(
username='adminuser',
email='admin@example.com',
password='adminpassword',
is_staff=True
)
# 赋予权限
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
content_type = ContentType.objects.get_for_model(Product)
add_permission = Permission.objects.get(
codename='add_product',
content_type=content_type
)
change_permission = Permission.objects.get(
codename='change_product',
content_type=content_type
)
delete_permission = Permission.objects.get(
codename='delete_product',
content_type=content_type
)
self.admin_user.user_permissions.add(add_permission)
self.admin_user.user_permissions.add(change_permission)
self.admin_user.user_permissions.add(delete_permission)
# 创建测试数据
self.category = Category.objects.create(name='Test Category')
self.product = Product.objects.create(
name='Test Product',
description='Test Description',
price=19.99,
category=self.category,
stock=100
)
def setup_request(self, url, user=None, method='get', data=None):
"""设置请求对象"""
if method == 'get':
request = self.factory.get(url)
elif method == 'post':
request = self.factory.post(url, data=data)
# 添加用户到请求
request.user = user or AnonymousUser()
# 设置会话
request.session = {}
# 设置消息存储
setattr(request, '_messages', FallbackStorage(request))
return request
def test_create_view_unauthenticated(self):
"""测试未认证用户访问产品创建页面"""
request = self.setup_request('/products/create/')
response = ProductCreateView.as_view()(request)
self.assertEqual(response.status_code, 302) # 重定向到登录页面
self.assertTrue(response.url.startswith('/login/'))
def test_create_view_without_permission(self):
"""测试无权限用户访问产品创建页面"""
request = self.setup_request('/products/create/', user=self.user)
response = ProductCreateView.as_view()(request)
self.assertEqual(response.status_code, 403) # 权限被拒绝
def test_create_view_with_permission(self):
"""测试有权限用户访问产品创建页面"""
request = self.setup_request('/products/create/', user=self.admin_user)
response = ProductCreateView.as_view()(request)
self.assertEqual(response.status_code, 200) # 显示表单
self.assertIn('form', response.context_data)
def test_create_product(self):
"""测试创建产品"""
data = {
'name': 'New Product',
'description': 'New Description',
'price': 29.99,
'category': self.category.id,
'stock': 50
}
request = self.setup_request(
'/products/create/',
user=self.admin_user,
method='post',
data=data
)
response = ProductCreateView.as_view()(request)
# 检查是否创建成功并重定向
self.assertEqual(response.status_code, 302)
# 检查产品是否实际创建
self.assertTrue(Product.objects.filter(name='New Product').exists())
11.3 模拟与打补丁
使用mock进行更高级的测试:
from django.test import TestCase
from django.contrib.auth.models import User
from django.urls import reverse
from unittest.mock import patch, MagicMock
from .views import OrderCreateView
class OrderCreateViewTest(TestCase):
"""测试订单创建视图"""
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpassword'
)
self.client.login(username='testuser', password='testpassword')
# 创建产品
self.product = Product.objects.create(
name='Test Product',
price=19.99,
stock=100
)
@patch('myapp.views.payment_gateway.create_payment')
def test_order_creation(self, mock_create_payment):
"""测试订单创建过程"""
# 配置mock
mock_create_payment.return_value = {
'payment_id': 'test_payment_123',
'status': 'pending',
'redirect_url': 'https://payment.example.com/test'
}
# 提交订单表单
response = self.client.post(reverse('create-order'), {
'product_id': self.product.id,
'quantity': 2,
'shipping_address': '123 Test St',
'payment_method': 'credit_card'
})
# 确保重定向到支付页面
self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.startswith('https://payment.example.com/test'))
# 验证支付网关API被正确调用
mock_create_payment.assert_called_once()
args, kwargs = mock_create_payment.call_args
self.assertEqual(kwargs['amount'], 39.98) # 2 * 19.99
# 检查订单是否创建
self.assertTrue(Order.objects.filter(user=self.user).exists())
order = Order.objects.get(user=self.user)
self.assertEqual(order.total_amount, 39.98)
self.assertEqual(order.status, 'pending')
# 检查库存是否更新
self.product.refresh_from_db()
self.assertEqual(self.product.stock, 98) # 100 - 2
12. 最佳实践与性能优化
12.1 CBV的性能优化技巧
提高类视图性能的方法:
from django.views.generic import ListView
from django.utils.functional import cached_property
from django.db.models import Prefetch
class OptimizedProductListView(ListView):
model = Product
template_name = 'products/optimized_list.html'
context_object_name = 'products'
paginate_by = 20
def get_queryset(self):
"""优化查询集"""
queryset = super().get_queryset().select_related(
'category', 'brand' # 减少外键查询
).prefetch_related(
Prefetch(
'reviews',
queryset=Review.objects.select_related('user').order_by('-created_at')[:3],
to_attr='recent_reviews'
),
'tags'
).defer(
'long_description', # 推迟加载大字段
'metadata',
'seo_keywords'
)
return queryset
@cached_property
def categories(self):
"""缓存分类列表"""
return list(Category.objects.all())
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = self.categories
# 一次性获取并处理额外数据
product_ids = [p.id for p in context['products']]
if product_ids:
# 获取库存状态
stock_status = dict(
ProductInventory.objects.filter(
product_id__in=product_ids
).values_list('product_id', 'status')
)
# 获取价格信息
price_info = dict(
ProductPrice.objects.filter(
product_id__in=product_ids
).values_list('product_id', 'current_price')
)
# 为产品添加额外数据
for product in context['products']:
product.stock_status = stock_status.get(product.id, 'unknown')
product.current_price = price_info.get(product.id, product.price)
return context
12.2 缓存策略
为视图添加缓存:
from django.views.generic import DetailView
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from django.core.cache import cache
@method_decorator(cache_page(60 * 15), name='dispatch') # 缓存15分钟
class CachedCategoryView(DetailView):
model = Category
template_name = 'products/category.html'
class SmartCachedProductView(DetailView):
model = Product
template_name = 'products/detail.html'
def get_object(self, queryset=None):
"""使用自定义缓存策略获取对象"""
# 尝试从缓存获取
slug = self.kwargs.get('slug')
cache_key = f'product_{slug}'
product = cache.get(cache_key)
if product is None:
# 缓存未命中,从数据库获取
product = super().get_object(queryset)
# 存入缓存,1小时有效
cache.set(cache_key, product, 60 * 60)
return product
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 为活跃数据使用短期缓存
product = self.object
reviews_cache_key = f'product_{product.id}_reviews'
reviews = cache.get(reviews_cache_key)
if reviews is None:
reviews = list(product.reviews.all()[:10])
cache.set(reviews_cache_key, reviews, 60 * 5) # 5分钟缓存
context['reviews'] = reviews
# 增加计数器无需重新缓存主对象
Product.objects.filter(id=product.id).update(view_count=F('view_count') + 1)
return context
12.3 CBV最佳实践总结
基于类的视图最佳实践:
-
保持视图精简:每个视图应只负责一个功能,避免复杂逻辑
-
合理使用继承:创建基类和Mixin,避免代码重复
-
使用描述性命名:视图和方法名称应清晰描述其功能
-
重用而非复制:使用Mixin和组合,避免复制代码
-
分离展示与业务逻辑:将业务逻辑移至模型和服务类
-
性能优化:使用select_related、prefetch_related和缓存
-
适当组织文件:大型应用应按功能拆分视图文件
-
权限控制至上:始终检查用户权限,即使在前端已有限制
-
妥善处理异常:捕获并友好地处理所有可能的异常
-
编写全面测试:为所有视图编写单元测试和集成测试
-
保持向后兼容:更新视图时考虑现有客户端的兼容性
-
文档化特殊行为:记录任何非标准或复杂的视图行为
总结
Django基于类的视图(CBV)提供了一种强大的方式来组织和重用Web应用的代码。它们利用面向对象编程的优势,允许开发者通过继承和Mixin创建可维护、可扩展的代码库。虽然CBV的学习曲线比函数视图更陡峭,但掌握它们将显著提高你的生产力和代码质量。
从基础的View类到复杂的通用视图,从简单的模板渲染到复杂的表单处理,Django的CBV系统几乎可以满足任何Web应用开发的需求。通过本文介绍的12个关键技巧,你已经掌握了从基础到高级的CBV知识,可以在实际项目中利用它们创建更优雅、更高效的Django应用。
在你的下一个Django项目中,考虑选择基于类的视图作为默认方案,尤其是对于需要重复使用相似功能的场景。当然,函数视图在某些简单场景下仍然有其位置,关键是根据项目需求选择最合适的工具。
在下一篇文章中,我们将深入探讨Django中间件开发与应用,敬请期待!