自定义admin管理工具(stark组件)
自定义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>




浙公网安备 33010602011771号