介绍
权限分配就是给角色分配权限,给用户分配角色,之前我们都是通过django admin来进行权限分配的,但是我们实现的是rbac组件,相应的权限分配功能我们应该也由自己实现并集成到rbac中。
功能
实现更友好的权限分配页面。
- 角色管理
- 用户管理
- 菜单和权限管理
- 批量权限操作
- 权限分配
角色管理
实现对角色增删改的功能,就是展示一个列表。
角色列表和添加
使用forms.ModelForm对角色列表进行展示渲染和添加。
rbac/urls.py
from django.conf.urls import url
from django.contrib import admin
from rbac.views import role
urlpatterns = [
url(r'^role/list/$', role.role_list, name='role_list'),
url(r'^role/add/$', role.role_add, name='role_add'),
]
项目级别路由匹配urls.py文件,路由下发到rbac组件中
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^rbac/', include('rbac.urls', namespace='rbac')),
url(r'^', include('web.urls')),
]
视图函数文件 rbac/views/role.py
from django.shortcuts import render, HttpResponse, redirect
from django.urls import reverse
from django import forms
from rbac import models
class RoleModelForm(forms.ModelForm):
class Meta:
model = models.Role
fields = ['title']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'})
}
def role_list(request):
"""
角色列表
:param request:
:return:
"""
role_queryset = models.Role.objects.all()
return render(request, 'rbac/role_list.html', {'roles': role_queryset})
def role_add(request):
"""
增加角色
:param request:
:return:
"""
if request.method == 'GET':
form = RoleModelForm()
return render(request, 'rbac/role_add.html', {'form': form})
form = RoleModelForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('rbac:role_list'))
# 验证失败的返回
return render(request, 'rbac/role_add.html', {'form': form})
模板层文件
rbac/templates/rbac/role_list.html
{% extends 'layout.html' %}
{% block content %}
<div class="luffy-container">
<div class="btn-group" style="margin: 5px 0">
<a class="btn btn-default" href="{% url 'rbac:role_add' %}">
<i class="fa fa-plus-square" aria-hidden="true"></i> 添加角色
</a>
</div>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>选项</th>
</tr>
</thead>
<tbody>
{% for row in roles %}
<tr>
<td>{{ row.id }}</td>
<td>{{ row.title }}</td>
<td>
<a style="color: #333333;" href="#">
<i class="fa fa-edit" aria-hidden="true"></i></a>
<a style="color: #d9534f;" href="#"><i class="fa fa-trash-o"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
rbac/templates/rbac/role_add.html
{% extends 'layout.html' %}
{% block content %}
<div class="luffy-container">
<form action="" method="post" class="form-horizontal" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-8">
{{ field }}
<span style="color:red;">{{ field.errors.0 }}</span>
</div>
</div>
{% endfor %}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-8">
<input type="submit" value="保 存" class="btn btn-primary">
</div>
</div>
</form>
</div>
{% endblock %}
效果展示
角色编辑
角色的编辑页面和角色添加页面显示是一致的,不同点是编辑页面中展示了已经想修改的角色内容。
我们所写的role_add.html是根据后台传递过来的form进行渲染展示的,根据传递form内容的不同展示不同的信息,所以这是一个共同的新增和修改模板(角色增改,用户增改都可以使用),将其重命名为change.html。
路由
url(r'^role/edit/(?P<pk>\d+)/$', role.role_edit, name='role_edit')
视图函数
def role_edit(request, pk):
"""
修改角色
:param request:
:return:
"""
obj = models.Role.objects.filter(id=pk).first()
if not obj:
return HttpResponse('角色不存在')
if request.method == 'GET':
form = RoleModelForm(instance=obj)
return render(request, 'rbac/change.html', {'form': form})
form = RoleModelForm(instance=obj, data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('rbac:role_list'))
return render(request, 'rbac/change.html', {'form': form})
change.html
{% extends 'layout.html' %}
{% block content %}
<div class="luffy-container">
<form action="" method="post" class="form-horizontal" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-8">
{{ field }}
<span style="color:red;">{{ field.errors.0 }}</span>
</div>
</div>
{% endfor %}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-8">
<input type="submit" value="保 存" class="btn btn-primary">
</div>
</div>
</form>
</div>
{% endblock %}
效果
角色删除
当删除角色时,需要用户进行二次确认才能删除,防止误点直接删除。
路由:
url(r'^role/del/(?P<pk>\d+)', role.role_del, name='role_del'),
视图函数
def role_del(request, pk):
"""
角色删除
:param request:
:param pk:要被删除的角色id
:return:
"""
origin_url = reverse('rbac:role_list')
if request.method == "GET":
return render(request, 'rbac/delete.html', {'cancel_url': origin_url})
models.Role.objects.filter(id=pk).delete()
return redirect(origin_url)
delete.html
{% extends 'layout.html' %}
{% block content %}
<div class="luffy-container">
<div class="alert alert-danger" role="alert">
<form action="" method="post">
{% csrf_token %}
<p style="font-size: 13px;">
<i class="fa fa-warning" aria-hidden="true"></i>删除后不可恢复,请确认是否进行删除?
<div style="margin-top:20px;">
<a href="{{ cancel_url }}" class="btn btn-default btn-sm">取消</a>
<input type="submit" class="btn btn-danger btn-sm" value="确 认">
</div>
</p>
</form>
</div>
</div>
{% endblock %}
知识点
- ModelForm
- 根据namespace和name反向生成
- 模板查找顺序,根目录下templates->根据每个注册app的顺序去app下templates下找,防止模板重名,找到了意料之外的模板,给设置目录进行区分。
- 在写模板的时候,尽量可以进行重用
用户管理
用户列表和添加
本质上是对一张表的操作,我们对name、password、email这三个字段进行管理,roles字段不要动,对于关系的管理我们都放在权限配置中进行管理,对用户信息只做曾闪改操作。
路由
url(r'^user/list/$', user.user_list, name='user_list'),
url(r'^user/add/$', user.user_add, name='user_add'),
视图函数
def user_list(request):
"""
角色列表
:param request:
:return:
"""
user_queryset = models.UserInfo.objects.all()
return render(request, 'rbac/user_list.html', {'users': user_queryset})
def user_add(request):
"""
增加角色
:param request:
:return:
"""
if request.method == 'GET':
form = UserModelForm()
return render(request, 'rbac/change.html', {'form': form})
form = UserModelForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('rbac:user_list'))
# 验证失败的返回
return render(request, 'rbac/change.html', {'form': form})
UserModelForm
此处重点是统一给生成的字段添加样式,使用__init__方法实现
,钩子函数对两次密码输入相等性进行检查
class UserModelForm(forms.ModelForm):
confirm_password = forms.CharField(label='确认密码', widget=forms.TextInput(attrs={'type': 'password'}))
class Meta:
model = models.UserInfo
fields = ["name", "email", "password", 'confirm_password']
widgets = {
'password': forms.TextInput(attrs={'type': 'password'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 统一给ModelForm生成的字段添加样式
for name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
def clean_confirm_password(self):
password = self.cleaned_data['password']
confirm_password = self.cleaned_data['confirm_password']
if password != confirm_password:
raise ValidationError('两次密码不一致')
return confirm_password
利用django的翻译功能对ModelForm的错误提示信息进行汉化
settings.py文件中修改LANGUAGE_CODE = 'zh-hans'
user_list.html
{% extends 'layout.html' %}
{% block content %}
<div class="luffy-container">
<div class="btn-group" style="margin: 5px 0">
<a class="btn btn-default" href="{% url 'rbac:user_add' %}">
<i class="fa fa-plus-square" aria-hidden="true"></i> 添加用户
</a>
</div>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>序号</th>
<th>用户名</th>
<th>邮件</th>
<th>选项</th>
</tr>
</thead>
<tbody>
{% for row in users %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ row.name }}</td>
<td>{{ row.email }}</td>
<td>
<a style="color: #333333;" href="#">
<i class="fa fa-edit" aria-hidden="true"></i></a>
<a style="color: #d9534f;" href="#"><i class="fa fa-trash-o"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
效果:
用户编辑
对于编辑时,是不能直接编辑密码的,对于密码的修改应该有一个独立的密码修改功能。编辑时除了密码之外的字段。
此时我们只需要修改name和email两个字段,就需要为其单独创建一个ModelForm
class UpdateUserModelForm(forms.ModelForm):
class Meta:
model = models.UserInfo
fields = ['name', 'email']
# 手动给每个字段添加样式
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.TextInput(attrs={'class': 'form-control'}),
}
视图函数
def user_edit(request, pk):
"""
修改用户
:param request:
:return:
"""
obj = models.UserInfo.objects.filter(id=pk).first()
if not obj:
return HttpResponse('角色不存在')
if request.method == 'GET':
form = UpdateUserModelForm(instance=obj)
return render(request, 'rbac/change.html', {'form': form})
form = UpdateUserModelForm(instance=obj, data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('rbac:user_list'))
return render(request, 'rbac/change.html', {'form': form})
效果
密码修改
同样也是重新创建一个ModelForm
class ResetPasswordUserModelForm(forms.ModelForm):
confirm_password = forms.CharField(label='确认密码', widget=forms.TextInput(attrs={'type': 'password'}))
class Meta:
model = models.UserInfo
fields = ["password", 'confirm_password']
widgets = {
'password': forms.TextInput(attrs={'type': 'password'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 统一给ModelForm生成的字段添加样式
for name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
def clean_confirm_password(self):
password = self.cleaned_data['password']
confirm_password = self.cleaned_data['confirm_password']
if password != confirm_password:
raise ValidationError('两次密码不一致')
return confirm_password
路由
url(r'^user/reset/password/(?P<pk>\d+)/$', user.user_reset_pwd, name='user_reset_pwd')
视图函数
def user_reset_pwd(request, pk):
"""
重置密码
:param request:
:param pk:
:return:
"""
obj = models.UserInfo.objects.filter(id=pk).first()
if not obj:
return HttpResponse('角色不存在')
if request.method == 'GET':
form = ResetPasswordUserModelForm()
return render(request, 'rbac/change.html', {'form': form})
form = ResetPasswordUserModelForm(instance=obj, data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('rbac:user_list'))
return render(request, 'rbac/change.html', {'form': form})
列表展示页新增密码修改项
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>序号</th>
<th>用户名</th>
<th>邮件</th>
<th>重置密码</th>
<th>选项</th>
</tr>
</thead>
<tbody>
{% for row in users %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ row.name }}</td>
<td>{{ row.email }}</td>
<td><a href="{% url 'rbac:user_reset_pwd' pk=row.id %}">重置密码</a></td>
<td>
<a style="color: #333333;" href="{% url 'rbac:user_edit' pk=row.id %}">
<i class="fa fa-edit" aria-hidden="true"></i></a>
<a style="color: #d9534f;" href="#"><i class="fa fa-trash-o"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
效果:
用户删除
删除重用之前角色删除的代码就可以了
def user_del(request, pk):
"""
用户删除
:param request:
:param pk:要被删除的用户id
:return:
"""
origin_url = reverse('rbac:user_list')
if request.method == "GET":
return render(request, 'rbac/delete.html', {'cancel_url': origin_url})
models.UserInfo.objects.filter(id=pk).delete()
return redirect(origin_url)
菜单和权限管理
将一级菜单、二级菜单、权限展示在一个页面,方便用户进行选择
一级菜单显示
使用到bootstrap的面板,在对数据进行展示。
路由
url(r'^menu/list/$', menu.menu_list, name='menu_list'),
视图函数 menu.py
def menu_list(request):
"""
菜单和权限列表
:param request:
:return:
"""
menus = models.Menu.objects.all()
return render(request, 'rbac/menu_list.html', {'menus': menus})
menu_list.html
{% extends 'layout.html' %}
{% block content %}
<div class="luffy-container">
<div class="col-md-3">
<div class="panel panel-info">
<!-- Default panel contents -->
<div class="panel-heading">
<i class="fa fa-book" aria-hidden="true"></i>一级菜单
<a href="" class="right btn btn-success btx-xs"
style="margin: -3px; padding: 2px 8px;">
<i class="fa fa-plus-circle" aria-hidden="true"></i>
新建
</a>
</div>
<!-- Table -->
<table class="table">
<thead>
<tr>
<th>名称</th>
<th>图标</th>
<th>选项</th>
</tr>
</thead>
<tbody>
{% for row in menus %}
<tr>
<td>{{ row.title }}</td>
<td><i class="fa {{ row.icon }}"></i></td>
<td>
<a style="color: #333333;" href="#">
<i class="fa fa-edit" aria-hidden="true"></i></a>
<a style="color: #d9534f;" href="#">
<i class="fa fa-trash-o"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
效果
一级菜单选中
点击名称的时候增加选中功能,点击名称能实现选中
1.当点击此标签时,自动将?mid=xx
添加到当前url作为参数传递到后端。
<a href="?mid={{ row.id }}">{{ row.title }}</a>
2.在后端获取传递过来的选中的菜单,传递到模板中,进行判断,对选中的id添加class=active
进行渲染,要注意后端传递到模板中的mid是字符串类型,row.id
是数字类型,在判断时进行类型转换。
menu_list.html
{% extends 'layout.html' %}
{% block css %}
<style>
tr.active{
border-left: 3px solid #fdc00f
}
</style>
{% endblock %}
{% block content %}
<div class="luffy-container">
<div class="col-md-3">
<div class="panel panel-info">
<!-- Default panel contents -->
<div class="panel-heading">
<i class="fa fa-book" aria-hidden="true"></i>一级菜单
<a href="" class="right btn btn-success btx-xs"
style="margin: -3px; padding: 2px 8px;">
<i class="fa fa-plus-circle" aria-hidden="true"></i>
新建
</a>
</div>
<!-- Table -->
<table class="table">
<thead>
<tr>
<th>名称</th>
<th>图标</th>
<th>选项</th>
</tr>
</thead>
<tbody>
{% for row in menus %}
<tr class="{% if mid == row.id|safe %}active{% endif %}">
<td>
<a href="?mid={{ row.id }}">{{ row.title }}</a>
</td>
<td><i class="fa {{ row.icon }}"></i></td>
<td>
<a style="color: #333333;" href="#">
<i class="fa fa-edit" aria-hidden="true"></i></a>
<a style="color: #d9534f;" href="#">
<i class="fa fa-trash-o"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
视图函数
def menu_list(request):
"""
菜单和权限列表
:param request:
:return:
"""
menus = models.Menu.objects.all()
menu_id = request.GET.get('mid')
# 可在后端转换,也可在前端转换
# if menu_id:
# menu_id = int(menu_id)
return render(request, 'rbac/menu_list.html', {'menus': menus, 'mid': menu_id})
效果
新建一级菜单并且保留一级菜单的原搜索条件
没有实现保存原搜索条件的版本
实现新建功能的思路是:
1.新建url
url(r'^menu/add/$', menu.menu_add, name='menu_add'),
2.新建Menu对应的ModelForm
class MenuModelForm(forms.ModelForm):
class Meta:
model = models.Menu
fields = ['title', 'icon']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
3.视图函数
def menu_add(request):
"""
新增一级菜单
:param request:
:return:
"""
if request.method == 'GET':
form = MenuModelForm()
return render(request, 'rbac/change.html', {'form': form})
form = MenuModelForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('rbac:menu_list'))
# 验证失败的返回
return render(request, 'rbac/change.html', {'form': form})
4.menu_list.html中增加对菜单新建功能的链接
href="{% url 'rbac:menu_add' %}
此时我们实现的在不选中一级菜单的某一项,点击新建一级菜单时,是没有问题的。但是当我们选中某个一级菜单时,再去点击新建,保存,之后返回到菜单列表,会发现我们之前选中的一级菜单没有被选中,回到了一开始最新没有选中任何菜单的状态
保留原搜索条件的技巧:将原url中的搜索条件打包携带到要跳转的url中。
原url:rbac/menu/list/?mid=1&age=61
新增按钮的url:rbac/menu/add/?_filter="mid=1&age=61"
在要跳转的url中将原搜索条件打包传递,_filter来接收
实现新增一级菜单的保留原搜索条件
使用simple_tag功能实现在后台的反向解析url和原始搜索条件的拼接。
@register.simple_tag
def memory_url(request, name):
"""
生成带有原搜索条件的url,替代了原来模板中的url
:param request:
:param name:
:return:
"""
basic_url = reverse(name)
# 对原url是有 ?xxx 携带参数进行判断,有参数进行拼接
if request.GET:
# 拼接
# old_params = request.GET.urlencode() # 拿到的是原url的搜索条件 mid=2&age=61
# tpl = "{}?_filter={}".format(basic_url, old_params)
# 这样拼接是有问题的,出来的结果是:/menu/add/?_filter=mid=2&age=61 ,对参数分割_filter,age
# 我们期望的是 /menu/add/?_filter="mid=2&age=61",mid=2&age=61是一个整体
# QueryDict进行转义拼接
query_dict = QueryDict(mutable=True)
query_dict['_filter'] = request.GET.urlencode()
# query_dict.urlencode() 会将筛选条件进行转义打包
return '{}?{}'.format(basic_url, query_dict.urlencode()) #
return basic_url
模板中使用
href="{% memory_url request 'rbac:menu_add' %}
效果:
上述是针对于那些不接收参数的这个是可以实现的,但是针对那些本来就接收参数的如:编辑url(r'^menu/edit/(?P<pk>\d+)/$', menu.menu_edit, name='menu_edit'),
本来是需要接收参数的,所以当前定义的memory_url的参数是不够的,所以进行对应的修改:
@register.simple_tag
def memory_url(request, name, *args, **kwargs):
"""
生成带有原搜索条件的url,替代了原来模板中的url
:param request:
:param name:
:return:
"""
basic_url = reverse(name, args=args, kwargs=kwargs) # reverse带参数的反向解析
# 对原url是有 ?xxx 携带参数进行判断,有参数进行拼接
if request.GET:
# 拼接
# old_params = request.GET.urlencode() # 拿到的是原url的搜索条件 mid=2&age=61
# tpl = "{}?_filter={}".format(basic_url, old_params)
# 这样拼接是有问题的,出来的结果是:/menu/add/?_filter=mid=2&age=61 ,对参数分割_filter,age
# 我们期望的是 /menu/add/?_filter="mid=2&age=61",mid=2&age=61是一个整体
# QueryDict进行转义拼接
query_dict = QueryDict(mutable=True)
query_dict['_filter'] = request.GET.urlencode()
return '{}?{}'.format(basic_url, query_dict.urlencode())
return basic_url
增加编辑的链接
<a style="color: #333333;" href="{% memory_url request 'rbac:menu_edit' pk=row.id %}">
选中一级菜单时点击编辑,也是会携带原搜索条件
到此实现了跳转时保存原搜索条件,当我们操作完后点击保存后,也需要将原搜索条件传递
def menu_add(request):
"""
新增一级菜单
:param request:
:return:
"""
if request.method == 'GET':
form = MenuModelForm()
return render(request, 'rbac/change.html', {'form': form})
form = MenuModelForm(data=request.POST)
if form.is_valid():
form.save()
basic_url = reverse('rbac:menu_list')
origin_url = request.GET.get('_filter')
return redirect("{}?{}".format(basic_url, origin_url) if origin_url else basic_url)
# 验证失败的返回
return render(request, 'rbac/change.html', {'form': form})
def menu_edit(request, pk):
"""
修改一级菜单
:param request:
:return:
"""
obj = models.Menu.objects.filter(id=pk).first()
if not obj:
return HttpResponse('角色不存在')
if request.method == 'GET':
form = MenuModelForm(instance=obj)
return render(request, 'rbac/change.html', {'form': form})
form = MenuModelForm(instance=obj, data=request.POST)
if form.is_valid():
form.save()
basic_url = reverse('rbac:menu_list')
origin_url = request.GET.get('_filter')
return redirect("{}?{}".format(basic_url, origin_url) if origin_url else basic_url)
return render(request, 'rbac/change.html', {'form': form})
到此我们就实现了添加、编辑菜单之后,依然会展示我们之前的选中信息。删除同理。
代码整合
整合重复的后台反向解析拼接功能,将memory_url函数和其放在同一个文件,方便后续修改。
from django.urls import reverse
from django.http import QueryDict
def memory_url(request, name, *args, **kwargs):
"""
生成带有原搜索条件的url,替代了原来模板中的url
:param request:
:param name:
:return:
"""
basic_url = reverse(name, args=args, kwargs=kwargs) # reverse带参数的反向解析
# 对原url是有 ?xxx 携带参数进行判断,有参数进行拼接
if request.GET:
# 拼接
query_dict = QueryDict(mutable=True)
query_dict['_filter'] = request.GET.urlencode()
return '{}?{}'.format(basic_url, query_dict.urlencode())
return basic_url
def memory_reverse(request, name, *args, **kwargs):
"""
反向生成url
在url中将原搜索条件获取,拼接返回
:param request:
:param name:
:param args:
:param kwargs:
:return:
"""
basic_url = reverse(name, args=args, kwargs=kwargs)
origin_url = request.GET.get('_filter')
if origin_url:
basic_url = "{}?{}".format(basic_url, origin_url)
return basic_url
rbac/templatetags/rbac.py
@register.simple_tag
def memory_url(request, name, *args, **kwargs):
"""
生成带有原搜索条件的url,替代了原来模板中的url
:param request:
:param name:
:return:
"""
return urls.memory_url(request, name, *args, **kwargs)
rbac/view/menu.py
from django.shortcuts import render, redirect, HttpResponse
from django.urls import reverse
from rbac import models
from rbac.forms.menu import MenuModelForm
from rbac.service.urls import memory_reverse
def menu_list(request):
"""
菜单和权限列表
:param request:
:return:
"""
menus = models.Menu.objects.all()
menu_id = request.GET.get('mid')
return render(request, 'rbac/menu_list.html', {'menus': menus, 'mid': menu_id})
def menu_add(request):
"""
新增一级菜单
:param request:
:return:
"""
if request.method == 'GET':
form = MenuModelForm()
return render(request, 'rbac/change.html', {'form': form})
form = MenuModelForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(memory_reverse(request, 'rbac:menu_list'))
# 验证失败的返回
return render(request, 'rbac/change.html', {'form': form})
def menu_edit(request, pk):
"""
修改一级菜单
:param request:
:return:
"""
obj = models.Menu.objects.filter(id=pk).first()
if not obj:
return HttpResponse('角色不存在')
if request.method == 'GET':
form = MenuModelForm(instance=obj)
return render(request, 'rbac/change.html', {'form': form})
form = MenuModelForm(instance=obj, data=request.POST)
if form.is_valid():
form.save()
return redirect(memory_reverse(request, 'rbac:menu_list'))
return render(request, 'rbac/change.html', {'form': form})
def menu_del(request, pk):
"""
一级菜单删除
:param request:
:param pk:要被删除的菜单id
:return:
"""
basic_url = memory_reverse(request, 'rbac:menu_list')
if request.method == "GET":
return render(request, 'rbac/delete.html', {'cancel_url': basic_url})
models.Menu.objects.filter(id=pk).delete()
return redirect(basic_url)
一级菜单编辑页面图标相关优化
在我们实现中图标是需要我们先从Font Awesome上查找到图标和对应值填入。这样做很不友好,我们应该做成给用户一系列得图标可以选择,只要选择图标就可以完成添加。
修改odelForm,将icon字段修改为RadioSelect属性。
from django.utils.safestring import mark_safe
class MenuModelForm(forms.ModelForm):
class Meta:
model = models.Menu
fields = ['title', 'icon']
widgets = {
'icon': forms.RadioSelect(
choices=[
['fa-user-circle', mark_safe('<i class="fa fa-user-circle" aria-hidden="true"></i>')], # 第一个值是value得属性值,传入后台存储,第二个是显示到页面的。
['fa-handshake-o', mark_safe('<i class="fa fa-handshake-o" aria-hidden="true"></i>')],
]
)
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
给RadioSelect自动生成的前端增加样式:
change.html
{% extends 'layout.html' %}
{% block css %}
<style>
ul{
list-style: none;
padding: 0;
}
ul li{
list-style: none;
float:left;
padding: 10px;
padding-left:0;
width:80px;
}
ul li i{
font-size: 18px;
margin-left: 5px;
color: #6d6565;
}
</style>
{% endblock %}
{% block content %}
<div class="luffy-container">
<form action="" method="post" class="form-horizontal" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-8">
{{ field }}
<span style="color:red;">{{ field.errors.0 }}</span>
</div>
</div>
{% endfor %}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-8">
<input type="submit" value="保 存" class="btn btn-primary">
</div>
</div>
</form>
</div>
{% endblock %}
效果:
将常用的图标都添加上去。
二级菜单显示、选中、增加、编辑、删除
显示:根据一级菜单选中的id,将其下面的二级菜单进行罗列展示。
选中:与一级菜单的选中一致,保留一级菜单和二级菜单的原搜索条件。
增加:选中了某个一级标签,点击二级标签新增时,理应是给这个一级菜单下增加二级菜单,所以在新增页面给一级菜单设定默认值。
编辑、删除:和一级菜单一致。
代码:
路由
# 二级菜单管理
url(r'^second/menu/add/(?P<menu_id>\d+)/$', menu.second_menu_add, name='second_menu_add'),
url(r'^second/menu/edit/(?P<pk>\d+)/$', menu.second_menu_edit, name='second_menu_edit'),
url(r'^second/menu/del/(?P<pk>\d+)/$', menu.second_menu_del, name='second_menu_del'),
ModelForm
class SecondMenuModelForm(forms.ModelForm):
class Meta:
model = models.Permission
fields = ["title", "url", "name", "menu"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
视图函数
def second_menu_add(request, menu_id):
"""
增加二级菜单
:param request:
:param menu_id: 已选择的一级菜单id,用于设置默认值
:return:
"""
menu_obj = models.Menu.objects.filter(pk=menu_id).first()
if request.method == 'GET':
# 给设置默认值
form = SecondMenuModelForm(initial={'menu': menu_obj})
return render(request, 'rbac/change.html', {'form': form})
form = SecondMenuModelForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(memory_reverse(request, 'rbac:menu_list'))
# 验证失败的返回
return render(request, 'rbac/change.html', {'form': form})
def second_menu_edit(request, pk):
"""
修改二级菜单
:param request:
:return:
"""
obj = models.Permission.objects.filter(id=pk).first()
if not obj:
return HttpResponse('菜单不存在')
if request.method == 'GET':
form = SecondMenuModelForm(instance=obj)
return render(request, 'rbac/change.html', {'form': form})
form = SecondMenuModelForm(instance=obj, data=request.POST)
if form.is_valid():
form.save()
return redirect(memory_reverse(request, 'rbac:menu_list'))
return render(request, 'rbac/change.html', {'form': form})
def second_menu_del(request, pk):
"""
二级菜单删除
:param request:
:param pk:要被删除的菜单id
:return:
"""
basic_url = memory_reverse(request, 'rbac:menu_list')
if request.method == "GET":
return render(request, 'rbac/delete.html', {'cancel_url': basic_url})
models.Permission.objects.filter(id=pk).delete()
return redirect(basic_url)
模板
<div class="col-md-4">
<div class="panel panel-info">
<!-- Default panel contents -->
<div class="panel-heading">
<i class="fa fa-coffee" aria-hidden="true"></i>二级菜单
{% if mid %}
<a href="{% memory_url request 'rbac:second_menu_add' menu_id=mid %}" class="right btn btn-success btx-xs"
style="margin: -3px; padding: 2px 8px;">
<i class="fa fa-plus-circle" aria-hidden="true"></i>
新建
</a>
{% endif %}
</div>
<!-- Table -->
<table class="table">
<thead>
<tr>
<th>名称</th>
<th>CODE & URL</th>
<th>选项</th>
</tr>
</thead>
<tbody>
{% for row in second_menus %}
<tr class="{% if sid == row.id|safe %}active{% endif %}">
<td rowspan="2">
<a href="?mid={{ mid }}&sid={{ row.id }}">{{ row.title }}</a>
</td>
<td>
{{ row.name }}
<td>
<a style="color: #333333;" href="{% memory_url request 'rbac:second_menu_edit' pk=row.id %}">
<i class="fa fa-edit" aria-hidden="true"></i></a>
<a style="color: #d9534f;" href="{% memory_url request 'rbac:second_menu_del' pk=row.id %}">
<i class="fa fa-trash-o"></i></a>
</td>
</tr>
<tr class="{% if sid == row.id|safe %}active{% endif %}">
<td colspan="2" style="border-top:0;">{{ row.url }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
我们编写的所有ModelForm中,会重写__init__()
方法给所有字段增加统一的样式,但是会在每个类下都写重复性的代码,用下面方式来解决:
a. 编写一个BootStrapModelForm基类
class BootSrapModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
b.后面所有的ModelForm直接继承BootStrapModelForm就可以了
class SecondMenuModelForm(BootStrapModelForm):
class Meta:
model = models.Permission
fields = ["title", "url", "name", "menu"]
权限(不能做菜单的url)操作
给二级菜单下添加权限,即是将那些不能作为菜单的权限给绑定一个二级菜单,用作在点击不能作为菜单的功能时展示二级菜单。
在添加时我们选择让用户不在可以自由选择二级菜单,哪个二级菜单被选中,就在这个二级菜单下添加权限,添加页面展示"title", "url", "name"
这三个字段让用户填写,对于二级菜单的id从url中获取url(r'^permission/add/(?P<second_menu_id>\d+)/$', menu.permission_add, name='permission_add')
。编辑和删除代码基本一致。
路由
# 二级菜单下的权限管理
url(r'^permission/add/(?P<second_menu_id>\d+)/$', menu.permission_add, name='permission_add'),
url(r'^permission/edit/(?P<pk>\d+)/$', menu.permission_edit, name='permission_edit'),
url(r'^permission/del/(?P<pk>\d+)/$', menu.permission_del, name='permission_del'),
ModelForm
class PermisssionMenuModelForm(BootStrapModelForm):
class Meta:
model = models.Permission
fields = ["title", "url", "name"]
视图函数
def permission_add(request, second_menu_id):
"""
二级菜单下的权限增加
:param request:
:param second_menu_id:
:return:
"""
second_menu_obj = models.Permission.objects.filter(pk=second_menu_id).first()
# 判断二级菜单是否存在
if not second_menu_obj:
return HttpResponse('二级菜单不存在')
if request.method == "GET":
form = PermisssionMenuModelForm()
return render(request, 'rbac/change.html', {'form': form})
form = PermisssionMenuModelForm(request.POST)
if form.is_valid():
# 将二级菜单传入到用户传入的对象中
'''
form.instance中包含用户提交的所有值,其机制是:
1. instance = models.Permission(title='xx', name='xx', url='xx')
2. instance.pid = second_menu_obj 给用户传入的数据添加值
3. instance.save() 保存数据
'''
form.instance.pid = second_menu_obj
form.save()
return redirect(memory_reverse(request, 'rbac:menu_list'))
# 验证失败的返回
return render(request, 'rbac/change.html', {'form': form})
def permission_edit(request, pk):
"""
二级菜单下的权限编辑
:param request:
:param second_menu_id:
:return:
"""
permission_obj = models.Permission.objects.filter(pk=pk).first()
# 判断二级菜单是否存在
if not permission_obj:
return HttpResponse('二级菜单不存在')
if request.method == "GET":
form = PermisssionMenuModelForm(instance=permission_obj)
return render(request, 'rbac/change.html', {'form': form})
form = SecondMenuModelForm(instance=permission_obj, data=request.POST)
if form.is_valid():
form.save()
return redirect(memory_reverse(request, 'rbac:menu_list'))
# 验证失败的返回
return render(request, 'rbac/change.html', {'form': form})
def permission_del(request, pk):
"""
二级菜单下权限删除
:param request:
:param pk:
:return:
"""
basic_url = memory_reverse(request, 'rbac:menu_list')
if request.method == "GET":
return render(request, 'rbac/delete.html', {'cancel_url': basic_url})
models.Permission.objects.filter(id=pk).delete()
return redirect(basic_url)
模板
<div class="col-md-4">
<div class="panel panel-info">
<!-- Default panel contents -->
<div class="panel-heading">
<i class="fa fa-coffee" aria-hidden="true"></i>权限
{% if sid %}
<a href="{% memory_url request 'rbac:permission_add' second_menu_id=sid %}" class="right btn btn-success btx-xs"
style="margin: -3px; padding: 2px 8px;">
<i class="fa fa-plus-circle" aria-hidden="true"></i>
新建
</a>
{% endif %}
</div>
<!-- Table -->
<table class="table">
<thead>
<tr>
<th>名称</th>
<th>CODE & URL</th>
<th>选项</th>
</tr>
</thead>
<tbody>
{% for row in permissions %}
<tr>
<td rowspan="2">
{{ row.title }}
</td>
<td>
{{ row.name }}
<td>
<a style="color: #333333;" href="{% memory_url request 'rbac:permission_edit' pk=row.id %}">
<i class="fa fa-edit" aria-hidden="true"></i></a>
<a style="color: #d9534f;" href="{% memory_url request 'rbac:permission_del' pk=row.id %}">
<i class="fa fa-trash-o"></i></a>
</td>
</tr>
<tr class="{% if sid == row.id|safe %}active{% endif %}">
<td colspan="2" style="border-top:0;">{{ row.url }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
效果
点击新建页面:
新建权限之后的页面:
知识点总结
- 如何保留url中原搜索条件
- 模板中数据类型不一致,导致默认选中的判断无效,
row.id|safe
进行转为字符串(整形转字符串) - ModelForm Radio的定制
- ModelForm 显示默认值
- ModelForm save之前对其instance进行修改
- 给所有字段统一定制样式