自定义admin管理工具(stark组件)

安稳与你 提交于 2020-02-09 04:31:18

自定义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>

创建项目

了解了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>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!