# sites
from django.urls import path, re_path
from django.shortcuts import HttpResponse, render, redirect
from app01 import models
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.core.exceptions import FieldDoesNotExist
from django.db.models import Q
class ShowList(object):
def __init__(self, request, config_obj, queryset):
self.request = request # 请求对象
self.config_obj = config_obj # 当前调用者函数对象
self.queryset = queryset # 显示数据
self.pager_queryset = self.get_pager_queryset() # 分页机制
def get_pager_queryset(self):
from stark.utils.page import Pagination
current_page = self.request.GET.get("page", 1)
self.pagination = Pagination(self.request, current_page, self.queryset,
per_page_num=self.config_obj.per_page_num or 5)
queryset = self.queryset[self.pagination.start:self.pagination.end] # 切片取展示的数据,到当前页
# print("get_pager_queryset----queryset", queryset, type(queryset))
return queryset
def get_header(self):
# 构建表头,将表头和表内数据分开成两个列表
header_list = []
for field_or_func in self.config_obj.get_new_list_display():
# print("field_or_func", field_or_func)
if callable(field_or_func): # 判断是否可调用对象
val = field_or_func(self, header=True) # 将函数header改成True并调用得到值做表头
header_list.append(val) # 是的话调用函数并将值添加到表头
else:
if field_or_func == "__str__":
val = self.config_obj.model._meta.model_name.upper()
else:
# app01.Book.title 取到对应models模型字段的对象
field_obj = self.config_obj.model._meta.get_field(field_or_func)
# 获取到对应表头名
val = field_obj.verbose_name
header_list.append(val)
return header_list
def get_body(self):
new_data = []
for obj in self.pager_queryset: # 将当前表数据循环获取出每一条的对象
temp = []
for field_or_func in self.config_obj.get_new_list_display(): # 循环新display列表, 并将内容取出来
if callable(field_or_func): # 如果是可调用对象
val = field_or_func(self.config_obj, obj) # 运行函数并获取值
else:
try:
from django.db.models.fields.related import ManyToManyField
field_obj = self.config_obj.model._meta.get_field(field_or_func) # 获取到对象
# 判断是否多对多字段,判断该函数需要导入上面模块,如果直接写多对多则报错,需要自己写函数
if type(field_obj) == ManyToManyField:
raise Exception("list_display can't many To many")
# 判断字段是否拥有 choices 属性
if field_obj.choices:
# obj.get_state_display() 该方法能获取到choices对象目前的值
val = getattr(obj, "get_%s_display" % field_or_func)()
else:
# 所有对象空间如果有属性值,利用以下方法即可反射获取值
val = getattr(obj, field_or_func)
if field_or_func in self.config_obj.list_display_links:
# 如果有自定制link方法,则走下面这条语句,obj则是当前表的该条对象,link方法完成
val = mark_safe("<a href='%s'>%s</a>" % (self.config_obj.get_change_url(obj), val))
except FieldDoesNotExist as e:
# 进入publish时,走上面语句则会报错,就会进入下面这个方法
val = getattr(obj, field_or_func)() # 直接用str获取当前对象的数据即可
temp.append(val) # 将数据一行一行的添加
new_data.append(temp)
# print("new_data", new_data)
return new_data
class ModelStark(object):
"""
默认配置类
"""
list_display = ("__str__",) # 创建一个变量,如果用户没有自定义则走默认的模型str(str在app01的models定义了),用来满足自定制
list_display_links = [] # 点击书名等也可以进行编辑
models_form_class = None # 设定默认使用默认模板,进行添加编辑业务数据显示及添加修改
per_page_num = None # 定义默认指定显示数据为0
search_fields = [] # 设定默认搜索区域即对象字段
search_val = None # 设置搜索跳转后,搜索框的内容
list_filter = [] # 多级过滤
actions = [] # 设置默认的批量
def __init__(self, model):
self.model = model
self.model_name = self.model._meta.model_name # 获取model名称
self.app_label = self.model._meta.app_label # 获取model所在app名称
# 反向解析当前访问表的增删改查URL
def get_list_url(self):
# 反向解析当前表的URL
list_url = reverse("%s_%s_list" % (self.app_label, self.model_name))
return list_url
def get_add_url(self):
# 反向解析当前表的添加URL
add_url = reverse("%s_%s_add" % (self.app_label, self.model_name))
return add_url
def get_delete_url(self, obj):
# 反向解析当前表的删除URL,args 获取当前obj
delete_url = reverse("%s_%s_delete" % (self.app_label, self.model_name), args=(obj.pk,))
return delete_url
def get_change_url(self, obj):
# 反向解析当前表的编辑URL
change_url = reverse("%s_%s_change" % (self.app_label, self.model_name), args=(obj.pk,))
return change_url
# 三个默认的列,即功能
def show_checkbox(self, obj=None, header=False):
"""
mark_safe: 将添加的字节进行编译,HTML默认将添加字节做字符串处理
:param obj: None
:param header: True
:return:
"""
if header:
return mark_safe("<input type='checkbox'>")
return mark_safe("<input name='_selected_action' value=%s type='checkbox'>" % obj.pk) # 给box绑定对象pk值
def show_delbtn(self, obj=None, header=False):
if header:
return "删除"
return mark_safe("<a href='%s'>删除</a>" % self.get_delete_url(obj))
def show_editbtn(self, obj=None, header=False):
"""
mark_safe: 将添加的字节进行编译,HTML默认将添加字节做字符串处理
:param obj: 当前表的对象
:param header: 如果添加header命名则走自定制页面
:return:
"""
if header:
return "编辑"
return mark_safe("<a href='%s'>编辑</a>" % self.get_change_url(obj))
# 构建新的list_display
def get_new_list_display(self):
temp = []
temp.extend(self.list_display) # 迭代增加元素,即将list_display拆开一个个添加进去
temp.append(ModelStark.show_delbtn) # 将删除模型类对象添加进去
temp.append(ModelStark.show_editbtn) # 将编辑模型类对象添加进去
temp.insert(0, ModelStark.show_checkbox) # 多选按钮插入到第0个位置
"""
[<function ModelStark.show_checkbox at 0x0000025CCDED1C80>,
'title', 'price', 'state', 'publisher',
<function BookConfig.show_authors at 0x0000025CCDED1400>,
<function ModelStark.show_delbtn at 0x0000025CCDED1D08>,
<function ModelStark.show_editbtn at 0x0000025CCDED1D90>]
"""
return temp # 此时temp如上
def get_search_condition(self, request):
val = request.GET.get('q')
from django.db.models import Q # 导入过滤功能
q = Q() # 由于的Q不能接收字符串进行过滤,所以需要实例化q 来过滤
if val:
self.search_val = val
q.connector = "or" # 实例化q 只有且的功能,所以加上or
for field in self.search_fields:
q.children.append((field + "__contains", val)) # 拿出对应的搜索以元祖的形式添加
else:
self.search_val = None # 如果前端搜索框没有请求,则清空内容,就不会自带着内容了
# 返回搜索到的表的对象
return q
def patch_delete(self, request, queryset):
# 设置默认的批量删除方法
queryset.delete()
patch_delete.desc = '批量删除' # 给该函数添加desc对应的一个字符串
def get_new_actions(self):
"""
获取当前默认配置及自定制的批量操作方法
:return:
"""
temp = []
temp.extend(self.actions) # 添加是否有自定制的actions
temp.append(self.patch_delete) # 将默认批量删除方法导入
# print("get_new_actions------->", temp, temp[0])
return temp
def get_action_dict(self):
actions_list = []
for func in self.get_new_actions():
actions_list.append({
'name': func.__name__, # 获取批量操作函数
'desc': func.desc, # 获取批量操作名称
})
# print("get_action_dict-------->", actions_list)
return actions_list
def get_list_filter_links(self):
"""
如何拿出相关url并放入返回到前端,
深度拷贝get拿出单签url里的值params,利用.model._meta.get_field(字符串类型的字段名)的方法获取字段对应的表模型
判断是否存在一对多,多对多的关系或者choices,仅对该情况进行处理,其它情况则不处理抛出错误(如:一对一)
用表模型.remote_field.model.objects.all()的方法获得整个表的数据
此时先进行对all(显示全部)的情况进行处理,url删除含有的当前对象字符串对应的键值对
以下则是将其它键值对添加进去且逐个添加到a标签,并以字典的形式存放,前端提取即可
:return:
"""
list_filter_links = {}
# print(self.list_filter) # ['publisher', 'authors', 'state']
for filter_field in self.list_filter: # 循环多级过滤是否有设定,拿出字段
import copy
params = copy.deepcopy(self.request.GET) # 深度拷贝,可修改
current_field_val = params.get(filter_field) # 当前循环url里面对象的值
filter_field_obj = self.model._meta.get_field(filter_field) # 获取字段对象
# print("get_list_filter_links------>", filter_field_obj) # app01.Book.publisher
from django.db.models.fields.related import ForeignKey, ManyToManyField # 导入一对多,多对多模型进行比较
if isinstance(filter_field_obj, ForeignKey) or isinstance(filter_field_obj, ManyToManyField):
# print("获取整个表---->", filter_field_obj.remote_field.model) # 该方法可以获取整个表 <class 'app01.models.Publish'>
data = filter_field_obj.remote_field.model.objects.all() # 获取整个表内数据<QuerySet [<Publish: 苹果出版社11>, <Publish: 橘子出版社>, <Publish: 西瓜出版社>, <Publish: 111>, <Publish: 西瓜出版社>]>
elif filter_field_obj.choices: # 如果是选择的字段类型
data = filter_field_obj.choices
else:
raise Exception('过滤字段不能太简单!')
# print(data) # <QuerySet [<Author: A某>, <Author: B某>]> [(1, '已出版'), (2, '未出版')]
# 为每一条数据构建一个a标签
temp = []
if params.get(filter_field): # 如果all情况,存在该过滤字符串则删除该url字段
del params[filter_field]
# 为all 过滤 添加取除该段对象href
all_content = "<a class='btn btn-default btn-sm' href='?%s'>显示所有</a>" % params.urlencode()
temp.append(all_content)
for item in data:
if type(item) == tuple:
# 如果是元组类型,则是choice [(1, '已出版'), (2, '未出版')]
pk, text = item
else:
# model对象类型
pk, text = item.pk, str(item)
params[filter_field] = pk
_url = '?%s' % (params.urlencode())
if current_field_val:
if current_field_val == str(pk):
link = "<a class='active btn btn-default btn-sm' href='%s'>%s</a>" % (_url, text)
else:
link = "<a class='btn btn-default btn-sm' style='display:none' href='%s'>%s</a>" % (_url, text)
temp.append(link)
else:
link = "<a class='btn btn-default btn-sm' href='%s'>%s</a>" % (_url, text)
temp.append(link)
list_filter_links[filter_field] = temp
print("-------list_filter_links", list_filter_links)
# style='display:none'
return list_filter_links
def get_list_filter_condition(self):
"""
一级搜索过滤
:return:搜索完得到的Queryset
"""
q = Q()
for filter_field, val in self.request.GET.items():
if filter_field in ['page', 'q']:
continue
q.children.append((filter_field, val)) # 搜索
return q
def list_view(self, request):
"""
主要视图函数主要负责数据的首次显示,
大部分调用都在这里
:param request:
:return:返回带数据的list视图
"""
self.request = request
# 处理批量函数
if request.method == 'POST':
action_func_str = request.POST.get('action')
# print('action_func_str_list_view------->', action_func_str)
if action_func_str:
# 如果有,即选择非------的选项,则走以下语句
action_func = getattr(self, action_func_str) # 反射得到对应批量函数
_selected_action = request.POST.getlist('_selected_action') # 拿nid
queryset = self.model.objects.filter(pk__in=_selected_action) # 之间获取即可,这是orm的优化
# print("action_func_str_if--------->", _selected_action, action_func, queryset)
action_func(request, queryset) # 该为函数调用,如果是默认即是调用patch_delete方法,执行完走以下语句即可
queryset = self.model.objects.all() # 获取当前对象的Queryset,即当前表数据
# search 过滤
search_condition = self.get_search_condition(request) # 过滤及判断
# list_filter多级过滤
list_filter_condition = self.get_list_filter_condition() # 先过滤判断出词条
queryset = queryset.filter(search_condition).filter(list_filter_condition) # 再进行多级过滤
show_list = ShowList(request, self, queryset) # 构建展示数据
table_name = self.model._meta.verbose_name # 当前表的名字
add_url = self.get_add_url() # 将add_url定义,此时local就会自动传到前端
return render(request, 'stark/list_view.html', locals())
def get_model_form(self):
from django.forms import ModelForm # 导入ModelForm模块,即将model 转化成form 的组件
# 设定默认模板
class BaseModelForm(ModelForm):
# 设定默认模板,其中__all__
class Meta:
model = self.model # 告诉ModelForm是关联哪张表
fields = "__all__" # 展示全部数据字段
# 如果没有自定制则返回默认展示方式
return self.models_form_class or BaseModelForm
def add_view(self, request):
base_model_form = self.get_model_form() # 获取当前展示字段
if request.method == "GET":
form_obj = base_model_form() # 获取当前展示字段并传到前端
return render(request, 'stark/add_view.html', locals())
else:
form_obj = base_model_form(request.POST)
if form_obj.is_valid(): # 判断用户输入的值是否符合model的规定
form_obj.save() # 如果符合则保存到数据库,并返回查看页面
return redirect(self.get_list_url())
else:
return render(request, "stark/add_view.html", locals()) # 否则将携带报错信息的form返回当前页面提示用户
def change_view(self, request, nid):
base_model_form = self.get_model_form() # 获取需要展示字段
edit_obj = self.model.objects.filter(pk=nid).first()
if request.method == 'GET':
form_obj = base_model_form(instance=edit_obj) # 这句话让modelform可以显示valus内容以供编辑
return render(request, 'stark/change_view.html', locals())
else:
form_obj = base_model_form(request.POST, instance=edit_obj) # instance 是告诉modelform是编辑,且编辑哪个对象
if form_obj.is_valid():
form_obj.save()
return redirect(self.get_list_url())
else:
return render(request, 'stark/change_view.html', locals())
def delete_view(self, request, nid):
if request.method == 'POST':
self.model.objects.filter(pk=nid).delete()
return redirect(self.get_list_url())
# 不删除则返回
list_url = self.get_list_url()
return render(request, 'stark/delete_view.html', locals())
@property
def get_urls(self):
# 二级分发路径, 反向解析各种路径
temp = [
path("", self.list_view, name="%s_%s_list" % (self.app_label, self.model_name)),
path("add/", self.add_view, name="%s_%s_add" % (self.app_label, self.model_name)),
re_path("(\d+)/change/", self.change_view, name="%s_%s_change" % (self.app_label, self.model_name)),
re_path("(\d+)/delete/", self.delete_view, name="%s_%s_delete" % (self.app_label, self.model_name)),
]
return (temp, None, None)
class StarkSite:
"""
stark 全局类
"""
def __init__(self):
# 类的单例模式,提高执行效率(都是用同一空间),减少开辟多个内存空间
self._registry = {}
def register(self, model, admin_class=None, **options):
# model就是用户模型类(指向空间,即book或publish),admin_class 如果用户没有自己创建样式装修则用默认的
admin_class = admin_class or ModelStark # or 的方法,选择为非0的对象赋值
self._registry[model] = admin_class(model) # 给字典不断添加键值对,以model为键,配置类为值
def get_urls(self):
# 动态为注册的模型类创建增删改查URL
# 创建temp存放path
temp = []
# {Book:ModelAdmin(Book),Publish:ModelAdmin(Publish)}
for model, config_obj in self._registry.items():
# 将上面函数拿到的各个模型类键值对提取出来,config_obj就是
model_name = model._meta.model_name # 该方法是拿model的名字(字符串)
app_label = model._meta.app_label # 该方法是拿model所在app的字符串
"""
二级分发小例子:
urls = [
path('index/', index),
path('book/',([
path('test01/', test01),
path('test02/', test02),
],None,None))
]
"""
# 给temp添加path,即book_manage/book 或 book_manage/publishb
# config_obj.get_urls 即是增删改查的路径(二级分发),默认就会有自己的二级temp
temp.append(
path("%s/%s/" % (app_label, model_name), config_obj.get_urls)
)
return temp
@property
def urls(self):
# property 方法是使调用它的时候直接使用urls即可调用(默认Admin做的,可以参考urls里面的Admin)
return self.get_urls(), None, None # 调用时返回这些值,None是固定传的,一个是app名字一个是空间名字所以直接None
site = StarkSite()
"""
将全局类实例化成一个对象,所有的调用都使用这个实例化对象,
这样空间就永远是一个,只是实例化键值对来指向它
"""
# page
class Pagination(object):
def __init__(self, request, current_page, all_data, per_page_num=3, max_page_count=11):
"""
封装分页相关数据
:param request: 请求对象
:param current_page:当前页
:param all_data: 总数据
:param per_page_num: 每页显示的数据条数
:param max_page_count: 最多显示的页码个数
"""
try:
current_page = int(current_page) # 是否有跳转页
except Exception as e:
current_page = 1 # 没有则默认1
# print("-------333", type(current_page))
if current_page < 1:
current_page = 1 # 如果为负,则默认1,容错机制
self.current_page = current_page
self.all_count = len(all_data) # 获取总数据长度即有多少个对象
self.per_page_num = per_page_num
all_pager, tmp = divmod(self.all_count, per_page_num) # 商除,取得商(显示多少页)和余(多出来的数据)
if tmp:
all_pager += 1 # 如果有多出来的数据,则页数+1
# 定义数据便于调用
self.all_pager = all_pager
self.max_page_count = max_page_count
self.max_page_count_half = int((max_page_count - 1) / 2) # 最大显示页数的一半
self.request = request
import copy
self.params = copy.deepcopy(self.request.GET) # 深度拷贝,因为之间拿出来的Queryset是不能修改值的
@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):
# 配置html页面
if self.all_pager <= self.max_page_count: # 如果总页码小于11个
pageRange = range(1, self.all_pager + 1)
else: # 如果大于总页码,分以下3种情况
# 如果当前页面小于页面上最多显示的(11-1)/2页码
if self.current_page <= self.max_page_count_half:
pageRange = range(1, self.max_page_count + 1) #
else: # 如果当前页大于5,分以下两种情况
if (self.current_page + self.max_page_count_half) > self.all_pager:
# 如果当前页数+5大于总页数,即已经翻到最后面了
pageRange = range(self.all_pager - self.max_page_count + 1, self.all_pager + 1)
else:
# 当前页码即是处于中间位置
pageRange = range(self.current_page - self.max_page_count_half,
self.current_page + self.max_page_count_half + 1)
page_html_list = [] # 设置前端页面
# 开始为首页和上一页添加a标签
self.params["page"] = 1 # 页数为1
first_page = '<nav aria-label="Page navigation"><ul class="pagination"><li><a href="?%s">首页</a></li>' % (
self.params.urlencode(),) # 首页跳转解析源码
page_html_list.append(first_page)
self.params['page'] = self.current_page - 1
if self.current_page <= 1:
prev_page = '<li class="disabled"><a href="#">上一页</a></li>' # 如果小于等于1了,表名已经在第一页不能跳转了
else:
prev_page = '<li><a href="?%s">上一页</a></li>' % (self.params.urlencode()) # 否则
page_html_list.append(prev_page)
# 为每个数字页码添加a标签
for i in pageRange:
self.params['page'] = i
if i == self.current_page: # 如果页码刚好等于当前页面,则添加active 特殊样式
temp = '<li class="active"><a href="?%s">%s</a></li>' % (self.params.urlencode(), i,)
else: # 否则添加普通样式即可
temp = '<li><a href="?%s">%s</a></li>' % (self.params.urlencode(), i,)
page_html_list.append(temp) # 写入
search_page = '<li><a href="?%s" id="search_id"><span aria-label="true">搜索</span</a></li> </ul></nav>' % (
self.params.urlencode(),)
# 为下一页和尾页添加a标签
self.params['page'] = self.current_page + 1
if self.current_page >= self.all_pager: # 如果是最后一页,则添加不可点击特殊样式
next_page = '<li class="disabled"><a href="#">下一页</a></li>'
else:
next_page = '<li><a href="?%s">下一页</a></li>' % (self.params.urlencode())
page_html_list.append(next_page)
self.params['page'] = self.all_pager
last_page = '<li><a href="?%s">尾页</a></li>' % (self.params.urlencode())
page_html_list.append(last_page)
val = self.request.GET.get('?')
import copy
params = copy.deepcopy(self.request.GET) # 深度拷贝,可修改
if not params:
# 如果有使用字符搜索过滤等,则关闭页数搜索功能
text_page = '<li><span><input type="text" id="text_id" style="height: 20px;width: 40px"></span></li>'
page_html_list.append(text_page)
page_html_list.append(search_page)
script_page = '\n<script>' \
'\n\t$("#text_id").on("blur", function () {' \
'\n\t\tlet num = $("#text_id").val();' \
'\n\t\tlet a_obj = document.getElementById("search_id");' \
'\n\t\tconsole.log(num);' \
'\n\t\ta_obj.href ="?page=" + num;})\n' \
'\n</script>'
page_html_list.append(script_page)
return ''.join(page_html_list) # 将所有代码拼接起来
# app01 stark
from stark.servise.sites import site, ModelStark
from .models import *
from django.forms import ModelForm
class BookModelForm(ModelForm):
class Meta:
model = Book
fields = "__all__"
error_messages = {
"title": {"required": " 该字段不能为空!"}
}
class BookConfig(ModelStark):
def show_authors(self, obj=None, header=False):
if header:
return "作者信息"
return " ".join([author.name for author in obj.authors.all()])
# 自定制的模板,继承ModelStark,也就是还有没有覆盖的ModelStark变量及方法
list_display = ["title", "price", "state", "publisher", show_authors]
list_display_links = ['title', ]
model_form_class = BookModelForm
per_page_num = 3
search_fields = ['title', 'price']
list_filter=["publisher","authors","state"]
def patch_init(self, request, queryset):
queryset.updata(price = 0)
patch_init.desc = '价格初始'
site.register(Book, BookConfig) # 疑问,为什么执行了两遍
site.register(Publish)
# print(site._registry)