Django Stark组件开发文档

Stark组件

前戏

1.django路由启动前,自定义执行某个py文件

django启动时,且在读取项目中的路由加载之前执行某个py文件

在任意app的apps.py中的Config类中定义ready方法,并调用autodiscover_modules

from django.apps import AppConfig
from django.utils.module_loading import autodiscover_modules
​
class App01Config(AppConfig):
    name = 'app01'
    
    def ready(self):
        autodiscover_modules('文件名')
        
# django在启动时就会去已注册的所用app的目录下找 files_name.py 并自动导入
# 如果执行两次,是因为django内部自动重启导致(内部有2个线程再运行,1个负责主程序,1个负责检测代码变化并重启),不想执行两次可以写上:
# python manage.py 120.0.0.1:8001 --noreload

 

2.单例模式

利用Python模块导入的特性:已经导入的文件需要再次被重新导入时,python不会重新解释一遍,而是从内存中直接将原来导入的值拿来用

# 文件1 utils.py:
class AdminSite(object):
    pass
site = AdminSite() # 为AdminSite类实例化一个对象
​
# 文件2 app.py:
import utils
print(utils.site)

提示: 如果以后存在一个单例模式对象,可以先在此对象中放入一些值,然后再在其他的文件中导入该对象,通过对象再次将值获取到(可以理解为就是同一个对象在不同的文件中的数据存取)

 

  1. django路由分发的本质:include

    方式一:
        from django.conf.urls import url,include
    ​
        urlpatterns = [
            url(r'^web/', include("app01.urls")),
        ]
    ​
    方式二:
        include函数主要返回有三个元素的元组。
        from django.conf.urls import url,include
        from app01 import urls
        urlpatterns = [
            url(r'^web/', (urls, app_name, namespace)), # 第一个参数是urls文件对象,通过此对象可以获取urls.patterns获取分发的路由。
        ]
    ​
    ​
        在源码内部,读取路由时:
            如有第一个参数有:urls.patterns 属性,那么子路由就从该属性中后去。
            如果第一个参数无:urls.patterns 属性,那么子路由就是第一个参数。
    ​
    方式三:
        urlpatterns = [
            url(r'^web/', ([
                url(r'^index/', views.index),
                url(r'^home/', views.home),
            ], app_name, namespace)), # 第一个参数是urls文件对象,通过此对象可以获取urls.patterns获取分发的路由。
        ]

示例:prev_luffy_stark.zip

 

 

 

 

开始

1.创建 django project

python3 manage.py startapp app01 
python3 manage.py startapp app02
python3 manage.py startapp stark  # 最后产出的组件

 

 

2.创建基础业务表

app01/models.py

Depart

UserInfo

app02/models.py

Host

 

 

3.对以上三张表做增删改查

3.1分析

  • 为每张表创建4个url(增删改查)

  • 为每张表创建4个url

    app01/models.py

    Depart

    /app01/depart/list/

    /app01/depart/add/

    /app01/depart/edit/(\d+)/

    /app01/depart/del/(\d+)/

    UserInfo

    /app01/userinfo/list/

    /app01/userinfo/add/

    /app01/userinfo/edit/(\d+)/

    /app01/userinfo/del/(\d+)/

    app02/models.py

    Host /app02/host/list/

    /app02/host/add/

    /app02/host/edit/(\d+)/

    /app02/host/del/(\d+)/

     

 

3.2 为app中的每个model类自动创建url以及相关视图函数

3.2.1 自动生成URL

文件路径:stark/service/v1.py

# 在stark.service下创建v1.py
from django.conf.urls import url
from django.shortcuts import HttpResponse
​
# 创建一个 StarkSite 类
class StarkSite(object):
    def __init__(self):
        self._registry = []  # 注册表
        self.app_name = 'stark'
        self.namespace = 'stark'
        
    # 定义 register 函数(self,model类,handler类)
    def register(self, model_class, handler_class):
        """
​
        :param model_class: 是models中的数据库表对应的类。 models.UserInfo
        :param handler_class: 处理请求的视图函数所在的类
        :return:
        """
        """
        self._registry = [
            {'model_class':models.Depart,'handler':DepartHandler(models.Depart)},
            {'model_class':models.UserInfo,'handler':UserInfoHandler(models.UserInfo)}
            {'model_class':models.Host,'handler': HostHandler(models.Host) }
        ]
        """
        self._registry.append({'model_class': model_class, 'handler': handler_class(model_class)})
​
    def get_urls(self):
        patterns = []
        for item in self._registry:
            model_class = item['model_class']  #model类
            handler = item['handler']  # handler对象
            # app名称 = model_class._meta.app_label  (eg.app01)
            # model类名 = model_class._meta.model_name  (eg.UserInfo)
            app_label, model_name = model_class._meta.app_label, model_class._meta.model_name
            # 拼接url(eg.r'app01/UserInfo/list/$'),添加到 patterns 中
            patterns.append(url(r'%s/%s/list/$' % (app_label, model_name,), handler.changelist_view))
            patterns.append(url(r'%s/%s/add/$' % (app_label, model_name,), handler.add_view))
            patterns.append(url(r'%s/%s/change/(\d+)/$' % (app_label, model_name,), handler.change_view))
            patterns.append(url(r'%s/%s/del/(\d+)/$' % (app_label, model_name,), handler.change_view))
