Django如何合并差异极小的视图函数并改写为类视图
重复视图合并与类视图改造方案
核心思路
两个视图95%以上逻辑完全一致,差异仅为4处可配置项:模板路径、关联的业务Info模型、User反向关联该模型的查询前缀、对应筛选字段名。只要把这几个差异点抽成可传入的参数,公共逻辑只写一次即可,后续新增同类型视图也不需要重复写代码。
一、函数视图合并实现
先把公共逻辑抽成一个内部复用的工具函数,差异部分通过参数传入,原来的两个视图只需要传对应配置就能用。
公共逻辑封装
from django.db.models import Q from django.core.paginator import Paginator from django.http import HttpResponseForbidden from collections import Counter def _gen_manager_dashboard_data(request, template_path, info_model, rel_key): """ 生成经理仪表盘所需的模板和上下文 :param rel_key: User关联Info模型的反向查询名,即scientificinfo/religiousinfo """ context = {} # 补全原代码缺失的权限拦截:非经理角色直接返回403 if request.user.role != 'manager': return HttpResponseForbidden(), None # 基础用户查询 users = User.objects.filter( role='applicant', isPreRegistered=True, **{f'{rel_key}__is_interviewed': True} ).order_by('-date_joined') approved_query = info_model.objects.filter(is_approved__exact='0') context['all_users'] = users.count() # 替换原len(users),避免全量加载数据提升性能 context['approved'] = approved_query.count() context['survey_choices'] = SURVEY_CHOICES # 新用户筛选 if request.GET.get('user', 'all') == 'new': users = users.filter(**{f'{rel_key}__is_approved__exact': '0'}) # 以下筛选逻辑两个视图完全一致,直接复用 # 研究领域筛选 field_list = request.GET.getlist('field') if field_list: if 'None' in field_list: users = users.filter(fields__title=None) | users.filter(fields__title__in=field_list) else: users = users.filter(fields__title__in=field_list) # 性别筛选 gender_list = request.GET.getlist('gender') if gender_list: users = users.filter(gender__in=gender_list) # 学历筛选 education_list = request.GET.getlist('education') if education_list: users = users.filter(educationalinfo__grade__in=education_list) # 工作职位筛选 work_list = request.GET.getlist('work') if work_list: users = users.filter(workinfo__position__in=work_list) # 所在省份筛选 province_list = request.GET.getlist('province') if province_list: if 'None' in province_list: users = users.filter(prevaddress__address_province__in=province_list) | users.filter( prevaddress__address_province=None) else: users = users.filter(prevaddress__address_province__in=province_list) # 关键词搜索 query_string = request.GET.get('query_string') if query_string: name_query = None for term in query_string.split(): if name_query: name_query = name_query & (Q(first_name__contains=term) | Q(last_name__contains=term)) else: name_query = Q(first_name__contains=term) | Q(last_name__contains=term) users = users.filter(name_query | Q(educationalinfo__field__contains=query_string) | Q(educationalinfo__tendency__contains=query_string) | Q(educationalinfo__university_name__contains=query_string) | Q(workinfo__organization__contains=query_string) | Q(ngoinfo__ngo_name__contains=query_string) | Q(melli_code__contains=query_string)) users = users.distinct() # 筛选项统计 context['grade_choices'] = [] for g, grade in EDUCATIONAL_GRADE_CHOICES: count = EducationalInfo.objects.filter(user__in=users, grade=g).count() context['grade_choices'].append((g, grade, count)) context['work_position_choices'] = [] for p, position in WORK_POSITION_CHOICES: count = WorkInfo.objects.filter(user__in=users, position=p).count() context['work_position_choices'].append((p, position, count)) provinces = users.values_list('prevaddress__address_province') provinces = Counter([d[0] for d in provinces]) context['provinces'] = provinces.items() # 分页处理 paginator = Paginator(users, 25) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) context['users'] = page_obj # 顶部统计卡片数据 context['is_interviewed'] = info_model.objects.filter(is_approved__exact='0', user__is_staff=False).count() context['not_interviewed'] = info_model.objects.filter(is_interviewed=True, user__is_staff=False).count() context['men'] = users.filter(gender="male").count() context['women'] = users.filter(gender="female").count() context['query_string'] = query_string return loader.get_template(template_path), context
简化后的原视图
原来两个几十行的视图现在只需要3行配置即可,后续改公共逻辑只需要改上面的工具函数,两个视图同步生效:
def manager_scientific(request, *args, **kwargs): template, context = _gen_manager_dashboard_data( request, template_path='reg/scientific-manager-dashboard.html', info_model=ScientificInfo, rel_key='scientificinfo' ) return template if context is None else HttpResponse(template.render(request=request, context=context)) def manager_religious(request, *args, **kwargs): template, context = _gen_manager_dashboard_data( request, template_path='reg/religious-manager-dashboard.html', info_model=ReligiousInfo, rel_key='religiousinfo' ) return template if context is None else HttpResponse(template.render(request=request, context=context))
二、基于类的视图改造
如果要改成类视图,推荐继承Django内置的TemplateView实现,把公共逻辑写到基类,差异项定义为类属性,扩展新视图比函数版更方便。
基类实现
from django.views.generic import TemplateView class BaseManagerDashboard(TemplateView): # 公共默认配置 paginate_by = 25 # 子类必须覆盖的配置项,默认留空 info_model = None rel_key = None def dispatch(self, request, *args, **kwargs): # 权限校验 if request.user.role != 'manager': return HttpResponseForbidden() # 校验子类是否配置了必填参数 assert self.info_model is not None, "必须配置info_model属性" assert self.rel_key is not None, "必须配置rel_key属性" return super().dispatch(request, *args, **kwargs) def _get_filtered_users(self): """封装用户筛选逻辑""" users = User.objects.filter( role='applicant', isPreRegistered=True, **{f'{self.rel_key}__is_interviewed': True} ).order_by('-date_joined') # 新用户筛选 if self.request.GET.get('user', 'all') == 'new': users = users.filter(**{f'{self.rel_key}__is_approved__exact': '0'}) # 通用筛选逻辑和函数版一致 field_list = self.request.GET.getlist('field') if field_list: if 'None' in field_list: users = users.filter(fields__title=None) | users.filter(fields__title__in=field_list) else: users = users.filter(fields__title__in=field_list) gender_list = self.request.GET.getlist('gender') if gender_list: users = users.filter(gender__in=gender_list) education_list = self.request.GET.getlist('education') if education_list: users = users.filter(educationalinfo__grade__in=education_list) work_list = self.request.GET.getlist('work') if work_list: users = users.filter(workinfo__position__in=work_list) province_list = self.request.GET.getlist('province') if province_list: if 'None' in province_list: users = users.filter(prevaddress__address_province__in=province_list) | users.filter( prevaddress__address_province=None) else: users = users.filter(prevaddress__address_province__in=province_list) query_string = self.request.GET.get('query_string') if query_string: name_query = None for term in query_string.split(): if name_query: name_query = name_query & (Q(first_name__contains=term) | Q(last_name__contains=term)) else: name_query = Q(first_name__contains=term) | Q(last_name__contains=term) users = users.filter(name_query | Q(educationalinfo__field__contains=query_string) | Q(educationalinfo__tendency__contains=query_string) | Q(educationalinfo__university_name__contains=query_string) | Q(workinfo__organization__contains=query_string) | Q(ngoinfo__ngo_name__contains=query_string) | Q(melli_code__contains=query_string)) return users.distinct(), query_string def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) users, query_string = self._get_filtered_users() context['all_users'] = users.count() context['approved'] = self.info_model.objects.filter(is_approved__exact='0').count() context['survey_choices'] = SURVEY_CHOICES # 筛选项统计 context['grade_choices'] = [] for g, grade in EDUCATIONAL_GRADE_CHOICES: count = EducationalInfo.objects.filter(user__in=users, grade=g).count() context['grade_choices'].append((g, grade, count)) context['work_position_choices'] = [] for p, position in WORK_POSITION_CHOICES: count = WorkInfo.objects.filter(user__in=users, position=p).count() context['work_position_choices'].append((p, position, count)) provinces = users.values_list('prevaddress__address_province') provinces = Counter([d[0] for d in provinces]) context['provinces'] = provinces.items() # 分页 paginator = Paginator(users, self.paginate_by) page_obj = paginator.get_page(self.request.GET.get('page', 1)) context['users'] = page_obj # 统计数据 context['is_interviewed'] = self.info_model.objects.filter(is_approved__exact='0', user__is_staff=False).count() context['not_interviewed'] = self.info_model.objects.filter(is_interviewed=True, user__is_staff=False).count() context['men'] = users.filter(gender="male").count() context['women'] = users.filter(gender="female").count() context['query_string'] = query_string return context
具体业务视图实现
两个业务视图只需要配置3个类属性即可,不需要写任何业务逻辑:
class ScientificManagerDashboard(BaseManagerDashboard): template_name = 'reg/scientific-manager-dashboard.html' info_model = ScientificInfo rel_key = 'scientificinfo' class ReligiousManagerDashboard(BaseManagerDashboard): template_name = 'reg/religious-manager-dashboard.html' info_model = ReligiousInfo rel_key = 'religiousinfo'
路由配置的时候直接调用as_view()即可:
urlpatterns = [ path('manager/scientific/', ScientificManagerDashboard.as_view(), name='manager_scientific'), path('manager/religious/', ReligiousManagerDashboard.as_view(), name='manager_religious'), ]
优化点说明
- 修复了原代码非经理角色访问时返回空模板的问题,统一返回403
- 把原代码中
len(users)的统计方式替换为users.count(),避免全量加载用户数据到内存,大幅提升大数量下的接口性能 - 所有动态关联字段用字典解包的方式传入查询,避免硬编码
内容的提问来源于stack exchange,提问作者Sam
相关产品推荐
相关产品推荐

