一、需求
仿照django的admin,開發自己的stark組件。實現類似數據庫客戶端的功能,對數據進行增刪改查。
二、實現
1、在settings配置中分別注冊這三個app
# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'app01.apps.App01Config', 'app02.apps.App02Config', 'stark.apps.StarkConfig', ]
注:python manage.py startapp app02 創建新項目
2、在app01和app02的models文件中創建數據類

from django.db import models # Create your models here. from django.contrib.auth.models import AbstractUser class UserInfo(models.Model): """ 用戶信息 """ nid = models.AutoField(primary_key=True) nickname = models.CharField(verbose_name='昵稱', max_length=32) telephone = models.CharField(max_length=11, null=True, unique=True) avatar = models.FileField(upload_to = 'avatars/',default="/avatars/default.png") create_time = models.DateTimeField(verbose_name='創建時間', auto_now_add=True) blog = models.OneToOneField(to='Blog', to_field='nid',null=True) def __str__(self): return self.nickname class Blog(models.Model): """ 博客信息 """ nid = models.AutoField(primary_key=True) title = models.CharField(verbose_name='個人博客標題', max_length=64) site = models.CharField(verbose_name='個人博客后綴', max_length=32, unique=True) theme = models.CharField(verbose_name='博客主題', max_length=32) # # def __str__(self): # return self.title class Category(models.Model): """ 博主個人文章分類表 """ nid = models.AutoField(primary_key=True) title = models.CharField(verbose_name='分類標題', max_length=32) blog = models.ForeignKey(verbose_name='所屬博客', to='Blog', to_field='nid') def __str__(self): return self.title class Tag(models.Model): nid = models.AutoField(primary_key=True) title = models.CharField(verbose_name='標簽名稱', max_length=32) blog = models.ForeignKey(verbose_name='所屬博客', to='Blog', to_field='nid') def __str__(self): return self.title class Article(models.Model): nid = models.AutoField(primary_key=True) title = models.CharField(max_length=50, verbose_name='文章標題') desc = models.CharField(max_length=255, verbose_name='文章描述') comment_count= models.IntegerField(default=0) up_count = models.IntegerField(default=0) down_count = models.IntegerField(default=0) create_time = models.DateTimeField(verbose_name='創建時間') homeCategory = models.ForeignKey(to='Category', to_field='nid', null=True) #siteDetaiCategory = models.ForeignKey(to='SiteCategory', to_field='nid', null=True) user = models.ForeignKey(verbose_name='作者', to='UserInfo', to_field='nid') tags = models.ManyToManyField( to="Tag", through='Article2Tag', through_fields=('article', 'tag'), ) def __str__(self): return self.title class ArticleDetail(models.Model): """ 文章詳細表 """ nid = models.AutoField(primary_key=True) content = models.TextField() article = models.OneToOneField(to='Article', to_field='nid') class Article2Tag(models.Model): nid = models.AutoField(primary_key=True) article = models.ForeignKey(verbose_name='文章', to="Article", to_field='nid') tag = models.ForeignKey(verbose_name='標簽', to="Tag", to_field='nid') class Meta: unique_together = [ ('article', 'tag'), ] def __str__(self): v=self.article.title+"----"+self.tag.title return v