​
        return patterns
​
    @property
    def urls(self):
        # include的本质:([],app_name,namespace)
        return self.get_urls(), self.app_name, self.namespace
​
​
site = StarkSite()

文件路径:app01/stark.py

from django.shortcuts import HttpResponse
from stark.service.v1 import site
from app01 import models
​
# Depart的视图函数类(增删改查)
class DepartHandler(object):
​
    def __init__(self, model_class):
        self.model_class = model_class
​
    def changelist_view(self, request):
        return HttpResponse('部门列表页面')
​
    def add_view(self, request):
        pass
​
    def change_view(self, request, pk):
        pass
​
    def delete_view(self, request, pk):
        pass
    
site.register(models.Depart, DepartHandler)  # (model类,handler类)
​
# UserInfo的视图函数类(增删改查)
class UserInfoHandler(object):
    def __init__(self, model_class):
        self.model_class = model_class
​
    def changelist_view(self, request):
        return HttpResponse('用户列表页面')
​
    def add_view(self, request): 
        pass
​
    def change_view(self, request, pk):
        pass
​
    def delete_view(self, request, pk):
        pass
​
site.register(models.UserInfo, UserInfoHandler)

文件路径:app02/stark.py

from django.shortcuts import HttpResponse
from stark.service.v1 import site
from app02 import models
​
​
class HostHandler(object):
    # 封装model类
    def __init__(self, model_class):
        self.model_class = model_class
​
    def changelist_view(self, request):
        return HttpResponse('主机列表页面')
​
    def add_view(self, request): 
        pass
​
    def change_view(self, request, pk):
        pass
​
    def delete_view(self, request, pk):
        pass
​
​
site.register(models.Host, HostHandler)
3.2.2 将视图函数提取到基类中

写完上一步骤的代码我们不难发现每一个model的类在执行自己的增删改查时都会调用自己的handler函数(eg. DepartHandler),但是每个handler函数内部的代码都是一样的,这样的话就会有大量重复的代码,所以我们应该优化一下我们的代码:

思路:

  • v1.py 中新创建一个类StarkHandler作为基类;

  • 将增删改查的代码写到基类中

  • 后续的model的handler继承这个基类;

class StarkHandler(object):
    def __init__(self, model_class):
        self.model_class = model_class
​
    def changelist_view(self, request):
        data_list = self.model_class.objects.all()
        return render(request, 'stark/changelist.html', {'data_list': data_list})
​
    def add_view(self, request):
        return HttpResponse('添加页面')
​
    def change_view(self, request, pk):
        # self.models_class
        return HttpResponse('编辑页面')
​
    def delete_view(self, request, pk):
        # self.models_class
        return HttpResponse('删除页面')
    
# 因为每一个model类的handler对象不同,所以继承同一个基类并不影响每个对象封装和调用自己的视图函数(重点)
3.2.3 URL分发扩展

写完上面的代码,我们发现在生成URL做拼接的时候每一个功能的前缀都是相同的(eg. r'%s/%s/list/$' & r'%s/%s/add/$'),那就还可以通过路由include的本质在此基础上再做上一层路由分发实现。

# 步骤1:将原StarkSite类中的 get_urls 函数中url拼接的代码更改为以下代码:
patterns.append(url(r'%s/%s/' % (app_label, model_name,), (handler.get_urls(), None, None)))
​
# 步骤2:在StarkHandler类中创建新函数 get_urls:
def get_urls(self):
    patterns = [
                url(r'^list/$', self.changelist_view),
                url(r'^add/$', self.add_view),
                url(r'^change/(\d+)/$', self.change_view),
                url(r'^delete/(\d+)/$', self.delete_view),
            ]
    # 拓展用的钩子函数
    patterns.extend(self.extra_urls())
    return patterns
​
# 步骤三:定义一个钩子函数使用户可以在自己的handler类拓展需要增加的url
def extra_urls(self):
    return []
​

好处:

  1. 可以在model自己的handler类中重写get_urls方法减少自动生成的url(默认是4个,可自定义)

  2. 可以在model自己的handler类中重写extra_urls方法及视图函数完成增加自动生成的url(默认是4个,可自定义)

3.2.4 为URL设置别名、前缀

之前开发rbac组件的时候我们知道需要为每个URL设置一个别名用来反向生成url,所以在stark组件中我们同样需要为每个url设置别名,也有可能会需要拓展前缀

需求:

  1. 部分url可能需要有自己的前缀(eg. /stark/app01/prve/UserInfo/list);

  1. URL反向生成(别名设置,用于后期业务实现跳转&保留搜索条件的跳转);

实现思路:

class StarkHandler(object):
    def __init__(self, model_class, prev):
        self.model_class = model_class
        self.prev = prev
