自定义 Django admin 组件

时间:2021-01-11 19:18:14

 

 摘要:学习 Django admin 组件,仿照源码的逻辑,自定义了一个简易的 stark 组件,实现类似 admin 的功能。

    可自动生成 url 路由,对于model 有与之相应的配置类对象,可进行增、删、改、查的操作。

    通过继承 ModelStark 配置类,重写类参数和方法,可自定义查找显示信息,模糊查询字段,批量操作方法等


 

那么让我们开始吧~

 

思路:

  在启动时------>  1、遍历所有app的stark.py文件,注册model

          2、自动生成,注册的model对应的url

  执行时 ------->  1、url路由分发,调用配置类中相应的方法

         2、在 ModelStark 配置类中处理数据、渲染页面 

         3、返回 Responce 对象


 

具体实现:

    一、先创建一个组件 stark

              自定义 Django admin 组件

  二、并在组件的 app.py 文件中,写下遍历的其他APP的方法

from django.apps import AppConfig
from django.utils.module_loading import autodiscover_modules


class StarkConfig(AppConfig):
    name = 'stark'

    def ready(self):
        #这句表示 自动遍历所有app 的 stark.py 文件
        autodiscover_modules('stark')    

   注意:在settings.py 文件中 ,注册 stark 组件时,一定要写上,只写 ‘stark’ 的话,不会执行ready的

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01.apps.App01Config',
"stark.apps.StarkConfig"
]

    三、 在 组件的 stark.py文件中

    定义一个 单例的 全局类 StrakSite ,该类控制以及路由的生成,并调用配置类,生成第二级的路由

class StrakSite:
    def __init__(self, name='stark'):
        self.name = name
        # 注册了的所有表与表的配置类对象
        self._registry = {}

    def register(self, model, model_config=None):
        if not model_config:
            #如果没有自定义配置类,就用父类的配置类
            model_config = ModelStark

        self._registry[model] = model_config(model)

    #  URL分发 第一级
    def get_urls(self):
        temp = []
        for model, model_config in self._registry.items():
            app_label = model._meta.app_label
            model_name = model._meta.model_name
            temp.append(url(r'^%s/%s/' % (app_label, model_name), model_config.urls))
        return temp

    @property
    def urls(self):
                    #路由分发
        return self.get_urls(), None, None



# 单例模式 所有的model共享同一个StrakSite对象
site = StrakSite()

 

     配置类,在python代码里生成前端HTML标签:    

class ModelStark:
    list_display = ['__str__']  # 列表时,定制显示的列。
    modelfoem_class = []  # 自定义配置类
    list_display_links = []  # list页面点击可以跳转到编辑页面的字段
    search_fields = []  # 搜索框,自定义的搜索字段
    actions = []  # 自定义批处理函数列表
    list_filter = []  # 自定义 右侧筛选字段

    def __init__(self, model):
        self.model = model
        self.app_label = model._meta.app_label
        self.model_name = model._meta.model_name

        meta= model._meta
        print(meta)

    #  生成复选框
    def checkbox_col(self, obj=None, is_header=False):
        '''
        生成复选框
        :param is_hander:
        :return:
        '''
        if is_header:
            return '选择'
        # _url=
        return mark_safe('<input type="checkbox" name="selected_action" value="%s">' % obj.pk)

    # 反向解析,获取URL
    def get_url(self, type, id=None):
        '''
        反向解析,获取URL
        :param type:
        :param id:
        :return:
        '''
        name = '%s_%s_%s' % (self.app_label, self.model_name, type)
        if id:
            return reverse(name, args=(id,))
        else:
            return reverse(name)

    # 生成删除按钮
    def del_col(self, obj=None, is_header=False):
        '''
        生成删除按钮
        :param is_hander:
        :return:
        '''
        if is_header:
            return '删除'
        _url = self.get_url('delete', obj.pk)
        return mark_safe('<a id="%s_delete" href="%s">删除</a>' % (self.model_name, _url))

    # 生成添加a标签
    def add_col(self):
        '''
        生成添加按钮
        :param is_hander:
        :return:
        '''
        _url = self.get_url('add')
        return mark_safe('<a id="%s_add" href="%s"><span class="glyphicon glyphicon-plus pull-lift"></span></a>'
                         % (self.model_name, _url))

    # 生成编辑按钮
    def edit_col(self, obj=None, is_header=False):
        '''
        生成编辑按钮
        :param is_hander:
        :return:
        '''
        if is_header:
            return '编辑'
        _url = self.get_url('edit', obj.pk)
        return mark_safe('<a id="%s_edit" href="%s">编辑</a>' % (self.model_name, _url))

    # 按照表格显示需要的列格式,将每个字段按顺序放进列表里
    def get_new_list_display(self):
        new_display = []
        new_display.extend(self.list_display)
        # 这里一定要传 类名.方法名  如果用self.方法名的话
        # 调用的时候 这里传入的方法会自动传self值,而我们自定义的display方法并不会,所以在通用方法处理时会报错
        new_display.insert(0, ModelStark.checkbox_col)
        new_display.extend([ModelStark.edit_col, ModelStark.del_col])
        return new_display

    #
    def list(self, request):

        self.data_list = self.model.objects.all()
        # 列表展示页面类
        show = ShowList(self, request, self.data_list)

        # 如果有自定义查询字段且输入了查询内容 该操作要写在分页之前,因为对data_list进行了修改
        self.search_info = request.GET.get("search")
        if self.search_fields and self.search_info:
            show.get_search(self.search_info)

        # 批量操作
        action_list = show.get_actions()
        if request.method == "POST":
            action_info = request.POST.get("action")
            if self.actions and action_info:
                id_list = request.POST.getlist("selected_action")
                data = self.model.objects.filter(pk__in=id_list)
                action = getattr(self, action_info)
                action(request,data)

        # 生成左侧筛选栏  该操作要写在分页之前,因为对data_list进行了修改
        filter_html = None
        if self.list_filter:
            filter_html = show.get_filter()

        # 分页HTML
        page_html = show.get_page_html()
        # 表头
        title_list = show.get_hander()
        # 表格内容
        table_list = show.get_body()
        # 生成 添加a标签
        add_opp = self.add_col()

        return render(request, 'stark/list.html',
                      {'title_list': title_list,
                       'table_list': table_list,
                       'page_html': page_html,
                       'add_opp': add_opp,
                       'action_list': action_list,
                       'search_about': (self.search_fields, self.search_info),
                       'filter_html': filter_html,
                       })

    #
    def add(self, request):

        if request.method == 'GET':
            # 生成modelform对象
            form = self.modelform_class()

            from django.forms.boundfield import BoundField
            # bfield 是 BoundField类的子类
            for bfield in form:
                if isinstance(bfield.field, ModelChoiceField):
                    bfield.is_pop = True
                    # 得到该字段的关联表
                    filed_rel_model = self.model._meta.get_field(bfield.name).rel.to
                    app_label = filed_rel_model._meta.app_label
                    model_name = filed_rel_model._meta.model_name
                    name = '%s_%s_add' % (app_label, model_name)
                    _url = reverse(name)
                    bfield.url = _url + "?pop_back_id=" + bfield.auto_id
            return render(request, 'stark/add.html', {'form': form})
        else:
            # 得到带参数的modelform对象
            form = self.modelform_class(request.POST)

            from django.forms.boundfield import BoundField
            # bfield 是 BoundField类的子类
            for bfield in form:
                if isinstance(bfield.field, ModelChoiceField):
                    bfield.is_pop = True
                    # 得到该字段的关联表
                    filed_rel_model = self.model._meta.get_field(bfield.name).rel.to
                    app_label = filed_rel_model._meta.app_label
                    model_name = filed_rel_model._meta.model_name
                    name = '%s_%s_add' % (app_label, model_name)
                    _url = reverse(name)
                    bfield.url = _url + "?pop_back_id=" + bfield.auto_id

                    # 校验页面填值
            if form.is_valid():
                # 保存添加的数据
                obj = form.save()

                try:
                    pop_back_id = request.GET.get('pop_back_id')
                    if pop_back_id:
                        return render(request, 'stark/close.html',
                                      {'pop_back_id': pop_back_id, 'text': str(obj), 'pk': obj.pk})
                except Exception:
                    pass

                # 跳转回list页面
                _url = self.get_url('list')
                return redirect(_url)
            else:
                return render(request, 'stark/add.html', {'form': form})

    #
    def delete(self, request, id):
        self.model.objects.get(pk=id).delete()
        _url = self.get_url('list')
        return redirect(_url)

    #
    def edit(self, request, id):
        model_obj = self.model.objects.get(pk=id)
        if request.method == 'GET':
            # 生成一个带有model对象内容的modelform对象
            form = self.modelform_class(instance=model_obj)
            return render(request, 'stark/edit.html', {'form': form})
        else:
            form = self.modelform_class(request.POST, instance=model_obj)
            if form.is_valid():
                form.save()
                _url = self.get_url('list')
                return redirect(_url)
            else: return render(request, 'stark/edit.html', {'form': form}) def extra_urls(self):
        return [] # URL分发 第二级
    def get_urls(self): temp = [] #
        temp.append(url(r'add/$', self.add,
                        name='%s_%s_add' % (self.app_label, self.model_name)))
        #
        temp.append(url(r'(?P<id>\d+)/delete/$', self.delete,
                        name='%s_%s_delete' % (self.app_label, self.model_name)))
        #
        temp.append(url(r'(?P<id>\d+)/edit/$', self.edit,
                        name='%s_%s_edit' % (self.app_label, self.model_name)))
        #
        temp.append(url(r'$', self.list,
                        name='%s_%s_list' % (self.app_label, self.model_name)))

        temp.extend(self.extra_urls())

        print(temp)

        return temp

    @property
    def urls(self):
        return self.get_urls(), None, None

    # 获取modelform类
    @property
    def modelform_class(self):
        if self.modelfoem_class:
            return self.modelfoem_class
        else:
            class ModelFormClass(forms.ModelForm):
                class Meta:
                    model = self.model
                    fields = '__all__'

            return ModelFormClass

 

     因为查询页面太大,所以单独抽成一个ShowList类,其中封装了list页面的表头、表格内容、分页、搜索栏、批量操作栏、标签查找栏等