from django.db import models # Create your models here. class Book(models.Model): title=models.CharField(max_length=32,verbose_name="標題")
python manage.py makemigrations
python manage.py migrate
3、掃描(加載)每一個app下的stark.py
在app01和app02下分別創建一個stark.py文件,在項目啟動時掃描每個app下的stark.py文件並執行
即在stark的apps.py中配置
from django.apps import AppConfig from django.utils.module_loading import autodiscover_modules class StarkConfig(AppConfig): name = 'stark' def ready(self): autodiscover_modules('stark') #自動掃描
4、注冊
仿照admin設置相關類,首先創建下面的文件
在執行admin.py文件時我們發現其實第一步就是導入admin,導入時通過單例模式生成了一個site對象,現在我們也來寫一個類,生成一個單例對象
class StarkSite(object):
def __init__(self): self._registry = {} site = StarkSite()
在app01和app02的stark.py文件中導入
from stark.service.stark import site
這樣我們也就得到了一個單例對象site,在注冊時admin使用的是site對象的register方法,我們也學着他寫一個register方法
class StarkSite(object): def __init__(self): self._registry={} def register(self,model,modle_stark=None): if not modle_stark: modle_stark=ModelStark self._registry[model]=modle_stark(model)
site = StarkSite()
這個方法的本質其實就是往self._registry這個字典中添加鍵值對,鍵就是我們的數據類(如Book類),值是一個類的對象,這個類就是我們要創建的第二個類,樣式類
class ModelStark(object): def __init__(self, model, site): self.model = model self.site = site
self.model指的是什么? 就是用戶訪問的model
通過這個類我們控制頁面展示的內容和樣式
做完這幾步我們就可以在app01和app02的stark.py文件中開始注冊了
#app01
from stark.service.stark import site from .models import * site.register(UserInfo,UserInfoConfig) site.register(Blog) site.register(Article) site.register(Category) site.register(Tag)
#app02
from stark.service.stark import site
from .models import *
site.register(Book,BookConfig)
注冊完成后,我們的site._registry字典中就有了我們注冊類對應的鍵值對,接下來就要配置url了
5、url配置
admin中的url配置
from django.conf.urls import url from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), ]
可以看到所有的url都是在admin.site.urls這個方法中生成的,我可以看看這個方法的源碼
@property def urls(self): return self.get_urls(), 'admin', self.name
其實就是做了一個分發,url是在self.get_urls()這個函數中生成的,接着看這個函數的主要代碼
def get_urls(self): from django.conf.urls import url, include # Since this module gets imported in the application's root package, # it cannot import models from other applications at the module level, # and django.contrib.contenttypes.views imports ContentType. from django.contrib.contenttypes import views as contenttype_views def wrap(view, cacheable=False): def wrapper(*args, **kwargs): return self.admin_view(view, cacheable)(*args, **kwargs) wrapper.admin_site = self return update_wrapper(wrapper, view) # Admin-site-wide views. urlpatterns = [ url(r'^$', wrap(self.index), name='index'), url(r'^login/$', self.login, name='login'), url(r'^logout/$', wrap(self.logout), name='logout'), url(r'^password_change/$', wrap(self.password_change, cacheable=True), name='password_change'), url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True), name='password_change_done'), url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'), url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', wrap(contenttype_views.shortcut), name='view_on_site'), ] # Add in each model's views, and create a list of valid URLS for the # app_index valid_app_labels = [] for model, model_admin in self._registry.items(): urlpatterns += [ url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)), ] if model._meta.app_label not in valid_app_labels: valid_app_labels.append(model._meta.app_label) # If there were ModelAdmins registered, we should have a list of app # labels for which we need to allow access to the app_index view, if valid_app_labels: regex = r'^(?P<app_label>' + '|'.join(valid_app_labels) + ')/$' urlpatterns += [ url(regex, wrap(self.app_index), name='app_list'), ] return urlpatterns
這里我們需要知道到是我們生成的url的格式都是admin/app名/表名,所以我們要想辦法取到app名和表名拼接起來
for model, model_admin in self._registry.items(): urlpatterns += [ url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)), ]
這里的model就是我們的數據類(如Book),如何通過他取到我們想要的呢
model._meta.app_label 取類所在的app名
model._meta.model_name 取類的名字
這樣我們就成功拼接出了我們要的url,但是每個url下又有增刪改查不同的url,這時又要再次進行分發,admin中使用了include方法,通過model_admin我們注冊時樣式類生成的對象下的url方法得到我們想要的
def get_urls(self): from django.conf.urls import url def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) wrapper.model_admin = self return update_wrapper(wrapper, view)
info = self.model._meta.app_label, self.model._meta.model_name urlpatterns = [ url(r'^$', wrap(self.changelist_view), name='%s_%s_changelist' % info), url(r'^add/$', wrap(self.add_view), name='%s_%s_add' % info), url(r'^(.+)/history/$', wrap(self.history_view), name='%s_%s_history' % info), url(r'^(.+)/delete/$', wrap(self.delete_view), name='%s_%s_delete' % info), url(r'^(.+)/change/$', wrap(self.change_view), name='%s_%s_change' % info), # For backwards compatibility (was the change url before 1.9) url(r'^(.+)/$', wrap(RedirectView.as_view( pattern_name='%s:%s_%s_change' % ((self.admin_site.name,) + info) ))), ] return urlpatterns @property def urls(self): return self.get_urls()
其實和之前一樣,只是做了又一次分發,並且對應了視圖函數,這里我們先不看視圖函數的內容,值得注意的是這一次的分發和視圖函數都是寫在樣式類中的,而不是寫在生成site的AdminStie類中
這樣有什么好處呢,我們知道當我們要注冊時,是可以自己定義一些屬性的,其實要顯示的頁面也是可以自己定義的,所以講這最后一層url分發和對應的函數寫在樣式類中可以方便我們進行自定義
看完了admin的做法,我們可以來寫我們自己的代碼了。
stark配置
首先在urls文件中配置
from django.conf.urls import url from django.contrib import admin from stark.service.stark import site urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^stark/', site.urls), ]
然后在我們創建的兩個類中添加相關的代碼,這里url對應的函數我們先簡寫
from django.conf.urls import url from django.shortcuts import HttpResponse, render class ModelStark(object): def __init__(self, model, site): self.model = model self.site = site def change_list(self, request): ret = self.model.objects.all() return render(request, "stark/change_list.html", locals()) def add_view(self, request): return HttpResponse("add_view") def del_view(self, request, id): return HttpResponse("del_view") def change_view(self, request, id): return HttpResponse("change_view") def get_url_func(self): temp = [] temp.append(url("^$", self.change_list)) temp.append(url("^add/$", self.add_view)) temp.append(url("^(\d+)/delete/$", self.del_view)) temp.append(url("^(\d+)/change/$", self.change_view)) return temp @property def urls(self): return self.get_url_func(), None, None class StarkSite(object): def __init__(self): self._registry = {} def register(self, model, model_config=None): if not model_config: model_config = ModelStark self._registry[model] = model_config(model, self) def get_urls(self): temp = [] for model, model_config in self._registry.items(): model_name = model._meta.model_name app_label = model._meta.app_label u = url("^%s/%s/" % (app_label, model_name), model_config.urls) temp.append(u) return temp @property def urls(self): return self.get_urls(), None, None site = StarkSite()
反向解析,別名的使用
在設置url對應的視圖函數時,我們可以給這個url添加一個別名,在使用時可以通過這個別名來反向生成url,這樣即使url有修改,這樣別名不變我們都不需要修改代碼
增加別名時要注意,由於每個數據類我們都生成了增刪改查4條url,所以在寫別名時應該有些區別,不然會引起混淆,所以我們設計別名的格式為app名_表名_*
def get_url_func(self): temp = [] model_name = self.model._meta.model_name app_label = self.model._meta.app_label app_model = (app_label, model_name) temp.append(url("^$", self.change_list, name="%s_%s_list" % app_model)) temp.append(url("^add/$", self.add_view, name="%s_%s_add" % app_model)) temp.append(url("^(\d+)/delete/$", self.del_view, name="%s_%s_delete" % app_model)) temp.append(url("^(\d+)/change/$", self.change_view, name="%s_%s_change" % app_model)) return temp @property def urls(self): return self.get_url_func(), None, None
6、列表展示頁面
url設計完成后,我們就需要來設計每個url對應的頁面了,我們注意到,其實不管是訪問哪張表,增刪改查都只對應相同的四個視圖函數,那么應該如何區分我們訪問的表呢
在樣式類ModelStark中,我們定義了self.model,這里的model其實就是我們訪問表的數據類,通過他我們就能拿到我們需要的數據顯示到頁面上,訪問不同的表時這個model是不同的,這時就做到了訪問什么表顯示什么表的內容
list_display
在使用admin時,默認給我們展示的是一個個的類對象,當我們想要看到其它內容時,可以通過list_display屬性設置
from django.contrib import admin from .models import * # Register your models here. admin.site.register(UserInfo) class RoleConfig(admin.ModelAdmin): list_display = ["id", "title"] admin.site.register(Role, RoleConfig)
通過上面的方法,在訪問admin頁面時點擊Role表就能看到id和title兩個字段的內容了,現在我們也來仿照admin寫一個list_display屬性
首先,這個屬性應該是可以自定制的,如果用戶沒有定制,那么他應該有一個默認值,所以我們可以在ModelStark樣式類中先自己定義一個list_display靜態屬性
class ModelStark(object): list_display = [] def __init__(self, model, site): self.model = model self.site = site
如果用戶需要定制他,可以在app對應的stark.py文件中做如下配置
class BookConfig(ModelStark): list_display = ["id", "title", "price"] site.register(Book, BookConfig)
這里我們寫在list_display中的內容都是表中有的字段,其實里面還可以寫我們自己定義的函數,用來將我們自己需要的內容顯示到頁面上
from stark.service.sites import site, ModelStark from .models import * from django.utils.safestring import mark_safe class BookConfig(ModelStark): def edit(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href='/stark/app01/book/%s/change'>編輯</a>" % obj.pk) def delete(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href='/stark/app01/book/%s/delete'>刪除</a>" % obj.pk) list_display = ["id", "title", "price", edit, delete] site.register(Book, BookConfig) class AuthorConfig(ModelStark): list_display = ["name", "age"] site.register(Author)
這里我們增加了編輯和刪除兩個函數,可以看到他們的返回值是一個a標簽,這樣就可以在頁面上顯示一個可以點擊的編輯和刪除,這里的mark_safe和前端渲染時用的safe是一樣的功能,可以使標簽正確的顯示在頁面上
這樣我們就可以讓頁面顯示成下面的樣子
當我們處理列表頁面對應的函數時就可以拿到list_display的值,再通過self.model取到對應的數據對象,從對象中拿到我們想要的數據,放到頁面上進行顯示
class ModelStark(object): list_display = [] def __init__(self, model, site): self.model = model self.site = site def change_list(self, request): # 生成表標頭 header_list = [] for field in self.list_display: if callable(field): # header_list.append(field.__name__) val = field(self, is_header=True) header_list.append(val) else: field_obj = self.model._meta.get_field(field) header_list.append(field_obj.verbose_name) # 生成表數據列表 data_list = self.model.objects.all() new_data_list = [] for obj in data_list: temp = [] for field in self.list_display: if callable(field): val = field(self, obj) else: val = getattr(obj, field) temp.append(val) new_data_list.append(temp) return render(request, "stark/change_list.html", locals())
表頭數據
首先,我們要生成表頭,表頭的內容應該根據list_display中寫到的內容進行顯示,這里要注意,如果我們在stark.py里自己寫了樣式類,那么list_display會優先從我們自己寫的樣式類中取,如果里面沒有才會找到ModelStark中的
取到list_display的值后我們對他進行循環,如果值為可調用的,說明值為一個函數,那么我們就執行函數,取到我們要的結果,這里要注意執行函數時,我們給函數傳了一個is_header=True,說明我們這次是取表頭,在函數中我們給這個參數定義一個默認值為False
進入函數時,首先對他進行判斷,如果為True,那么我們直接返回一個表頭的信息就行了
class BookConfig(ModelStark): def edit(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href='/stark/app01/book/%s/change'>編輯</a>" % obj.pk) def delete(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href='/stark/app01/book/%s/delete'>刪除</a>" % obj.pk) list_display = ["id", "title", "price", edit, delete] site.register(Book, BookConfig)
上面的內容可以看到我們返回的表頭內容為操作
如果我們循環list_display得到的值是一個字符串,那么說明這應該是表中的一個字段,這時我們可以通過self.model._meta.get_field(字段名)的方法取到這個字段的對象,這個對象有一個verbose_name的屬性,這個屬性是用來描述一個字段的,在models中可以進行定義
class Book(models.Model): title = models.CharField(verbose_name="標題", max_length=32) price = models.DecimalField(verbose_name="價格", decimal_places=2, max_digits=5, default=12) def __str__(self): return self.title
我們可以通過self.model._meta.get_field(字段名).verbose_name取到這個屬性,將他作為表頭,如果沒有定義這個屬性,那么默認值為字段名
表內容數據
取表內容數據時,和表頭一樣要做判斷,判斷list_display中的每一個值,如果是可調用的就執行函數取值,這里執行時,我們要將對應的數據對象傳進去,這樣在生成url時才能使用相關的id值
如果這個值是一個字符串,那么我們可以通過反射,取到數據對象中的值,最后將這些值組成下面形式的數據格式發給前端渲染
''' [ [1, "python", 12], [2, "linux", 12], [3,"php"], 12 ] '''
前端頁面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> </head> <body> <h3>數據展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <table class="table table-striped table-hover"> <thead> <tr> {% for foo in header_list %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in new_data_list %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </div> </div> </div> </body> </html>
添加checkbox選擇框
在使用admin時可以看到展示頁面上每條記錄前都有一個選擇框,可以選擇多條記錄進行批量操作,我們也給我們的組件增加這一功能,其實實現方法和編輯按鈕類似
我們先自己定義一個checkbox函數,返回一個checkbox類型的input標簽,然后將這個函數添加到list_display中即可
class BookConfig(ModelStark): def edit(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href=%s>編輯</a>" % reverse("%s_%s_change" % self.app_model, args=(obj.pk,))) def delete(self, obj=None, is_header=False): if is_header: return "操作" return mark_safe("<a href=%s>刪除</a>" % reverse("%s_%s_delete" % self.app_model, args=(obj.pk,))) def select(self, obj=None, is_header=False): if is_header: return "選擇" return mark_safe("<input type='checkbox' value=%s />" % obj.pk) list_display = [select, "id", "title", "price", edit, delete] site.register(Book, BookConfig)
全選
這里checkbox標簽的value值可以設置為該記錄的主鍵值,方便以后使用,當我們點擊最上面的復選框時應該還有全選和全部取消的功能,這里只需要添加一段Js代碼即可
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> </head> <body> <h3>數據展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <table class="table table-striped table-hover"> <thead> <tr> {% for foo in header_list %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in new_data_list %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </div> </div> </div>
#新添加的js代碼 <script> $("#action-toggle").click(function () { if ($(this).prop("checked")){ $("tbody :checkbox").prop("checked",true) }else{ $("tbody :checkbox").prop("checked",false) } }) </script> </body> </html>
我們還注意到,在編輯和刪除函數中我們在生成url時采用了反向解析,利用我們之前使用的別名來反向生成url,這樣就不會把url寫死了
7、list_display的默認情況
上面的內容我們都是考慮了用戶自己定制了list_display的情況,如果用戶沒用進行自定制呢,那么我們所使用的list_display就應該是ModelStark中定義好的
我們仿照admin將默認的list_display設置為__str__,這樣在生成表頭時我們需要多做一步判斷,當為__str__時,直接將表名的大寫添加到header_list中即可
class ModelStark(object): list_display = ["__str__",] def __init__(self, model, site): self.model = model self.site = site self.app_model = (self.model._meta.app_label, self.model._meta.model_name) # 查看數據視圖 def change_list(self, request): # 生成表標頭 header_list = [] for field in self.list_display: if callable(field): # header_list.append(field.__name__) val = field(self, is_header=True) header_list.append(val) else: if field == "__str__": header_list.append(self.model._meta.model_name.upper()) else: field_obj = self.model._meta.get_field(field) header_list.append(field_obj.verbose_name) # 生成表數據列表 data_list = self.model.objects.all() new_data_list = [] for obj in data_list: temp = [] for field in self.list_display: if callable(field): val = field(self, obj) else: val = getattr(obj, field) print(val) temp.append(val) new_data_list.append(temp) return render(request, "stark/change_list.html", locals())
這樣我們就完成了默認情況的設置,但是我們發現在admin中不論用戶如何設置list_display,其實我們都能看到復選框和編輯刪除功能,所以我們也將編輯、刪除和復選框的函數直接放入到ModelStark中作為默認配置,然后設置一個get_list_display函數,對所有的list_play都增加這三個功能
class ModelStark(object): # 編輯按鈕 def edit(self, obj=None, is_header=False): if is_header: return "操作" name = "%s_%s_change" % self.app_model return mark_safe("<a href=%s>編輯</a>" % reverse(name, args=(obj.pk,))) # 刪除按鈕 def delete(self, obj=None, is_header=False): if is_header: return "操作" name = "%s_%s_delete" % self.app_model return mark_safe("<a href=%s>刪除</a>" % reverse(name, args=(obj.pk,))) # 復選框 def checkbox(self, obj=None, is_header=False): if is_header: return mark_safe("<input type='checkbox' id='action-toggle'>") return mark_safe("<input type='checkbox' value=%s>" % obj.pk) def get_list_display(self): new_list_display = [] new_list_display.extend(self.list_display) new_list_display.append(ModelStark.edit) new_list_display.append(ModelStark.delete) new_list_display.insert(0, ModelStark.checkbox) return new_list_display list_display = ["__str__",] def __init__(self, model, site): self.model = model self.site = site self.app_model = (self.model._meta.app_label, self.model._meta.model_name) # 查看數據視圖 def change_list(self, request): # 生成表標頭 header_list = [] for field in self.get_list_display(): if callable(field): # header_list.append(field.__name__) val = field(self, is_header=True) header_list.append(val) else: if field == "__str__": header_list.append(self.model._meta.model_name.upper()) else: field_obj = self.model._meta.get_field(field) header_list.append(field_obj.verbose_name) # 生成表數據列表 data_list = self.model.objects.all() new_data_list = [] for obj in data_list: temp = [] for field in self.get_list_display(): if callable(field): val = field(self, obj) else: val = getattr(obj, field) print(val) temp.append(val) new_data_list.append(temp) return render(request, "stark/change_list.html", locals())
我們還注意到通過自定制get_list_display函數我們可以實現一些我們自己的邏輯,比如根據權限判斷是否需要加入編輯按鈕等
8、list_display_links
使用admin時,我們還可以通過list_display_links設置一些字段,點擊這些字段也能進入編輯頁面
我們也來實現一下這個功能,首先在ModelStark中定義一個默認的list_display_links,當用戶自己定制了這個屬性時,我們只要在生成表數據時多做一步判斷,如果字段在list_display_links中,則在返回時給字段加上一個a標簽,使他可以跳轉到編輯頁即可
由於我們經常要用到增刪改查的url,所以我們在ModelStark中定義4個方法,分別獲取增刪改查的url
class ModelStark(object): list_display = ["__str__", ] list_display_links = [] def __init__(self, model, site): self.model = model self.site = site self.app_model = (self.model._meta.app_label, self.model._meta.model_name) # 獲取當前查看表的編輯url def get_edit_url(self, obj): edit_url = reverse("%s_%s_change" % self.app_model, args=(obj.pk,)) return edit_url # 獲取當前查看表的刪除url def get_delete_url(self, obj): del_url = reverse("%s_%s_delete" % self.app_model, args=(obj.pk,)) return del_url # 獲取當前查看表的增加url def get_add_url(self): add_url = reverse("%s_%s_add" % self.app_model) return add_url # 獲取當前查看表的查看url def get_list_url(self): list_url = reverse("%s_%s_list" % self.app_model) return list_url # 查看數據視圖 def change_list(self, request): add_url = self.get_add_url() # 生成表標頭 header_list = [] for field in self.get_list_display(): if callable(field): # header_list.append(field.__name__) val = field(self, is_header=True) header_list.append(val) else: if field == "__str__": header_list.append(self.model._meta.model_name.upper()) else: field_obj = self.model._meta.get_field(field) header_list.append(field_obj.verbose_name) # 生成表數據列表 data_list = self.model.objects.all() new_data_list = [] for obj in data_list: temp = [] for field in self.get_list_display(): if callable(field): val = field(self, obj) else: val = getattr(obj, field) if field in self.list_display_links: val = mark_safe("<a href=%s>%s</a>" % (self.get_edit_url(obj), val)) temp.append(val) new_data_list.append(temp) return render(request, "stark/change_list.html", locals())
這樣當用戶在stark.py中自己定義了list_display_links屬性時,我們就能看到下面的效果了
from stark.service.sites import site, ModelStark from .models import * class BookConfig(ModelStark): list_display = ["id", "title", "price"] list_display_links = ["id"] site.register(Book, BookConfig)
如果能夠點擊字段內容進入編輯頁面,那么我們自己定義的編輯按鈕就可以不用顯示了,所以可以在get_list_display中再做一次判斷
def get_list_display(self): new_list_display = [] new_list_display.extend(self.list_display) if not self.list_display_links: new_list_display.append(ModelStark.edit) new_list_display.append(ModelStark.delete) new_list_display.insert(0, ModelStark.checkbox) return new_list_display
9、添加和編輯頁面(modelform)
編輯頁面和添加頁面的功能我們通過ModelForm實現,但是生成ModelForm時,由於用戶訪問的表可能是不一樣的,所以里面的詳細字段我們不能寫死,所以我們只能定義一個簡單的ModelForm類,然后在ModelStark中設置一個model_form_class,默認為None
用戶如果想要對ModelForm的詳細字段做設置,可以自己定制一個類,並將該類設置為model_form_class的值
class ModelStark(object): list_display = ["__str__", ] model_form_class = None list_display_links = [] def get_modelform_class(self): class ModelFormClass(ModelForm): class Meta: model = self.model fields = "__all__" if not self.model_form_class: return ModelFormClass else: return self.model_form_class
可以看到當用戶未設置model_form_class時,我們用自己的類,當用戶設置了,則使用用戶自己的類
from stark.service.sites import site, ModelStark from django.forms import ModelForm from .models import * from django.forms import widgets as wid class BookModelForm(ModelForm): class Meta: model = Book fields = "__all__" error_messages = { "title": {"required": "不能為空"}, "price": {"required": "不能為空"} } class BookConfig(ModelStark): list_display = ["id", "title", "price"] model_form_class = BookModelForm list_display_links = ["id"] site.register(Book, BookConfig)
用戶設置時就可以設置明確的字段信息了
添加和編輯的函數
# 添加數據視圖 def add_view(self, request): ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass() return render(request, "stark/add_view.html", locals()) else: form = ModelFormClass(data=request.POST) if form.is_valid(): form.save() return redirect(self.get_list_url()) else: return render(request, "stark/add_view.html", locals()) # 編輯數據視圖 def change_view(self, request, id): edit_obj = self.model.objects.filter(pk=id).first() ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass(instance=edit_obj) return render(request, "stark/change_view.html", locals()) else: form = ModelFormClass(data=request.POST, instance=edit_obj) if form.is_valid(): form.save() return redirect(self.get_list_url()) else: return render(request, "stark/change_view.html", locals())
就是通過ModelForm來實現添加和編輯
前端頁面,由於前端的頁面基本相同,所以我們可以把相同的部分寫到一個頁面中,然后應include調用
<div class="container"> <div class="row"> <div class="col-md-6"> <form action="" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group"> <label for="">{{ field.label }}</label> <div> {{ field }} <span class="error pull-right"> {{ field.errors.0 }} </span> </div> </div> {% endfor %} <p><input type="submit" class="btn btn-default"></p> </form> </div> </div> </div>
添加頁面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>添加</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .form-group input{ display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075); -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; } </style> </head> <body> <h3>添加數據</h3> {% include 'stark/form.html' %} </body> </html>
刪除頁面
當點擊刪除時,我們不直接將數據刪除,而是給用戶返回一個確認頁面,用戶點擊確認才真的刪除,點擊取消還跳回列表頁面
# 刪除數據視圖 def del_view(self, request, id): del_obj = self.model.objects.filter(pk=id).first() if request.method == "GET": list_url = self.get_list_url() return render(request, "stark/del_view.html", locals()) else: del_obj.delete() return redirect(self.get_list_url())
前端頁面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>刪除</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> </head> <body> <div> <p>{{ del_obj }}</p> </div> <form action="" method="post"> {% csrf_token %} <input type="submit" value="確認刪除" class="btn btn-danger"> <a href="{{ list_url }}" class="btn btn-primary">取消</a> </form> </body> </html>
10、分頁功能(url數據保留)
當數據較多時,我們在列表頁面需要進行分頁,這里分頁時我們直接調用以前寫好的分頁組件使用即可
在使用分頁組件時,我們在原有組件的基礎上添加一個功能,就是點擊頁碼跳轉時,保留原來url上的數據
分頁組件如下

class Pagination(object): def __init__(self,current_page,all_count,base_url,params,per_page_num=2,pager_count=11): """ 封裝分頁相關數據 :param current_page: 當前頁 :param all_count: 數據庫中的數據總條數 :param per_page_num: 每頁顯示的數據條數 :param base_url: 分頁中顯示的URL前綴 :param pager_count: 最多顯示的頁碼個數 """ try: current_page = int(current_page) except Exception as e: current_page = 1 if current_page <1: current_page = 1 self.current_page = current_page self.all_count = all_count self.per_page_num = per_page_num self.base_url = base_url import copy params = copy.deepcopy(params) params._mutable = True self.params = params # 總頁碼 all_pager, tmp = divmod(all_count, per_page_num) if tmp: all_pager += 1 self.all_pager = all_pager self.pager_count = pager_count self.pager_count_half = int((pager_count - 1) / 2) @property def start(self): return (self.current_page - 1) * self.per_page_num @property def end(self): return self.current_page * self.per_page_num def page_html(self): # 如果總頁碼 < 11個: if self.all_pager <= self.pager_count: pager_start = 1 pager_end = self.all_pager + 1 # 總頁碼 > 11 else: # 當前頁如果<=頁面上最多顯示11/2個頁碼 if self.current_page <= self.pager_count_half: pager_start = 1 pager_end = self.pager_count + 1 # 當前頁大於5 else: # 頁碼翻到最后 if (self.current_page + self.pager_count_half) > self.all_pager: pager_end = self.all_pager + 1 pager_start = self.all_pager - self.pager_count + 1 else: pager_start = self.current_page - self.pager_count_half pager_end = self.current_page + self.pager_count_half + 1 page_html_list = [] self.params["page"] = 1 first_page = '<li><a href="%s?%s">首頁</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(first_page) if self.current_page <= 1: prev_page = '<li class="disabled"><a href="#">上一頁</a></li>' else: self.params["page"] = self.current_page - 1 prev_page = '<li><a href="%s?%s">上一頁</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(prev_page) for i in range(pager_start, pager_end): self.params["page"] = i if i == self.current_page: temp = '<li class="active"><a href="%s?%s">%s</a></li>' % (self.base_url, self.params.urlencode(), i,) else: temp = '<li><a href="%s?%s">%s</a></li>' % (self.base_url, self.params.urlencode(), i,) page_html_list.append(temp) if self.current_page >= self.all_pager: next_page = '<li class="disabled"><a href="#">下一頁</a></li>' else: self.params["page"] = self.current_page + 1 next_page = '<li><a href="%s?%s">下一頁</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(next_page) self.params["page"] = self.all_pager last_page = '<li><a href="%s?%s">尾頁</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(last_page) return ''.join(page_html_list) 分頁組件
可以看到我們在原來的基礎上多傳了一個參數params,這個參數就是當前頁的request.GET,這是一個QueryDict的數據類型(和字典類似),我們取到他后,發現無法直接進行修改,這是應為QueryDict默認是不讓修改的,需要修改mutable參數為True才能修改
修改前我們為了防止直接修改request.GET而造成后面的影響,所以先用深拷貝拷貝一份數據,再進行修改,修改時,我們將pape改為當前頁的頁碼,再利用QueryDict的urlencode方法將字典類型的數據轉換成a=1&b=2類型的字符串數據,然后在生成頁碼a標簽時在a標簽的href屬性后面加上生成的字符串,這樣我們點擊頁面跳轉時就可以保留url上的數據了
分頁組件的使用
from stark.utils.page import Pagination current_page = request.GET.get("page", 1) all_count = self.model.objects.all().count() base_url = request.path_info params = request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.model.objects.all()[pagination.start: pagination.end]
編輯頁面實現url數據保留
上面我們在寫編輯頁面時並沒有考慮保留頁面url上的數據,現在我們增加上這個功能
首先點擊編輯按鈕進入編輯頁面時我們需要保留url上的數據,這就需要對編輯按鈕這個a標簽的href屬性進行修改,在后面加上url上要保留的數據,同時,為了讓加上的數據不和編輯頁面可能有的數據沖突,所以我們單獨定義一個list_filter鍵來存放這些數據
def get_link_tag(self, obj, val): params = self.request.GET params = copy.deepcopy(params) params._mutable = True from django.http import QueryDict qd = QueryDict(mutable=True) qd["list_filter"] = params.urlencode() s = mark_safe("<a href=%s?%s>%s</a>" % (self.get_edit_url(obj), qd.urlencode(), val)) return s
在列表視圖中將原來使用get_edit_url的方法換成上面的方法即可
當編輯頁面完成編輯后點擊提交后我們需要跳轉回列表頁面,這時我們需要將url上保留的數據還原為原來的形式
# 編輯數據視圖 def change_view(self, request, id): edit_obj = self.model.objects.filter(pk=id).first() ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass(instance=edit_obj) return render(request, "stark/change_view.html", locals()) else: form = ModelFormClass(data=request.POST, instance=edit_obj) if form.is_valid(): form.save() params = request.GET.get("list_filter") url = "%s?%s" % (self.get_list_url(), params) return redirect(url) else: return render(request, "stark/change_view.html", locals())
這里我們在原來的url后面加上了我們從request.GET中取出的數據
11、生成為列表頁面服務的類ChangeList
寫了這么多內容我們發現我們的列表頁面的視圖函數內容較多,同時列表頁面還有很多功能未添加,為了能夠減少列表頁面的代碼,我們生成一個專門為列表視圖函數服務的類,將一些主要的邏輯放到這個類中
# ChangeList服務於change_list視圖 class ChangeList(object): def __init__(self, config, request, queryset): self.config = config self.request = request self.queryset = queryset from stark.utils.page import Pagination current_page = self.request.GET.get("page", 1) all_count = self.queryset.count() base_url = self.request.path_info params = self.request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.queryset[pagination.start: pagination.end] self.pagination = pagination self.data_list = data_list def get_header(self): # 生成表標頭 header_list = [] for field in self.config.get_list_display(): if callable(field): # header_list.append(field.__name__) val = field(self.config, is_header=True) header_list.append(val) else: if field == "__str__": header_list.append(self.config.model._meta.model_name.upper()) else: field_obj = self.config.model._meta.get_field(field) header_list.append(field_obj.verbose_name) return header_list def get_body(self): # 生成表數據列表 new_data_list = [] for obj in self.data_list: temp = [] for field in self.config.get_list_display(): if callable(field): val = field(self.config, obj) else: val = getattr(obj, field) if field in self.config.list_display_links: val = self.config.get_link_tag(obj, val) temp.append(val) new_data_list.append(temp) return new_data_list
我們將現在的生成表頭、表數據和分頁的功能都放到該類中,初始化時的config參數就是ModelStark類的實例化對象,queryset是我們從數據庫中取出的需要渲染到頁面上的數據
這時列表視圖函數只要保留下面的內容就行了
# 查看數據視圖 def change_list(self, request): self.request = request add_url = self.get_add_url() queryset = self.model.objects.filter(search_condition) cl = ChangeList(self, request, queryset) return render(request, "stark/change_list.html", locals())
頁面渲染時我們只要利用ChangeList類的實例化對象cl就可以渲染出我們想要的內容
12、search模糊查詢
使用admin時我們能定義一個search_fields列表來生成一個查詢框,可以根據列表中的字段進行模糊查詢
我們也來定義這么一個參數
class ModelStark(object): list_display = ["__str__", ] model_form_class = None list_display_links = [] search_fields = []
默認讓他為一個空列表,當用戶定義了值時,我們就需要在頁面生成一個搜索框,並且根據模糊查詢得到需要的數據展示到頁面上,這里需要注意如果用戶在列表中定義了多個字段,那么多個字段查詢時應該是或的關系
Q查詢利用字符串進行查詢的方式
在查詢時由於是或的關系所以我們要用到Q查詢,但是我們之前使用Q查詢時都是直接使用的字段名,現在我們只能拿到字段名的字符串,所以需要用Q查詢的另外一種方式
def get_search_condition(self): from django.db.models import Q search_condition = Q() search_condition.connector = "or" # 設置關系為或 if self.search_fields: key_word = self.request.GET.get("q") if key_word: for search_field in self.search_fields: search_condition.children.append((search_field + "__contains", key_word)) return search_condition
先生成一個Q對象,設置為或的關系,然后通過循環將要查詢的字段的字符串和查詢關鍵字以元組的形式添加到Q對象中,這里要注意,由於是模糊查詢,我們在字段字符串后拼接了__contains
最后在列表視圖函數中取到這個Q對象,根據他進行查詢
# 查看數據視圖 def change_list(self, request): self.request = request add_url = self.get_add_url() # 關於search的模糊查詢 search_condition = self.get_search_condition() queryset = self.model.objects.filter(search_condition) cl = ChangeList(self, request, queryset) return render(request, "stark/change_list.html", locals())
前端頁面
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> </head> <body> <h3>數據展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <a href="{{ add_url }}"><button class="btn btn-primary">添加數據</button></a> {% if cl.config.search_fields %} <div class="pull-right form-group"> <form action="" method="get" class="form-inline"> <input type="text" class="form-control" name="q"> <input type="submit" class="btn btn-primary" value="search"> </form> </div> {% endif %} <table class="table table-striped table-hover"> <thead> <tr> {% for foo in cl.get_header %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in cl.get_body %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> <nav aria-label="Page navigation" class="pull-right"> <ul class="pagination"> {{ cl.pagination.page_html|safe }} </ul> </nav> </div> </div> </div> <script> $("#action-toggle").click(function () { if ($(this).prop("checked")){ $("tbody :checkbox").prop("checked",true) }else{ $("tbody :checkbox").prop("checked",false) } }) </script> </body> </html>
生成搜索框時需要做判斷,如果用戶沒有定義search_fields,則不需要生成搜索框
13、action功能(批量操作)
使用admin時,我們發現有一個action功能,有一個下拉菜單可以選擇功能批量操作,現在我們也來實現這個功能
首先在ModelStark中定義一個變量actions,默認為一個空列表
class ModelStark(object): list_display = ["__str__", ] model_form_class = None list_display_links = [] search_fields = [] actions = []
這表示如果用戶沒有自己定制actions,那么則沒有任何功能,但是我們使用admin時,發現默認有一個批量刪除的功能,所以我們也來寫一個批量刪除
def patch_delete(self, queryset): queryset.delete() patch_delete.desc = "批量刪除"
python中一切皆對象,我們給這個函數對象一個新的desc屬性,這個屬性的值就是我們想要在頁面上展示給別人看的這個函數的用途,然后我們要將這個函數添加到actions中,同時也要考慮用戶自己定制時的情況
# 獲取真正展示的actions def get_actions(self): temp = [] temp.extend(self.actions) temp.append(ModelStark.patch_delete) return temp
這樣通過ModelStark中的get_actions我們就能拿到最終的actions列表,上面我們自己定制了一個ChangeList類,專門為列表頁面服務,actions功能也是在列表頁面中使用的,所以我們在ChangeList類中定義一個方法# ChangeList服務於change_list視圖
class ChangeList(object): def __init__(self, config, request, queryset): self.config = config self.request = request self.queryset = queryset from stark.utils.page import Pagination current_page = self.request.GET.get("page", 1) all_count = self.queryset.count() base_url = self.request.path_info params = self.request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.queryset[pagination.start: pagination.end] self.pagination = pagination self.data_list = data_list # actions self.actions = self.config.get_actions() def handle_action(self): temp =[] for action_func in self.actions: temp.append({"name": action_func.__name__, "desc": action_func.desc}) return temp
這里我們通過這個方法獲得的是一個列表,列表中有一個個的字典,字典里放着函數的名字和我們要展示的描述
前端頁面

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .filter a{ padding: 5px 3px; border: 1px solid grey; background-color: #336699; color: white; } .active{ background-color: white!important; color: black!important; } </style> </head> <body> <h3>數據展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <a href="{{ add_url }}"><button class="btn btn-primary">添加數據</button></a> {% if cl.config.search_fields %} <div class="pull-right form-group"> <form action="" method="get" class="form-inline"> <input type="text" class="form-control" name="q"> <input type="submit" class="btn btn-primary" value="search"> </form> </div> {% endif %} <form action="" method="post"> {% csrf_token %} <div> <select class="form-control" name="action" id="" style="width: 200px;margin: 5px 0;display: inline-block;vertical-align: -1px" > <option value="">---------</option> {% for item in cl.handle_action %} <option value="{{ item.name }}">{{ item.desc }}</option> {% endfor %} </select> <button type="submit" class="btn btn-default">Go</button> </div> <table class="table table-striped table-hover"> <thead> <tr> {% for foo in cl.get_header %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in cl.get_body %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </form> <nav aria-label="Page navigation" class="pull-right"> <ul class="pagination"> {{ cl.pagination.page_html|safe }} </ul> </nav> </div> </div> </div> </div> <script> $("#action-toggle").click(function () { if ($(this).prop("checked")){ $("tbody :checkbox").prop("checked",true) }else{ $("tbody :checkbox").prop("checked",false) } }) </script> </body> </html>
這里每一個下拉菜單的option的value值就是我們定義的函數名,還要注意要將select標簽和復選框標簽都放到同一個form表單中,這樣在發送數據時我們發送了,選擇的批量操作函數和被選中的數據的pk值
后端操作
后端接收到選擇的批量操作函數和被選中的數據的pk值就可以進行操作了
# 查看數據視圖 def change_list(self, request): if request.method == "POST": func_name = request.POST.get("action") pk_list = request.POST.getlist("_selected_action") queryset = self.model.objects.filter(pk__in=pk_list) func = getattr(self, func_name) func(queryset) self.request = request add_url = self.get_add_url() # 關於search的模糊查詢 search_condition = self.get_search_condition() queryset = self.model.objects.filter(search_condition) cl = ChangeList(self, request, queryset) return render(request, "stark/change_list.html", locals())
首先判斷,當請求為POST請求時,取到批量操作函數的函數名,和選擇數據的pk值列表(由於是復選框,可能選擇了多個值,所以這里用getlist取值),然后通過pk值列表查找到要操作的數據的queryset集合,利用反射通過函數名的字符串取到批量操作函數,最后將取到的quertset傳給函數執行
14、多級過濾
和其它功能一樣,我們先在ModelStark中定義一個list_filter空列表
class ModelStark(object): list_display = ["__str__", ] model_form_class = None list_display_links = [] search_fields = [] actions = [] # 多級過濾 list_filter = []
當用戶自己定義了這個列表時,我們要取到列表中的字段,然后查處該字段對應的內容,顯示到頁面上,當用戶點擊某一個內容時,要過濾出和這個內容相關的數據
當用戶點擊這個內容的a標簽時,我們要向后台發送一個get請求,請求帶着我們要過濾的內容,內容的鍵為字段的名稱,值為你選中值的pk值
第一步我們要先想辦法將list_filter中字段的對應數據都顯示到頁面上,先要取到所有數據
# ChangeList服務於change_list視圖 class ChangeList(object): def __init__(self, config, request, queryset): self.config = config self.request = request self.queryset = queryset from stark.utils.page import Pagination current_page = self.request.GET.get("page", 1) all_count = self.queryset.count() base_url = self.request.path_info params = self.request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.queryset[pagination.start: pagination.end] self.pagination = pagination self.data_list = data_list # actions self.actions = self.config.get_actions() # filter self.list_filter = self.config.list_filter def get_filter_link_tag(self): # link_tags = [] for filter_field_name in self.list_filter: current_id = int(self.request.GET.get(filter_field_name, 0)) filter_field_obj = self.config.model._meta.get_field(filter_field_name) filter_field = FilterField(filter_field_name, filter_field_obj)
在ChangeList中我們先拿到拿到list_filter,並定義self.list_filter,然后定義一個get_filter_link_tag方法,循環self.list_filter,循環的每一個值就是各個字段的名稱,然后通過字段名稱拿到這個字段的對象filter_field_obj,然后通過字段名稱和字段對象通過FilterField類實例化出一個對象,這個新的類內容如下
# 為每一個過濾的字段封裝成整體類 class FilterField(object): def __init__(self, filter_field_name, filter_field_obj): self.filter_field_name = filter_field_name self.filter_field_obj = filter_field_obj def get_data(self): if isinstance(self.filter_field_obj, ForeignKey) or isinstance(self.filter_field_obj, ManyToManyField): return self.filter_field_obj.rel.to.objects.all() elif self.filter_field_obj.choices: return self.filter_field_obj.choices else: pass
通過這個類的對象我們可以調用get_data方法拿到需要的數據列表(queryset或元組里套元組),這里我們暫時不考慮普通字段,由於需要的字段變多了,我們將Book表的字段進行一些修改,增加外鍵和多對多的關系
from django.db import models # Create your models here. class Book(models.Model): title = models.CharField(verbose_name="標題", max_length=32) price = models.DecimalField(verbose_name="價格", decimal_places=2, max_digits=5, default=12) state = models.IntegerField(choices=((1, "已出版"), (2, "未出版")), default=1) publish = models.ForeignKey(to="Publish", default=1) authors = models.ManyToManyField(to="Author", default=1) def __str__(self): return self.title class Author(models.Model): name = models.CharField(max_length=32) age = models.IntegerField() def __str__(self): return self.name class Publish(models.Model): name = models.CharField(max_length=32) def __str__(self): return self.name
拿到數據的列表后,我們再循環每一個數據,通過判斷字段對象的類型,生成不同的a標簽,同時我們也取到當前get求情的數據,如果當前標簽為選中的標簽,我們要給他增加一個active屬性
還有需要保留之前的選擇,我們也要取到每次get請求的數據,保留到生成的a標簽中
# ChangeList服務於change_list視圖 class ChangeList(object): def __init__(self, config, request, queryset): self.config = config self.request = request self.queryset = queryset from stark.utils.page import Pagination current_page = self.request.GET.get("page", 1) all_count = self.queryset.count() base_url = self.request.path_info params = self.request.GET pagination = Pagination(current_page, all_count, base_url, params) data_list = self.queryset[pagination.start: pagination.end] self.pagination = pagination self.data_list = data_list # actions self.actions = self.config.get_actions() # filter self.list_filter = self.config.list_filter def get_filter_link_tag(self): # link_tags = [] for filter_field_name in self.list_filter: current_id = int(self.request.GET.get(filter_field_name, 0)) filter_field_obj = self.config.model._meta.get_field(filter_field_name) filter_field = FilterField(filter_field_name, filter_field_obj) def inner(filter_field, current_id): for obj in filter_field.get_data(): params = copy.deepcopy(self.request.GET) params._mutable = True if isinstance(filter_field.filter_field_obj, ForeignKey) or isinstance(filter_field.filter_field_obj, ManyToManyField): params[filter_field.filter_field_name] = obj.pk if current_id == obj.pk: yield mark_safe("<a href='?%s' class='active'>%s</a>" % (params.urlencode(), obj)) else: yield mark_safe("<a href='?%s'>%s</a>" % (params.urlencode(), obj)) elif filter_field.filter_field_obj.choices: params[filter_field.filter_field_name] = obj[0] if current_id == obj[0]: yield mark_safe("<a href='?%s' class='active'>%s</a>" % (params.urlencode(), obj[1])) else: yield mark_safe("<a href='?%s'>%s</a>" % (params.urlencode(), obj[1])) else: pass yield inner(filter_field, current_id) # link_tags.append(temp) # return link_tags
這里我們使用的yield功能,在渲染模板時需要注意,當for循環中還套了一個for循環時,會先取到第一個for循環的所有內容,再進行內部的for循環,這在使用yield時會出現一些問題,所以我們在內部的生成器函數中要直接將當前的數據傳進去,避免出錯
前端頁面

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .filter a{ padding: 5px 3px; border: 1px solid grey; background-color: #336699; color: white; } .active{ background-color: white!important; color: black!important; } </style> </head> <body> <h3>數據展示</h3> <div class="container"> <div class="row"> <div class="col-md-8"> <a href="{{ add_url }}"><button class="btn btn-primary">添加數據</button></a> {% if cl.config.search_fields %} <div class="pull-right form-group"> <form action="" method="get" class="form-inline"> <input type="text" class="form-control" name="q"> <input type="submit" class="btn btn-primary" value="search"> </form> </div> {% endif %} <form action="" method="post"> {% csrf_token %} <div> <select class="form-control" name="action" id="" style="width: 200px;margin: 5px 0;display: inline-block;vertical-align: -1px" > <option value="">---------</option> {% for item in cl.handle_action %} <option value="{{ item.name }}">{{ item.desc }}</option> {% endfor %} </select> <button type="submit" class="btn btn-default">Go</button> </div> <table class="table table-striped table-hover"> <thead> <tr> {% for foo in cl.get_header %} <td>{{ foo }}</td> {% endfor %} </tr> </thead> <tbody> {% for data in cl.get_body %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> </form> <nav aria-label="Page navigation" class="pull-right"> <ul class="pagination"> {{ cl.pagination.page_html|safe }} </ul> </nav> </div> <div class="col-md-4"> <div class="filter"> {% for filter_link_tag in cl.get_filter_link_tag %} <p class="field">{% for data in filter_link_tag %} {{ data }} {% endfor %} </p> {% endfor %} </div> </div> </div> </div> <script> $("#action-toggle").click(function () { if ($(this).prop("checked")){ $("tbody :checkbox").prop("checked",true) }else{ $("tbody :checkbox").prop("checked",false) } }) </script> </body> </html>
數據的過濾
上面我們已經能在頁面上生成對應的過濾標簽了,當通過點擊這些標簽時,會向后端發送含有相應條件的GET請求,我們要在后端拿到條件並進行過濾,將過濾后的數據顯示在頁面上,這個功能和我們上面做的search類似
# 獲取filter的查詢條件Q對象 def get_filter_condition(self): from django.db.models import Q filter_condition = Q() for field, val in self.request.GET.items(): if field in self.list_filter: filter_condition.children.append((field, val)) return filter_condition # 查看數據視圖 def change_list(self, request): if request.method == "POST": func_name = request.POST.get("action") pk_list = request.POST.getlist("_selected_action") queryset = self.model.objects.filter(pk__in=pk_list) func = getattr(self, func_name) func(queryset) self.request = request add_url = self.get_add_url() # 關於search的模糊查詢 search_condition = self.get_search_condition() # filter多級過濾 filter_condition = self.get_filter_condition() queryset = self.model.objects.filter(search_condition).filter(filter_condition) cl = ChangeList(self, request, queryset) return render(request, "stark/change_list.html", locals())
這里我們需要做一次判斷,當get請求的數據中有page時,需要過濾掉,不然會報錯
普通字段
上面的過濾我們都沒有考慮普通字段,如果是一個普通字段,我們可以按下面的代碼執行
# 針對((),()),[[],[]]數據類型構建a標簽 class LinkTagGen(object): def __init__(self, data, filter_field, request): self.data = data self.filter_field = filter_field self.request = request def __iter__(self): current_id = self.request.GET.get(self.filter_field.filter_field_name, 0) params = copy.deepcopy(self.request.GET) params._mutable = True if params.get(self.filter_field.filter_field_name): del params[self.filter_field.filter_field_name] _url = "%s?%s" % (self.request.path_info, params.urlencode()) yield mark_safe("<a href='%s'>全部</a>" % _url) else: _url = "%s?%s" % (self.request.path_info, params.urlencode()) yield mark_safe("<a href='%s' class='active'>全部</a>" % _url) for item in self.data: if self.filter_field.filter_field_obj.choices: pk, text = str(item[0]), item[1] elif isinstance(self.filter_field.filter_field_obj, ForeignKey) or isinstance(self.filter_field.filter_field_obj, ManyToManyField): pk, text = str(item.pk), item else: pk, text = item[1], item[1] params[self.filter_field.filter_field_name] = pk _url = "%s?%s" % (self.request.path_info, params.urlencode()) if current_id == pk: link_tag = "<a href='%s' class='active'>%s</a>" % (_url, text) else: link_tag = "<a href='%s'>%s</a>" % (_url, text) yield mark_safe(link_tag) # 為每一個過濾的字段封裝成整體類 class FilterField(object): def __init__(self, filter_field_name, filter_field_obj, config): self.filter_field_name = filter_field_name self.filter_field_obj = filter_field_obj self.config = config def get_data(self): if isinstance(self.filter_field_obj, ForeignKey) or isinstance(self.filter_field_obj, ManyToManyField): return self.filter_field_obj.rel.to.objects.all() elif self.filter_field_obj.choices: return self.filter_field_obj.choices else: return self.config.model.objects.values_list("pk", self.filter_field_name)
14、POPUP功能
使用admin添加數據時我們會發現在外鍵和多對多的字段旁邊有一個小加號,點擊后會彈出一個小窗口,在小窗口中可以直接添加外鍵和多對多字段對應的表的數據
這里我們使用popup來實現這個功能,我們的邏輯應該是在添加頁面的視圖函數中
# 添加數據視圖 def add_view(self, request): ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass() else: form = ModelFormClass(data=request.POST) if form.is_valid(): obj = form.save() pop_id = request.GET.get("pop_id") if pop_id: res = {"pk": obj.pk, "text": str(obj), "pop_id": pop_id} import json return render(request, "stark/pop_res.html", {"res": json.dumps(res)}) return redirect(self.get_list_url()) from django.forms.models import ModelChoiceField for bound_field in form: if isinstance(bound_field.field, ModelChoiceField): bound_field.is_pop = True app_label = bound_field.field.queryset.model._meta.app_label model_name = bound_field.field.queryset.model._meta.model_name _url = "%s_%s_add" % (app_label, model_name) bound_field.url = reverse(_url) + "?pop_id=id_%s" % bound_field.name else: bound_field.is_pop = False bound_field.url = None return render(request, "stark/add_view.html", locals())
我們的添加頁面是利用ModelForm生成的,在請求過來時我們會使用ModelForm實例化一個對象,我們使用for循環遍歷這個對象,可以得到這個對象的每一個字段(其實就是ModelForm對應表的每一個字段),這個字段是BoundField類型的,下面有兩個方法.name和.field,.name可以得到字段的名稱,而.field則可以得到字段在ModelForm中的類型,外鍵類型在Form中對應的是ModelChoiceField類型,而多對多在Form中對應的是ModelMultipleChoiceField類型(繼承ModelChoiceField),所以我們只要判斷bound_field.field是否是ModelChoiceField類型的對象就能知道這個字段是否是外鍵或多對多的字段,如果是的話,我們給這個字段對象添加一個is_pop=True的屬性,不是則為False,在前端頁面我們可以根據這個屬性判斷是否需要在字段的框后面添加+號,這個+號應該綁定一個點擊事件,點擊后可以彈出一個窗口讓我們添加數據(pop請求),彈出窗口的url應該就是該字段對應表的添加url,如何取到字段對應的表呢?
前面我們取到了外鍵和多對多在Form中對應的字段bound_field.field,這里面有一個queryset屬性可以取到這個外鍵字段對應表中的數據集合,而queryset這個數據類型中有個model屬性可以得到數據集合對應的表,這樣我們就可以通過bound_field.field.queryset.model得到外鍵或多對多對應的表,再取出表名和app名,通過反向解析就可以得到對應的添加url,這里要注意,我們要在這個url后面加上一個?pop_id=id_當前字段名,這樣當添加的數據通過post請求過來時,我們才能知道是哪個字段添加的數據,數據返回時我們才能找到這個字段對應的select框,給他添加一個option標簽
當添加的數據通過post請求過來時,如果數據驗證成功了,我們先取pop_id,如果能取到,說明這個請求是通過彈出框來的,我們取到相關數據放到一個字典中,將字典返回給pop響應頁面,響應頁面中我們可以通過opener獲得是哪個頁面彈出的這個窗口,然后調用這個opener中的函數,並把后端接收的字典傳給他,這樣就可以利用這個函數在頁面上添加option標簽了。如果取不到pop_id則直接保存數據,跳轉到列表頁面即可。如果post請求發來的數據沒有驗證成功,那么我們依然要做上面兩段提到的內容,所以我們將上面兩段的邏輯放到了函數的最后
前端頁面
add_view.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>添加</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .form-group input,select{ display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075); -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; } </style> </head> <body> <h3>添加數據</h3> {% include 'stark/form.html' %} <script> function foo(res) { var res=JSON.parse(res); var ele_option=document.createElement("option"); ele_option.value=res.pk; ele_option.innerHTML=res.text; ele_option.selected="selected"; document.getElementById(res.pop_id).appendChild(ele_option) } </script> </body> </html>
這里的foo就是pop響應頁面調用的函數
form.html
<div class="container"> <div class="row"> <div class="col-md-6 col-xs-8"> <form action="" method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group" style="position: relative"> <label for="">{{ field.label }}</label> <div> {{ field }} <span class="error pull-right"> {{ field.errors.0 }} </span> </div> {% if field.is_pop %} <a href="" onclick="pop('{{ field.url }}')" class="pop_btn" style="position: absolute;top: 45%;right: -23px"><span class="pull-right" style="font-size: 22px">+</span></a> {% endif %} </div> {% endfor %} <p><input type="submit" class="btn btn-default"></p> </form> </div> </div> </div> <script> function pop(url) { window.open(url,"","width=500,height=400") } </script>
通過is_pop來判斷能是否添加+號,並給這個加號綁定點擊事件,彈出pop框
pop_res.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> </head> <body> <script> opener.foo('{{ res|safe }}'); window.close() </script> </body> </html>
執行opener的函數,並直接關閉彈出框
編輯頁面和添加頁面同時實現popup功能
通過上面的方式我們就實現了添加頁面的popup功能,但是當我們點擊編輯時,我們發現在編輯頁面上也需要有popup的功能,我們可以將上面的邏輯再在編輯頁面中寫一份,但是這樣的話會造成代碼的重復
這里我們使用自定義標簽的形式,添加和編輯頁面用的都是form.html頁面中的內容,而這個頁面中的內容中我們只需要提供一個form
所以我們在stark組件中創建一個目錄templatetags,在該目錄中自定義我們的標簽my_tags
from django import template from django.shortcuts import reverse register = template.Library() @register.inclusion_tag("stark/form.html") def get_form(form): from django.forms.models import ModelChoiceField for bound_field in form: if isinstance(bound_field.field, ModelChoiceField): bound_field.is_pop = True app_label = bound_field.field.queryset.model._meta.app_label model_name = bound_field.field.queryset.model._meta.model_name _url = "%s_%s_add" % (app_label, model_name) bound_field.url = reverse(_url) + "?pop_id=id_%s" % bound_field.name return {"form": form}
這樣在添加和編輯視圖中就不用再寫這么多邏輯了,直接將form對象傳給前端就行了
# 添加數據視圖 def add_view(self, request): ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass() else: form = ModelFormClass(data=request.POST) if form.is_valid(): obj = form.save() pop_id = request.GET.get("pop_id") if pop_id: res = {"pk": obj.pk, "text": str(obj), "pop_id": pop_id} import json return render(request, "stark/pop_res.html", {"res": json.dumps(res)}) return redirect(self.get_list_url()) return render(request, "stark/add_view.html", locals()) # 編輯數據視圖 def change_view(self, request, id): edit_obj = self.model.objects.filter(pk=id).first() ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass(instance=edit_obj) return render(request, "stark/change_view.html", locals()) else: form = ModelFormClass(data=request.POST, instance=edit_obj) if form.is_valid(): form.save() params = request.GET.get("list_filter") url = "%s?%s" % (self.get_list_url(), params) return redirect(url) else: return render(request, "stark/change_view.html", locals())
前端收到這個form對象,直接調用自定義標簽即可
{% load my_tags %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>添加</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .form-group input,select{ display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075); -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; } </style> </head> <body> <h3>編輯數據</h3> {% get_form form %} <script> function foo(res) { var res=JSON.parse(res); var ele_option=document.createElement("option"); ele_option.value=res.pk; ele_option.innerHTML=res.text; ele_option.selected="selected"; document.getElementById(res.pop_id).appendChild(ele_option) } </script> </body> </html>
15、list_display補充
list_display可以定義我們在列表頁面上顯示哪些內容,如果展示的內容是一個包含choices的字段的話(比如說Book的state字段),按我們之前寫的,在頁面上只能看到1或者2這樣的數字,如果想要看到已出版或未出版該怎么辦呢,可以讓用戶自己定制,在stark.py中
class BookConfig(ModelStark): def state(self, obj=None, is_header=False): if is_header: return "狀態" return obj.get_state_display() list_display = ["id", "title", "price", "publish", state] model_form_class = BookModelForm list_display_links = ["id"] search_fields = ["title", "price"] def patch_init(self, queryset): queryset.update(price=100) patch_init.desc = "批量初始化" actions = [patch_init, ] list_filter = ["title", "state", "publish", "authors"] site.register(Book, BookConfig)
自己定義一個函數,obj.get_state_display()方法就可以取到choice中的內容,這個方法的state是字段名,get和display是固定用法
在list_display中增加多對多字段
在admin中我們不能往list_display中增加多對多字段,在我們自己寫的stark中我們來實現這一功能,其實就是在ChangList類中的get_body方法中多做一次判斷
def get_body(self): # 生成表數據列表 new_data_list = [] for obj in self.data_list: temp = [] for field in self.config.get_list_display(): if callable(field): val = field(self.config, obj) else: field_obj = self.config.model._meta.get_field(field) if isinstance(field_obj, ManyToManyField): t = [] for i in getattr(obj, field).all(): t.append(str(i)) val = ",".join(t) else: val = getattr(obj, field) if field in self.config.list_display_links: val = self.config.get_link_tag(obj, val) temp.append(val) new_data_list.append(temp) return new_data_list
當list_display中的值是不可調用的時,我們先取出其對應的字段對象,如果是多對多的類型,則通過getattr的方法拿到多對多的內容,並通過join生成字符串