​
    def changelist_view(self, request):
        data_list = self.model_class.objects.all()
        return render(request, 'stark/changelist.html', {'data_list': data_list})
​
    def add_view(self, request):
        return HttpResponse('添加页面')
​
    def change_view(self, request, pk):
        # self.models_class
        return HttpResponse('编辑页面')
​
    def delete_view(self, request, pk):
        # self.models_class
        return HttpResponse('删除页面')
​
    def get_urls(self):
        app_label, model_name = self.model_class._meta.app_label, self.model_class._meta.model_name
        if self.prev:
            patterns = [
                url(r'^list/$', self.changelist_view, name='%s_%s_%s_list' % (app_label, model_name, self.prev)),
                url(r'^add/$', self.add_view, name='%s_%s_%s_add' % (app_label, model_name, self.prev)),
                url(r'^change/(\d+)/$', self.change_view, name='%s_%s_%s_change' % (app_label, model_name, self.prev)),
                url(r'^delete/(\d+)/$', self.delete_view, name='%s_%s_%s_delete' % (app_label, model_name, self.prev)),
            ]
        else:
            patterns = [
                url(r'^list/$', self.changelist_view, name='%s_%s_list' % (app_label, model_name,)),
                url(r'^add/$', self.add_view, name='%s_%s_add' % (app_label, model_name,)),
                url(r'^change/(\d+)/$', self.change_view, name='%s_%s_change' % (app_label, model_name,)),
                url(r'^delete/(\d+)/$', self.delete_view, name='%s_%s_delete' % (app_label, model_name,)),
            ]
        patterns.extend(self.extra_urls())
        return patterns
​
    def extra_urls(self):
        return []
​
​
class StarkSite(object):
    def __init__(self):
        self._registry = []
        self.app_name = 'stark'
        self.namespace = 'stark'
​
    def register(self, model_class, handler_class=None, prev=None):
        """
​
        :param model_class: 是models中的数据库表对应的类。 models.UserInfo
        :param handler_class: 处理请求的视图函数所在的类
        :param prev: 生成URL的前缀
        :return:
        """
        """
        self._registry = [
            {'prev':None, 'model_class':models.Depart,'handler': DepartHandler(models.Depart,prev)对象中有一个model_class=models.Depart   },
            {'prev':'private', 'model_class':models.UserInfo,'handler':  StarkHandler(models.UserInfo,prev)对象中有一个model_class=models.UserInfo   }
            {'prev':None, 'model_class':models.Host,'handler':  HostHandler(models.Host,prev)对象中有一个model_class=models.Host   }
        ]
        """
        if not handler_class:
            handler_class = StarkHandler
        self._registry.append({'model_class': model_class, 'handler': handler_class(model_class, prev), 'prev': prev})
​
    def get_urls(self):
        patterns = []
        for item in self._registry:
            model_class = item['model_class']
            handler = item['handler']
            prev = item['prev']
            app_label, model_name = model_class._meta.app_label, model_class._meta.model_name
            if prev:
                patterns.append(url(r'^%s/%s/%s/' % (app_label, model_name, prev,), (handler.get_urls(), None, None)))
            else:
                patterns.append(url(r'%s/%s/' % (app_label, model_name,), (handler.get_urls(), None, None)))
​
        return patterns
​
    @property
    def urls(self):
        return self.get_urls(), self.app_name, self.namespace
​
​
site = StarkSite()
3.2.5 知识点总结
  • 自动生成URL:

    • URL拼接;

    • app名称获取:self.model_class._meta.app_label

    • model名称获取:self.model_class._meta.model_name;

  • 类的封装、继承;

  • 钩子函数实现功能自定制。

 

 

3.3 定制页面显示的列

3.3.1 基本列表页面的定制(自定义list_display实现)
  • 列表页面表头、内容

  • 未定义 list_display 字段的页面,默认显示对象

# v1.py
class StarkHandler(object):
    list_display = []
    
    def changelist_view(self, request):
        """
        列表页面
        :param request:
        :return:
        """
​
        # 1. 处理表格的表头
        if list_display:
            header_list = []
            for key in self.list_display:
                verbose_name = self.model_class._meta.get_field(key).verbose_name
                header_list.append(verbose_name)
        else:
            header_list.append(self.model_class._meta.model_name)
​
        # 2. 处理表的内容
        data_list = self.model_class.objects.all()
        body_list = []
        for row in data_list:
            tr_list = []
            if list_display:
                for key in self.list_display:
                    tr_list.append(getattr(row, key))
            else:
                tr_list.append(row)
            body_list.append(tr_list)
​
        return render(
            request,
            'stark/changelist.html',
            {
                'data_list': data_list,
                'header_list': header_list,
                'body_list': body_list
            }
        )
# app01/stark.py
from django.conf.urls import url
from django.shortcuts import HttpResponse
from stark.service.v1 import site, StarkHandler
from app01 import models
​
​
# http://127.0.0.1:8000/stark/app01/depart/list/
class DepartHandler(StarkHandler):
    list_display = ['title']  # 定义页面需要显示的字段
