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_view,self.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())
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