zoukankan      html  css  js  c++  java
  • 可插拔式后台管理系统(Django)

    1.实现效果

      研究了下django admin的功能,自己实现了一个简单的可插拔式后台管理系统,方便自定义特殊的功能,而且作为一个独立单独的django app,可以整体拷贝到其他项目中作为后台数据管理系统,对数据进行增删改查和自定义操作。下图是拷贝到一个图书管理系统中的后台效果:

    2.实现思路

      2.1 url的设计和分发

             Django自带的admin,对于不同app的不同model表,都会动态的生成类似下面的四条url,分别对应着后台数据的增删改查页面。而为了实现动态路由需要配置两处,一是在项目全局urls.py文件中urlpatterns = [ url(r'^admin/', admin.site.urls),], 二是在每个app的admin.py文件中对model表进行了注册admin.site.register(model),这两处都涉及到了一个admin.site对象,因此我们需要实现自己的site对象即可。

          查看:http://127.0.0.1:8008/admin/app01/book/

          添加:http://127.0.0.1:8008/admin/app01/book/add/

          更新:http://127.0.0.1:8008/admin/app01/book/1/change/

          删除:http://127.0.0.1:8008/admin/app01/book/1/delete/

        另外,对于django的url多级路由格式需要了解下:url(r" ",([ url(), url()], None, None)), 为 一个三元元祖([],None,None),而元祖中的列表[]又可以嵌套多个相同格式的url([],None,None),如下面的代码实现了三条路由:

         url(r'^myAdmin2/', ([ url(r'^book1/',views.index1),
            url(r'^book2/',([ url(r'^change/',views.index2), url(r'^add/',views.index3)],None,None ))],
             None,None),)

      对应的url如下:

          http://127.0.0.1:8008/myAdmin2/book1/

          http://127.0.0.1:8008/myAdmin2/book2/change/

          http://127.0.0.1:8008/myAdmin2/book2/add/

      根据上述的思路和多级url路由,可以定义同样的路由设置,一是设置urls.py中全局路由,二是在app的admin.py文件中注册model,三是实现自己的myAdmin.site对象。对应的代码依次如下:

    urls.py

    from django.conf.urls import urlfrom myAdmin.service.site import site  # 引入自定义的site.py 文件中生成的site单例对象
    urlpatterns = [
        url(r'^myAdmin/', site.urls),
    ]

    app01/admin.py

    from myAdmin.service.site import site
    from app01 import models
    site.register(models.Book)
    site.register(models.Author)
    site.register(models.Publish)

    myAdmin/service/site.py

    class ModelAdmin(object):
        def __init__(self, model):
            self.model = model
            self.model_name = self.model._meta.model_name
            self.app_label = self.model._meta.app_label
        
        @property
        def urls(self):
            return self.get_urls(), None, None
    
        def get_urls(self):
            patterns = [url(r'^$', self.list_view, name='%s_%s_list'%(self.model_name,self.app_label)),
                        url(r'^add/$', self.add_view,name='%s_%s_add'%(self.model_name,self.app_label)),
                        url(r'^(.+)/change/$', self.change_view,name='%s_%s_change'%(self.model_name,self.app_label)),
                        url(r'^(.+)/delete/$', self.delete_view,name='%s_%s_delete'%(self.model_name,self.app_label)),
                        ]
            return patterns
    
    class AdminSite(object):
        def __init__(self):
            self._registry = {}
    
        def register(self, model, admin_class=None):      #对应model表注册时的site.register()
            if not admin_class:
                admin_class = ModelAdmin
            admin_obj = admin_class(model)
            self._registry[model] = admin_obj
    
        @property
        def urls(self):   #对应全局路由中的site.urls
            return self.get_urls(), None, None
    
        def get_urls(self):
            patterns = []
            for model, admin_obj in self._registry.items():
                urls = url(r'^{0}/{1}/'.format(model._meta.app_label, model._meta.model_name), admin_obj.urls)
                patterns.append(urls)
            return patterns
    
    site = AdminSite()

       在site.py代码中有三处值得注意,

        1. site = AdminSite(),  这里是采用了python模块的天然单例模式,由于每个app中都会采用site对象,因此在整个项目中只能有一个site对象。

        2. AdminSite中的get_urls(self)函数

          urls = url(r'^{0}/{1}/'.format(model._meta.app_label, model._meta.model_name), admin_obj.urls) 实现了第一级动态路由,即/app01/model/

        3. ModelAdmin中的get_urls(self)函数

          patterns = [url(r'^$', self.list_view, name='%s_%s_list'%(self.model_name,self.app_label)),

                 url(r'^add/$', self.add_view,name='%s_%s_add'%(self.model_name,self.app_label)),

                 url(r'^(.+)/change/$', self.change_view,name='%s_%s_change'%(self.model_name,self.app_label)),

                                           url(r'^(.+)/delete/$', self.delete_view,name='%s_%s_delete'%(self.model_name,self.app_label)), ]

         实现了第二级动态路由,即 app01/model, app01/model/add/, app01/model/id/change/, app01/model/id/delete 增删改查四条路径。

     2.2 实现增删改查处理函数

       在上面url设计中,在ModelAdmin类中定义了相应的处理函数,如下面self.list_view,self.add_view,self.change_viewself.delete_view,需要对其依次实现。

          patterns = [url(r'^$', self.list_view, name='%s_%s_list'%(self.model_name,self.app_label)),

                 url(r'^add/$', self.add_view,name='%s_%s_add'%(self.model_name,self.app_label)),

                 url(r'^(.+)/change/$', self.change_view,name='%s_%s_change'%(self.model_name,self.app_label)),

                                           url(r'^(.+)/delete/$', self.delete_view,name='%s_%s_delete'%(self.model_name,self.app_label)), ]

      实现后的site.py代码如下:由于需要用到ModelForm类,定义了两个辅助方法get_modelform_class()和 change_modelform()
     def get_modelform_class(self):
            class Model_form(ModelForm):
                class Meta:
                    model = self.model
                    fields = '__all__'
            return Model_form                  #返回类对象
    
        def change_modelform(self,modelform):
            for item in modelform:
                if isinstance(item.field, ModelChoiceField):  # ModelChoiceField表示field字段对应的为外键或多对对关系
                    pop_item_name = item.name
                    item.is_pop=True #为实例动态绑定属性
                    item_model_name = item.field.queryset.model._meta.model_name
                    item_app_label = item.field.queryset.model._meta.app_label
                    item.pop_url = '/myAdmin/{0}/{1}/add/?pop_item_name={2}'.format(item_app_label, item_model_name,pop_item_name)
            return modelform   
       
    
      #添加数据
        def add_view(self, request):
            modelform_class = self.get_modelform_class()
            form = modelform_class()
            form = self.change_modelform(form)
            if request.method == 'POST':
                form = modelform_class(request.POST)
                field_obj = form.save()
                # url = request.path[:-4]
                # print url
                pop_item_name = request.GET.get('pop_item_name')
                if pop_item_name:
                    result = {'pk':field_obj.pk,'text':str(field_obj),'pop_item_name':pop_item_name}
                    return render(request, 'process_pop.html', {'result':result})
                return redirect(self.get_list_url())
            return render(request, 'add_view.html', locals())
    
        # 改变数据
        def change_view(self, request, number):
            modelform_class = self.get_modelform_class()
            model_obj = self.model.objects.filter(id=number).first()
            form = modelform_class(instance=model_obj)
            form = self.change_modelform(form)
            if request.method == 'POST':
                form = modelform_class(request.POST, instance=model_obj)
                form.save()
                return redirect(self.get_list_url())
            return render(request, 'change_view.html', locals())
    
        # 删除数据
        def delete_view(self, request, number):
            model_obj = self.model.objects.get(id=number)
            list_url = self.get_list_url()
            edit_url = self.get_change_url(model_obj)
            if request.method=='POST':
                model_obj.delete()
                return redirect(list_url)
            return render(request, 'delete_view.html', locals())
    site.py

      list_view 较为复杂,上面没列出,下面单独列出代码。因为list_view界面中还支持搜索,分类和批量处理三个功能,list_view必须对这三种请求进行捕获和处理。从下面代码中可以看到:定义了两个辅助方法,get_search_condition()和get_filter_condition()来处理搜索和分类过滤(详细见自定义字段的实现)。搜索框和分类过滤器请求通过GET请求提交,get_search_condition()处理搜索框提交的GET请求,将搜索条件封装成一个Q对象返回,get_filter_condition()处理过滤器提交的GET请求,返回Q对象。actions的请求(批量处理)通过POST请求提交,将批处理函数和选定项提交到request.POST,然后list_view进行处理。实现代码如下:

     #处理搜索框提交的请求
        def get_search_condition(self,request):
            search_connector = Q()
            if request.method=='GET':
                search_content = request.GET.get('search_content','')
                search_connector.connector = 'or'
                if search_content and self.search_field:
                    for field in self.search_field:
                        # field_obj = self.model._meta.get_field(field)
                        # if isinstance(field_obj,ManyToManyField) or isinstance(field_obj,ForeignKey):
                        #     search_connector.children.append((field + '__name__contains', search_content)) #对于多对多关系,如何实现动态?
                        # else:
                        search_connector.children.append((field + '__contains', search_content))
            return search_connector
    
        #处理过滤标签的<a>标签提交的请求
        def get_filter_condition(self,request):
            filter_connector = Q()
            if request.method == 'GET':
                for filter_field, value in request.GET.items():
                    if filter_field in self.list_filter:        # 设置分页后url会出现page参数,不应做为过滤条件
                        filter_connector.children.append((filter_field, value))
            return filter_connector
    
    #查看:显示数据
        def list_view(self, request):
            model = self.model
            if request.method == 'POST':
                choice_item = request.POST.get('choice_item')
                selected_item = request.POST.getlist('selected_item')
                action_func = getattr(self,choice_item)
                queryset = model.objects.filter(id__in =selected_item)
                action_func(queryset)
            search_condition = self.get_search_condition(request)
            filter_condition = self.get_filter_condition(request)
            model_list = model.objects.all().filter(search_condition).filter(filter_condition)
            showlist= Showlist(self,model_list,model,request)    #单独抽象出一个类,用来配置前端数据的显示
            return render(request, 'list_view.html', locals())

     2.3 自定义字段的实现

      在ModelAdmin中可以定义相应的字段,对数据管理显示界面进行设置,从而在model进行注册时能根据需求对这些字段进行更改和扩展,展示出不同的显示效果。下面定义了显示字段,过滤器,搜索和批处理等:

    class ModelAdmin(object):
        list_display = ('__str__',)   #自定义显示的字段
        list_display_links = ()    #自定义超链接字段,点击进入编辑页面
        list_filter = ()        #自定义过滤器字段,根据该字段分类
        search_field = ()        #自定义搜索字段,搜索的内容和这些字段进行匹配
        actions = ()           #自定义批处理函数

      list_display 的扩展

        下面代码为list_display的实现,首先在list_display中加入默认的选择框,编辑和删除超链接,然后对用户配置的list_play进行扩展,得到完整的list_play,再进行渲染,代码如下:

     # 定义默认要显式的内容, 编辑,删除操作和选择框,并扩展list_display
        def edit(self,model_obj=None,isHeader=False):         #model_obj: 一个model表对象,isHeader是否是表格的表头字段
            if isHeader:
                return '操作'
            return mark_safe(
                '<a href="%s/change/">编辑</a>' % model_obj.pk)  # 注意href="%s/change/ 和 href="/%s/change/的区别,前者为当前目录,后者为根目录
    
        def checkbox(self,model_obj=None, isHeader=False):
            if isHeader:
                return '选择'
            return mark_safe('<input type="checkbox" value="%s" name="selected_item"/>'%model_obj.pk)
    
        def delete(self,model_obj=None, isHeader=False):
            if isHeader:
                return '操作'
            return mark_safe(
                '<a href="%s/delete/">删除</a>' % model_obj.pk)
    
        def get_list_display(self):
            new_list_display = []
            new_list_display.append(ModelAdmin.checkbox)  #加入选择框
            new_list_display.extend(self.list_display)  #加入用户配置的list_display
            if not self.list_display_links:
                new_list_display.append(ModelAdmin.edit)   #如果用户未配置超链接字段,加入编辑操作
            new_list_display.append(ModelAdmin.delete)    #加入删除操作
            return new_list_display

             上面代码拿到了一个完整的list_display=(checkbox, '', '', exit, delete),而对于list_display的处理和前端显示见下面Showlist 类。

      actions 的扩展

        和list_diaplay一样,下面代码中,在actions加入默认的批量删除函数,并扩展用户配置的actions批处理函数,拿到了一个完整的actions=(batch_delete, ), 其处理和前端显示见后面Showlist 类

    #定义默认的批量删除函数,并扩展用户actions
        def batch_delete(self,queryset):
            queryset.delete()
        batch_delete.short_description = '批量删除'
    
        def get_actions(self):
            new_actions = []
            new_actions.append(ModelAdmin.batch_delete)   # 加入批量删除操作
            new_actions.extend(self.actions)        # 扩展用户配置actions
            return new_actions

        在实现list_view()函数时,用到了一个单独的类Showlist, 其中定义了 list_diaplay, list_play_links, list_filter, search_field 和 actions的处理和前端显示逻辑,代码如下:

    class Showlist(object):
        '''
            需要四个参数来初始化实例:
            model_config: ModelAdmin 的实例对象,决定了其相关配置项
            model_list: 发送给前端的表格中要展示的数据对象(Queryset)
            model:数据表对象
            request:视图函数中的request参数
        '''
    
        def __init__(self,model_config,model_list,model,request):
            self.model_config = model_config
            self.model_list = model_list
            self.model = model
            self.request = request
    
            # 设置分页
            current_page = int(request.GET.get('page',1))
            params = self.request.GET
            base_url = self.request.path
            all_count = self.model_list.count()
            #print 'all_count',all_count
            self.page = page.Pagination(current_page, all_count, base_url, params, per_page_num=4, pager_count=3,)
            self.page_data = self.model_list[self.page.start:self.page.end]

      # 前端actions的显示数据
    def get_action_desc(self): # actions list_actions = [] if self.model_config.get_actions(): for action in self.model_config.get_actions(): list_actions.append({ "name": action.__name__, #批处理函数的名字 "desc": action.short_description }) return list_actions  
      # 前端过滤器的显示数据
    def get_filter_dict(self): filter_dict = {} for field in self.model_config.list_filter: params = copy.deepcopy(self.request.GET) selection = self.request.GET.get(field, 0) field_obj = self.model._meta.get_field(field) if isinstance(field_obj, ForeignKey) or isinstance(field_obj, ManyToManyField): #对于多对多或外键字段的处理 data_list = field_obj.rel.to.objects.all()           #field_obj.rel.to 能拿到多对多或外键字段对应的另一张model表对象 else: data_list = self.model.objects.all().values('pk', field) temp = [] if params.get(field): #url参数的过滤条件中,如果有该字段的过滤条件,则点击全部时应该删除该字段的过滤条件,从而显示全部数据; del params[field] temp.append("<a href='?%s' class='list-group-item is_selected'>全部</a>" % params.urlencode()) else:          #不含有该字段的过滤条件,点击时不处理 temp.append("<a href='#' class='list-group-item'>全部</a>") for item in data_list: if isinstance(field_obj, ForeignKey) or isinstance(field_obj, ManyToManyField): #多对多或外键字段,拿到的为对象 id = item.pk text = str(item) params[field] = id #多对多或外键字段,以id做为过滤条件 else: #普通字段拿到的为字典 id = item['pk'] text = item[field] params[field] = text #普通字段以字段名称做为过滤条件 tag_url = params.urlencode() if selection == str(id) or selection == text: #判断此时url过滤字段中选中的条件,为其添加特殊style样式 temp.append("<a href='?%s' class='list-group-item is_selected'>%s</a>" % (tag_url, text)) else: temp.append("<a href='?%s' class='list-group-item'>%s</a>" % (tag_url, text)) filter_dict[field_obj] = temp return filter_dict   #前端表格表头的显示数据 def get_head_list(self): head_list = [] for field in self.model_config.get_list_display(): if isinstance(field, str): #判断函数和字符窜 if field == '__str__':    #用户未配置时默认的list_play=('__str__',) value = self.model._meta.model_name else: field_obj = self.model._meta.get_field(field) # 拿到字符窜对应的field对象 value = field_obj.verbose_name # 通过拿到verbose_name 来显示中文 else: value = field(self.model_config, isHeader=True) # 获取标题,传入isHeader, 注意此处传入的self.model_config if value: head_list.append(value) return head_list   #前端表格内容的显示数据 def get_data_list(self): data_list = [] for model_obj in self.page_data: #分页截取的某一页的数据列表 row_list = [] for field in self.model_config.get_list_display(): if isinstance(field, str): #判断是字符窜或函数 try : field_obj = self.model_config.model._meta.get_field(field) #判断设置的显式列是否为多对多关系,处理相应的多个数据 if isinstance(field_obj, ManyToManyField): temp_list = getattr(model_obj, field).all() #print temp_list ret = [] for temp in temp_list: #print temp ret.append(str(temp)) #转换为字符窜后进行拼接 value = ','.join(ret) #print value else: value = getattr(model_obj, field) # 通过反射拿到字符窜对应的值 if field in self.model_config.list_display_links: # 判断该字段是否设置为链接,放在此处表明了多对多关系设置在超链接列中无效 value = mark_safe('<a href="%s/change/">%s</a>' % (model_obj.pk, value)) except Exception as e: #print e value = getattr(model_obj, field) else: value = field(self.model_config, model_obj) # 获取内容,传入model_obj,不用传入isHeader if value: row_list.append(value) data_list.append(row_list) # print data_list return data_list

      2.4 增加自定义的url 路径

          可以为某个model表单独增加一个url接口,来处理特殊的业务;首先需要在site.py 文件中定义接口,然后在app的admin.py注册文件中进行定义处理逻辑。在下面的代码中model表通过覆盖父类的extra_urls()函数来增加了一条url和相应的处理逻辑。

    site.py定义的接口如下:

        def get_urls(self):
    
            patterns = [url(r'^$', self.list_view, name='%s_%s_list'%(self.model_name,self.app_label)),
                        url(r'^add/$', self.add_view,name='%s_%s_add'%(self.model_name,self.app_label)),
                        url(r'^(.+)/change/$', self.change_view,name='%s_%s_change'%(self.model_name,self.app_label)),
                        url(r'^(.+)/delete/$', self.delete_view,name='%s_%s_delete'%(self.model_name,self.app_label)),
                        ]
            patterns.extend(self.extra_url())
            return patterns
    
        #定义url接口,modelConfigure通过继承覆盖来配置额外的url
        def extra_url(self):
            return []

    admin.py 定义处理逻辑如下:

    class BookConfig(ModelAdmin):
    # 通过下面三个函数,为book添加一条单独的url处理逻辑,实现点击id值,为title添加喜欢或不喜欢 def list_id(self,model_obj=None, isHeader=False): if isHeader: return 'ID' return mark_safe('<a href="like_book/%s">%s</a>'%(model_obj.pk, model_obj.pk)) def like_book(self,request,obj_id): model_obj = models.Book.objects.get(id = obj_id) if '(喜欢)' not in model_obj.title: new_title = '%s (喜欢)'%model_obj.title else: new_title = model_obj.title.replace('(喜欢)','') models.Book.objects.filter(id=obj_id).update(title = new_title) return redirect(self.get_list_url()) def extra_url(self): temp = [url(r'like_book/(d+)',self.like_book)] return temp list_display = (list_id, 'title','price','author','publish')
      site.register(models.Book,BookConfig)

    3 总结

      通过上述部分,实现了一个完成的后台管理系统,有两个小特色,一是插拔式,方便在其他项目中进行复用;二是代码中保留了扩展字段和自定义url接口,能够根据不同的业务需求扩展特殊的功能。项目源代码及基本使用见下面github。

    项目源代码: https://github.com/silence-cho/Myadmin

  • 相关阅读:
    Median Value
    237. Delete Node in a Linked List
    206. Reverse Linked List
    160. Intersection of Two Linked Lists
    83. Remove Duplicates from Sorted List
    21. Merge Two Sorted Lists
    477. Total Hamming Distance
    421. Maximum XOR of Two Numbers in an Array
    397. Integer Replacement
    318. Maximum Product of Word Lengths
  • 原文地址:https://www.cnblogs.com/silence-cho/p/9752045.html
Copyright © 2011-2022 走看看