自定义admin管理工具(stark组件)
创建项目
了解了admin的功能后,我们可以开始仿照admin编写我们自己的管理工具stark组件
首先创建一个新的项目,并创建三个app
stark就是我们要编写的组件,我们的主要逻辑就写在里面
在settings配置中分别注册这三个app
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'app01.apps.App01Config', 'app02.apps.App02Config', 'stark.apps.StarkConfig' ]
在app01和app02的models文件中创建数据类
# app01 from django.db import models # Create your models here. class Book(models.Model): title = models.CharField(max_length=32) def __str__(self): return self.title # app02 class Menu(models.Model): caption = models.CharField(max_length=32) def __str__(self): return self.caption class UserInfo(models.Model): name = models.CharField(max_length=32) pwd = models.CharField(max_length=32, default=123) email = models.EmailField() roles = models.ManyToManyField(to="Role") def __str__(self): return self.name class Role(models.Model): title = models.CharField(max_length=32) permissions = models.ManyToManyField(to="Permission") def __str__(self): return self.title class Permission(models.Model): url = models.CharField(max_length=32) title = models.CharField(max_length=32) permission_group = models.ForeignKey("PermissionGroup", default=1) code = models.CharField(max_length=32, default="") parent = models.ForeignKey("self", default=1, null=True, blank=True) def __str__(self): return self.title class PermissionGroup(models.Model): caption = models.CharField(max_length=32) menu = models.ForeignKey("Menu", default=1) def __str__(self): return self.caption
在app01和app02下分别创建一个stark.py文件
使用admin时,我们知道启动项目时,会扫描每个app下的admin.py文件并执行
def autodiscover(): autodiscover_modules('admin', register_to=site)
其实就是上面的方法实现了该功能,现在我们也仿照该方法,让项目启动时,扫描每个app下的stark.py文件并执行
在stark的apps.py中配置
from django.apps import AppConfig from django.utils.module_loading import autodiscover_modules class StarkConfig(AppConfig): name = 'stark' def ready(self): autodiscover_modules('stark')
这个ready函数的内容就实现了我们的需求
仿照admin设置相关类
首先创建下面的文件
在执行admin.py文件时我们发现其实第一步就是导入admin,导入时通过单例模式生成了一个site对象,现在我们也来写一个类,生成一个单例对象
class StarkSite(object): def __init__(self): self._registry = {} site = StarkSite()
在app01和app02的stark.py文件中导入
from stark.service.sites import site
这样我们也就得到了一个单例对象site,在注册时admin使用的是site对象的register方法,我们也学着他写一个register方法
class StarkSite(object): def __init__(self): self._registry = {} def register(self, model, model_config=None): if not model_config: model_config = ModelStark self._registry[model] = model_config(model, self)
这个方法的本质其实就是往self._registry这个字典中添加键值对,键就是我们的数据类(如Book类),值是一个类的对象,这个类就是我们要创建的第二个类,样式类
class ModelStark(object): def __init__(self, model, site): self.model = model self.site = site
通过这个类我们控制页面展示的内容和样式
做完这几步我们就可以在app01和app02的stark.py文件中开始注册了
# app01 from stark.service.sites import site from .models import * site.register(Book) # app02 from stark.service.sites import site from .models import * site.register(UserInfo) site.register(Role)
注册完成后,我们的site._registry字典中就有了我们注册类对应的键值对,接下来就要配置url了
url配置
admin中的url配置
from django.conf.urls import url from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), ]
可以看到所有的url都是在admin.site.urls这个方法中生成的,我可以看看这个方法的源码
@property def urls(self): return self.get_urls(), 'admin', self.name
其实就是做了一个分发,url是在self.get_urls()这个函数中生成的,接着看这个函数的主要代码
def get_urls(self): from django.conf.urls import url, include # Since this module gets imported in the application's root package, # it cannot import models from other applications at the module level, # and django.contrib.contenttypes.views imports ContentType. from django.contrib.contenttypes import views as contenttype_views def wrap(view, cacheable=False): def wrapper(*args, **kwargs): return self.admin_view(view, cacheable)(*args, **kwargs) wrapper.admin_site = self return update_wrapper(wrapper, view) # Admin-site-wide views. urlpatterns = [ url(r'^$', wrap(self.index), name='index'), url(r'^login/$', self.login, name='login'), url(r'^logout/$', wrap(self.logout), name='logout'), url(r'^password_change/$', wrap(self.password_change, cacheable=True), name='password_change'), url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True), name='password_change_done'), url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'), url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', wrap(contenttype_views.shortcut), name='view_on_site'), ] # Add in each model's views, and create a list of valid URLS for the # app_index valid_app_labels = [] for model, model_admin in self._registry.items(): urlpatterns += [ url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)), ] if model._meta.app_label not in valid_app_labels: valid_app_labels.append(model._meta.app_label) # If there were ModelAdmins registered, we should have a list of app # labels for which we need to allow access to the app_index view, if valid_app_labels: regex = r'^(?P<app_label>' + '|'.join(valid_app_labels) + ')/$' urlpatterns += [ url(regex, wrap(self.app_index), name='app_list'), ] return urlpatterns
这里我们需要知道到是我们生成的url的格式都是admin/app名/表名,所以我们要想办法取到app名和表名拼接起来
for model, model_admin in self._registry.items(): urlpatterns += [ url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)), ]
这里的model就是我们的数据类(如Book),如何通过他取到我们想要的呢
model._meta.app_label 取类所在的app名
model._meta.model_name 取类的名字
这样我们就成功拼接出了我们要的url,但是每个url下又有增删改查不同的url,这时又要再次进行分发,admin中使用了include方法,通过model_admin我们注册时样式类生成的对象下的url方法得到我们想要的
这个方法的内容如下
def get_urls(self): from django.conf.urls import url def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) wrapper.model_admin = self return update_wrapper(wrapper, view) info = self.model._meta.app_label, self.model._meta.model_name urlpatterns = [ url(r'^$', wrap(self.changelist_view), name='%s_%s_changelist' % info), url(r'^add/$', wrap(self.add_view), name='%s_%s_add' % info), url(r'^(.+)/history/$', wrap(self.history_view), name='%s_%s_history' % info), url(r'^(.+)/delete/$', wrap(self.delete_view), name='%s_%s_delete' % info), url(r'^(.+)/change/$', wrap(self.change_view), name='%s_%s_change' % info), # For backwards compatibility (was the change url before 1.9) url(r'^(.+)/$', wrap(RedirectView.as_view( pattern_name='%s:%s_%s_change' % ((self.admin_site.name,) + info) ))), ] return urlpatterns @property def urls(self): return self.get_urls()
其实和之前一样,只是做了又一次分发,并且对应了视图函数,这里我们先不看视图函数的内容,值得注意的是这一次的分发和视图函数都是写在样式类中的,而不是写在生成site的AdminStie类中
这样有什么好处呢,我们知道当我们要注册时,是可以自己定义一些属性的,其实要显示的页面也是可以自己定义的,所以讲这最后一层url分发和对应的函数写在样式类中可以方便我们进行自定义
看完了admin的做法,我们可以来写我们自己的代码了,首先在urls文件中配置
from django.conf.urls import url from django.contrib import admin from stark.service.sites import site urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^stark/', site.urls), ]
然后在我们创建的两个类中添加相关的代码,这里url对应的函数我们先简写
from django.conf.urls import url from django.shortcuts import HttpResponse, render class ModelStark(object): def __init__(self, model, site): self.model = model self.site = site def change_list(self, request): ret = self.model.objects.all() return render(request, "stark/change_list.html", locals()) def add_view(self, request): return HttpResponse("add_view") def del_view(self, request, id): return HttpResponse("del_view") def change_view(self, request, id): return HttpResponse("change_view") def get_url_func(self): temp = [] temp.append(url("^$", self.change_list)) temp.append(url("^add/$", self.add_view)) temp.append(url("^(\d+)/delete/$", self.del_view)) temp.append(url("^(\d+)/change/$", self.change_view)) return temp @property def urls(self): return self.get_url_func(), None, None class StarkSite(object): def __init__(self): self._registry = {} def register(self, model, model_config=None): if not model_config: model_config = ModelStark self._registry[model] = model_config(model, self) def get_urls(self): temp = [] for model, model_config in self._registry.items(): model_name = model._meta.model_name app_label = model._meta.app_label u = url("^%s/%s/" % (app_label, model_name), model_config.urls) temp.append(u) return temp @property def urls(self): return self.get_urls(), None, None site = StarkSite()
别名的使用
在设置url对应的视图函数时,我们可以给这个url添加一个别名,在使用时可以通过这个别名来反向生成url,这样即使url有修改,这样别名不变我们都不需要修改代码
增加别名时要注意,由于每个数据类我们都生成了增删改查4条url,所以在写别名时应该有些区别,不然会引起混淆,所以我们设计别名的格式为app名_表名_增
def get_url_func(self): temp = [] model_name = self.model._meta.model_name app_label = self.model._meta.app_label app_model = (app_label, model_name) temp.append(url("^$", self.change_list, name="%s_%s_list" % app_model)) temp.append(url("^add/$", self.add_view, name="%s_%s_add" % app_model)) temp.append(url("^(\d+)/delete/$", self.del_view, name="%s_%s_delete" % app_model)) temp.append(url("^(\d+)/change/$", self.change_view, name="%s_%s_change" % app_model)) return temp @property def urls(self): return self.get_url_func(), None, None
列表展示页面
url设计完成后,我们就需要来设计每个url对应的页面了,我们注意到,其实不管是访问哪张表,增删改查都只对应相同的四个视图函数,那么应该如何区分我们访问的表呢
在样式类ModelStark中,我们定义了self.model,这里的model其实就是我们访问表的数据类,通过他我们就能拿到我们需要的数据显示到页面上,访问不同的表时这个model是不同的,这时就做到了访问什么表显示什么表的内容
list_display
在使用admin时,默认给我们展示的是一个个的类对象,当我们想要看到其它内容时,可以通过list_display属性设置
from django.contrib import admin from .models import * # Register your models here. admin.site.register(UserInfo) class RoleConfig(admin.ModelAdmin): list_display = ["id", "title"] admin.site.register(Role, RoleConfig)
通过上面的方法,在访问admin页面时点击Role表就能看到id和title两个字段的内容了,现在我们也来仿照admin写一个list_display属性
首先,这个属性应该是可以自定制的,如果用户没有定制,那么他应该有一个默认值,所以我们可以在ModelStark样式类中先自己定义一个list_display静态属性
class ModelStark(object): list_display = [] def __init__(self, model, site): self.model = model self.site = site
如果用户需要定制他,可以在app对应的stark.py文件中做如下配置
class BookConfig(ModelStark): list_display = ["id", "title", "price"] site.register(Book, BookConfig)
这里我们写在list_display中的内容都是表中有的字段,其实里面还可以写我们自己定义的函数,用来将我们自己需要的内容显示到页面上
from stark.service.sites import site, ModelStark from .models import * from django.utils.safestring import mark_safe class BookConfig(ModelStark): def edit(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href='/stark/app01/book/%s/change'>编辑</a>" % obj.pk) def delete(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href='/stark/app01/book/%s/delete'>删除</a>" % obj.pk) list_display = ["id", "title", "price", edit, delete] site.register(Book, BookConfig) class AuthorConfig(ModelStark): list_display = ["name", "age"] site.register(Author)
这里我们增加了编辑和删除两个函数,可以看到他们的返回值是一个a标签,这样就可以在页面上显示一个可以点击的编辑和删除,这里的mark_safe和前端渲染时用的safe是一样的功能,可以使标签正确的显示在页面上
这样我们就可以让页面显示成下面的样子
当我们处理列表页面对应的函数时就可以拿到list_display的值,再通过self.model取到对应的数据对象,从对象中拿到我们想要的数据,放到页面上进行显示
class ModelStark(object): list_display = [] def __init__(self, model, site): self.model = model self.site = site def change_list(self, request): # 生成表标头 header_list = [] for field in self.list_display: if callable(field): # header_list.append(field.__name__) val = field(self, is_header=True) header_list.append(val) else: field_obj = self.model._meta.get_field(field) header_list.append(field_obj.verbose_name) # 生成表数据列表 data_list = self.model.objects.all() new_data_list = [] for obj in data_list: temp = [] for field in self.list_display: if callable(field): val = field(self, obj) else: val = getattr(obj, field) temp.append(val) new_data_list.append(temp) return render(request, "stark/change_list.html", locals())
表头数据
首先,我们要生成表头,表头的内容应该根据list_display中写到的内容进行显示,这里要注意,如果我们在stark.py里自己写了样式类,那么list_display会优先从我们自己写的样式类中取,如果里面没有才会找到ModelStark中的
取到list_display的值后我们对他进行循环,如果值为可调用的,说明值为一个函数,那么我们就执行函数,取到我们要的结果,这里要注意执行函数时,我们给函数传了一个is_header=True,说明我们这次是取表头,在函数中我们给这个参数定义一个默认值为False
进入函数时,首先对他进行判断,如果为True,那么我们直接返回一个表头的信息就行了
class BookConfig(ModelStark): def edit(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href='/stark/app01/book/%s/change'>编辑</a>" % obj.pk) def delete(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href='/stark/app01/book/%s/delete'>删除</a>" % obj.pk) list_display = ["id", "title", "price", edit, delete] site.register(Book, BookConfig)
上面的内容可以看到我们返回的表头内容为操作
如果我们循环list_display得到的值是一个字符串,那么说明这应该是表中的一个字段,这时我们可以通过self.model._meta.get_field(字段名)的方法取到这个字段的对象,这个对象有一个verbose_name的属性,这个属性是用来描述一个字段的,在models中可以进行定义
class Book(models.Model): title = models.CharField(verbose_name="标题", max_length=32) price = models.DecimalField(verbose_name="价格", decimal_places=2, max_digits=5, default=12) def __str__(self): return self.title
我们可以通过self.model._meta.get_field(字段名).verbose_name取到这个属性,将他作为表头,如果没有定义这个属性,那么默认值为字段名
表内容数据
取表内容数据时,和表头一样要做判断,判断list_display中的每一个值,如果是可调用的就执行函数取值,这里执行时,我们要将对应的数据对象传进去,这样在生成url时才能使用相关的id值
如果这个值是一个字符串,那么我们可以通过反射,取到数据对象中的值,最后将这些值组成下面形式的数据格式发给前端渲染
''' [ [1, "python", 12], [2, "linux", 12], [3,"php"], 12 ] '''
前端页面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> </head> <body> <h3>数据展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <table class="table table-striped table-hover"> <thead> <tr> {% for foo in header_list %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in new_data_list %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </div> </div> </div> </body> </html>
添加checkbox选择框
在使用admin时可以看到展示页面上每条记录前都有一个选择框,可以选择多条记录进行批量操作,我们也给我们的组件增加这一功能,其实实现方法和编辑按钮类似
我们先自己定义一个checkbox函数,返回一个checkbox类型的input标签,然后将这个函数添加到list_display中即可
class BookConfig(ModelStark): def edit(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href=%s>编辑</a>" % reverse("%s_%s_change" % self.app_model, args=(obj.pk,))) def delete(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href=%s>删除</a>" % reverse("%s_%s_delete" % self.app_model, args=(obj.pk,))) def select(self, obj=None, is_header=False): if is_header: return "选择" return mark_safe("<input type='checkbox' value=%s />" % obj.pk) list_display = [select, "id", "title", "price", edit, delete] site.register(Book, BookConfig)
这里checkbox标签的value值可以设置为该记录的主键值,方便以后使用,当我们点击最上面的复选框时应该还有全选和全部取消的功能,这里只需要添加一段Js代码即可
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> </head> <body> <h3>数据展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <table class="table table-striped table-hover"> <thead> <tr> {% for foo in header_list %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in new_data_list %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </div> </div> </div> <script> $("#action-toggle").click(function () { if ($(this).prop("checked")){ $("tbody :checkbox").prop("checked",true) }else{ $("tbody :checkbox").prop("checked",false) } }) </script> </body> </html>
我们还注意到,在编辑和删除函数中我们在生成url时采用了反向解析,利用我们之前使用的别名来反向生成url,这样就不会把url写死了
list_display的默认情况
上面的内容我们都是考虑了用户自己定制了list_display的情况,如果用户没用进行自定制呢,那么我们所使用的list_display就应该是ModelStark中定义好的
我们仿照admin将默认的list_display设置为__str__,这样在生成表头时我们需要多做一步判断,当为__str__时,直接将表名的大写添加到header_list中即可
class ModelStark(object): list_display = ["__str__",] def __init__(self, model, site): self.model = model self.site = site self.app_model = (self.model._meta.app_label, self.model._meta.model_name) # 查看数据视图 def change_list(self, request): # 生成表标头 header_list = [] for field in self.list_display: if callable(field): # header_list.append(field.__name__) val = field(self, is_header=True) header_list.append(val) else: if field == "__str__": header_list.append(self.model._meta.model_name.upper()) else: field_obj = self.model._meta.get_field(field) header_list.append(field_obj.verbose_name) # 生成表数据列表 data_list = self.model.objects.all() new_data_list = [] for obj in data_list: temp = [] for field in self.list_display: if callable(field): val = field(self, obj) else: val = getattr(obj, field) print(val) temp.append(val) new_data_list.append(temp) return render(request, "stark/change_list.html", locals())
这样我们就完成了默认情况的设置,但是我们发现在admin中不论用户如何设置list_display,其实我们都能看到复选框和编辑删除功能,所以我们也将编辑、删除和复选框的函数直接放入到ModelStark中作为默认配置,然后设置一个get_list_display函数,对所有的list_play都增加这三个功能
class ModelStark(object): # 编辑按钮 def edit(self, obj=None, is_header=False): if is_header: return "操作" name = "%s_%s_change" % self.app_model return mark_safe("<a href=%s>编辑</a>" % reverse(name, args=(obj.pk,))) # 删除按钮 def delete(self, obj=None, is_header=False): if is_header: return "操作" name = "%s_%s_delete" % self.app_model return mark_safe("<a href=%s>删除</a>" % reverse(name, args=(obj.pk,))) # 复选框 def checkbox(self, obj=None, is_header=False): if is_header: return mark_safe("<input type='checkbox' id='action-toggle'>") return mark_safe("<input type='checkbox' value=%s>" % obj.pk) def get_list_display(self): new_list_display = [] new_list_display.extend(self.list_display) new_list_display.append(ModelStark.edit) new_list_display.append(ModelStark.delete) new_list_display.insert(0, ModelStark.checkbox) return new_list_display list_display = ["__str__",] def __init__(self, model, site): self.model = model self.site = site self.app_model = (self.model._meta.app_label, self.model._meta.model_name) # 查看数据视图 def change_list(self, request): # 生成表标头 header_list = [] for field in self.get_list_display(): if callable(field): # header_list.append(field.__name__) val = field(self, is_header=True) header_list.append(val) else: if field == "__str__": header_list.append(self.model._meta.model_name.upper()) else: field_obj = self.model._meta.get_field(field) header_list.append(field_obj.verbose_name) # 生成表数据列表 data_list = self.model.objects.all() new_data_list = [] for obj in data_list: temp = [] for field in self.get_list_display(): if callable(field): val = field(self, obj) else: val = getattr(obj, field) print(val) temp.append(val) new_data_list.append(temp) return render(request, "stark/change_list.html", locals())
我们还注意到通过自定制get_list_display函数我们可以实现一些我们自己的逻辑,比如根据权限判断是否需要加入编辑按钮等
list_display_links
使用admin时,我们还可以通过list_display_links设置一些字段,点击这些字段也能进入编辑页面
我们也来实现一下这个功能,首先在ModelStark中定义一个默认的list_display_links,当用户自己定制了这个属性时,我们只要在生成表数据时多做一步判断,如果字段在list_display_links中,则在返回时给字段加上一个a标签,使他可以跳转到编辑页即可
由于我们经常要用到增删改查的url,所以我们在ModelStark中定义4个方法,分别获取增删改查的url
class ModelStark(object): list_display = ["__str__", ] list_display_links = [] def __init__(self, model, site): self.model = model self.site = site self.app_model = (self.model._meta.app_label, self.model._meta.model_name) # 获取当前查看表的编辑url def get_edit_url(self, obj): edit_url = reverse("%s_%s_change" % self.app_model, args=(obj.pk,)) return edit_url # 获取当前查看表的删除url def get_delete_url(self, obj): del_url = reverse("%s_%s_delete" % self.app_model, args=(obj.pk,)) return del_url # 获取当前查看表的增加url def get_add_url(self): add_url = reverse("%s_%s_add" % self.app_model) return add_url # 获取当前查看表的查看url def get_list_url(self): list_url = reverse("%s_%s_list" % self.app_model) return list_url # 查看数据视图 def change_list(self, request): add_url = self.get_add_url() # 生成表标头 header_list = [] for field in self.get_list_display(): if callable(field): # header_list.append(field.__name__) val = field(self, is_header=True) header_list.append(val) else: if field == "__str__": header_list.append(self.model._meta.model_name.upper()) else: field_obj = self.model._meta.get_field(field) header_list.append(field_obj.verbose_name) # 生成表数据列表 data_list = self.model.objects.all() new_data_list = [] for obj in data_list: temp = [] for field in self.get_list_display(): if callable(field): val = field(self, obj) else: val = getattr(obj, field) if field in self.list_display_links: val = mark_safe("<a href=%s>%s</a>" % (self.get_edit_url(obj), val)) temp.append(val) new_data_list.append(temp) return render(request, "stark/change_list.html", locals())
这样当用户在stark.py中自己定义了list_display_links属性时,我们就能看到下面的效果了
from stark.service.sites import site, ModelStark from .models import * class BookConfig(ModelStark): list_display = ["id", "title", "price"] list_display_links = ["id"] site.register(Book, BookConfig)
如果能够点击字段内容进入编辑页面,那么我们自己定义的编辑按钮就可以不用显示了,所以可以在get_list_display中再做一次判断
def get_list_display(self): new_list_display = [] new_list_display.extend(self.list_display) if not self.list_display_links: new_list_display.append(ModelStark.edit) new_list_display.append(ModelStark.delete) new_list_display.insert(0, ModelStark.checkbox) return new_list_display
添加和编辑页面
编辑页面和添加页面的功能我们通过ModelForm实现,但是生成ModelForm时,由于用户访问的表可能是不一样的,所以里面的详细字段我们不能写死,所以我们只能定义一个简单的ModelForm类,然后在ModelStark中设置一个model_form_class,默认为None
用户如果想要对ModelForm的详细字段做设置,可以自己定制一个类,并将该类设置为model_form_class的值
class ModelStark(object): list_display = ["__str__", ] model_form_class = None list_display_links = [] def get_modelform_class(self): class ModelFormClass(ModelForm): class Meta: model = self.model fields = "__all__" if not self.model_form_class: return ModelFormClass else: return self.model_form_class
可以看到当用户未设置model_form_class时,我们用自己的类,当用户设置了,则使用用户自己的类
from stark.service.sites import site, ModelStark from django.forms import ModelForm from .models import * from django.forms import widgets as wid class BookModelForm(ModelForm): class Meta: model = Book fields = "__all__" error_messages = { "title": {"required": "不能为空"}, "price": {"required": "不能为空"} } class BookConfig(ModelStark): list_display = ["id", "title", "price"] model_form_class = BookModelForm list_display_links = ["id"] site.register(Book, BookConfig)
用户设置时就可以设置明确的字段信息了
添加和编辑的函数
# 添加数据视图 def add_view(self, request): ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass() return render(request, "stark/add_view.html", locals()) else: form = ModelFormClass(data=request.POST) if form.is_valid(): form.save() return redirect(self.get_list_url()) else: return render(request, "stark/add_view.html", locals()) # 编辑数据视图 def change_view(self, request, id): edit_obj = self.model.objects.filter(pk=id).first() ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass(instance=edit_obj) return render(request, "stark/change_view.html", locals()) else: form = ModelFormClass(data=request.POST, instance=edit_obj) if form.is_valid(): form.save() return redirect(self.get_list_url()) else: return render(request, "stark/change_view.html", locals())
就是通过ModelForm来实现添加和编辑
前端页面,由于前端的页面基本相同,所以我们可以把相同的部分写到一个页面中,然后应include调用
<div class="container"> <div class="row"> <div class="col-md-6"> <form action="" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group"> <label for="">{{ field.label }}</label> <div> {{ field }} <span class="error pull-right"> {{ field.errors.0 }} </span> </div> </div> {% endfor %} <p><input type="submit" class="btn btn-default"></p> </form> </div> </div> </div>
添加页面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>添加</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .form-group input{ display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075); -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; } </style> </head> <body> <h3>添加数据</h3> {% include 'stark/form.html' %} </body> </html>
编辑页面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>添加</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .form-group input{ display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075); -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; } </style> </head> <body> <h3>编辑数据</h3> {% include 'stark/form.html' %} </body> </html>
删除页面
当点击删除时,我们不直接将数据删除,而是给用户返回一个确认页面,用户点击确认才真的删除,点击取消还跳回列表页面
# 删除数据视图 def del_view(self, request, id): del_obj = self.model.objects.filter(pk=id).first() if request.method == "GET": list_url = self.get_list_url() return render(request, "stark/del_view.html", locals()) else: del_obj.delete() return redirect(self.get_list_url())
前端页面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>删除</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> </head> <body> <div> <p>{{ del_obj }}</p> </div> <form action="" method="post"> {% csrf_token %} <input type="submit" value="确认删除" class="btn btn-danger"> <a href="{{ list_url }}" class="btn btn-primary">取消</a> </form> </body> </html>
分页功能(url数据保留)
当数据较多时,我们在列表页面需要进行分页,这里分页时我们直接调用以前写好的分页组件使用即可
在使用分页组件时,我们在原有组件的基础上添加一个功能,就是点击页码跳转时,保留原来url上的数据
分页组件如下
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
class Pagination(object): def __init__(self,current_page,all_count,base_url,params,per_page_num=2,pager_count=11): """ 封装分页相关数据 :param current_page: 当前页 :param all_count: 数据库中的数据总条数 :param per_page_num: 每页显示的数据条数 :param base_url: 分页中显示的URL前缀 :param pager_count: 最多显示的页码个数 """ try: current_page = int(current_page) except Exception as e: current_page = 1 if current_page <1: current_page = 1 self.current_page = current_page self.all_count = all_count self.per_page_num = per_page_num self.base_url = base_url import copy params = copy.deepcopy(params) params._mutable = True self.params = params # 总页码 all_pager, tmp = divmod(all_count, per_page_num) if tmp: all_pager += 1 self.all_pager = all_pager self.pager_count = pager_count self.pager_count_half = int((pager_count - 1) / 2) @property def start(self): return (self.current_page - 1) * self.per_page_num @property def end(self): return self.current_page * self.per_page_num def page_html(self): # 如果总页码 < 11个: if self.all_pager <= self.pager_count: pager_start = 1 pager_end = self.all_pager + 1 # 总页码 > 11 else: # 当前页如果<=页面上最多显示11/2个页码 if self.current_page <= self.pager_count_half: pager_start = 1 pager_end = self.pager_count + 1 # 当前页大于5 else: # 页码翻到最后 if (self.current_page + self.pager_count_half) > self.all_pager: pager_end = self.all_pager + 1 pager_start = self.all_pager - self.pager_count + 1 else: pager_start = self.current_page - self.pager_count_half pager_end = self.current_page + self.pager_count_half + 1 page_html_list = [] self.params["page"] = 1 first_page = '<li><a href="%s?%s">首页</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(first_page) if self.current_page <= 1: prev_page = '<li class="disabled"><a href="#">上一页</a></li>' else: self.params["page"] = self.current_page - 1 prev_page = '<li><a href="%s?%s">上一页</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(prev_page) for i in range(pager_start, pager_end): self.params["page"] = i if i == self.current_page: temp = '<li class="active"><a href="%s?%s">%s</a></li>' % (self.base_url, self.params.urlencode(), i,) else: temp = '<li><a href="%s?%s">%s</a></li>' % (self.base_url, self.params.urlencode(), i,) page_html_list.append(temp) if self.current_page >= self.all_pager: next_page = '<li class="disabled"><a href="#">下一页</a></li>' else: self.params["page"] = self.current_page + 1 next_page = '<li><a href="%s?%s">下一页</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(next_page) self.params["page"] = self.all_pager last_page = '<li><a href="%s?%s">尾页</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(last_page) return ''.join(page_html_list)
可以看到我们在原来的基础上多传了一个参数params,这个参数就是当前页的request.GET,这是一个QueryDict的数据类型(和字典类似),我们取到他后,发现无法直接进行修改,这是应为QueryDict默认是不让修改的,需要修改mutable参数为True才能修改
修改前我们为了防止直接修改request.GET而造成后面的影响,所以先用深拷贝拷贝一份数据,再进行修改,修改时,我们将pape改为当前页的页码,再利用QueryDict的urlencode方法将字典类型的数据转换成a=1&b=2类型的字符串数据,然后在生成页码a标签
时在a标签的href属性后面加上生成的字符串,这样我们点击页面跳转时就可以保留url上的数据了
组件的使用
from stark.utils.page import Pagination current_page = request.GET.get("page", 1) all_count = self.model.objects.all().count() base_url = request.path_info params = request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.model.objects.all()[pagination.start: pagination.end]
编辑页面实现url数据保留
上面我们在写编辑页面时并没有考虑保留页面url上的数据,现在我们增加上这个功能
首先点击编辑按钮进入编辑页面时我们需要保留url上的数据,这就需要对编辑按钮这个a标签的href属性进行修改,在后面加上url上要保留的数据,同时,为了让加上的数据不和编辑页面可能有的数据冲突,所以我们单独定义一个list_filter键来存放这些数据
def get_link_tag(self, obj, val): params = self.request.GET params = copy.deepcopy(params) params._mutable = True from django.http import QueryDict qd = QueryDict(mutable=True) qd["list_filter"] = params.urlencode() s = mark_safe("<a href=%s?%s>%s</a>" % (self.get_edit_url(obj), qd.urlencode(), val)) return s
在列表视图中将原来使用get_edit_url的方法换成上面的方法即可
当编辑页面完成编辑后点击提交后我们需要跳转回列表页面,这时我们需要将url上保留的数据还原为原来的形式
# 编辑数据视图 def change_view(self, request, id): edit_obj = self.model.objects.filter(pk=id).first() ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass(instance=edit_obj) return render(request, "stark/change_view.html", locals()) else: form = ModelFormClass(data=request.POST, instance=edit_obj) if form.is_valid(): form.save() params = request.GET.get("list_filter") url = "%s?%s" % (self.get_list_url(), params) return redirect(url) else: return render(request, "stark/change_view.html", locals())
这里我们在原来的url后面加上了我们从request.GET中取出的数据
生成为列表页面服务的类ChangeList
写了这么多内容我们发现我们的列表页面的视图函数内容较多,同时列表页面还有很多功能未添加,为了能够减少列表页面的代码,我们生成一个专门为列表视图函数服务的类,将一些主要的逻辑放到这个类中
# ChangeList服务于change_list视图 class ChangeList(object): def __init__(self, config, request, queryset): self.config = config self.request = request self.queryset = queryset from stark.utils.page import Pagination current_page = self.request.GET.get("page", 1) all_count = self.queryset.count() base_url = self.request.path_info params = self.request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.queryset[pagination.start: pagination.end] self.pagination = pagination self.data_list = data_list def get_header(self): # 生成表标头 header_list = [] for field in self.config.get_list_display(): if callable(field): # header_list.append(field.__name__) val = field(self.config, is_header=True) header_list.append(val) else: if field == "__str__": header_list.append(self.config.model._meta.model_name.upper()) else: field_obj = self.config.model._meta.get_field(field) header_list.append(field_obj.verbose_name) return header_list def get_body(self): # 生成表数据列表 new_data_list = [] for obj in self.data_list: temp = [] for field in self.config.get_list_display(): if callable(field): val = field(self.config, obj) else: val = getattr(obj, field) if field in self.config.list_display_links: val = self.config.get_link_tag(obj, val) temp.append(val) new_data_list.append(temp) return new_data_list
我们将现在的生成表头、表数据和分页的功能都放到该类中,初始化时的config参数就是ModelStark类的实例化对象,queryset是我们从数据库中取出的需要渲染到页面上的数据
这时列表视图函数只要保留下面的内容就行了
# 查看数据视图 def change_list(self, request): self.request = request add_url = self.get_add_url() queryset = self.model.objects.filter(search_condition) cl = ChangeList(self, request, queryset) return render(request, "stark/change_list.html", locals())
页面渲染时我们只要利用ChangeList类的实例化对象cl就可以渲染出我们想要的内容
search模糊查询
使用admin时我们能定义一个search_fields列表来生成一个查询框,可以根据列表中的字段进行模糊查询
我们也来定义这么一个参数
class ModelStark(object): list_display = ["__str__", ] model_form_class = None list_display_links = [] search_fields = []
默认让他为一个空列表,当用户定义了值时,我们就需要在页面生成一个搜索框,并且根据模糊查询得到需要的数据展示到页面上,这里需要注意如果用户在列表中定义了多个字段,那么多个字段查询时应该是或的关系
Q查询利用字符串进行查询的方式
在查询时由于是或的关系所以我们要用到Q查询,但是我们之前使用Q查询时都是直接使用的字段名,现在我们只能拿到字段名的字符串,所以需要用Q查询的另外一种方式
def get_search_condition(self): from django.db.models import Q search_condition = Q() search_condition.connector = "or" # 设置关系为或 if self.search_fields: key_word = self.request.GET.get("q") if key_word: for search_field in self.search_fields: search_condition.children.append((search_field + "__contains", key_word)) return search_condition
先生成一个Q对象,设置为或的关系,然后通过循环将要查询的字段的字符串和查询关键字以元组的形式添加到Q对象中,这里要注意,由于是模糊查询,我们在字段字符串后拼接了__contains
最后在列表视图函数中取到这个Q对象,根据他进行查询
# 查看数据视图 def change_list(self, request): self.request = request add_url = self.get_add_url() # 关于search的模糊查询 search_condition = self.get_search_condition() queryset = self.model.objects.filter(search_condition) cl = ChangeList(self, request, queryset) return render(request, "stark/change_list.html", locals())
前端页面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> </head> <body> <h3>数据展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <a href="{{ add_url }}"><button class="btn btn-primary">添加数据</button></a> {% if cl.config.search_fields %} <div class="pull-right form-group"> <form action="" method="get" class="form-inline"> <input type="text" class="form-control" name="q"> <input type="submit" class="btn btn-primary" value="search"> </form> </div> {% endif %} <table class="table table-striped table-hover"> <thead> <tr> {% for foo in cl.get_header %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in cl.get_body %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> <nav aria-label="Page navigation" class="pull-right"> <ul class="pagination"> {{ cl.pagination.page_html|safe }} </ul> </nav> </div> </div> </div> <script> $("#action-toggle").click(function () { if ($(this).prop("checked")){ $("tbody :checkbox").prop("checked",true) }else{ $("tbody :checkbox").prop("checked",false) } }) </script> </body> </html>
生成搜索框时需要做判断,如果用户没有定义search_fields,则不需要生成搜索框
action功能
使用admin时,我们发现有一个action功能,有一个下拉菜单可以选择功能批量操作,现在我们也来实现这个功能
首先在ModelStark中定义一个变量actions,默认为一个空列表
class ModelStark(object): list_display = ["__str__", ] model_form_class = None list_display_links = [] search_fields = [] actions = []
这表示如果用户没有自己定制actions,那么则没有任何功能,但是我们使用admin时,发现默认有一个批量删除的功能,所以我们也来写一个批量删除
def patch_delete(self, queryset): queryset.delete() patch_delete.desc = "批量删除"
python中一切皆对象,我们给这个函数对象一个新的desc属性,这个属性的值就是我们想要在页面上展示给别人看的这个函数的用途,然后我们要将这个函数添加到actions中,同时也要考虑用户自己定制时的情况
# 获取真正展示的actions def get_actions(self): temp = [] temp.extend(self.actions) temp.append(ModelStark.patch_delete) return temp
这样通过ModelStark中的get_actions我们就能拿到最终的actions列表,上面我们自己定制了一个ChangeList类,专门为列表页面服务,actions功能也是在列表页面中使用的,所以我们在ChangeList类中定义一个方法# ChangeList服务于change_list视图
class ChangeList(object): def __init__(self, config, request, queryset): self.config = config self.request = request self.queryset = queryset from stark.utils.page import Pagination current_page = self.request.GET.get("page", 1) all_count = self.queryset.count() base_url = self.request.path_info params = self.request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.queryset[pagination.start: pagination.end] self.pagination = pagination self.data_list = data_list # actions self.actions = self.config.get_actions()
def handle_action(self): temp =[] for action_func in self.actions: temp.append({"name": action_func.__name__, "desc": action_func.desc}) return temp
这里我们通过这个方法获得的是一个列表,列表中有一个个的字典,字典里放着函数的名字和我们要展示的描述
前端页面渲染
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .filter a{ padding: 5px 3px; border: 1px solid grey; background-color: #336699; color: white; } .active{ background-color: white!important; color: black!important; } </style> </head> <body> <h3>数据展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <a href="{{ add_url }}"><button class="btn btn-primary">添加数据</button></a> {% if cl.config.search_fields %} <div class="pull-right form-group"> <form action="" method="get" class="form-inline"> <input type="text" class="form-control" name="q"> <input type="submit" class="btn btn-primary" value="search"> </form> </div> {% endif %} <form action="" method="post"> {% csrf_token %} <div> <select class="form-control" name="action" id="" style="width: 200px;margin: 5px 0;display: inline-block;vertical-align: -1px" > <option value="">---------</option> {% for item in cl.handle_action %} <option value="{{ item.name }}">{{ item.desc }}</option> {% endfor %} </select> <button type="submit" class="btn btn-default">Go</button> </div> <table class="table table-striped table-hover"> <thead> <tr> {% for foo in cl.get_header %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in cl.get_body %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </form> <nav aria-label="Page navigation" class="pull-right"> <ul class="pagination"> {{ cl.pagination.page_html|safe }} </ul> </nav> </div> </div> </div> </div> <script> $("#action-toggle").click(function () { if ($(this).prop("checked")){ $("tbody :checkbox").prop("checked",true) }else{ $("tbody :checkbox").prop("checked",false) } }) </script> </body> </html>
这里每一个下拉菜单的option的value值就是我们定义的函数名,还要注意要将select标签和复选框标签都放到同一个form表单中,这样在发送数据时我们发送了,选择的批量操作函数和被选中的数据的pk值
后端操作
后端接收到选择的批量操作函数和被选中的数据的pk值就可以进行操作了
# 查看数据视图 def change_list(self, request): if request.method == "POST": func_name = request.POST.get("action") pk_list = request.POST.getlist("_selected_action") queryset = self.model.objects.filter(pk__in=pk_list) func = getattr(self, func_name) func(queryset) self.request = request add_url = self.get_add_url() # 关于search的模糊查询 search_condition = self.get_search_condition() queryset = self.model.objects.filter(search_condition) cl = ChangeList(self, request, queryset) return render(request, "stark/change_list.html", locals())
首先判断,当请求为POST请求时,取到批量操作函数的函数名,和选择数据的pk值列表(由于是复选框,可能选择了多个值,所以这里用getlist取值),然后通过pk值列表查找到要操作的数据的queryset集合,利用反射通过函数名的字符串取到批量操作函数,最后将取到的quertset传给函数执行
多级过滤
和其它功能一样,我们先在ModelStark中定义一个list_filter空列表
class ModelStark(object): list_display = ["__str__", ] model_form_class = None list_display_links = [] search_fields = [] actions = [] # 多级过滤 list_filter = []
当用户自己定义了这个列表时,我们要取到列表中的字段,然后查处该字段对应的内容,显示到页面上,当用户点击某一个内容时,要过滤出和这个内容相关的数据
当用户点击这个内容的a标签时,我们要向后台发送一个get请求,请求带着我们要过滤的内容,内容的键为字段的名称,值为你选中值的pk值
第一步我们要先想办法将list_filter中字段的对应数据都显示到页面上,先要取到所有数据
# ChangeList服务于change_list视图 class ChangeList(object): def __init__(self, config, request, queryset): self.config = config self.request = request self.queryset = queryset from stark.utils.page import Pagination current_page = self.request.GET.get("page", 1) all_count = self.queryset.count() base_url = self.request.path_info params = self.request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.queryset[pagination.start: pagination.end] self.pagination = pagination self.data_list = data_list # actions self.actions = self.config.get_actions() # filter self.list_filter = self.config.list_filter def get_filter_link_tag(self): # link_tags = [] for filter_field_name in self.list_filter: current_id = int(self.request.GET.get(filter_field_name, 0)) filter_field_obj = self.config.model._meta.get_field(filter_field_name) filter_field = FilterField(filter_field_name, filter_field_obj)
在ChangeList中我们先拿到拿到list_filter,并定义self.list_filter,然后定义一个get_filter_link_tag方法,循环self.list_filter,循环的每一个值就是各个字段的名称,然后通过字段名称拿到这个字段的对象filter_field_obj,然后通过字段名称和字段对象通过FilterField类实例化出一个对象,这个新的类内容如下
# 为每一个过滤的字段封装成整体类 class FilterField(object): def __init__(self, filter_field_name, filter_field_obj): self.filter_field_name = filter_field_name self.filter_field_obj = filter_field_obj def get_data(self): if isinstance(self.filter_field_obj, ForeignKey) or isinstance(self.filter_field_obj, ManyToManyField): return self.filter_field_obj.rel.to.objects.all() elif self.filter_field_obj.choices: return self.filter_field_obj.choices else: pass
通过这个类的对象我们可以调用get_data方法拿到需要的数据列表(queryset或元组里套元组),这里我们暂时不考虑普通字段,由于需要的字段变多了,我们将Book表的字段进行一些修改,增加外键和多对多的关系
from django.db import models # Create your models here. class Book(models.Model): title = models.CharField(verbose_name="标题", max_length=32) price = models.DecimalField(verbose_name="价格", decimal_places=2, max_digits=5, default=12) state = models.IntegerField(choices=((1, "已出版"), (2, "未出版")), default=1) publish = models.ForeignKey(to="Publish", default=1) authors = models.ManyToManyField(to="Author", default=1) def __str__(self): return self.title class Author(models.Model): name = models.CharField(max_length=32) age = models.IntegerField() def __str__(self): return self.name class Publish(models.Model): name = models.CharField(max_length=32) def __str__(self): return self.name
拿到数据的列表后,我们再循环每一个数据,通过判断字段对象的类型,生成不同的a标签,同时我们也取到当前get求情的数据,如果当前标签为选中的标签,我们要给他增加一个active属性
还有需要保留之前的选择,我们也要取到每次get请求的数据,保留到生成的a标签中
# ChangeList服务于change_list视图 class ChangeList(object): def __init__(self, config, request, queryset): self.config = config self.request = request self.queryset = queryset from stark.utils.page import Pagination current_page = self.request.GET.get("page", 1) all_count = self.queryset.count() base_url = self.request.path_info params = self.request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.queryset[pagination.start: pagination.end] self.pagination = pagination self.data_list = data_list # actions self.actions = self.config.get_actions() # filter self.list_filter = self.config.list_filter def get_filter_link_tag(self): # link_tags = [] for filter_field_name in self.list_filter: current_id = int(self.request.GET.get(filter_field_name, 0)) filter_field_obj = self.config.model._meta.get_field(filter_field_name) filter_field = FilterField(filter_field_name, filter_field_obj) def inner(filter_field, current_id): for obj in filter_field.get_data(): params = copy.deepcopy(self.request.GET) params._mutable = True if isinstance(filter_field.filter_field_obj, ForeignKey) or isinstance(filter_field.filter_field_obj, ManyToManyField): params[filter_field.filter_field_name] = obj.pk if current_id == obj.pk: yield mark_safe("<a href='?%s' class='active'>%s</a>" % (params.urlencode(), obj)) else: yield mark_safe("<a href='?%s'>%s</a>" % (params.urlencode(), obj)) elif filter_field.filter_field_obj.choices: params[filter_field.filter_field_name] = obj[0] if current_id == obj[0]: yield mark_safe("<a href='?%s' class='active'>%s</a>" % (params.urlencode(), obj[1])) else: yield mark_safe("<a href='?%s'>%s</a>" % (params.urlencode(), obj[1])) else: pass yield inner(filter_field, current_id) # link_tags.append(temp) # return link_tags
这里我们使用的yield功能,在渲染模板时需要注意,当for循环中还套了一个for循环时,会先取到第一个for循环的所有内容,再进行内部的for循环,这在使用yield时会出现一些问题,所以我们在内部的生成器函数中要直接将当前的数据传进去,避免出错
前端页面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .filter a{ padding: 5px 3px; border: 1px solid grey; background-color: #336699; color: white; } .active{ background-color: white!important; color: black!important; } </style> </head> <body> <h3>数据展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <a href="{{ add_url }}"><button class="btn btn-primary">添加数据</button></a> {% if cl.config.search_fields %} <div class="pull-right form-group"> <form action="" method="get" class="form-inline"> <input type="text" class="form-control" name="q"> <input type="submit" class="btn btn-primary" value="search"> </form> </div> {% endif %} <form action="" method="post"> {% csrf_token %} <div> <select class="form-control" name="action" id="" style="width: 200px;margin: 5px 0;display: inline-block;vertical-align: -1px" > <option value="">---------</option> {% for item in cl.handle_action %} <option value="{{ item.name }}">{{ item.desc }}</option> {% endfor %} </select> <button type="submit" class="btn btn-default">Go</button> </div> <table class="table table-striped table-hover"> <thead> <tr> {% for foo in cl.get_header %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in cl.get_body %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </form> <nav aria-label="Page navigation" class="pull-right"> <ul class="pagination"> {{ cl.pagination.page_html|safe }} </ul> </nav> </div> <div class="col-md-4"> <div class="filter"> {% for filter_link_tag in cl.get_filter_link_tag %} <p class="field">{% for data in filter_link_tag %} {{ data }} {% endfor %} </p> {% endfor %} </div> </div> </div> </div> <script> $("#action-toggle").click(function () { if ($(this).prop("checked")){ $("tbody :checkbox").prop("checked",true) }else{ $("tbody :checkbox").prop("checked",false) } }) </script> </body> </html>
单独封装一个类的方法
上面的方法中,我们使用的是双重for循环并且用到了两个生成器,这样容易出现问题,现在我们将内部的生成器单独提取出来,封装到一个类中
# 针对((),()),[[],[]]数据类型构建a标签 class LinkTagGen(object): def __init__(self, data, filter_field, request): self.data = data self.filter_field = filter_field self.request = request def __iter__(self): current_id = int(self.request.GET.get(self.filter_field.filter_field_name, 0)) params = copy.deepcopy(self.request.GET) params._mutable = True if params.get(self.filter_field.filter_field_name): del params[self.filter_field.filter_field_name] _url = "%s?%s" % (self.request.path_info, params.urlencode()) yield mark_safe("<a href='%s'>全部</a>" % _url) else: _url = "%s?%s" % (self.request.path_info, params.urlencode()) yield mark_safe("<a href='%s' class='active'>全部</a>" % _url) for item in self.data: if self.filter_field.filter_field_obj.choices: pk, text = item[0], item[1] elif isinstance(self.filter_field.filter_field_obj, ForeignKey) or isinstance(self.filter_field.filter_field_obj, ManyToManyField): pk, text = item.pk, item params[self.filter_field.filter_field_name] = pk _url = "%s?%s" % (self.request.path_info, params.urlencode()) if current_id == pk: link_tag = "<a href='%s' class='active'>%s</a>" % (_url, text) else: link_tag = "<a href='%s'>%s</a>" % (_url, text) yield mark_safe(link_tag)
在ChangeList类中调用时,我们直接拿到这个类的对象并返回
# ChangeList服务于change_list视图 class ChangeList(object): def __init__(self, config, request, queryset): self.config = config self.request = request self.queryset = queryset from stark.utils.page import Pagination current_page = self.request.GET.get("page", 1) all_count = self.queryset.count() base_url = self.request.path_info params = self.request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.queryset[pagination.start: pagination.end] self.pagination = pagination self.data_list = data_list # actions self.actions = self.config.get_actions() # filter self.list_filter = self.config.list_filter def get_filter_link_tag(self): # link_tags = [] for filter_field_name in self.list_filter: filter_field_obj = self.config.model._meta.get_field(filter_field_name) filter_field = FilterField(filter_field_name, filter_field_obj) val = LinkTagGen(filter_field.get_data(), filter_field, self.request) yield val
这样在前端我们就可以对这个类对象进行for循环(执行对象的__iter__方法),这里我们多增加了一个全部标签,当点击某一个字段的全部标签时,我们就将GET方法中有关这个字段的数据给删除
前端页面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .filter a{ padding: 5px 3px; border: 1px solid grey; background-color: #336699; color: white; } .active{ background-color: white!important; color: black!important; } </style> </head> <body> <h3>数据展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <a href="{{ add_url }}"><button class="btn btn-primary">添加数据</button></a> {% if cl.config.search_fields %} <div class="pull-right form-group"> <form action="" method="get" class="form-inline"> <input type="text" class="form-control" name="q"> <input type="submit" class="btn btn-primary" value="search"> </form> </div> {% endif %} <form action="" method="post"> {% csrf_token %} <div> <select class="form-control" name="action" id="" style="width: 200px;margin: 5px 0;display: inline-block;vertical-align: -1px" > <option value="">---------</option> {% for item in cl.handle_action %} <option value="{{ item.name }}">{{ item.desc }}</option> {% endfor %} </select> <button type="submit" class="btn btn-default">Go</button> </div> <table class="table table-striped table-hover"> <thead> <tr> {% for foo in cl.get_header %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in cl.get_body %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </form> <nav aria-label="Page navigation" class="pull-right"> <ul class="pagination"> {{ cl.pagination.page_html|safe }} </ul> </nav> </div> <div class="col-md-4"> <div class="filter"> {% for filter_link_tag in cl.get_filter_link_tag %} <div class="well">{% for link_tag in filter_link_tag %} <p>{{ link_tag }}</p> {% endfor %} </div> {% endfor %} </div> </div> </div> </div> <script> $("#action-toggle").click(function () { if ($(this).prop("checked")){ $("tbody :checkbox").prop("checked",true) }else{ $("tbody :checkbox").prop("checked",false) } }) </script> </body> </html>
数据的过滤
上面我们已经能在页面上生成对应的过滤标签了,当通过点击这些标签时,会向后端发送含有相应条件的GET请求,我们要在后端拿到条件并进行过滤,将过滤后的数据显示在页面上,这个功能和我们上面做的search类似
# 获取filter的查询条件Q对象 def get_filter_condition(self): from django.db.models import Q filter_condition = Q() for field, val in self.request.GET.items(): if field in self.list_filter: filter_condition.children.append((field, val)) return filter_condition # 查看数据视图 def change_list(self, request): if request.method == "POST": func_name = request.POST.get("action") pk_list = request.POST.getlist("_selected_action") queryset = self.model.objects.filter(pk__in=pk_list) func = getattr(self, func_name) func(queryset) self.request = request add_url = self.get_add_url() # 关于search的模糊查询 search_condition = self.get_search_condition() # filter多级过滤 filter_condition = self.get_filter_condition() queryset = self.model.objects.filter(search_condition).filter(filter_condition) cl = ChangeList(self, request, queryset) return render(request, "stark/change_list.html", locals())
这里我们需要做一次判断,当get请求的数据中有page时,需要过滤掉,不然会报错
普通字段
上面的过滤我们都没有考虑普通字段,如果是一个普通字段,我们可以按下面的代码执行
# 针对((),()),[[],[]]数据类型构建a标签 class LinkTagGen(object): def __init__(self, data, filter_field, request): self.data = data self.filter_field = filter_field self.request = request def __iter__(self): current_id = self.request.GET.get(self.filter_field.filter_field_name, 0) params = copy.deepcopy(self.request.GET) params._mutable = True if params.get(self.filter_field.filter_field_name): del params[self.filter_field.filter_field_name] _url = "%s?%s" % (self.request.path_info, params.urlencode()) yield mark_safe("<a href='%s'>全部</a>" % _url) else: _url = "%s?%s" % (self.request.path_info, params.urlencode()) yield mark_safe("<a href='%s' class='active'>全部</a>" % _url) for item in self.data: if self.filter_field.filter_field_obj.choices: pk, text = str(item[0]), item[1] elif isinstance(self.filter_field.filter_field_obj, ForeignKey) or isinstance(self.filter_field.filter_field_obj, ManyToManyField): pk, text = str(item.pk), item else: pk, text = item[1], item[1] params[self.filter_field.filter_field_name] = pk _url = "%s?%s" % (self.request.path_info, params.urlencode()) if current_id == pk: link_tag = "<a href='%s' class='active'>%s</a>" % (_url, text) else: link_tag = "<a href='%s'>%s</a>" % (_url, text) yield mark_safe(link_tag) # 为每一个过滤的字段封装成整体类 class FilterField(object): def __init__(self, filter_field_name, filter_field_obj, config): self.filter_field_name = filter_field_name self.filter_field_obj = filter_field_obj self.config = config def get_data(self): if isinstance(self.filter_field_obj, ForeignKey) or isinstance(self.filter_field_obj, ManyToManyField): return self.filter_field_obj.rel.to.objects.all() elif self.filter_field_obj.choices: return self.filter_field_obj.choices else: return self.config.model.objects.values_list("pk", self.filter_field_name)
POPUP功能
使用admin添加数据时我们会发现在外键和多对多的字段旁边有一个小加号,点击后会弹出一个小窗口,在小窗口中可以直接添加外键和多对多字段对应的表的数据
这里我们使用popup来实现这个功能,我们的逻辑应该是在添加页面的视图函数中
# 添加数据视图 def add_view(self, request): ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass() else: form = ModelFormClass(data=request.POST) if form.is_valid(): obj = form.save() pop_id = request.GET.get("pop_id") if pop_id: res = {"pk": obj.pk, "text": str(obj), "pop_id": pop_id} import json return render(request, "stark/pop_res.html", {"res": json.dumps(res)}) return redirect(self.get_list_url()) from django.forms.models import ModelChoiceField for bound_field in form: if isinstance(bound_field.field, ModelChoiceField): bound_field.is_pop = True app_label = bound_field.field.queryset.model._meta.app_label model_name = bound_field.field.queryset.model._meta.model_name _url = "%s_%s_add" % (app_label, model_name) bound_field.url = reverse(_url) + "?pop_id=id_%s" % bound_field.name else: bound_field.is_pop = False bound_field.url = None return render(request, "stark/add_view.html", locals())
我们的添加页面是利用ModelForm生成的,在请求过来时我们会使用ModelForm实例化一个对象,我们使用for循环遍历这个对象,可以得到这个对象的每一个字段(其实就是ModelForm对应表的每一个字段),这个字段是BoundField类型的,下面有两个方法.name和.field,.name可以得到字段的名称,而.field则可以得到字段在ModelForm中的类型,外键类型在Form中对应的是ModelChoiceField类型,而多对多在Form中对应的是ModelMultipleChoiceField类型(继承ModelChoiceField),所以我们只要判断bound_field.field是否是ModelChoiceField类型的对象就能知道这个字段是否是外键或多对多的字段,如果是的话,我们给这个字段对象添加一个is_pop=True的属性,不是则为False,在前端页面我们可以根据这个属性判断是否需要在字段的框后面添加+号,这个+号应该绑定一个点击事件,点击后可以弹出一个窗口让我们添加数据(pop请求),弹出窗口的url应该就是该字段对应表的添加url,如何取到字段对应的表呢?
前面我们取到了外键和多对多在Form中对应的字段bound_field.field,这里面有一个queryset属性可以取到这个外键字段对应表中的数据集合,而queryset这个数据类型中有个model属性可以得到数据集合对应的表,这样我们就可以通过bound_field.field.queryset.model得到外键或多对多对应的表,再取出表名和app名,通过反向解析就可以得到对应的添加url,这里要注意,我们要在这个url后面加上一个?pop_id=id_当前字段名,这样当添加的数据通过post请求过来时,我们才能知道是哪个字段添加的数据,数据返回时我们才能找到这个字段对应的select框,给他添加一个option标签
当添加的数据通过post请求过来时,如果数据验证成功了,我们先取pop_id,如果能取到,说明这个请求是通过弹出框来的,我们取到相关数据放到一个字典中,将字典返回给pop响应页面,响应页面中我们可以通过opener获得是哪个页面弹出的这个窗口,然后调用这个opener中的函数,并把后端接收的字典传给他,这样就可以利用这个函数在页面上添加option标签了。如果取不到pop_id则直接保存数据,跳转到列表页面即可。如果post请求发来的数据没有验证成功,那么我们依然要做上面两段提到的内容,所以我们将上面两段的逻辑放到了函数的最后
前端页面
add_view.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>添加</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .form-group input,select{ display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075); -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; } </style> </head> <body> <h3>添加数据</h3> {% include 'stark/form.html' %} <script> function foo(res) { var res=JSON.parse(res); var ele_option=document.createElement("option"); ele_option.value=res.pk; ele_option.innerHTML=res.text; ele_option.selected="selected"; document.getElementById(res.pop_id).appendChild(ele_option) } </script> </body> </html>
这里的foo就是pop响应页面调用的函数
form.html
<div class="container"> <div class="row"> <div class="col-md-6 col-xs-8"> <form action="" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group" style="position: relative"> <label for="">{{ field.label }}</label> <div> {{ field }} <span class="error pull-right"> {{ field.errors.0 }} </span> </div> {% if field.is_pop %} <a href="" onclick="pop('{{ field.url }}')" class="pop_btn" style="position: absolute;top: 45%;right: -23px"><span class="pull-right" style="font-size: 22px">+</span></a> {% endif %} </div> {% endfor %} <p><input type="submit" class="btn btn-default"></p> </form> </div> </div> </div> <script> function pop(url) { window.open(url,"","width=500,height=400") } </script>
通过is_pop来判断能是否添加+号,并给这个加号绑定点击事件,弹出pop框
pop_res.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> </head> <body> <script> opener.foo('{{ res|safe }}'); window.close() </script> </body> </html>
执行opener的函数,并直接关闭弹出框
编辑页面和添加页面同时实现popup功能
通过上面的方式我们就实现了添加页面的popup功能,但是当我们点击编辑时,我们发现在编辑页面上也需要有popup的功能,我们可以将上面的逻辑再在编辑页面中写一份,但是这样的话会造成代码的重复
这里我们使用自定义标签的形式,添加和编辑页面用的都是form.html页面中的内容,而这个页面中的内容中我们只需要提供一个form
所以我们在stark组件中创建一个目录templatetags,在该目录中自定义我们的标签my_tags
from django import template from django.shortcuts import reverse register = template.Library() @register.inclusion_tag("stark/form.html") def get_form(form): from django.forms.models import ModelChoiceField for bound_field in form: if isinstance(bound_field.field, ModelChoiceField): bound_field.is_pop = True app_label = bound_field.field.queryset.model._meta.app_label model_name = bound_field.field.queryset.model._meta.model_name _url = "%s_%s_add" % (app_label, model_name) bound_field.url = reverse(_url) + "?pop_id=id_%s" % bound_field.name return {"form": form}
这样在添加和编辑视图中就不用再写这么多逻辑了,直接将form对象传给前端就行了
# 添加数据视图 def add_view(self, request): ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass() else: form = ModelFormClass(data=request.POST) if form.is_valid(): obj = form.save() pop_id = request.GET.get("pop_id") if pop_id: res = {"pk": obj.pk, "text": str(obj), "pop_id": pop_id} import json return render(request, "stark/pop_res.html", {"res": json.dumps(res)}) return redirect(self.get_list_url()) return render(request, "stark/add_view.html", locals()) # 编辑数据视图 def change_view(self, request, id): edit_obj = self.model.objects.filter(pk=id).first() ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass(instance=edit_obj) return render(request, "stark/change_view.html", locals()) else: form = ModelFormClass(data=request.POST, instance=edit_obj) if form.is_valid(): form.save() params = request.GET.get("list_filter") url = "%s?%s" % (self.get_list_url(), params) return redirect(url) else: return render(request, "stark/change_view.html", locals())
前端收到这个form对象,直接调用自定义标签即可
{% load my_tags %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>添加</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .form-group input,select{ display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075); -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; } </style> </head> <body> <h3>编辑数据</h3> {% get_form form %} <script> function foo(res) { var res=JSON.parse(res); var ele_option=document.createElement("option"); ele_option.value=res.pk; ele_option.innerHTML=res.text; ele_option.selected="selected"; document.getElementById(res.pop_id).appendChild(ele_option) } </script> </body> </html>
list_display补充
list_display可以定义我们在列表页面上显示哪些内容,如果展示的内容是一个包含choices的字段的话(比如说Book的state字段),按我们之前写的,在页面上只能看到1或者2这样的数字,如果想要看到已出版或未出版该怎么办呢,可以让用户自己定制,在stark.py中
class BookConfig(ModelStark): def state(self, obj=None, is_header=False): if is_header: return "状态" return obj.get_state_display() list_display = ["id", "title", "price", "publish", state] model_form_class = BookModelForm list_display_links = ["id"] search_fields = ["title", "price"] def patch_init(self, queryset): queryset.update(price=100) patch_init.desc = "批量初始化" actions = [patch_init, ] list_filter = ["title", "state", "publish", "authors"] site.register(Book, BookConfig)
自己定义一个函数,obj.get_state_display()方法就可以取到choice中的内容,这个方法的state是字段名,get和display是固定用法
在list_display中增加多对多字段
在admin中我们不能往list_display中增加多对多字段,在我们自己写的stark中我们来实现这一功能,其实就是在ChangList类中的get_body方法中多做一次判断
def get_body(self): # 生成表数据列表 new_data_list = [] for obj in self.data_list: temp = [] for field in self.config.get_list_display(): if callable(field): val = field(self.config, obj) else: field_obj = self.config.model._meta.get_field(field) if isinstance(field_obj, ManyToManyField): t = [] for i in getattr(obj, field).all(): t.append(str(i)) val = ",".join(t) else: val = getattr(obj, field) if field in self.config.list_display_links: val = self.config.get_link_tag(obj, val) temp.append(val) new_data_list.append(temp) return new_data_list
当list_display中的值是不可调用的时,我们先取出其对应的字段对象,如果是多对多的类型,则通过getattr的方法拿到多对多的内容,并通过join生成字符串