一、准备:
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))