​
​
site.register(models.Depart, DepartHandler)
​
​
# http://127.0.0.1:8000/stark/app01/userinfo/list/
class UserInfoHandler(StarkHandler):
    # 定制页面显示的列
    list_display = ['name', 'age', 'email']
​
​
site.register(models.UserInfo, UserInfoHandler)

 

3.3.2 为页面显示的列预留一个钩子函数

目前可以根据model自己的handler所写的list_display来让页面显示相应的列,但是如果后期需要根据不同用户(可能拥有不同权限)而出现需要显示不同的字段应该怎么解决呢?

思路:

  • 预留钩子函数get_list_display,使后期自定义显示的列成为可能;

# v1.py
    def get_list_display(self):
        """
        获取页面上应该显示的列,预留的自定义扩展,例如:以后根据用户的不同显示不同的列
        :return:
        """
        value = []
        value.extend(self.list_display)
        return value
    
    
    def changelist_view(self, request):
        """
        列表页面
        :param request:
        :return:
        """
​
        list_display = self.get_list_display()
        # 1. 处理表格的表头
        header_list = []
        if list_display:
            for key in list_display:
                verbose_name = self.model_class._meta.get_field(key).verbose_name
                header_list.append(verbose_name)
        else:
            header_list.append(self.model_class._meta.model_name)
​
        # 2. 处理表的内容
        data_list = self.model_class.objects.all()
        body_list = []
        for row in data_list:
            tr_list = []
            if list_display:
                for key in list_display:
                    tr_list.append(getattr(row, key))
            else:
                tr_list.append(row)
            body_list.append(tr_list)
​
        return render(
            request,
            'stark/changelist.html',
            {
                'data_list': data_list,
                'header_list': header_list,
                'body_list': body_list
            }
        )
  • modelhandler重写get_list_play实现字段定制:

class UserInfoHandler(StarkHandler):
    # 定制页面显示的列
    list_display = ['name', 'age', 'email']
​
    def get_list_display(self):
        """
        自定义扩展,例如:根据用户的不同显示不同的列
        :return:
        """
        return ['name', 'age']
​
​
site.register(models.UserInfo, UserInfoHandler)
3.3.3 为页面提供自定义显示的函数(用于添加”编辑“”删除“按钮)

list_display目前只能实现表头和内容的展示,但是如果想在页面上显示例如“编辑”,“删除”的按钮应该怎么办呢?

这里就需要用到自定义显示的的函数:

  1. 先在列表页面进行相关的代码更改:

    def changelist_view(self, request):
        """
        列表页面
        :param request:
        :return:
        """
​
        list_display = self.get_list_display()
        # 1. 处理表格的表头
        header_list = []
        if list_display:
            for key_or_func in list_display:
                # 判断 list_display 里面的是否是函数
                if isinstance(key_or_func, FunctionType):
                    # 是函数直接调用函数(由类直接调用,手动传如所有3个参数)
                    verbose_name = key_or_func(self, obj=None, is_header=True)
                else:
                    verbose_name = self.model_class._meta.get_field(key_or_func).verbose_name
                header_list.append(verbose_name)
        else:
            header_list.append(self.model_class._meta.model_name)
​
        # 2. 处理表的内容
        data_list = self.model_class.objects.all()
​
        body_list = []
        for row in data_list:
            tr_list = []
            if list_display: 
                # 判断 key_or_func 是否是函数
                for key_or_func in list_display:
                    if isinstance(key_or_func, FunctionType):
                        # 此时对象为quaryset对象row,因为不是标题 is_header = False
                        tr_list.append(key_or_func(self, row, is_header=False))
                    else:  # 不是函数就去取对象.字段
                        tr_list.append(getattr(row, key_or_func))  # obj.gender
            else:
                tr_list.append(row)
            body_list.append(tr_list)
​
        return render(
            request,
            'stark/changelist.html',
            {
                'data_list': data_list,
                'header_list': header_list,
                'body_list': body_list
            }
        )
​
  1. 然后自定义函数后写到 list_display 中 (默认写在基类StrakHandler里方便其他handler对象调用)

# 默认写在基类StrakHandler里方便其他handler对象调用,也可以在model自己的handler里重写自定义显示函数  
    def display_edit(self, obj=None, is_header=None):
        """
        自定义页面显示的列(表头和内容)
        :param obj:
        :param is_header:
        :return:
        """
        # is_header 默认为None(表示不是表头)
        if is_header:
            return "编辑"
        # 之前在StarkHandler类中写过get_change_url_name方法
        # 注意拿到的url还缺了跟路由的namespace,需要拼接
        name = "%s:%s" % (self.site.namespace, self.get_change_url_name,)
        # marksafe表示让前端渲染此字符串
        return mark_safe('<a href="%s">编辑</a>' % reverse(name, args=(obj.pk,)))
​
    def display_del(self, obj=None, is_header=None):
        if is_header:
            return "删除"
        name = "%s:%s" % (self.site.namespace, self.get_delete_url_name,)
        return mark_safe('<a href="%s">删除</a>' % reverse(name, args=(obj.pk,)))

到这里大家代码写到这里一定觉得此刻如沐春风,一切都是那么的完美,但是还有一点美中不足,是什么呢?

如果model表结构里出现了choice字段,那么列表页面会显示什么呢?

列表页面会显示数字,因为数据库中存的是数字:

解决方式:

方法一:自定义显示字段方法(choice字段不多的情况下可以用)

注意:choice字段可以通过对象._字段名_display()显示

 

方法二:通过闭包函数实现(适用于choice字段很多的情况)

# 闭包函数
def get_choice_text(title, field):
    """
    对于Stark组件中定义列时,choice如果想要显示中文信息,调用此方法即可。
    :param title: 希望页面显示的表头
    :param field: 字段名称
    :return:
    """
​
    def inner(self, obj=None, is_header=None):
        if is_header:
            return title
        method = "get_%s_display" % field
        return getattr(obj, method)()
​
    return inner

在把函数按如下方式写入list_display中:

 

3.3.4 应用模板样式(BootStrap)
  • 静态文件的导入(路径:stark/static)

 

 

3.3.5 列表页面添加分页功能(pagenation组件+BootStrap分页组件)

 

 

3.3.6 页面添加按钮
3.3.6.1 如何实现添加按钮

在实现添加按钮的时候,我们应该考虑到这样一个使用场景:要根据后期用户的权限来控制按钮是否显示,这就需要为此功能用钩子方法做上拓展。

  1. 我们先在StarkHandler类里写上一个get_add_btn方法:

# v1.py
    has_add_btn = True  # 默认显示按钮
    def get_add_btn(self):
        if self.has_add_btn:
            return "<a class='btn btn-primary' href='#'>添加</a>"
        return None
  1. 然后在列表页面写上添加按钮:

# ##########3. 添加按钮 #########
        add_btn = self.get_add_btn()
  1. 接着用户就可以在model类的handler中自定制是否显示添加按钮:

# app01/stark.py
class UserInfoHandler(StarkHandler):
    # 定制页面显示的列
    list_display = ['name',
                    get_choice_text('性别', 'gender'),
                    get_choice_text('班级', 'classes'),
                    'age', 'email', 'depart',
                    StarkHandler.display_edit,
                    StarkHandler.display_del]
​
    per_page_count = 1
​
    has_add_btn = True
3.3.6.2 添加按钮的URL

还记的rbac中保留当前页面条件的跳转吗?在这里我们点击了添加按钮后,还需要把当前的搜索条件保留带到跳转页面中,添加完后在保留带回跳转前的页面来实现保留原页面搜索条件的功能:

  1. 点击添加按钮跳转至添加页面:(需携带原页面GET请求体)

    def reverse_add_url(self):
        name = "%s:%s" % (self.site.namespace, self.get_add_url_name,)
        base_url = reverse(name)
        if not self.request.GET:  # 如果没有携带GET参数
            add_url = base_url
        else:
            param = self.request.GET.urlencode()  # 获取到GET参数
            new_query_dict = QueryDict(mutable=True)  # 实现修改
            new_query_dict['_filter'] = param  # 转义
            add_url = "%s?%s" % (base_url, new_query_dict.urlencode())  # 拼接URL
        return add_url
    
    # get_add_btn调用reverse_add_url拿到url执行跳转
    def get_add_btn(self):
        if self.has_add_btn:
            return "<a class='btn btn-primary' href='%s'>添加</a>" % self.reverse_add_url()
        return None
  1. 添加完成跳转回原页面:(将原来的GET参数携带跳回)

    def reverse_list_url(self):
        name = "%s:%s" % (self.site.namespace, self.get_list_url_name,)
        base_url = reverse(name)
        param = self.request.GET.get('_filter')  # 获取到参数
        if not param:  # 没取到表示没有携带的参数
            return base_url
        return "%s?%s" % (base_url, param,)
3.3.6.3 添加页面进行添加数据(ModelForm)

跳转到添加页面后我们就可以通过我们熟悉的ModelForm来实现添加字段的展示啦!

还是熟悉的配方,熟悉的味道!

首先我们先在v1.py中写一个get_model_form_class方法,并留下一个钩子,如下:

    def get_model_form_class(self):
        if self.model_form_class:  # 设置钩子,允许handler对象自定义显示字段
            return self.model_form_class
​
        class DynamicModelForm(forms.ModelForm):
            class Meta:
                model = self.model_class
                fields = "__all__"  # 默认展示model的所有字段
​
        return DynamicModelForm

接着可以在handler对象中重写model_form_class自定义显示字段:

class UserInfoModelForm(StarkModelForm):
    class Meta:
        model = models.UserInfo
        fields = ['name', 'gender', 'classes', 'age', 'email']