自定义 Django admin 组件自定义 Django admin 组件
class ShowList(object):
    def __init__(self, conf_obj, request, data_list):
        self.conf_obj = conf_obj
        self.data_list = data_list
        self.new_list_display = self.conf_obj.get_new_list_display()
        self.request = request

    # 获取列表的表头
    def get_hander(self):
        # 表头
        title_list = []
        for field in self.new_list_display:
            if isinstance(field, str):
                # 如果没有自己传list_display进来,默认的是'__str__',则打印大写的表名
                if field == '__str__':
                    field = self.conf_obj.model_name.upper()

                else:
                    # 获取字段的对象
                    field_obj = self.conf_obj.model._meta.get_field(field)
                    # 从字段对象中获取字段的verbose_name,在model模型类里写的
                    field = field_obj.verbose_name
            else:
                # 如果不是表字段的话,执行对应的方法 eg: edit delete
                field = field(self.conf_obj, is_header=True)
            title_list.append(field)
        return title_list

    # 获取表格内容
    def get_body(self):
        table_list = []
        for obj in self.data_list:
            list_info = []
            for field in self.new_list_display:
                if isinstance(field, str):
                    try:
                        if self.conf_obj.model._meta.get_field(field).choices:
                            field_val = getattr(obj, 'get_%s_display' %field )
                        else:
                            field_val = getattr(obj, field)
                        if field in self.conf_obj.list_display_links:
                            _url = self.conf_obj.get_url('edit', obj.pk)
                            field_val = mark_safe(
                            '<a id="%s_edit" href="%s">%s</a>' % (self.conf_obj.model_name, _url, field_val))
                    except Exception as e:
                        #  __str__ 的字段
                        field_val = getattr(obj, field)
                else:
                    field_val = field(self.conf_obj, obj=obj)
                list_info.append(field_val)
            table_list.append(list_info)
        return table_list

    # 获取自定义分页的THML
    def get_page_html(self):
        # 来源url
        url_prefix = self.request.get_full_path()
        # 数据总量
        total_num = self.data_list.count()
        # 当前页
        current_page = self.request.GET.get('page')
        # 生成页面对象
        self.page_obj = Page(total_num, current_page, url_prefix, per_page=10, show_page_num=9)
        # 得到当前页展示的数据列表
        self.data_list = self.data_list[self.page_obj.data_start:self.page_obj.data_end]
        # 得到对应的分页HTML
        page_html = self.page_obj.page_html()
        return page_html

    # 搜索栏操作
    def get_search(self, search_info):
        search_condition = Q()
        search_condition.connector = "or"
        for field in self.conf_obj.search_fields:
            # 模糊查询
            search_condition.children.append((field + "__icontains", search_info))
        self.data_list = self.conf_obj.model.objects.filter(search_condition)

    # 自定义批量操作
    def get_actions(self):
        temp = []
        for action in self.conf_obj.actions:
            temp.append({
                'name': action.__name__,
                'desc': action.desc
            })
        return temp

    # 右侧筛选栏
    def get_filter(self):

        self.params = copy.deepcopy(self.request.GET)
        self.params['page'] = None
        self.params.pop("page")

        filter_dic = {}
        filter_condition = Q()
        for k, v in self.params.items():
            if k in self.conf_obj.list_filter and v != 'all':
                filter_dic[k] = v
                filter_condition.children.append((k, v))
                self.data_list = self.data_list.filter(filter_condition)

        temp = {}
        for filter in self.conf_obj.list_filter:
            filter_field_obj = self.conf_obj.model._meta.get_field(filter)
            val_html = []
            queryset = filter_field_obj.rel.to.objects.all()

            self.params[filter] = 'all'
            val_html.append('<a href="?%s">ALL</a>' % (self.params.urlencode()))

            for obj in queryset:
                self.params[filter] = obj.pk
                if (filter in filter_dic) and (filter_dic[filter] == str(obj.pk)):
                    val_html.append('<a class="red href="?%s">%s</a>' % (self.params.urlencode(), str(obj)))
                else:
                    val_html.append('<a href="?%s">%s</a>' % (self.params.urlencode(), str(obj)))

            temp[filter] = val_html
        return temp
