Xadmin组件构建之增删改查
settings.py
'Xadmin.apps.XadminConfig', 'app01.apps.App01Config', 'app02.apps.App02Config',
Xadmin实现流程:
models.py
1 启动:在Django执行的这一刻把每一个叫Xadmin.py文件都加载一遍
#Xadmin/apps.py from django.apps import AppConfig from django.utils.module_loading import autodiscover_modules class XadminConfig(AppConfig): name = 'Xadmin' def ready(self): #当加载这个类的时候,这个方法自动执行 autodiscover_modules('Xadmin') #通过此步设置,Django一启动就是扫描每一个叫Xadmin.py的文件
2 创建一个单例对象:完成注册功能
Xadmin\service\Xadmin.py
# 定义每张表的配置类样式 class ModelXadmin(object): def __init__(self, model, site): self.model = model self.site = site # site为单例对象 #定义一个全局类,用于创建单例对象,在这个类中完成了注册操作并设计了url class XadminSite(object): def __init__(self, name='admin'): self._registry = {} def register(self, model, admin_class=None, **options): if not admin_class: admin_class = ModelXadmin # 如果用户没有定制样式类,使用默认的样式类 #site.register(Book,BookConfig)这句代码就相当于下面的注册 self._registry[model] = admin_class(model, self) # {Book:BookConfig(Book,site)} #通过这一次注册就填加了一个键值对,键就是注册的模型表,值就是对应的定制样式类,后面跟了模型表的名字作为参数 site = XadminSite() # 创建单例对象
模型表的简单注册:
from Xadmin.service.Xadmin import site,ModelXadmin from app01.models import * class BookConfig(ModelXadmin) pass site.register(Book,BookConfig) print("_registry:",site._registry) #_registry: {<class 'app01.models.Book'>: <app01.Xadmin.BookConfig object at 0x04156D70>}
3 设计url
urls.py
from Xadmin.service.Xadmin import site urlpatterns = [ url(r'^Xadmin/', site.urls), ]
Xadmin\service\Xadmin.py
class XadminSite(object): # 一级分发 def get_urls(self): print(self._registry) # {Book:modelAdmin(Book),.......}注册过的模型表都存放在字典_registry中 temp = [] # 循环在Django一启动时就执行 for model, admin_class_obj in self._registry.items(): # 获取当前循环的model的字符串与所在app的字符串,为了拼接路径 app_name = model._meta.app_label # "app01" model_name = model._meta.model_name # "book" #分发增删改查 temp.append(url(r'^{0}/{1}/'.format(app_name, model_name), admin_class_obj.urls2), ) return temp @property def urls(self): return self.get_urls(), None, None ''' 当一个页面被请求时,Django就会创建一个包含本次请求原信息的HttpRequest对象。 Django会将这个对象自动传递给响应的视图函数,一般视图函数约定俗成地使用 request 参数承接这个对象。 ''' # 查看视图 def list_view(self, request): return HttpResponse("list") # 增加视图 def add_view(self): return HttpResponse("add") # 编辑视图 def change_view(self, request, id): return HttpResponse("change") # 删除视图 def delete_view(self, request, id): return HttpResponse("delete") # 二级分发 def get_urls2(self): temp = [] app_label = self.model._meta.app_label model_name = self.model._meta.model_name # 以上取app名称和model名称是为了给以下路径起别名(因为每一个app下的model名都是不一样的),用于反向解析 temp.append(url(r"^$", self.list_view, name="%s_%s_list" % (app_label, model_name))) temp.append(url(r"^add/$", self.add_view, name="%s_%s_add" % (app_label, model_name))) temp.append(url(r"^(\d+)/change/$", self.change_view, name="%s_%s_change" % (app_label, model_name))) temp.append(url(r"^(\d+)/delete/$", self.delete_view, name="%s_%s_delete" % (app_label, model_name))) return temp @property def urls2(self): return self.get_urls2(), None, None
上面这么做有一个问题,那就是我们访问book、userinfo以及其它表的增删改查页面是返回的都是同一个增删改查页面,这样做明显不符合逻辑,我们访问每张不同的表应该返回不同的增删改查页面,出现上面现象的原因是我们把二级分发的url以及增删改查的视图函数都定义到一个类(XadminSite)下了,我们最终使用的一直用都是这个类的单例对象site,只要我们调的是这里面的对象所有人用的配置信息都是一样的,
我们应该这样做,如果有定制的信息就用定制的,没有定制的信息就用默认的,
分析上面的代码有一个东西我们一直都没有用到,那就是admin_class_obj,这个代表的是每张表的配置类对象,这个样式类对象中有两个参数(model,site)。分析之后我们做如下修改:
from django.conf.urls import url from django.shortcuts import HttpResponse, render, redirect # 定义每张表的配置类样式 class ModelXadmin(object): def __init__(self, model, site): self.model = model self.site = site # site为单例对象 # 查看视图 def list_view(self, request): return HttpResponse("list") # 增加视图 def add_view(self): return HttpResponse("add") # 编辑视图 def change_view(self, request, id): return HttpResponse("change") # 删除视图 def delete_view(self, request, id): return HttpResponse("delete") # 二级分发 def get_urls2(self): (4) temp = [] app_label = self.model._meta.app_label model_name = self.model._meta.model_name # 以上取app名称和model名称是为了给以下路径起别名(因为每一个app下的model名都是不一样的),用于反向解析 temp.append(url(r"^$", self.list_view, name="%s_%s_list" % (app_label, model_name))) temp.append(url(r"^add/$", self.add_view, name="%s_%s_add" % (app_label, model_name))) temp.append(url(r"^(\d+)/change/$", self.change_view, name="%s_%s_change" % (app_label, model_name))) temp.append(url(r"^(\d+)/delete/$", self.delete_view, name="%s_%s_delete" % (app_label, model_name))) return temp @property def urls2(self): # urls2跨类定义 (3) return self.get_urls2(), None, None class XadminSite(object): # 一级分发 def get_urls(self): (2) print(self._registry) # {Book:modelAdmin(Book),.......}注册过的模型表都存放在字典_registry中 temp = [] # 循环在Django一启动时就执行 for model, admin_class_obj in self._registry.items(): # 获取当前循环的model的字符串与所在app的字符串,为了拼接路径 app_name = model._meta.app_label # "app01" model_name = model._meta.model_name # "book" #分发增删改查 temp.append(url(r'^{0}/{1}/'.format(app_name, model_name), admin_class_obj.urls2),) #这时候不能用self.urls2,因为这个类中已经没有urls2方法了,我们想调的urls2在ModelXadmin类下面, # 可以通过admin_class_obj.urls2调到urls2 ''' url(r"app01/book",BookConfig(Book).urls2) ''' return temp @property def urls(self): (1) return self.get_urls(), None, None
当用户访问app01/book的时候,走的是BookConfig(Book).urls2,首先BookConfig(Book)实例化一个对象,先走BookConfig类中的__init__方法,发现没有,然后走它父类ModelXadmin中的__init__方法:
class ModelXadmin(object): def __init__(self, model, site): self.model = model self.site = site
此时self.model就是Book
4 增删改查页面
(1) list_view
app01\Xadmin.py
#定义我们自己的样式 class BookConfig(ModelXadmin): list_display=["nid","title","publish","price","authors"] #定义查看页面有哪些字段 list_display_links = ["title"] #定义点击哪个字段进入编辑页面
Xadmin\service\Xadmin.py
from django.conf.urls import url from django.shortcuts import HttpResponse, render, redirect from django.urls import reverse from django.db.models import Q from django.utils.safestring import mark_safe from django.db.models.fields.related import ManyToManyField,ForeignKey from Xadmin.utils.page import Pagination # 构建表头数据和构建表单数据本应该放在ModelXadminl类下面list_view视图函数中,但数据太多放在里面会会显得杂乱 # 这里定义一个类专门用来在页面展示数据,把ModelXadminl类中的self以及list_view函数中的data_list和request三个参数传过来 class show_list(object): def __init__(self, config, data_list, request): self.config = config # config代表传过来的self, self.config就是ModelXadminl类中的实例化对象self self.data_list = data_list self.request = request def get_header(self): # 构建表头数据 header_list = [] print("header", self.config.new_list_display()) # [check,"nid","title","publish","price",edit,delete] for field in self.config.new_list_display(): if isinstance(field, str): if field == "__str__": # 说明是默认的样式,展示大写表名 val = self.config.model._meta.model_name.upper() else: field_obj = self.config.model._meta.get_field(field) # 获取表中字段对象 val = field_obj.verbose_name # 获取字段中定义的名称 else: # 说明是定义的函数 val = field(self.config, is_header=True) # 获取表头,传is_header=True header_list.append(val) print(header_list) # ["<input id='choice' type='checkbox'>", ' 编号', '书籍名称', 'publish', 'price', '操作', '操作'] return header_list def get_body(self): # 构建表单数据 new_data_list = [] for obj in self.page_data: # data_list:<QuerySet [<Book: 北京折叠>, <Book: 三体>]> temp = [] for field in self.config.new_list_display(): # ["__str__"] [check,"nid","title","publish","price","authors",edit,delete] if isinstance(field, str): # 判断字段是不是str类型 try:#为了捕捉__str__,防止报错 field_obj = self.config.model._meta.get_field(field) #拿模型表字段对象 # 判断是不是多对多字段(把authors取出来) if isinstance(field_obj, ManyToManyField): ret = getattr(obj, field).all() #取出所有作者 :<QuerySet [<Author: xiaohei>, <Author: xiaobai>……]> t=[] for mobj in ret: #千万记住多层循环的时候循环名称不要重复这里是obj和mobj t.append(str(mobj)) val=",".join(t) else: if field_obj.choices:#如果当前字段对象有choices属性 #相当于把((1, '男'), (2, '女'))中的男或女拿出来 val = getattr(obj, "get_"+field+"_display") else: val = getattr(obj, field) # 由字符串找对象的属性,相当于obj.field,也就是把'nid',title',''publish',price'拿出来了 if field in self.config.list_display_links: # "app01/userinfo/(\d+)/change" _url = self.config.get_change_url(obj) val = mark_safe("<a href='%s'>%s</a>" % (_url, val)) except Exception as e: # 如果出错误说明是__str__ val = getattr(obj, field) else: val = field(self.config, obj) # 如果字段是函数,把当前处理的对象传给当前的函数(这一步为了拿到obj.pk, # 然后拼接路径,比如我们在点击“编辑”的时候跳转到编辑界面,这里就用到了pk),执行后拿到返回结果 temp.append(val) new_data_list.append(temp) ''' new_data_list=[ [check,1,北京折叠",苹果出版社,<a href=''>编辑</a>,<a href=''>删除</a>], [check,2,"三体", 苹果出版社,<a href=''>编辑</a>,<a href=''>删除</a>], ] ''' return new_data_list # 定义每张表的配置类样式 class ModelXadmin(object): list_display = ["__str__", ] # 走默认的样式类的话默认显示"__str__"内容 list_display_links = [] # 控制点哪个字段进入编辑界面,如果里面添加'publish'则点击publish就能进入编辑界面 def __init__(self, model, site): self.model = model self.site = site # site为单例对象 # url路径反向解析 def get_change_url(self, obj): model_name = self.model._meta.model_name app_label = self.model._meta.app_label _url = reverse("%s_%s_change" % (app_label, model_name), args=(obj.pk,)) return _url def get_delete_url(self, obj): model_name = self.model._meta.model_name app_label = self.model._meta.app_label _url = reverse("%s_%s_delete" % (app_label, model_name), args=(obj.pk,)) return _url def get_add_url(self): model_name = self.model._meta.model_name app_label = self.model._meta.app_label _url = reverse("%s_%s_add" % (app_label, model_name)) return _url def get_list_url(self): model_name = self.model._meta.model_name app_label = self.model._meta.app_label _url = reverse("%s_%s_list" % (app_label, model_name)) return _url # 复选框、删除、编辑,定义在这里是为了在new_list_display函数中进行拼接 # 在查看每一张表页面时都会出现这三项 def check(self, obj=None, is_header=False): if is_header: return mark_safe("<input id='choice' type='checkbox'>") # 添加name='selected'_pk value='%s'是为批量操作actions做准备 return mark_safe("<input class='choice_item' type='checkbox' name='selected_pk' value='%s'>"%obj.pk) def edit(self, obj=None, is_header=False): if is_header: return "操作" # 方案1:固定url # return mark_safe("<a href='/Xadmin/app01/book/%s/change/'>编辑</a>"%obj.pk) #这样做把路径写死了 # 方案2:拼接url # return mark_safe("<a href='%s/change/'>编辑</a>"%obj.pk) #比较safe方法,safe用在母版中的 # %s前面没加/,当点击编辑的时候,按照当前页面的路径在后面把href里面的路径添加进去,这样做为了不把路径写死 # 方案3:反向解析 _url = self.get_change_url(obj) return mark_safe("<a href='%s'>编辑</a>" % _url) def delete(self, obj=None, is_header=False): if is_header: return "操作" _url = self.get_delete_url(obj) return mark_safe("<a href='%s'>删除</a>" % _url) #扩展list_display列表,配置每个表默认都有复选框、删除、编辑三个操作,而且固定它们的位置 def new_list_display(self): temp = [] temp.append(ModelXadmin.check) # append只能加一个元素 temp.extend(self.list_display) # extend扩展一个列表进来 ['nid','title','publish','price','authors'] if not self.list_display_links: temp.append(ModelXadmin.edit) temp.append(ModelXadmin.delete) return temp # 查看视图 def list_view(self, request): # 这里注册用哪个样式类(默认、自定义),self就是谁 print("self.model", self.model) # 用户访问的是哪张表,self.model就是哪张表,这就是urls跨类定义最大的意义所在 # 这里首先要弄清楚self是什么,我们一层一层的找,self->list_view->get_urls2->urls2->admin_class_obj, # 所以这里self就相当于admin_class_obj,这里就要搞清楚admin_class_obj是默认样式类的实例化对象还是自己定义样式类的实例化对象 # 以访问Book表为例(这里Book表使用自定义的样式类,以下都是以访问Book表为例), # 上面打印内容为,self.model <class 'app01.models.Book'> model_name = self.model._meta.model_name # 获取表名 # 筛选获取当前表所有数据 data_list = self.model.objects.all() # <QuerySet [<Book: 北京折叠>, …… ]> # 按照showlist展示数据 showlist = show_list(self, data_list, request) # 实例化一个对象showlist,并传三个参数,其中self是当前ModelXadmin的实例化对象 # 把self传给show_list类后,它里面的__init__进行接收(上面show_list函数用来接收self的参数是config) # 构建一个增加url路径 add_url = self.get_add_url() return render(request, 'list_view.html', locals())
<table class="table table-bordered table-striped"> <thead> <tr> {% for item in showlist.get_header %} <th>{{ item }}</th> {% endfor %} </tr> </thead> <tbody> {% for data in showlist.get_body %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <script src="/static/jquery-3.3.1.js"></script> <style> .filter a { text-decoration: none; {# 去除a标签默认样式的下划线 #} color: grey; } .active { color: blue !important; } </style> </head> <body> <h3>查看{{ model_name }}数据</h3> <div class="container"> <div class="row"> <div class="col-lg-9"> <!--添加数据--> <a href="{{ add_url }}" class="btn btn-primary">添加数据</a> <!--搜索数据开始--> {% if showlist.config.search_fields %} <form action="" class="pull-right"> <input type="text" name="q" value="{{ showlist.config.key_word }}"> <button>submit</button> </form> {% endif %} <!--搜索数据结束--> <!--添加form表单是为了在点击Go时确定发送数据的范围(不包括上面submit里面的内容)--> <form action="" method="post"> {% csrf_token %} <!--action操作开始--> <select name="action" id="" style="width: 200px;padding: 5px 8px;display: inline-block"> <option value="">---------------</option> {% for item in showlist.get_action_list %} <option value="{{ item.name }}">{{ item.desc }}</option> {% endfor %} </select> <button type="submit" class="btn btn-info">Go</button> <!--action操作结束--> <!--表格数据开始--> <table class="table table-bordered table-striped"> <thead> <tr> {% for item in showlist.get_header %} <th>{{ item }}</th> {% endfor %} </tr> </thead> <tbody> {% for data in showlist.get_body %} <tr> {% for item in data %} <td>{{ item }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> <!--表格数据结束--> <!--分页开始--> <nav class="pull-right"> <ul class="pagination"> {{ showlist.pagination.page_html|safe }} </ul> </nav> <!--分页结束--> </form> </div> <!--filter开始--> <div class="col-md-3"> <!--如果list_filter有值说明是用户自定义的,展示除来--> {% if showlist.config.list_filter %} <div class="filter"> <h4 style="">Filter</h4> {% for filter_field,linktags in showlist.get_filter_linktags.items %} <div class="well"> <!-- class="well"为加面板--> <p>By {{ filter_field.upper }}</p> {% for link in linktags %} <p>{{ link|safe }}</p> {% endfor %} </div> {% endfor %} </div> {% endif %} </div> <!--filter结束--> </div> </div> <script> //给表头复选框加上点击事件(点击表头复选框,下面框全部选中,反之全部取消) $("#choice").click(function () { if ($(this).prop("checked")) { //prop() 方法用于设置或返回被选元素的属性和值 $(".choice_item").prop("checked", true) //把所有的复选框都设置为选中 } else { $(".choice_item").prop("checked", false) } }) </script> </body> </html>
(2) add_view
app01\Xadmin.py
from Xadmin.service.Xadmin import site,ModelXadmin from django.forms import ModelForm from django.forms import widgets as wid #为book表定制modelform,用于添加页面 class BookModelForm(ModelForm): class Meta: model = Book fields = "__all__" labels={ "title":"书籍名称", "price":"价格", "publishDate":"出版日期" } #编辑页面展示效果,一对多、多对多字段不能在这里设置,可以在model表中通过verbose_name="",进行设置 # widgets={ #应用这个然后页面就会显示样式,把浏览器的样式复制到母版中使用 # "title":wid.TextInput(attrs={"class":"form-control"}) # } #定义我们自己的样式 class BookConfig(ModelXadmin): modelform_class=BookModelForm #定义编辑页面使用我们自定义的
添加按钮
<a href="{{ add_url }}" class="btn btn-primary">添加数据</a>
# 定义每张表的配置类样式 class ModelXadmin(object): modelform_class = [] # 增加视图 def add_view(self, request): model_name = self.model._meta.model_name # 获取表名 ModelFormDemo = self.get_modelform_class() # 获取modelform类变量 form = ModelFormDemo() # 实例化一个对象form if request.method == "POST": form = ModelFormDemo(request.POST) if form.is_valid(): obj=form.save() return redirect(self.get_list_url()) #校验失败,返回当前失败的添加页面,为了让校验正确的数据不丢失 return render(request,"add_view.html",locals()) return render(request, 'add_view.html', locals()) #用户访问哪张表我们不知道,用ModelForm做添加页面
获取modelform类: def get_modelform_class(self): # 如果用户没有定制,就使用默认配置的ModelFormDemo if not self.modelform_class: from django.forms import ModelForm from django.forms import widgets as wid class ModelFormDemo(ModelForm): class Meta: model = self.model fields = "__all__" return ModelFormDemo # 如果用户定制了自己的modelform就用用户自己的(app01/Xadmin.py) else: return self.modelform_class
添加页面和编辑界面差不多,让添加页面和编辑页面继承form
<div class="container"> <div class="row"> <div class="col-md-6 col-md-offset-3"> <form action="" method="post" novalidate> {% csrf_token %} {% for field in form %} <div> <label for="">{{ field.label }}</label> <!--渲染每个标签的名字--> {{ field }} <span class=" error pull-right">{{ field.errors.0 }}</span> </div> {% endfor %} <button type="submit" class="btn btn-default pull-right">提交</button> </form> </div> </div> </div>
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Title</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.css"> <script src="/static/js/jquery-1.12.4.min.js"></script> <style> 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; } .error{ color: red; } </style> </head> <body> <h3>添加页面</h3> {% include 'form.html' %} </body> </html>
(3) change_view
class ModelXadmin(object): # 编辑视图 def change_view(self, request, id): model_name = self.model._meta.model_name # 获取表名 ModelFormDemo = self.get_modelform_class() edit_obj = self.model.objects.filter(pk=id).first() # 取出要编辑的对象 if request.method == "POST": form = ModelFormDemo(request.POST, instance=edit_obj) # 更新当前修改的数据 if form.is_valid(): form.save() return redirect(self.get_list_url()) return render(request, 'add_view.html', locals()) form = ModelFormDemo(instance=edit_obj) # 传给instance,此时编辑页面就有值了 return render(request, 'change_view.html', locals())
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Title</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.css"> <script src="/static/js/jquery-1.12.4.min.js"></script> <style> 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; } .error{ color: red; } </style> </head> <body> <h3>编辑页面</h3> {% include 'form.html' %} </body> </html>
(4) delete_view
点击删除后跳转到确认删除界面,点击取消跳转到查看界面,点击确认删除进行删除后跳转查看界面
class ModelXadmin(object): # 删除视图 def delete_view(self, request, id): model_name = self.model._meta.model_name # 获取表名 url = self.get_list_url() if request.method == "POST": self.model.objects.filter(pk=id).delete() return redirect(self.get_list_url()) return render(request, "delete_view.html", locals())
delete_view.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h3>删除{{ model_name }}数据</h3> <form action="" method="post"> {% csrf_token %} <button>确认删除?</button> <a href="{{ url }}">取消</a> </form> </body> </html>
浙公网安备 33010602011771号