​
​
class UserInfoHandler(StarkHandler):
    # 定制页面显示的列
    list_display = ['name',
                    get_choice_text('性别', 'gender'),
                    get_choice_text('班级', 'classes'),
                    'age', 'email', 'depart',
                    StarkHandler.display_edit,
                    StarkHandler.display_del]
​
    per_page_count = 1
​
    has_add_btn = True
​
    model_form_class = UserInfoModelForm
​
    def save(self, form, is_update=False):
        form.instance.depart_id = 1
        form.save()

用户在填写完数据后提交到后台做验证:

添加页面后台验证:

# v1.py
    def save(self, form, is_update=False):
        """
        在使用ModelForm保存数据之前预留的钩子方法
        :param form:
        :param is_update:
        :return:
        """
        form.save()
    
    
    def add_view(self, request):
        """
        添加页面
        :param request:
        :return:
        """
        model_form_class = self.get_model_form_class()
        if request.method == 'GET':
            form = model_form_class()
            return render(request, 'stark/change.html', {'form': form})
        form = model_form_class(data=request.POST)
        if form.is_valid():
            self.save(form, is_update=False)  # 这里重写了save方法
            # 在数据库保存成功后,跳转回列表页面(携带原来的参数)。
            return redirect(self.reverse_list_url())
        return render(request, 'stark/change.html', {'form': form})

app01/stark.py

class UserInfoModelForm(StarkModelForm):
    class Meta:
        model = models.UserInfo
        fields = ['name', 'gender', 'classes', 'age', 'email']
​
​
class UserInfoHandler(StarkHandler):
    # 定制页面显示的列
    list_display = ['name',
                    get_choice_text('性别', 'gender'),
                    get_choice_text('班级', 'classes'),
                    'age', 'email', 'depart',
                    StarkHandler.display_edit,
                    StarkHandler.display_del]
​
    per_page_count = 1
​
    has_add_btn = True
​
    model_form_class = UserInfoModelForm
​
    def save(self, form, is_update=False):
        form.instance.depart_id = 1
        form.save()

但是在自定义页面显示字段时可能会有字段需要提交数据,但是开发人员并不需要用户提交的情况,这时就需要设置字段默认值form.instance.depart_id = 1,所以就重写了save方法,目的是抛出一个钩子让handler自定默认值。

同时这里还有一个小问题需要处理,在添加页面做添加操作时需要orm拿到数据库里的数据,就需要request参数,那么问题来了,怎么拿到呢?

通过装饰器:

    def wrapper(self, func):
        @functools.wraps(func)  # 保证原函数不变
        def inner(request, *args, **kwargs):
            self.request = request
            return func(request, *args, **kwargs)
​
        return inner
​
    def get_urls(self):
        patterns = [
            url(r'^list/$', self.wrapper(self.changelist_view), name=self.get_list_url_name),
            url(r'^add/$', self.wrapper(self.add_view), name=self.get_add_url_name),
            url(r'^change/(\d+)/$', self.wrapper(self.change_view), name=self.get_change_url_name),
            url(r'^delete/(\d+)/$', self.wrapper(self.delete_view), name=self.get_delete_url_name),
        ]
​
        patterns.extend(self.extra_urls())
        return patterns

到此页面添加功能就全部实现了

 

3.3.7 页面编辑按钮&删除按钮
  • 编辑按钮(删除按钮)

  • 页面操作

 

4.其他常用功能(重点)

4.1 排序

配置order_list来实现排序功能:

# v1.py
class StarkHandler(object):
    order_list = []
​
    def get_order_list(self):
        return self.order_list or ['-id', ]
    
    def changelist_view(self, request):
        # ########## 1. 获取排序 ##########
        order_list = self.get_order_list()  # 排序的规则
        queryset = self.model_class.objects.all().order_by(*order_list)  # 查找数据库

handler基类中配置上order_list,继承基类的handler可以通过get_order_list方法重写order_list来自定义排序规则,不写的话order_list默认为空,get_order_list默认已id倒序排列。

class UserInfoHandler(StarkHandler):
    # 定制页面显示的列
    list_display = ['name',
                    get_choice_text('性别', 'gender'),
                    get_choice_text('班级', 'classes'),
                    'age', 'email', 'depart',
                    StarkHandler.display_edit,
                    StarkHandler.display_del]
​
    per_page_count = 10
    has_add_btn = True
    model_form_class = UserInfoModelForm
​
    order_list = ['id']  # 配置order_list定制排序规则
​
    # 姓名中含有关键字或邮箱中含有关键字
    search_list = ['name__contains', 'email__contains']
​
    def save(self, form, is_update=False):
        form.instance.depart_id = 1
        form.save()

 

4.2 模糊搜索

  • 实现思路:

    在页面上设置form表单,搜索:以GET形式提交请求到后台。后台获取数据然后进行筛选。

    后端获取到关键字之后,根据定义的列进行查找(多列的话可以按照 ” 或 “ 查询)

    def get_search_list(self):
        return self.search_list  # 钩子
    
    
    def changelist_view(self, request):
        """
        列表页面
        :param request:
        :return:
        """
