可插拔式后台管理系統(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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM