CRM项目之权限管理4

介绍

权限分配就是给角色分配权限,给用户分配角色,之前我们都是通过django admin来进行权限分配的,但是我们实现的是rbac组件,相应的权限分配功能我们应该也由自己实现并集成到rbac中。

功能

实现更友好的权限分配页面。

  1. 角色管理
  2. 用户管理
  3. 菜单和权限管理
  4. 批量权限操作
  5. 权限分配

角色管理

实现对角色增删改的功能,就是展示一个列表。

角色列表和添加

使用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 %}

知识点

  1. ModelForm
  2. 根据namespace和name反向生成
  3. 模板查找顺序,根目录下templates->根据每个注册app的顺序去app下templates下找,防止模板重名,找到了意料之外的模板,给设置目录进行区分。
  4. 在写模板的时候,尽量可以进行重用

用户管理

用户列表和添加

本质上是对一张表的操作,我们对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>

效果
点击新建页面:
在这里插入图片描述

新建权限之后的页面:
在这里插入图片描述

知识点总结

  1. 如何保留url中原搜索条件
  2. 模板中数据类型不一致,导致默认选中的判断无效,row.id|safe进行转为字符串(整形转字符串)
  3. ModelForm Radio的定制
  4. ModelForm 显示默认值
  5. ModelForm save之前对其instance进行修改
  6. 给所有字段统一定制样式
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值