​
        search_list = self.get_search_list()  # 钩子
        search_value = request.GET.get('q', '')  # 获取form表单提交的请求
        # Django Q查询
        conn = Q()
        conn.connector = 'OR'
        if search_value:
            for item in search_list:
                conn.children.append((item, search_value))
class UserInfoHandler(StarkHandler):
    # 定制页面显示的列
    list_display = ['name',
                    get_choice_text('性别', 'gender'),
                    get_choice_text('班级', 'classes'),
                    'age', 'email', 'depart',
                    StarkHandler.display_edit,
                    StarkHandler.display_del]
​
    per_page_count = 10
    has_add_btn = True
    model_form_class = UserInfoModelForm
​
    order_list = ['id']
​
    # 姓名中含有关键字或邮箱中含有关键字
    # 去掉__contains表示精确查找
    search_list = ['name__contains', 'email__contains']
​
    def save(self, form, is_update=False):
        form.instance.depart_id = 1
        form.save()

4.3 批量操作

为什么要使用批量操作?

在处理数据时容易遇到重复的编辑或删除操作,效率低下,因而需要批量操作的功能

实现方式:通过checkbox + input标签下拉框实现

  1. 模板部分:

    构建checkbox,下拉框和form表单submit提交请求

{% if action_dict %}  # 如果action_dict为空页面不显示下拉框
                <div style="float: left;margin: 5px 10px 5px 0;">
                    <div class="form-inline">
                        <div class="form-group">
                            <select class="form-control" name="action">
                                <option value="">请选择操作</option>
                                {% for func_name,func_text in action_dict.items %}
                                    <option value="{{ func_name }}">{{ func_text }}</option>
                                {% endfor %}
                            </select>
                            <input class="btn btn-primary" type="submit" value="执行"/>
                        </div>
                    </div>
                </div>
            {% endif %}

 

  1. 后台部分

    v1.py

    def action_multi_delete(self, request, *args, **kwargs):
        """
        批量删除(如果想要定制执行成功后的返回值,那么就为action函数设置返回值即可。)
        :return:
        """
        pk_list = request.POST.getlist('pk')
        self.model_class.objects.filter(id__in=pk_list).delete()
​
    action_multi_delete.text = "批量删除"
    
    
    
    action_list = []  # 钩子
​
    def get_action_list(self):
        return self.action_list
    
    def changelist_view(self, request, *args, **kwargs):
        # ########## 1. 处理Action ##########
        action_list = self.get_action_list()
        action_dict = {func.__name__: func.text for func in action_list}
        # {'multi_delete':'批量删除','multi_init':'批量初始化'}
        # 字典封装传递后把action_dict传递给模板做下拉框,不然模板无法显示
​
        # 收到用户提交的post请求(批量操作请求)
        if request.method == 'POST':
            action_func_name = request.POST.get('action')
            if action_func_name and action_func_name in action_dict:
                action_response = getattr(self, action_func_name)(request, *args, **kwargs) # 映射执行函数
                if action_response:
                    return action_response
​

app01/stark

class UserInfoHandler(StarkHandler):
    # 定制页面显示的列
    list_display = [StarkHandler.display_checkbox,
                    'name',
                    get_choice_text('性别', 'gender'),
                    get_choice_text('班级', 'classes'),
                    'age', 'email', 'depart',
                    StarkHandler.display_edit,
                    StarkHandler.display_del]
​
    per_page_count = 10
    has_add_btn = True
    model_form_class = UserInfoModelForm
​
    order_list = ['id']
​
    search_list = ['name__contains', 'email__contains']
    
    # 列表中放置的是函数
    action_list = [StarkHandler.action_multi_delete, ]
​
    def save(self, form, is_update=False):
        form.instance.depart_id = 1
        form.save()

 

 

