Django之Stark组件开发小记
一. 简介
网站后台无非就是对数据库表的增删改查。对于频繁的写url,写增删改查功能,写此组件帮助我们快速完成URL的编写和对表的增删改查。
功能预览:
二 . 前戏
1.路由分发的本质
include函数返回有三个元素的元组:(urls文件对象,app_name,namespace)
这个urls文件对象其实就是urls.patterns那个列表。
2. 单例模式
其实python中导入模块就是一个典型的单例模式,同一个项目中不同文件中,导入相同的模块对象时,其实都是同一个对象。
3.Django项目运行前,执行固定的文件
在项目的app文件:
from django.apps import AppConfig from django.utils.module_loading import autodiscover_modules class StarkConfig(AppConfig): name = 'stark' def ready(self): autodiscover_modules('stark')
在各个app中就可以创建stark文件,执行相关功能了。在此项目中,主要是为了生成url。
三. 自动生成URL
注意:path的路径
class StarkSite: def __init__(self): self._registry = [] self.namespace = 'stark' self.app_name = 'stark' def register(self, model_class, handler_class=None, prev=None): 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_label, model_name = model_class._meta.app_label, model_class._meta.model_name if prev: patterns.append( path('%s/%s/%s/' % (app_label, model_name, prev), (handler.get_urls(), None, None)) ) else: patterns.append( path('%s/%s/' % (app_label, model_name), (handler.get_urls(), None, None)) ) return patterns @property def urls(self): return self.get_urls(), self.app_name, self.namespace # 这就是include返回的三个元组 site = StarkSite()
通过一个装饰器,把每个试图函数的request赋值给self, 用于其他地方的使用。
def wrapper(self, func):
@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('change/<int:pk>/', self.wrapper(self.change_view), name=self.get_change_url_name), path('delete/<int:pk>/', self.wrapper(self.delete_view), name=self.get_delete_url_name), ] patterns.extend(self.extra_url()) return patterns
name属性是拼接出来,通过model_class._meta.app_label等。
def get_url_name(self, param): app_label, model_name = self.model_class._meta.app_label, self.model_class._meta.model_name if self.prev: return '%s_%s_%s_%s' % (app_label, model_name, self.prev, param) return '%s_%s_%s' % (app_label, model_name, param) @property def get_list_url_name(self): return self.get_url_name('list') @property def get_add_url_name(self): return self.get_url_name('add') @property def get_change_url_name(self): return self.get_url_name('change') @property def get_delete_url_name(self): return self.get_url_name('delete')
反向生成带参数的URL:
注意:request.GET和request.POST获取到的值是一个QueryDict类型的字典,默认是不允许修改的,可以通过把_mutable属性值改为True就可以修改了。
def reverse_commen_url(self, name, *args, **kwargs): name = '%s:%s' % (self.site.namespace, name) base_url = reverse(name, args=args, kwargs=kwargs) if not self.request.GET: url = base_url else: param = self.request.GET.urlencode() query_dict = QueryDict(mutable=True) query_dict['_filter'] = param url = '%s?%s' % (base_url, query_dict.urlencode()) return url def reverse_add_url(self, *args, **kwargs): return self.reverse_commen_url(self.get_add_url_name, *args, **kwargs) def reverse_change_url(self, *args, **kwargs): return self.reverse_commen_url(self.get_change_url_name, *args, **kwargs) def reverse_delete_url(self, *args, **kwargs): return self.reverse_commen_url(self.get_delete_url_name, *args, **kwargs) def reverse_list_url(self, *args, **kwargs): name = '%s:%s' % (self.site.namespace, self.get_list_url_name) base_url = reverse(name, args=args, kwargs=kwargs) param = self.request.GET.get('_filter') if not param: return base_url return '%s?%s' % (base_url, param)
四. 自定列展示
注意:
key_or_func代表这个可以是一个字段,也可以是一个方法,用于显示m2m,一对多,choice等字典的值。
list_display = self.get_list_display(*args, **kwargs) # ['title',] header_list = [] if list_display: for key_or_func in list_display: if isinstance(key_or_func, FunctionType): verbose_name = key_or_func(self, obj=None, is_header=True) else: verbose_name = self.model_class._meta.get_field(key_or_func).verbose_name header_list.append(verbose_name) else: header_list.append(self.model_class._meta.model_name) # 处理表内容 body_list = [] for row in data_list: tr_list = [] if list_display: for key_or_func in list_display: if isinstance(key_or_func, FunctionType): tr_list.append(key_or_func(self, obj=row, is_header=False, *args, **kwargs)) else: tr_list.append(getattr(row, key_or_func)) else: tr_list.append(row) body_list.append(tr_list)
通过一个闭包的形式来实现。
自定义类:
list_display = ['title', StarkHandler.display_edit_del]
def get_choice_text(title, field): def inner(self, obj=None, is_header=None, *args, **kwargs): if is_header: return title method = 'get_%s_display' % field return getattr(obj, method)() return inner def get_m2m_text(title, field): def inner(self, obj=None, is_header=None, *args, **kwargs): if is_header: return title queryset = getattr(obj, field).all() text_list = [str(row) for row in queryset] return ','.join(text_list) return inner
def display_edit_del(self, obj=None, is_header=None, *args, **kwargs):
if is_header:
return '编辑|删除'
return mark_safe('<a href="%s">编辑</a>|<a href="%s">删除</a>' % (
self.reverse_change_url(pk=obj.pk), self.reverse_delete_url(pk=obj.pk)))
分页功能
代码:
""" 分页组件 """ class Pagination(object): def __init__(self, current_page, all_count, base_url, query_params, per_page=20, pager_page_count=11): """ 分页初始化 :param current_page: 当前页码 :param per_page: 每页显示数据条数 :param all_count: 数据库中总条数 :param base_url: 基础URL :param query_params: QueryDict对象,内部含所有当前URL的原条件 :param pager_page_count: 页面上最多显示的页码数量 """ self.base_url = base_url try: self.current_page = int(current_page) if self.current_page <= 0: raise Exception() except Exception as e: self.current_page = 1 self.query_params = query_params self.per_page = per_page self.all_count = all_count self.pager_page_count = pager_page_count pager_count, b = divmod(all_count, per_page) if b != 0: pager_count += 1 self.pager_count = pager_count half_pager_page_count = int(pager_page_count / 2) self.half_pager_page_count = half_pager_page_count @property def start(self): """ 数据获取值起始索引 :return: """ return (self.current_page - 1) * self.per_page @property def end(self): """ 数据获取值结束索引 :return: """ return self.current_page * self.per_page def page_html(self): """ 生成HTML页码 :return: """ # 如果数据总页码pager_count<11 pager_page_count if self.pager_count < self.pager_page_count: pager_start = 1 pager_end = self.pager_count else: # 数据页码已经超过11 # 判断: 如果当前页 <= 5 half_pager_page_count if self.current_page <= self.half_pager_page_count: pager_start = 1 pager_end = self.pager_page_count else: # 如果: 当前页+5 > 总页码 if (self.current_page + self.half_pager_page_count) > self.pager_count: pager_end = self.pager_count pager_start = self.pager_count - self.pager_page_count + 1 else: pager_start = self.current_page - self.half_pager_page_count pager_end = self.current_page + self.half_pager_page_count page_list = [] if self.current_page <= 1: prev = '<li><a href="#">上一页</a></li>' else: self.query_params['page'] = self.current_page - 1 prev = '<li><a href="%s?%s">上一页</a></li>' % (self.base_url, self.query_params.urlencode()) page_list.append(prev) for i in range(pager_start, pager_end + 1): self.query_params['page'] = i if self.current_page == i: tpl = '<li class="active"><a href="%s?%s">%s</a></li>' % ( self.base_url, self.query_params.urlencode(), i,) else: tpl = '<li><a href="%s?%s">%s</a></li>' % (self.base_url, self.query_params.urlencode(), i,) page_list.append(tpl) if self.current_page >= self.pager_count: nex = '<li><a href="#">下一页</a></li>' else: self.query_params['page'] = self.current_page + 1 nex = '<li><a href="%s?%s">下一页</a></li>' % (self.base_url, self.query_params.urlencode(),) page_list.append(nex) page_str = "".join(page_list) return page_str
all_count = queryset.count() query_param = request.GET.copy() query_param._mutable = True pager = Pagination( current_page=request.GET.get('page'), all_count=all_count, base_url=request.path_info, query_params=query_param, per_page=self.per_page_count, ) data_list = queryset[pager.start:pager.end]
五. 增删改查
class StarkModelForm(forms.ModelForm): # 给每一个字段定制样式
def __init__(self, *args, **kwargs):
super(StarkModelForm, self).__init__(*args, **kwargs)
for name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
########添加###### has_btn = True def get_add_btn(self, *args, **kwargs): if self.has_btn: return mark_safe('<a href="%s" class="btn btn-primary">添加</a>' % self.reverse_add_url(*args, **kwargs)) model_class_form = None def get_model_form(self, is_add, request, *args, **kwargs): if self.model_class_form: return self.model_class_form class Dynanmic_model_form(StarkModelForm): class Meta: model = self.model_class fields = '__all__' return Dynanmic_model_form
比较简单,通过modelform来实现。
def save(self, request, form, is_update=None, *args, **kwargs): form.save() def add_view(self, request, *args, **kwargs): model_form_class = self.get_model_form(True, 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(): response = self.save(request, form, is_update=False, *args, **kwargs) return response or redirect(self.reverse_list_url(*args, **kwargs)) all_error = form.errors.get('__all__') if all_error: all_error = all_error[0] return render(request, 'stark/change.html', {'form': form, 'all_error': all_error}) def get_change_object(self, request, pk, *args, **kwargs): return self.model_class.objects.filter(pk=pk).first() def change_view(self, request, pk, *args, **kwargs): model_form_class = self.get_model_form(False, request, *args, **kwargs) current_obj = self.get_change_object(request, pk, *args, **kwargs) if not current_obj: return HttpResponse('修改的对象不存在!') if request.method == "GET": form = model_form_class(instance=current_obj) return render(request, 'stark/change.html', {'form': form}) form = model_form_class(instance=current_obj, data=request.POST) if form.is_valid(): response = self.save(request, form, is_update=True, *args, **kwargs) return response or redirect(self.reverse_list_url(*args, **kwargs)) all_error = form.errors.get('__all__') if all_error: all_error = all_error[0] return render(request, 'stark/change.html', {'form': form, 'all_error': all_error}) def delete_object(self, request, pk, *args, **kwargs): self.model_class.objects.filter(id=pk).delete() def delete_view(self, request, pk, *args, **kwargs): origin_url = self.reverse_list_url(*args, **kwargs) if request.method == 'GET': return render(request, 'stark/delete.html', {'cancel': origin_url}) response = self.delete_object(request, pk, *args, **kwargs) return response or redirect(self.reverse_list_url(*args, **kwargs))
六. 排序和模糊搜索
注意:
1.可以对Django的Q对象(model中),进行拆分。
######## 排序##### order_list = [] def get_order_list(self): return self.order_list or ['-id'] ##############模糊搜索############ search_list = [] def get_search_list(self): return self.search_list ###########批量删除############
########################2. 模糊搜索################# search_list = self.get_search_list() search_value = request.GET.get('q', '') conn = Q() conn.connector = 'OR' if search_value: for item in search_list: conn.children.append((item, search_value)) ########################3. 排序#################### order_list = self.get_order_list() search_group_condition = self.get_search_group_condition() queryset = self.model_class.objects.filter(conn).order_by(*order_list) ######################## 4 分页 ####################
页面中:
{% if search_list %} <div style="margin: 5px 0;float: right"> <form class="form-inline" method="get"> <div class="form-group"> <input type="text" class="form-control" 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 %}
七. 批量操作
###########批量删除############
action_list = []
def get_action_list(self):
return self.action_list
def multi_delete(self, request, *args, **kwargs):
pk_list = request.POST.getlist('pk')
self.model_class.objects.filter(id__in=pk_list).delete()
multi_delete.text = '批量删除'
#######################1.批量操作################## action_list = self.get_action_list() action_func_name = request.POST.get('action') action_func_dict = {item.__name__: item.text for item in action_list} if action_func_name and action_func_name in action_func_dict: response = getattr(self, action_func_name)(request, *args, **kwargs) if response: return response
在表格的第一列定制一个checkbox
def display_checkbox(self, obj=None, is_header=None, *args, **kwargs): if is_header: return '操作' return mark_safe('<input type="checkbox" name="pk" value="%s">' % obj.pk)
页面:
<form method="post"> {% csrf_token %} {% if action_list %} <div style="float: left;margin: 5px"> <div class="form-inline"> <div class="form-group"> <select name="action" class="form-control"> <option>请选择操作</option> {% for func_name, func_text in action_func_dict.items %} <option value="{{ func_name }}">{{ func_text }}</option> {% endfor %} </select> <input type="submit" value="执行" class="btn btn-primary"> </div> </div> </div> {% endif %} {% if add_btn %} <div style="margin: 5px 0;float: left"> {{ add_btn|safe }} </div> {% endif %} <table class="table table-bordered table-striped"> <thead> <tr> {% for item in header_list %} <th>{{ item }}</th> {% endfor %} </tr> </thead> <tbody> {% for item in body_list %} <tr> {% for per in item %} <td>{{ per }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </form>
八. 组合搜索
search_group可以传一个option对象(field, db_condition=None,is_multi=None,text_func=None, value_func=None)
通过option去获取queryset或者是choice字段值。
通过option中的get_queryset_or_tuple方法,返回一个迭代器对象(SearchGroupRow),然后再网页中渲染。
一个类中,自己实现了__iter__方法,那么它的对象就是一个可迭代对象。
##############组合搜索############## search_group = [] def get_search_group(self): return self.search_group def get_search_group_condition(self): condition = {} for option_object in self.get_search_group(): if option_object.is_multi: pk_list = self.request.GET.getlist(option_object.field) if not pk_list: continue condition['%s__in' % option_object.field] = pk_list else: pk = self.request.GET.get(option_object.field) if not pk: continue condition[option_object.field] = pk print(condition) return condition
class starkhandler中:
##############组合搜索############### search_group_row_list = [] search_group = self.get_search_group() for option_object in search_group: row = option_object.get_queryset_or_tuple(self.model_class, request, *args, **kwargs) search_group_row_list.append(row)
class SearchGroupRow: def __init__(self, title, queryset_or_tuple, option, query_dict): self.title = title self.queryset_or_tuple = queryset_or_tuple 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_value_dict = self.query_dict.copy() total_value_dict._mutable = True origin_value_list = self.query_dict.getlist(self.option.field) if not origin_value_list: yield '<a href="?%s" class="active">全部</a>' % total_value_dict.urlencode() else: total_value_dict.pop(self.option.field) yield '<a href="?%s">全部</a>' % total_value_dict.urlencode() for item in self.queryset_or_tuple: text = self.option.get_text(item) value = str(self.option.get_value(item)) query_dict = self.query_dict.copy() query_dict._mutable = True if not self.option.is_multi: query_dict[self.option.field] = value if value in origin_value_list: query_dict.pop(self.option.field) yield '<a href="?%s" class="active">%s</a>' % (query_dict.urlencode(), text) else: yield '<a href="?%s">%s</a>' % (query_dict.urlencode(), text) else: multi_value_list = query_dict.getlist(self.option.field) if value in multi_value_list: multi_value_list.remove(value) query_dict.setlist(self.option.field, multi_value_list) yield '<a href="?%s" class="active">%s</a>' % (query_dict.urlencode(), text) else: multi_value_list.append(value) query_dict.setlist(self.option.field, multi_value_list) yield '<a href="?%s">%s</a>' % (query_dict.urlencode(), text) yield '</div>' class Option: def __init__(self, field, db_condition=None, is_multi=None, text_func=None, value_func=None): self.field = field if not db_condition: db_condition = {} self.db_condition = db_condition self.is_multi = is_multi self.text_func = text_func self.value_func = value_func self.is_choice = False def get_queryset_or_tuple(self, model_class, request, *args, **kwargs): field_object = model_class._meta.get_field(self.field) title = field_object.verbose_name db_condition = self.db_condition if isinstance(field_object, related.RelatedField): return SearchGroupRow(title, field_object.related_model.objects.all(), self, request.GET) else: self.is_choice = True return SearchGroupRow(title, field_object.choices, self, request.GET) def get_text(self, field_object): if self.text_func: return self.text_func(field_object) if self.is_choice: return field_object[1] return str(field_object) def get_value(self, field_object): if self.value_func: return self.value_func(field_object) if self.is_choice: return field_object[0] return field_object.pk
注意:
1. 一个对象,要是能通过for循环进行迭代的话,内部一定实现了__iter__方法。
2. 对QueryDict进行赋值,setlist是先清空在赋值。
3. 判断一个对象是不是m2m,12m字段:
from django.db.models.fields import related
f isinstance(field_object, related.RelatedField):
return SearchGroupRow(title, field_object.related_model.objects.all(), self, request.GET)
else:
self.is_choice = True
return SearchGroupRow(title, field_object.choices, self, request.GET)