仿照django的admin,開發本身的stark組件。實現相似數據庫客戶端的功能,對數據進行增刪改查。javascript
# 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 建立新項目php
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
在app01和app02下分別建立一個stark.py文件,在項目啓動時掃描每一個app下的stark.py文件並執行css
即在stark的apps.py中配置html
from django.apps import AppConfig from django.utils.module_loading import autodiscover_modules class StarkConfig(AppConfig): name = 'stark' def ready(self): autodiscover_modules('stark') #自動掃描
仿照admin設置相關類,首先建立下面的文件前端
在執行admin.py文件時咱們發現其實第一步就是導入admin,導入時經過單例模式生成了一個site對象,如今咱們也來寫一個類,生成一個單例對象java
class StarkSite(object):
def __init__(self): self._registry = {} site = StarkSite()
在app01和app02的stark.py文件中導入python
from stark.service.stark import site
這樣咱們也就獲得了一個單例對象site,在註冊時admin使用的是site對象的register方法,咱們也學着他寫一個register方法jquery
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類),值是一個類的對象,這個類就是咱們要建立的第二個類,樣式類linux
class ModelStark(object): def __init__(self, model, site): self.model = model self.site = site
self.model指的是什麼? 就是用戶訪問的modelgit
經過這個類咱們控制頁面展現的內容和樣式
作完這幾步咱們就能夠在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了
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的作法,咱們能夠來寫咱們本身的代碼了。
首先在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
url設計完成後,咱們就須要來設計每一個url對應的頁面了,咱們注意到,其實無論是訪問哪張表,增刪改查都只對應相同的四個視圖函數,那麼應該如何區分咱們訪問的表呢
在樣式類ModelStark中,咱們定義了self.model,這裏的model其實就是咱們訪問表的數據類,經過他咱們就能拿到咱們須要的數據顯示到頁面上,訪問不一樣的表時這個model是不一樣的,這時就作到了訪問什麼表顯示什麼表的內容
在使用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>
在使用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寫死了
上面的內容咱們都是考慮了用戶本身定製了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函數咱們能夠實現一些咱們本身的邏輯,好比根據權限判斷是否須要加入編輯按鈕等
使用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
編輯頁面和添加頁面的功能咱們經過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>
當數據較多時,咱們在列表頁面須要進行分頁,這裏分頁時咱們直接調用之前寫好的分頁組件使用便可
在使用分頁組件時,咱們在原有組件的基礎上添加一個功能,就是點擊頁碼跳轉時,保留原來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上的數據,這就須要對編輯按鈕這個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中取出的數據
寫了這麼多內容咱們發現咱們的列表頁面的視圖函數內容較多,同時列表頁面還有不少功能未添加,爲了可以減小列表頁面的代碼,咱們生成一個專門爲列表視圖函數服務的類,將一些主要的邏輯放到這個類中
# 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就能夠渲染出咱們想要的內容
使用admin時咱們能定義一個search_fields列表來生成一個查詢框,能夠根據列表中的字段進行模糊查詢
咱們也來定義這麼一個參數
class ModelStark(object): list_display = ["__str__", ] model_form_class = None list_display_links = [] search_fields = []
默認讓他爲一個空列表,當用戶定義了值時,咱們就須要在頁面生成一個搜索框,而且根據模糊查詢獲得須要的數據展現到頁面上,這裏須要注意若是用戶在列表中定義了多個字段,那麼多個字段查詢時應該是或的關係
在查詢時因爲是或的關係因此咱們要用到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,則不須要生成搜索框
使用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傳給函數執行
和其它功能同樣,咱們先在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)
使用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的功能,咱們能夠將上面的邏輯再在編輯頁面中寫一份,可是這樣的話會形成代碼的重複
這裏咱們使用自定義標籤的形式,添加和編輯頁面用的都是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>
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是固定用法
在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生成字符串