一、准备:
1、组件部分(数据增删改查实现):模仿django admin开发一个组件
效果:
(1)、启动服务(先在settings中添加app)
from django.apps import AppConfig from django.utils.module_loading import autodiscover_modules class XadminConfig(AppConfig): name = \'xadmin\' def ready(self): autodiscover_modules(\'xadmin\')
(2)、注册表
from django.db import models # Create your models here. class Author(models.Model): nid = models.AutoField(primary_key=True) name=models.CharField( max_length=32) age=models.IntegerField() # 与AuthorDetail建立一对一的关系 authorDetail=models.OneToOneField(to="AuthorDetail",on_delete=models.CASCADE) def __str__(self): return self.name class AuthorDetail(models.Model): nid = models.AutoField(primary_key=True) birthday=models.DateTimeField() telephone=models.BigIntegerField() addr=models.CharField( max_length=64) def __str__(self): return self.addr class Publish(models.Model): nid = models.AutoField(primary_key=True) name=models.CharField( max_length=32) city=models.CharField( max_length=32) email=models.EmailField() def __str__(self): return self.name class Book(models.Model): nid = models.AutoField(primary_key=True, verbose_name=" 编号") title = models.CharField( max_length=32, verbose_name="书籍名称") publishDate=models.DateTimeField(null=True, verbose_name=\'日期\') price=models.DecimalField(max_digits=5, decimal_places=2) # 与Publish建立一对多的关系,外键字段建立在多的一方 publish=models.ForeignKey(to="Publish",to_field="nid",on_delete=models.CASCADE) # 与Author表建立多对多的关系,ManyToManyField可以建在两个模型中的任意一个,自动创建第三张表 authors=models.ManyToManyField(to=\'Author\',) def __str__(self): return self.title
from xadmin.service.start__xadim import site, ModelAdmin from app01 import models from django import forms from django.utils.safestring import mark_safe from django.urls import reverse class BookConfig(ModelAdmin): # def add_tag_change(self, obj=None, is_header=False): # if is_header: # 如果是表头 # return \'修改操作\' # return mark_safe(\'<a href="%s">修改</a>\' % reverse(\'change_data\', args=(obj.pk,))) # 如果是表单数据 # # def add_tag_delete(self, obj=None, is_header=False): # if is_header: # return \'删除操作\' # return mark_safe(\'<a href="%s/delete">删除</a>\' % reverse(\'delete_data\', args=(obj.pk,))) # # def add_tag_checked(self, obj=None, is_header=False): # if is_header: # return mark_safe(\'<input type="checkbox" name="checked_data" class="all">\') # return mark_safe(\'<input type="checkbox" name="checked_data" value="%s" class="list_all">\' % obj.pk) class ModelForms(forms.ModelForm): class Meta: model = models.Book fields = \'__all__\' widgets = {\'publishDate\': forms.widgets.TextInput(attrs={\'type\': \'date\', }), } labels = { \'title\': \'书名\', \'price\': \'价格\', } # def action_test(self, queryset, request): # print(self,type(self)) # for i in queryset: # i.price = 100 # i.save() # action_test.short_description = "批量初始化" # action函数功能实现简介 list_display = [\'nid\', \'title\', \'price\', \'publish\', \'publishDate\', \'authors\'] list_display_links = [\'title\'] model_forms_chinese_field = ModelForms list_search_fields = [\'title\', \'price\'] # list_action_func = [action_test] list_filter = [\'title\', \'publish\', \'authors\'] class PublishConfig(ModelAdmin): list_display = [\'nid\', \'title\', \'price\'] site.register(models.Book, BookConfig) site.register(models.Publish) site.register(models.Author) site.register(models.AuthorDetail)
(3)、服务(url分发,数据增删改查,展示等等)
注意问题:
url二级分发为什么在配置类实现?
url地址反向解析
自定义显示哪些字段内容,多对多字段该如何处理,修改和删除标签、哪个字段可点击进入到修改页面,该如何实现
自定义哪些字段可以搜索,获取搜索值后用Q方法处理条件
自定义批量处理函数,页面标签value值应该放什么(函数名,选中数据的pk值)
自定义哪些字段可以筛选(关系字段如何获取(all方法),普通字段如何获取)
field_obj = self.model_admin_self.model._meta.get_field(filter_field) if isinstance(field_obj, ManyToManyField) or isinstance(field_obj, ForeignKey): # 过滤标签是不是多对多或者多对一关系 queryset_list = field_obj.remote_field.model.objects.all() # 是的话取到所有关联表对象
修改页面时,关系字段应该可以直接点击添加按钮添加关系字段对象(浏览器父类窗口,子类窗口的使用)
window.opener.pop_response(\'{{ ret.pk }}\',"{{ ret.text }}",\'{{ ret.pop_res_id }}\') //执行父页面的函数pop_response,向里面传递参数,用于数据更新后的DOM操作
function pop(url) { window.open(url,"","width=600,height=400,top=100,left=100") // 为a标签“+”的点击事件函数,打开一个添加数据的子窗口 } function pop_response(pk, text, pop_res_id) { console.log(pk, text, pop_res_id) var $option = $(\'<option>\') //创建一个<option>标签 console.log($option) $option.val(pk) //标签value值为这条数据的主键值 $option.text(text) // 文本为__str__ $option.attr("selected","selected") //option标签为选中状态 $(\'#\'+pop_res_id).append($option) // 将这条option标签插入到select标签, } </script>
主页面展示的数据应该放在一个类下,比如分页数据、表头数据、表单数据、函数数据、过滤字段数据等。只需要在视图函数中创建这个类对象,把所有数据对象、request、当前models的配置类self对象作为参数传入,把创建的这个类对象传入模板语言,模板语言直接通过这个对象可以调用类下的所有方法,用于展示分页、表头、表单等数据。
修改和增加页面使用modelform模块快速构建一个html页面,modelform在视图的使用,关系字段在增加和修改页面如何实时添加数据
def add(self, request): # 获取一个modelform类,并创建一个modelform类对象,用于添加或者修改页面的渲染 modelform = self.get_model_forms() forms_obj = modelform() # print(forms_obj) # modelform对象 from django.forms.boundfield import BoundField # from django.forms.models import ModelMultipleChoiceField from django.forms.models import ModelChoiceField # ModelMultipleChoiceField继承ModelChoiceField for forms_field_obj in forms_obj: # print(\'-------\', forms_field_obj, type(forms_field_obj)) # django.forms.boundfield.BoundField # print(\'+++++++\', forms_field_obj.field, type(forms_field_obj.field)) # <class \'django.forms.models.ModelMultipleChoiceField\'> # modelform字段对象 # print(\'*******\', forms_field_obj.name, type(forms_field_obj.name)) # authors <class \'str\'> 字段名字(字符串) if isinstance(forms_field_obj.field, ModelChoiceField): # 如果这个字段对象是多对多或者一对多字段 forms_field_obj.is_pop = True # 用于模板语言判断是否为关系字段 # print(forms_field_obj.field.queryset, type(forms_field_obj.field.queryset)) # 返回这个关系字段下所有的对象 # print(forms_field_obj.field.queryset.model, type(forms_field_obj.field.queryset.model)) # 返回这个关系字段的关联模型表 app_label = forms_field_obj.field.queryset.model._meta.app_label # 获取关联字段的关系模型表所在的app model_name = forms_field_obj.field.queryset.model._meta.model_name # 获取关联字段的关系模型表的表名 url = reverse(\'{}_{}_add_data\'.format(app_label, model_name)) forms_field_obj.url = url+\'?pop_res_id=id_{}\'.format(forms_field_obj.name) # 用于添加页面关系字段‘+’的链接拼接 ,参数用于判断是返回当前添加页还是主添加页,以及为哪个select标签进行DOM操作 if request.method == \'POST\': forms_obj = modelform(request.POST) if forms_obj.is_valid(): add_obj = forms_obj.save() pop_res_id = request.GET.get(\'pop_res_id\') if pop_res_id: ret = {"pk": add_obj.pk, "text": str(add_obj), \'pop_res_id\': pop_res_id} return render(request, \'pop.html\', {\'ret\': ret}) return redirect(\'%s\' % (self.addtag.get_show_reverse_url(self.model))) return render(request, \'add_data.html\', {\'forms_obj\': forms_obj})
<script> window.opener.pop_response(\'{{ ret.pk }}\',"{{ ret.text }}",\'{{ ret.pop_res_id }}\') //执行父页面的函数pop_response,向里面传递参数,用于数据更新后的DOM操作 {#console.log(\'{{ ret.pk }}\',"{{ ret.text }}",\'{{ ret.pop_res_id }}\')#} window.close() </script>
<div class="container"> <div class="row"> <div class="col-md-6 col-md-offset-3" style="padding-top: 100px"> <hr> <form action="" method="post" novalidate> {% csrf_token %} {% for field in forms_obj %} <p style="position: relative">{{ field.label }} {{ field }} <span>{{ field.errors.0 }}</span> {% if field.is_pop %} <a onclick="pop(\'{{ field.url }}\')" style="position: absolute;right: -30px; top: 25px; font-size: 20px; height: 50%">+</a> {% endif %} </p> {% endfor %} <div class="submit-btn"> <button class="btn btn-info ">提交</button> </div> </form> </div> </div> </div> <script> function pop(url) { window.open(url,"","width=600,height=400,top=100,left=100") // 为a标签“+”的点击事件函数,打开一个添加数据的子窗口 } function pop_response(pk, text, pop_res_id) { console.log(pk, text, pop_res_id) var $option = $(\'<option>\') //创建一个<option>标签 console.log($option) $option.val(pk) //标签value值为这条数据的主键值 $option.text(text) // 文本为__str__ $option.attr("selected","selected") //option标签为选中状态 $(\'#\'+pop_res_id).append($option) // 将这条option标签插入到select标签, } </script>
url路径保存搜索条件:先深copy之前的url条件,然后再往条件添加新的条件,添加后换成url编码,最后把它拼到a标签
params = copy.deepcopy(self.request.GET) #{‘name’: aike} params[id] = 1 #{\'name\': aike, \'id\': 1} url = params.urlencode() # # 把字典转为URL编码(?name=aike&id=1) link_tag = mark_safe("<a href=\'?%s\' class=\'filter_field active\'>%s</a>" % (url, text))
(4)、url
from django.contrib import admin from xadmin.service.start__xadim import site from django.urls import path, re_path urlpatterns = [ path(\'admin/\', admin.site.urls), path(\'xadmin/\', site.urls),]
2、权限管理部分:RBAC权限介绍,用django制作一个简单的权限组件
(1)、表设计:用户表、角色表、权限表、权限分组表
用户拥有角色,角色拥有权限(一条url代表一条权限),权限拥有分组。一张表的增删改查分为一组,例如学生表,查看学生,编辑学生,删除学生,添加学生可归为学生管理。
from django.db import models # Create your models here. from django.db import models # Create your models here. class User(models.Model): nid = models.AutoField(primary_key=True) username = models.CharField(max_length=32, ) password = models.CharField(max_length=32,) roles = models.ManyToManyField(to=\'Roles\') def __str__(self): return self.username class Meta: verbose_name = \'用户表\' class Roles(models.Model): nid = models.AutoField(primary_key=True) title = models.CharField(max_length=12, verbose_name=\'角色名\') permission = models.ManyToManyField(to=\'Permission\', verbose_name=\'权限\') def __str__(self): return self.title class Meta: verbose_name = \'角色表\' class Permission(models.Model): nid = models.AutoField(primary_key=True) title = models.CharField(max_length=12) urls = models.CharField(max_length=120) # action和group用于第二种实现权限校验方式需要添加的字段,不添加这些也能实现 # 主要作用是在HTML页面当中用模板语言判断是否有权限时,url地址不用写很死 action = models.CharField(max_length=32, null=True) # 功能 增删改查 group = models.ForeignKey(to=\'PermissionGroup\', on_delete=models.CASCADE, null=True) # 权限分组 def __str__(self): return self.title class Meta: verbose_name = \'权限表\' # 权限分组, 用户分组、角色分组等。用于第二种权限校验方式 class PermissionGroup(models.Model): nid = models.AutoField(primary_key=True) title = models.CharField(max_length=32) def __str__(self): return self.title
(2)、登录成功注册session:session中应该存放哪些数据,用户主键、用户名、用户权限url列表、权限分组等。
def reg_session(request, user): """ 登录后将权限和用户id存入session,存入后才能进行权限校验 :param request: :param user: 登录对象 :return: 没有返回值 """ request.session[\'user_id\'] = user.pk request.session[\'username\'] = user.username # 将用户的所有权限url以列表形式添加到session permission_urls_obj_list = user.roles.values(\'permission__urls\').distinct() permission_urls_list = [permission_urls[\'permission__urls\'] for permission_urls in permission_urls_obj_list] request.session[\'permission_urls_list\'] = permission_urls_list # 将用户权限分组nid为键(1为用户管理,2为角色管理),action和url为值,以字典形式存入到session, # {1: { # \'urls\': [\'/users/\', \'/users/add/\', \'/users/delete/(\\d+)\', \'users/edit/(\\d+)\'], # \'actions\': [\'list\', \'add\', \'delete\', \'edit\']}, # 第二种校验方式,可以在HTML页面不写死url判断是否有删改的操作,即该不该显示删除、修改或者增加的按钮 permission_dict = {} urls_action_nid = user.roles.values(\'permission__urls\', \'permission__action\', \'permission__group__nid\').distinct() for i in urls_action_nid: nid = i[\'permission__group__nid\'] if nid not in permission_dict: permission_dict[nid] = { \'urls\': [i[\'permission__urls\']], \'actions\': [i[\'permission__action\']] } else: permission_dict[nid][\'urls\'].append(i[\'permission__urls\']) permission_dict[nid][\'actions\'].append(i[\'permission__action\']) request.session[\'permission_dict\'] = permission_dict # 菜单栏显示哪些权限,角色管理或者用户管理 menu_permission = user.roles.filter(permission__action=\'show\').values(\'permission__urls\', \'permission__group__title\') menu_permission_list = [] for item in menu_permission: menu_permission_list.append(item) request.session[\'menu_permission_list\'] = menu_permission_list
(3)、中间件权限校验:权限url以正则方式存储在数据库,校验时应该以正则方式校验。白名单处理:登录、admin等。判断是否登录。最后进行权限校验
from django.utils.deprecation import MiddlewareMixin import re from django.shortcuts import render, redirect, HttpResponse def re_checkout(permission_urls_list, path): flag = False for permission_urls in permission_urls_list: permission_urls = \'^%s$\' % permission_urls ret = re.search(permission_urls, path) if ret: flag = True break return flag class PermissionVerify(MiddlewareMixin): def process_request(self, request): # 校验白名单 path = request.path filter_urls = [\'/login/\', \'/reg/\', \'/admin/.*\', \'/logout/\'] for urls in filter_urls: ret = re.search(urls, path) # print(\'******\', ret) if ret: return None # 校验是否登录 user_id = request.session.get(\'user_id\', None) if not user_id: return redirect(\'/login/\') # 网址存在再进行权限校验 # path_list = [\'/user/edit/(\d+)/\', \'/login/\', \'/user/\', \'/user/add/\', \'/roles/\'] # flag = re_checkout(path_list, path) # if not flag: # return HttpResponse(\'404\') # 校验权限 # permission_urls_list = request.session[\'permission_urls_list\'] # # flag = re_checkout(permission_urls_list, path) # if not flag: # return HttpResponse(\'没有权限\') # # return None # 第二种校验方式,可以在HTML页面不写死url判断是否有删改的操作,即该不该显示删除、修改或者增加的按钮 permission_dict = request.session.get(\'permission_dict\') for urls_actions in permission_dict.values(): permission_urls_list = urls_actions[\'urls\'] for permission_urls in permission_urls_list: new_urls = \'^{}$\'.format(permission_urls) ret = re.match(new_urls, path) print(ret) # 将用户权限分组:nid为键(1为用户管理,2为角色管理),action和url为值,以字典形式存入到session, # permission_urls_list={ # 1: { # \'urls\': [\'/users/\', \'/users/add/\', \'/users/delete/(\\d+)\', \'users/edit/(\\d+)\'], # \'actions\': [\'show\', \'add\', \'delete\', \'edit\']}, # 2: { # \'urls\': [\'/roles/\'], # \'actions\': [\'show\']} # } # 可以实现访问哪个权限分组(用户管理,角色管理),就能得到这个分组所有的权限url和对action, # 当用户访问的是用户管理时,对应键值为1,那么通过遍历,就能取到这个分组拥有的权限 if ret: request.permission_actions_list = urls_actions[\'actions\'] print(request.permission_actions_list) return None return HttpResponse(\'没有权限\')
二、导入组件:
将权限和admin组件的所有静态文件、templates等文件一并拷贝至项目即可。然后注册app,加入中间件,项目用户表与权限组件用户表一对一关系。自己写的admin组件注册表,选择实现自定义配置类
三、功能扩展:
需求将全部在项目app中实现,例如登录注册,自定义配置类。其中自定义配置类是在为表注册组件时实现,所有需要扩展功能时,是在自身配置类中实现。
项目中遇到的小问题:
self与类名调用类方法的区别,self调用会实例化方法,而类调用不会。所有传参数时,如果是实例化的方法,不需要传self,而类调用方法不会被实例化,使用时需要传self参数。
使用ajax时,视图返回的值最好使用JsonResponse返回一个字典,非字典时需要设置safe=False
def func(request): ret = {\'name\':aike} return JsonResponse(ret) def func1(request): ret = [1,2,3], return JsonResponse(ret,safe=False)
删除多对多字段中的值用remove方法,删除queryset或者obj对象用delete方法。
关系字段,通过字段对象取得关联表所有对象
field_obj = self.model_admin_self.model._meta.get_field(filter_field) if isinstance(field_obj, ManyToManyField) or isinstance(field_obj, ForeignKey): # 过滤标签是不是多对多或者多对一关系 queryset_list = field_obj.remote_field.model.objects.all() # 是的话取到所有关联表对象 else: queryset_list = self.model_admin_self.model.objects.values(\'pk\', filter_field) # 不是的话取到对应字段值即可
Q方法的进阶使用
search_connection = Q() # 创建一个Q对象 search_connection.connector = \'or\' # Q对象查询条件为或关系 search_connection.children.append(条件)# 将查询条件添加到children列表,children接收一个元祖,一个为字段,一个为字段值 search_connection 为一个查询条件
不清空筛选条件
params = copy.deepcopy(self.request.GET) #{‘name’: aike} params[id] = 1 #{\'name\': aike, \'id\': 1} url = params.urlencode() # # 把字典转为URL编码(?name=aike&id=1) link_tag = mark_safe("<a href=\'?%s\' class=\'filter_field active\'>%s</a>" % (url, text))