4.4 组合搜索(重难点)

  • 什么是组合搜索

  • 如何实现组合搜索

    • 实现思路,根据字段找到相关联的数据:choices、FK、M2M

    • 第一步:配置(基类中配置)

    # v1.py
        search_group = []
    ​
        def get_search_group(self):  # 钩子
            return self.search_group
        
    # ########## 7. 组合搜索 #########
        search_group = self.get_search_group()  # ['gender', 'depart']
        for option_object in search_group:
            option_object.get_queryset_or_tuple(self.model_class, request, *args, **kwargs)
    • 第二步:根据配置获取关联数据

        def get_queryset_or_tuple(self, model_class, request, *args, **kwargs):
            """
            根据字段去获取数据库关联的数据
            :return:
            """
            # 根据gender或depart字符串,去自己对应的Model类中找到 字段对象
            field_object = model_class._meta.get_field(self.field)
            # 获取关联数据,判断是元组(choice字段)还是queryset(FK、M2M)
            if isinstance(field_object, ForeignKey) or isinstance(field_object, ManyToManyField):
                # FK和M2M,应该去获取其关联表中的数据
                db_condition = self.get_db_condition(request, *args, **kwargs)
                print(self.field, field_object.related_model.objects.filter(**db_condition))
            else:
                # 获取choice中的数据
                print(self.field, field_object.choices)
    • 第三步:根据配置获取关联数据(含条件)

      思路:

      用类封装搜索条件搜索字段

    # v1.py
    class Option(object):
        def __init__(self, field, db_condition=None):
            """
            :param field: 组合搜索关联的字段
            :param db_condition: 数据库关联查询时的条件
            """
            self.field = field
            if not db_condition:
                db_condition = {}
            self.db_condition = db_condition
        
        # 钩子:允许handler类继承option重写类方法get_db_condition
        def get_db_condition(self, request, *args, **kwargs):  
            return self.db_condition
        
        
        def get_queryset_or_tuple(self, model_class, request, *args, **kwargs):
            """
            根据字段去获取数据库关联的数据
            :return:
            """
            # 根据gender或depart字符串,去自己对应的Model类中找到 字段对象
            field_object = model_class._meta.get_field(self.field)
            # 获取关联数据
            if isinstance(field_object, ForeignKey) or isinstance(field_object, ManyToManyField):
                # FK和M2M,应该去获取其关联表中的数据
                db_condition = self.get_db_condition(request, *args, **kwargs)
                print(self.field, field_object.related_model.objects.filter(**db_condition))
            else:
                # 获取choice中的数据
                print(self.field, field_object.choices)
    class MyOption(Option):
        def get_db_condition(self, request, *args, **kwargs):
            return {}
    ​
    ​
    class UserInfoHandler(StarkHandler):
        # 将search_group列表里的元素从字符串变为option对象
        search_group = [
            Option('gender'),
            MyOption('depart', {'id__gt': 2}),
        ]

    至此已经获取到了组合搜索所需要的数据

    • 第四步:在页面上显示组合搜索的按钮

      • queryset和元组进行封装(封装到自定义类中,并实例化为此类对象,运用 __iter__ +yield(生成器)方法生成前端页面标签展示)

     # ########## 7. 组合搜索 #########
            # 存的是SearchGroupRow的对象
            search_group_row_list = []
            search_group = self.get_search_group()  # ['gender', 'depart']
            for option_object in search_group:
                row = option_object.get_queryset_or_tuple(self.model_class, request, *args, **kwargs)
                search_group_row_list.append(row)

     

        def get_queryset_or_tuple(self, model_class, request, *args, **kwargs):
            """
            根据字段去获取数据库关联的数据
            :return:
            """
            # 根据gender或depart字符串,去自己对应的Model类中找到 字段对象
            field_object = model_class._meta.get_field(self.field)
            title = field_object.verbose_name
            # 获取关联数据
            if isinstance(field_object, ForeignKey) or isinstance(field_object, ManyToManyField):
                # FK和M2M,应该去获取其关联表中的数据: QuerySet
                db_condition = self.get_db_condition(request, *args, **kwargs)
                return SearchGroupRow(title, field_object.rel.model.objects.filter(**db_condition), self, request.GET)
            else:
                # 获取choice中的数据:元组
                self.is_choice = True
                return SearchGroupRow(title, field_object.choices, self, request.GET)

     

    • 第五步:为组合搜索按钮生成URL

      • 生成URL时不影响其他组的条件

      • 条件筛选

      class SearchGroupRow(object):
          def __init__(self, title, queryset_or_tuple, option, query_dict):
              """
      ​
              :param title: 组合搜索的列名称
              :param queryset_or_tuple: 组合搜索关联获取到的数据
              :param option: 配置
              :param query_dict: request.GET
              """
              self.title = title
              self.queryset_or_tuple = queryset_or_tuple
              self.option = option
              self.query_dict = query_dict
      ​
          def __iter__(self):
              yield '<div class="whole">'
              yield self.title
              yield '</div>'
              yield '<div class="others">'
              total_query_dict = self.query_dict.copy()
              total_query_dict._mutable = True
      ​
              origin_value_list = self.query_dict.getlist(self.option.field)
              if not origin_value_list:
                  yield "<a class='active' href='?%s'>全部</a>" % total_query_dict.urlencode()
              else:
                  total_query_dict.pop(self.option.field)
                  yield "<a href='?%s'>全部</a>" % total_query_dict.urlencode()
      ​
              for item in self.queryset_or_tuple:
                  text = self.option.get_text(item)
                  value = self.option.get_value(item)
                  query_dict = self.query_dict.copy()
                  query_dict._mutable = True
      ​
                  query_dict[self.option.field] = value
                  if str(value) in origin_value_list:
                      query_dict.pop(self.option.field)
                      yield "<a class='active' href='?%s'>%s</a>" % (query_dict.urlencode(), text)
                  else:
                      yield "<a href='?%s'>%s</a>" % (query_dict.urlencode(), text)
      ​
              yield '</div>'
      • 多选

 

总结

Stark组件实现功能:

  • 页面:列表、添加、编辑、删除

  • 排序、模糊搜索、批量操作、组合搜索

posted @ 2022-04-06 17:00  楽仔  阅读(72)  评论(0)    收藏  举报