stark组件(11):组合搜索
效果图:
新增函数和类
Option 获取字段的对象或元组
SearchGroupRow 封装数据,展示到前端
get_search_group 获取组合搜索的字段
get_search_group_condition 获取组合搜索的筛选条件
一、stark组件
import functools from types import FunctionType from django import forms from django.db.models import Q from django.db.models import ForeignKey, ManyToManyField from django.http import QueryDict from django.urls import path from django.utils.safestring import mark_safe from django.shortcuts import HttpResponse, render, reverse, redirect from stark.utils.pagination import Pagination class StarkModelForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(StarkModelForm, self).__init__(*args, **kwargs) # 统一给ModelForm生成字段添加样式 for name, field in self.fields.items(): field.widget.attrs['class'] = 'form-control' class SearchGroupRow(object): def __init__(self, title, queryset_or_tutple, option, query_dict): """ :param title: 组合搜索的列名称 :param queryset_or_tutple:组合搜索关联获取到的数据 :param 配置,Option的对象 :param query_dict : request.GET """ self.title = title self.queryset_or_tutple = queryset_or_tutple self.option = option self.query_dict = query_dict def __iter__(self): yield '<div class="whole">' yield self.title + ':' yield '</div>' yield '<div class="others">' total_query_dict = self.query_dict.copy() total_query_dict._mutable = True origin_value_list = self.query_dict.getlist(self.option.field) if not origin_value_list: # 比如说:没有传递gender,那么就还是当前的url。不能写成井号,因为如果传递了department,gender的全部的url应该是?department=x yield f'<a class="active" href="?{total_query_dict.urlencode()}">全部</a>' else: # 如果全部没有被选中,那么全部的url就是移除掉被选中字段后的url total_query_dict.pop(self.option.field) yield f'<a href="?{total_query_dict.urlencode()}">全部</a>' for item in self.queryset_or_tutple: text = self.option.get_text(item) value = str(self.option.get_values(item)) # 需要request.GET # 获取组合搜索按钮文本背后对应的值 # QueryDict = {gender: ['1', ], depart: ['2', ]} gender=1&depart=2 # print(self.query_dict) query_dict = self.query_dict.copy() query_dict._mutable = True # 如果url有参数,比如?gender=1,query_dict就会带着这个参数。下面给query_dict赋值的时候,如果字段是gender会把gender覆盖掉。 # 如果字段不是gender,就会变成加上这个字段,那部门来举例就会变成?gender=1&department=1 if not self.option.is_multi: query_dict[self.option.field] = value if value in origin_value_list: query_dict.pop(self.option.field) # gender=x移除,此url就会编程href="?",当再次点击的时候就等于取消选择了 yield f'<a class="active" href="?{query_dict.urlencode()}">{text}</a>' else: yield f'<a href="?{query_dict.urlencode()}">{text}</a>' else: multi_value_list = query_dict.getlist(self.option.field) if value in multi_value_list: multi_value_list.remove(value) # 我们操作的是multi_value_list,query_dict并没有被修改,所以要把multi_value_dict的值赋给query_dict query_dict.setlist(self.option.field, multi_value_list) yield f'<a class="active" href="?{query_dict.urlencode()}">{text}</a>' else: multi_value_list.append(value) query_dict.setlist(self.option.field, multi_value_list) yield f'<a href="?{query_dict.urlencode()}">{text}</a>' yield '</div>' class Option(object): def __init__(self, field, is_multi=False, db_condition=None, text_func=None, value_func=None): """ :param field: 组合搜索关联的字段 :param is_multi: 是否支持多选 :param db_condition: 数据库关联查询时的条件 :param text_func: 此函数用于显示组合搜索的按钮页面文本 :param value_func: 此函数用于显示组合搜索的按钮值 """ self.field = field self.is_multi = is_multi if not db_condition: db_condition = {} self.db_condition = db_condition self.text_func = text_func self.value_func = value_func self.is_choice = False def get_db_condition(self, request, *args, **kwargs): """ 获取筛选条件 :param request: :param args: :param kwargs: :return: """ return self.db_condition def get_queryset_or_tuple(self, model_class, request, *args, **kwargs): """ 根据字段去获取数据库关联的数据 :param model_class: :param request: :param args: :param kwargs: :return: """ # 去自己对应的Model类中找到字段对象,例:web.UserInfo.department,web.UserInfo.gender filed_object_or_tuple = model_class._meta.get_field(self.field) title = filed_object_or_tuple.verbose_name if isinstance(filed_object_or_tuple, ForeignKey) or isinstance(filed_object_or_tuple, ManyToManyField): # FK和M2M,应该去获取其关联表中的数据 # django2用field_obj.related_model来获取model # django1用field_obj.rel.model来获取model db_condition = self.get_db_condition(request, *args, **kwargs) return SearchGroupRow(title, filed_object_or_tuple.related_model.objects.filter(**db_condition), self, request.GET) else: # 获取choice中的数据 self.is_choice = True return SearchGroupRow(title, filed_object_or_tuple.choices, self, request.GET) def get_text(self, filed_object_or_tuple): """ 获取文本函数 :param filed_object: :return: """ if self.text_func: return self.text_func(filed_object_or_tuple) if self.is_choice: return filed_object_or_tuple[1] return str(filed_object_or_tuple) def get_values(self, filed_object_or_tuple): """ 获取文本的值 :param filed_object_or_tuple: :return: """ if self.value_func: return self.value_func(filed_object_or_tuple) if self.is_choice: return filed_object_or_tuple[0] return filed_object_or_tuple.pk def get_choice_text(title, field): """ 对于Stark组件中定义列时,choice如果想要显示中文信息,调用此方法即可。 :param title: 希望页面显示的表头 :param field: 字段名称 :return: """ def inner(self, obj=None, is_header=None, *args, **kwargs): if is_header: return title method = f"get_{field}_display" return getattr(obj, method)() # GENDER_CHOICES = ((MALE, '男'),(FEMALE, '女'),) # 对于choice字段,如果想获取获取第二个值,可以通过:对象.get_字段名_display() return inner class StarkHandler(object): list_display = [] order_list = [] search_list = [] search_group = [] per_page_data = 10 has_add_btn = True model_form_class = None list_template = None add_template = None edit_template = None delete_template = None action_list = [] def __init__(self, site, model_class, prev): self.site = site self.model_class = model_class self.prev = prev self.request = None def display_checkbox(self, obj=None, is_header=None, *args, **kwargs): """ 复选框 :param obj: :param is_header: :param args: :param kwargs: :return: """ if is_header: return '选择' return mark_safe(f'<input type="checkbox" name="pk" value="{obj.pk}" />') def display_edit(self, obj=None, is_header=None, *args, **kwargs): """ 自定义页面显示的列(表头和内容) :param obj: :param is_header: :return: """ if is_header: return '编辑' name = f'{self.site.namespace}:{self.get_edit_url_name}' return mark_safe(f'<a href="{reverse(name, args=(obj.pk,))}">编辑</a>') def display_delete(self, obj=None, is_header=None, *args, **kwargs): if is_header: return '删除' name = f'{self.site.namespace}:{self.get_delete_url_name}' return mark_safe(f'<a href="{reverse(name, args=(obj.pk,))}">删除</a>') def get_list_display(self, request, *args, **kwargs): """ 获取页面上应该显示的列,预留的自定义扩展,例如:以后根据用户的不同显示不同的列 :return: """ value = [] value.extend(self.list_display) return value def get_search_list(self): return self.search_list def get_add_btn(self, *args, **kwargs): if self.has_add_btn: return f'<a class="btn btn-primary" href="{self.reverse_add_url(*args, **kwargs)}">添加</a>' def get_model_form_class(self, request, *args, **kwargs): if self.model_form_class: return self.model_form_class class DynamicModelForm(StarkModelForm): class Meta: model = self.model_class fields = '__all__' return DynamicModelForm def get_order_list(self): return self.order_list or ['-id', ] def get_action_list(self): return self.action_list def action_multi_delete(self, request, *args, **kwargs): """ 批量删除(如果想要定制执行成功后的返回值,那么就为action函数设置返回值即可) :param request: :param args: :param kwargs: :return: """ pk_list = request.POST.getlist('pk') self.model_class.objects.filter(id__in=pk_list).delete() action_multi_delete.text = '批量删除' def get_search_group(self): return self.search_group def get_search_group_condition(self, request): """ 获取组合搜索的条件 :param request: :return: """ condition = {} for option in self.get_search_group(): if option.is_multi: values_list = request.GET.getlist(option.field) if not values_list: continue condition[f'{option.field}__in'] = values_list else: values = request.GET.get(option.field) if not values: continue condition[f'{option.field}'] = values return condition def list_view(self, request, *args, **kwargs): """ 列表页面 :param request: :return: """ # 1. 处理Action action_list = self.get_action_list() action_dict = {func.__name__: func.text for func in action_list} if request.method == 'POST': action_func_name = request.POST.get('action') if action_func_name and action_func_name in action_dict: action_response = getattr(self, action_func_name)(request, *args, **kwargs) if action_response: return action_response # 2. 处理搜索 # 搜索列表写ORM语句,如:['name__contains','email__contains','id__gt','gender'] search_list = self.get_search_list() search_value = request.GET.get('q', '') conn = Q() conn.connector = 'OR' # 通过or链接 if search_value: for item in search_list: conn.children.append((item, search_value)) # conn.children.append('name__contains','张三') # 3. 获取排序 order_list = self.get_order_list() search_group_condition = self.get_search_group_condition(request) # 获取组合搜索的条件 queryset = self.model_class.objects.filter(conn).filter(**search_group_condition).order_by(*order_list) # 4. 分页处理 all_count = queryset.count() query_params = request.GET.copy() # 深copy query_params._mutable = True # query_params默认不可修改 pager = Pagination( current_page=request.GET.get('page'), all_count=all_count, base_url=request.path_info, query_params=query_params, per_page_data=self.per_page_data, ) data_list = queryset[pager.start:pager.end] # 5. 处理表格 list_display = self.get_list_display(request, *args, **kwargs) # 会优先调用UserInfoHandler里的get_list_display()方法。 # 5.1 处理表格的表头 header_list = [] if list_display: for field_or_func in list_display: if isinstance(field_or_func, FunctionType): verbose_name = field_or_func(self, obj=None, is_header=True, *args, **kwargs) else: verbose_name = self.model_class._meta.get_field(field_or_func).verbose_name header_list.append(verbose_name) else: header_list.append(self.model_class._meta.model_name) # 如果用户没有填写list_display,就显示表名 # 5.2 处理表的内容 body_list = [] for obj in data_list: tr_list = [] if list_display: for field_or_func in list_display: if isinstance(field_or_func, FunctionType): tr_list.append(field_or_func(self, obj, is_header=False, *args, **kwargs)) else: tr_list.append(getattr(obj, field_or_func)) else: tr_list.append(obj) # 如果用户没有填写list_display,就显示表对象,所以表类要定义__str__方法 body_list.append(tr_list) # 6. 添加按钮 add_btn = self.get_add_btn(*args, **kwargs) # 7. 组合搜索 search_group_row_list = [] # 放的是SearchGroupRow的对象 search_group = self.get_search_group() for option_object in search_group: queryset_or_tuple = option_object.get_queryset_or_tuple(self.model_class, request, *args, **kwargs) search_group_row_list.append(queryset_or_tuple) context = { 'data_list': data_list, 'header_list': header_list, 'body_list': body_list, 'pager': pager, 'add_btn': add_btn, 'search_list': search_list, 'search_value': search_value, 'action_dict': action_dict, 'search_group_row_list': search_group_row_list, } return render(request, self.list_template or 'stark/data_list.html', context) def save(self, form, is_update=False, *args, **kwargs): """ 在使用ModelForm保存数据之前预留的钩子方法 :param form: :param is_update: :return: """ form.save() def add_view(self, request, *args, **kwargs): """ 添加页面 :param request: :return: """ model_form_class = self.get_model_form_class(request, *args, **kwargs) if request.method == 'GET': form = model_form_class() return render(request, 'stark/change.html', {'form': form}) form = model_form_class(data=request.POST) if form.is_valid(): self.save(form, False, *args, **kwargs) # 在数据库保存成功后,跳转回列表页面(携带原来的参数)。 return redirect(self.reverse_list_url(*args, **kwargs)) return render(request, self.add_template or 'stark/change.html', {'form': form}) def edit_view(self, request, pk, *args, **kwargs): """ 编辑页面 :param request: :return: """ current_edit_object = self.model_class.objects.filter(pk=pk).first() if not current_edit_object: return HttpResponse('要修改的数据不存在,请重新选择') model_form_class = self.get_model_form_class(request, *args, **kwargs) if request.method == 'GET': form = model_form_class(instance=current_edit_object) return render(request, 'stark/change.html', {'form': form}) form = self.model_form_class(data=request.POST, instance=current_edit_object) if form.is_valid: self.save(form, True, *args, **kwargs) # 在数据库保存成功后,跳转回列表页面(携带原来的参数) return redirect(self.reverse_list_url(*args, **kwargs)) return render(request, 'stark/change.html', {'form': form}) def delete_view(self, request, pk, *args, **kwargs): """ 删除页面 :param request: :param pk: :return: """ original_list_url = self.reverse_list_url(*args, **kwargs) if request.method == 'GET': return render(request, 'stark/delete.html', {'cancel': original_list_url}) self.model_class.objects.filter(pk=pk).delete() return redirect(original_list_url) def get_url_name(self, params): app_label, model_name = self.model_class._meta.app_label, self.model_class._meta.model_name if self.prev: return f'{app_label}_{model_name}_{self.prev}_{params}' return f'{app_label}_{model_name}__{params}' @property def get_list_url_name(self): """ 获取列表页面URL的name :return: """ return self.get_url_name('list') @property def get_add_url_name(self): """ 获取添加页面URL的name :return: """ return self.get_url_name('add') @property def get_edit_url_name(self): """ 获取编辑页面URL的name :return: """ return self.get_url_name('edit') @property def get_delete_url_name(self): """ 获取删除页面URL的name :return: """ return self.get_url_name('delete') def reverse_common_url(self, name, *args, **kwargs): """ 生成带有原搜索条件的URL :param name: url :param args: :param kwargs: :return: """ name = f'{self.site.namespace}:{name}' base_url = reverse(name, args=args, kwargs=kwargs) if not self.request.GET: reverse_url = base_url else: params = self.request.GET.urlencode() new_query_dict = QueryDict(mutable=True) new_query_dict['_filter'] = params reverse_url = f'{base_url}?{new_query_dict.urlencode()}' return reverse_url def reverse_add_url(self, *args, **kwargs): """ 带有原搜索条件的增加URL :param args: :param kwargs: :return: """ return self.reverse_common_url(self.get_add_url_name, *args, **kwargs) def reverse_edit_url(self, *args, **kwargs): """ 带有原搜索条件的编辑URL :param args: :param kwargs: :return: """ return self.reverse_common_url(self.get_edit_url_name, *args, **kwargs) def reverse_delete_url(self, *args, **kwargs): """ 带有原搜索条件的删除URL :param args: :param kwargs: :return: """ return self.reverse_common_url(self.get_delete_url_name, *args, **kwargs) def reverse_list_url(self, *args, **kwargs): name = f'{self.site.namespace}:{self.get_list_url_name}' base_url = reverse(name, args=args, kwargs=kwargs) params = self.request.GET.get('_filter') if not params: return base_url return f'{base_url}?{params}' def wrapper(self, func): """ 当每一个request请求进来的时候,把request赋值给类的数据属性self.request :param func: request请求对应的视图函数 :return: """ @functools.wraps(func) # 保留原函数的原信息,写装饰器建议写上这个。 def inner(request, *args, **kwargs): self.request = request return func(request, *args, **kwargs) return inner def get_urls(self): patterns = [ path('list/', self.wrapper(self.list_view), name=self.get_list_url_name), path('add/', self.wrapper(self.add_view), name=self.get_add_url_name), path('edit/<int:pk>/', self.wrapper(self.edit_view), name=self.get_edit_url_name), path('delete/<int:pk>/', self.wrapper(self.delete_view), name=self.get_delete_url_name), ] patterns.extend(self.extra_urls()) return patterns def extra_urls(self): return [] class StarkSite(object): def __init__(self): self._registry = [] self.app_name = 'stark' self.namespace = 'stark' def register(self, model_class, handler_class=None, prev=None): """ :param model_class: 是models中的数据库表对应的类。 :param handler_class: 处理请求的视图函数所在的类 :param prev: 生成URL的前缀 :return: """ if not handler_class: handler_class = StarkHandler self._registry.append( {'model_class': model_class, 'handler': handler_class(self, model_class, prev), 'prev': prev}) def get_urls(self): patterns = [] for item in self._registry: model_class = item['model_class'] handler = item['handler'] prev = item['prev'] app_name, model_name = model_class._meta.app_label, model_class._meta.model_name if prev: patterns.append( path(f'{app_name}/{model_name}/{prev}/', (handler.get_urls(), None, None))) else: patterns.append( path(f'{app_name}/{model_name}/', (handler.get_urls(), None, None))) return patterns @property def urls(self): return self.get_urls(), self.app_name, self.namespace site = StarkSite()
二、业务代码
from stark.bin.core import (site, StarkHandler, StarkModelForm, get_choice_text, Option) from web import models class UserInfoModelForm(StarkModelForm): class Meta: model = models.UserInfo fields = ['name', 'gender', 'classes', 'age', 'email'] class DepartmentHandler(StarkHandler): list_display = ['title'] class UserInfoHandler(StarkHandler): per_page_data = 5 order_list = ['gender'] model_form_class = UserInfoModelForm search_list = ['name__contains', 'email__contains', ] action_list = [StarkHandler.action_multi_delete, ] search_group = [Option('gender', is_multi=True, ), Option('department', text_func=lambda x: x.title ), ] list_display = [ StarkHandler.display_checkbox, 'name', get_choice_text('性别', 'gender', ), get_choice_text('班级', 'classes'), 'age', 'email', 'department', StarkHandler.display_edit, StarkHandler.display_delete, ] def save(self, form, is_update=False, *args, **kwargs): form.instance.department_id = 1 form.save() site.register(models.Department, DepartmentHandler) site.register(models.UserInfo, UserInfoHandler)
三、模板渲染
{% extends 'layout.html' %} {% block css %} <link rel="stylesheet" href=""> {% endblock css %} {% block content %} <div class="custom-container"> <!-- 组合搜索 --> {% if search_group_row_list %} <div class="panel panel-default"> <div class="panel-heading"> <i class="fa fa-filter" aria-hidden="true"></i> 快速筛选 </div> <div class="panel-body"> <div class="search-group"> {% for row in search_group_row_list %} <div class="row"> {% for obj in row %} {{ obj|safe }} {% endfor %} </div> {% endfor %} </div> </div> </div> {% endif %} <!-- 组合搜搜结束 --> <!-- 搜索 --> {% if search_list %} <div class="up-down-space right"> <form method="get" class="form-inline"> <div class="form-group"> <input class="form-control" type="text" name="q" value="{{ search_value }}" placeholder="关键字搜搜"> <button class="btn btn-primary" type="submit"> <i class="fa fa-search" aria-hidden="true"></i> </button> </div> </form> </div> {% endif %} <!-- 搜索结束 --> <!-- 批量操作 --> <form method="post" class="form-inline"> {% csrf_token %} {% if action_dict %} <div class="left up-down-space"> <div class="form-group"> <select class="form-control" name="action"> <option value="">请选择操作</option> {% for func_name,func_text in action_dict.items %} <option value="{{ func_name }}">{{ func_text }}</option> {% endfor %} </select> <input class="btn btn-primary" type="submit" value="执行"/> </div> </div> {% endif %} <!-- 批量操作结束 --> <!-- 添加按钮开始 --> {% if add_btn %} <div class="up-down-space left add_btn"> {{ add_btn|safe }} </div> {% endif %} <!-- 添加按钮结束 --> <table class="table table-bordered"> <thead> <tr> {% for item in header_list %} <th>{{ item }}</th> {% endfor %} </tr> </thead> <tbody> {% for row in body_list %} <tr> {% for ele in row %} <td>{{ ele }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </form> <nav> <ul class="pagination"> {{ pager.page_html|safe }} </ul> </nav> </div> {% endblock content %}
四、css
.search-group { padding: 5px 10px; } .search-group .row .whole { min-width: 40px; float: left; display: inline-block; padding: 5px 0 5px 8px; margin: 3px; font-weight: bold; } .search-group .row .others { padding-left: 60px; } .search-group .row a { display: inline-block; padding: 5px 8px; margin: 3px; border: 1px solid #d4d4d4; } .search-group .row a { display: inline-block; padding: 5px 8px; margin: 3px; border: 1px solid #d4d4d4; } .search-group a.active { color: #fff; background-color: #337ab7; border-color: #2e6da4; }