05【终极攻略】Django基于类的视图:12个高级技巧打造完美Web应用

【终极攻略】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 视图处理流程

基于类的视图处理请求的标准流程:

  1. dispatch(): 确定使用哪个HTTP方法处理函数
  2. http_method_not_allowed(): 处理不允许的方法
  3. 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">&laquo; 首页</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 }}">末页 &raquo;</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最佳实践总结

基于类的视图最佳实践:

  1. 保持视图精简:每个视图应只负责一个功能,避免复杂逻辑

  2. 合理使用继承:创建基类和Mixin,避免代码重复

  3. 使用描述性命名:视图和方法名称应清晰描述其功能

  4. 重用而非复制:使用Mixin和组合,避免复制代码

  5. 分离展示与业务逻辑:将业务逻辑移至模型和服务类

  6. 性能优化:使用select_related、prefetch_related和缓存

  7. 适当组织文件:大型应用应按功能拆分视图文件

  8. 权限控制至上:始终检查用户权限,即使在前端已有限制

  9. 妥善处理异常:捕获并友好地处理所有可能的异常

  10. 编写全面测试:为所有视图编写单元测试和集成测试

  11. 保持向后兼容:更新视图时考虑现有客户端的兼容性

  12. 文档化特殊行为:记录任何非标准或复杂的视图行为

总结

Django基于类的视图(CBV)提供了一种强大的方式来组织和重用Web应用的代码。它们利用面向对象编程的优势,允许开发者通过继承和Mixin创建可维护、可扩展的代码库。虽然CBV的学习曲线比函数视图更陡峭,但掌握它们将显著提高你的生产力和代码质量。

从基础的View类到复杂的通用视图,从简单的模板渲染到复杂的表单处理,Django的CBV系统几乎可以满足任何Web应用开发的需求。通过本文介绍的12个关键技巧,你已经掌握了从基础到高级的CBV知识,可以在实际项目中利用它们创建更优雅、更高效的Django应用。

在你的下一个Django项目中,考虑选择基于类的视图作为默认方案,尤其是对于需要重复使用相似功能的场景。当然,函数视图在某些简单场景下仍然有其位置,关键是根据项目需求选择最合适的工具。

在下一篇文章中,我们将深入探讨Django中间件开发与应用,敬请期待!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Is code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值