ShowList

     

  四、HTML页面和,静态文件,以及工具文件(mypage分页)也都放在组件里,在查找时,最外层的找不到,会直接到组件里找对应的文件,所以可以放在组件里

        自定义 Django admin 组件 

Django 之 modelForm (edit.html页面的编写)

自定义分页

 


使用方法: 

     在app的 stark.py 文件里,  

         1、 注册 model,生成对应的url 
     2、 #自定义配置类
          list_display = ["title", "price", "publish"]     #配置该表显示的字段列
          list_display_links = ["title"]        #配置点击哪些字段可以跳转到编辑页面 
          search_fields = ["price"]      #配置哪些字段可以作为可模糊的字段
          list_filter = ["publish"]         #右侧边分类栏,可分类现实数据

        #自定义 批量操作方法
from django.shortcuts import HttpResponse, render, redirect

from app01.models import *
from django.http import JsonResponse

#注册 model,生成对应的url
site.register(User)
site.register(Role)

#自定义配置类
class BookConfig(ModelStark):
    list_display = ["title", "price", "publish"]     #配置该表显示的字段列
    list_display_links = ["title"]        #配置点击哪些字段可以跳转到编辑页面 
    search_fields = ["price"]      #配置哪些字段可以作为可模糊的字段
    list_filter = ["publish"]         #右侧边分类栏,可分类现实数据

    #自定义 批量操作方法
    def delete_action(self, queryset):       
        queryset.delete()
    
     #批量操作方法的名称
    delete_action.desc = "批量删除"
    
    def init_price_action(self, queryset):
        queryset.update(price=100.0)

    init_price_action.desc = "批量初始化"

   #批量操作方法列表
    actions = [delete_action, init_price_action]

#注册该类,将对应的配置类传入
site.register(models.Book, BookConfig)

     #自定义列  

     #自定义视图

     #自定义url   

class StudentConfig(ModelStark):
      #自定义列 方法
    def class_display(self, obj=None, is_header=False):
        if is_header:
            return "已报班级"
        temp = []
        for i in obj.class_list.all():
            temp.append(str(i))
        return ",".join(temp)
     
     #自定义列
    def score_display(self, obj=None, is_header=False):
        if is_header:
            return "学习详情"
        return mark_safe("<a href='/stark/app01/student/score/%s'>学习详情</a>" % obj.pk)
    
     #自定义列 方法列表
    list_display = ["customer", class_display, score_display]
    #自定义视图
    def score_view(self, request, sid):

        if request.is_ajax():
            sid = request.GET.get("sid")
            cid = request.GET.get("cid")
            # 查询学生sid在班级cid在的所有成绩
            ret = StudentStudyRecord.objects.filter(student=sid, classstudyrecord__class_obj=cid).values_list(
                "classstudyrecord__day_num", "score")
            print("ret", ret)

            data = [["day%s" % i[0], i[1]] for i in ret]
            return JsonResponse(data, safe=False)

        student_obj = Student.objects.filter(pk=sid).first()
        class_list = student_obj.class_list.all()

        return render(request, "score_view.html", locals())
    
    #自定义url
    def extra_urls(self):
        temp = []
        temp.append(
            url("score/(\d+)", self.score_view)
        )

        return temp

site.register(Student, StudentConfig)