昨日内容回顾

  1. 1. 权限系统的流程?
  2. 2. 权限的表有几个?
  3. 3. 技术点
  4. 中间件
  5. session
  6. orm
  7. - 去重
  8. - 去空
  9. inclusion_tag
  10. filter
  11. 有序字典
  12. settings配置
  13. 引入静态文件
  14. url别名
  15. namespace
  16. 路由分发
  17. 构造数据结构
  18. ModelForm
  19. 组件应用
  20. admin
  21. icon爬虫
  22. mark_safe
  23. 下载文件

一、客户管理之 编辑权限(二)

下载代码:

链接:https://pan.baidu.com/s/1xYkyWFwmOZIFK4cqWWUizg 密码:zzs9

效果如下:

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图1

上面的权限管理,需要构造一个字典

构造字典

权限管理数据结构如下:

  1. "1": {
  2. "title": "客户列表",
  3. "url": "/customer/list/",
  4. "name": "customer_list",
  5. "children": [
  6. {"title": "添加客户","url": "/customer/add/","name": "customer_add"}
  7. {"title": "编辑客户","url": "/customer/edit/","name": "customer_edit"}
  8. ],
  9. },

这里的1指的是权限id。也就Permission表的主键

修改 rbac—>urls.py,增加路径test,用来做测试。

  1. from django.conf.urls import url
  2. from rbac.views import role,menu,permission
  3. urlpatterns = [
  4. url(r'^role/list/$', role.role_list,name='role_list'),
  5. url(r'^role/add/$', role.role_add,name='role_add'),
  6. url(r'^role/edit/(?P<rid>\d+)/$', role.role_edit,name='role_edit'),
  7. url(r'^menu/list/$', menu.menu_list,name='menu_list'),
  8. url(r'^menu/add/$', menu.menu_add,name='menu_add'),
  9. url(r'^menu/edit/(?P<pk>\d+)/$', menu.menu_edit,name='menu_edit'),
  10. url(r'^menu/del/(?P<pk>\d+)/$', menu.menu_del,name='menu_del'),
  11. url(r'^permission/edit/(?P<pk>\d+)/$', menu.permission_edit, name='permission_edit'),
  12. url(r'^permission/del/(?P<pk>\d+)/$', menu.permission_del, name='permission_del'),
  13. url(r'^permission/test/$', permission.test,name='test'),
  14. ]

进入目录 rbac—>views,创建文件permission.py

  1. from django.shortcuts import render, redirect,HttpResponse
  2. from rbac import models
  3. def test(request):
  4. permission_queryset = models.Permission.objects.all().values("id","title","url","name","parent_id")
  5. root_permission_dict = {}
  6. for item in permission_queryset:
  7. print(item['parent_id'])
  8. # 判断parent_id不为空时,也就是能做菜单的url
  9. if not item['parent_id']:
  10. # 以id为key
  11. root_permission_dict[item['id']] = {
  12. 'title':item['title'],
  13. 'url': item['url'],
  14. 'name': item['name'],
  15. 'children':[] # 子列表默认为空
  16. }
  17. return HttpResponse('ok')

访问测试页面: http://127.0.0.1:8000/rbac/permission/test/,效果如下:

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图2

查看Pycharm控制台输出:

  1. None
  2. 1
  3. 1
  4. 1
  5. 1
  6. None
  7. 7
  8. 7
  9. 7
  10. None

再筛选不能做菜单的url,也就是parent_id不能为空的

  1. from django.shortcuts import render, redirect,HttpResponse
  2. from rbac import models
  3. def test(request):
  4. permission_queryset = models.Permission.objects.all().values("id","title","url","name","parent_id")
  5. root_permission_dict = {}
  6. # 先添加二级菜单
  7. for item in permission_queryset:
  8. # print(item['parent_id'])
  9. # 判断parent_id不为空时,也就是能做菜单的url
  10. if not item['parent_id']:
  11. # 以id为key
  12. root_permission_dict[item['id']] = {
  13. 'title':item['title'],
  14. 'url': item['url'],
  15. 'name': item['name'],
  16. 'children':[] # 子列表默认为空
  17. }
  18. # 再添加非菜单的url
  19. for item in permission_queryset:
  20. # 获取pid
  21. parent_id = item['parent_id']
  22. if parent_id: # 判断pid是否存在
  23. # 追加到parent_id对应的children中
  24. root_permission_dict[parent_id]['children'].append({
  25. 'title': item['title'],
  26. 'url': item['url'],
  27. 'name': item['name'],
  28. })
  29. # 查看最终数据
  30. for root in root_permission_dict.values():
  31. print(root['title'],root['name'],root['url'])
  32. for node in root['children']:
  33. print('--->',node['title'],node['name'],node['url'])
  34. return HttpResponse('ok')

刷新页面,查看Pycharm控制台输出:

  1. 客户列表 customer_list /customer/list/
  2. ---> 添加客户1 customer_add /customer/add/
  3. ---> 编辑客户 customer_edit /customer/edit/(?P<cid>\d+)/
  4. ---> 删除客户 customer_del /customer/del/(?P<cid>\d+)/
  5. ---> 批量导入客户 customer_import /customer/import/
  6. 角色列表 role /role/
  7. 账单列表 payment_list /payment/list/
  8. ---> 添加账单 payment_add /payment/add/
  9. ---> 编辑账单 payment_edit /payment/edit/(?P<pid>\d+)/
  10. ---> 删除账单 payment_del /payment/del/(?P<pid>\d+)/

点击菜单里面的超链接,url会有mid

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图3

这个时候,需要修改ORM查询语句

修改 rbac—>views—>menu.py

  1. from django.shortcuts import render, redirect,HttpResponse
  2. from django.urls import reverse
  3. from rbac import models
  4. from django import forms
  5. from django.utils.safestring import mark_safe
  6. ICON_LIST = [
  7. ['fa-address-book', '<i aria-hidden="true" class="fa fa-address-book"></i>'],
  8. ['fa-address-book-o', '<i aria-hidden="true" class="fa fa-address-book-o"></i>'],
  9. ['fa-address-card', '<i aria-hidden="true" class="fa fa-address-card"></i>'],
  10. ['fa-address-card-o', '<i aria-hidden="true" class="fa fa-address-card-o"></i>'],
  11. ['fa-adjust', '<i aria-hidden="true" class="fa fa-adjust"></i>'],
  12. ]
  13. for item in ICON_LIST:
  14. item[1] = mark_safe(item[1])
  15. def menu_list(request):
  16. """
  17. 权限管理和分配
  18. :param request:
  19. :return:
  20. """
  21. menus = models.Menu.objects.all()
  22. mid = request.GET.get('mid')
  23. root_permission_list = []
  24. if mid:
  25. # 找到可以成为菜单的权限 + 某个菜单下的
  26. permissions = models.Permission.objects.filter(menu_id=mid).order_by('-id')
  27. else:
  28. # 找到可以成为菜单的权限
  29. permissions = models.Permission.objects.filter(menu__isnull=False).order_by('-id')
  30. root_permission_queryset = permissions.values('id', 'title', 'url', 'name', 'menu__title')
  31. root_permission_dict = {}
  32. for item in root_permission_queryset:
  33. item['children'] = []
  34. root_permission_list.append(item)
  35. root_permission_dict[item['id']] = item
  36. # 找到可以成为菜单的权限的所有子权限
  37. node_permission_list = models.Permission.objects.filter(parent_id__in=permissions).order_by('-id').values('id',
  38. 'title',
  39. 'url',
  40. 'name',
  41. 'parent_id')
  42. for node in node_permission_list:
  43. pid = node['parent_id']
  44. root_permission_dict[pid]['children'].append(node)
  45. return render(
  46. request,
  47. 'rbac/menu_list.html',
  48. {
  49. 'menu_list': menus,
  50. 'root_permission_list': root_permission_list,
  51. 'mid': mid
  52. }
  53. )
  54. class MenuModelForm(forms.ModelForm):
  55. class Meta:
  56. model = models.Menu
  57. fields = ['title','icon']
  58. widgets = {
  59. # title字段添加class
  60. 'title': forms.TextInput(attrs={'class': 'form-control'}),
  61. 'icon': forms.RadioSelect(
  62. choices=ICON_LIST
  63. )
  64. }
  65. error_messages = {
  66. 'title': {
  67. 'required': '菜单名称不能为空'
  68. },
  69. 'icon': {
  70. 'required': '请选择图标'
  71. }
  72. }
  73. def menu_add(request):
  74. if request.method == "GET":
  75. form = MenuModelForm()
  76. else:
  77. form = MenuModelForm(request.POST)
  78. if form.is_valid():
  79. form.save()
  80. return redirect(reverse('rbac:menu_list'))
  81. return render(request,"rbac/menu_add.html",{'form':form})
  82. def menu_edit(request,pk):
  83. obj = models.Menu.objects.filter(id=pk).first()
  84. if request.method =='GET':
  85. # instance参数,如果存在那么save()方法会更新这个实例,否则会创建一个新的实例
  86. form = MenuModelForm(instance=obj)
  87. return render(request,'rbac/menu_edit.html',{'form':form})
  88. else:
  89. form = MenuModelForm(data=request.POST,instance=obj)
  90. if form.is_valid():
  91. form.save()
  92. return redirect(reverse('rbac:menu_list'))
  93. else:
  94. return render(request, 'rbac/menu_edit.html', {'form': form})
  95. def menu_del(request, pk):
  96. """
  97. 删除菜单
  98. :param request:
  99. :param pk:
  100. :return:
  101. """
  102. obj = models.Menu.objects.filter(id=pk).delete()
  103. # print(obj)
  104. return redirect(reverse('rbac:menu_list'))
  105. class PermissionModelForm(forms.ModelForm):
  106. class Meta:
  107. model = models.Permission
  108. fields = '__all__'
  109. help_texts = {
  110. 'pid': '父级权限,无法作为菜单的权限才需要选择。',
  111. 'menu': "选中,表示该权限可以作为菜单;否则,不可做菜单。"
  112. }
  113. widgets = {
  114. 'title': forms.TextInput(attrs={'class': "form-control", 'placeholder': '请输入权限名称'}),
  115. 'url': forms.TextInput(attrs={'class': "form-control", 'placeholder': '请输入URL'}),
  116. 'name': forms.TextInput(attrs={'class': "form-control", 'placeholder': '请输入URL别名'}),
  117. 'pid': forms.Select(attrs={'class': "form-control", 'placeholder': '请选择父级权限'}),
  118. 'menu': forms.Select(attrs={'class': "form-control", 'placeholder': '请选择菜单'}),
  119. }
  120. def __init__(self, *args, **kwargs):
  121. super().__init__(*args, **kwargs)
  122. def clean(self):
  123. menu = self.cleaned_data.get('menu')
  124. pid = self.cleaned_data.get('pid')
  125. if menu and pid:
  126. self.add_error('menu', '菜单和根权限同时只能选择一个')
  127. def permission_edit(request, pk):
  128. """
  129. 编辑权限
  130. :param request:
  131. :return:
  132. """
  133. obj = models.Permission.objects.filter(id=pk).first()
  134. if not obj:
  135. return HttpResponse('权限不存在')
  136. if request.method == 'GET':
  137. form = PermissionModelForm(instance=obj)
  138. else:
  139. form = PermissionModelForm(request.POST, instance=obj)
  140. if form.is_valid():
  141. form.save()
  142. return redirect(reverse('rbac:menu_list'))
  143. return render(request, 'rbac/permission_edit.html', {'form': form})
  144. def permission_del(request, pk):
  145. """
  146. 删除权限
  147. :param request:
  148. :return:
  149. """
  150. models.Permission.objects.filter(id=pk).delete()
  151. return redirect(request.META['HTTP_REFERER'])

修改 rbac—>templates—>rbac—>menu_list.html

  1. {% extends 'layout.html' %}
  2. {% block css %}
  3. <style>
  4. tr.root {
  5. background-color: #f1f7fd;
  6. }
  7. .menu-area tr.active {
  8. background-color: #f1f7fd;
  9. border-left: 3px solid #fdc00f;
  10. }
  11. #menuBody td[mid], #permissionBody > .root .title {
  12. cursor: pointer;
  13. }
  14. #permissionBody tr.root {
  15. background-color: #f1f7fd;
  16. }
  17. td a {
  18. margin: 0 2px;
  19. cursor: pointer;
  20. }
  21. table {
  22. font-size: 12px;
  23. }
  24. .panel-body {
  25. font-size: 12px;
  26. }
  27. .panel-body .form-control {
  28. font-size: 12px;
  29. }
  30. </style>
  31. {% endblock %}
  32. {% block content %}
  33. <div style="padding: 10px">
  34. <h1>菜单管理</h1>
  35. <div class="col-sm-3 menu-area">
  36. <div class="panel panel-default">
  37. <!-- Default panel contents -->
  38. <div class="panel-heading">
  39. <i class="fa fa-book" aria-hidden="true"></i>
  40. 菜单管理
  41. <a href="{% url 'rbac:menu_add' %}" class="btn btn-xs btn-success right"
  42. style="padding: 2px 8px;margin: -3px;">
  43. <i class="fa fa-plus-circle" aria-hidden="true"></i>
  44. 新建
  45. </a>
  46. </div>
  47. <!-- Table -->
  48. <table class="table">
  49. <th>名称</th>
  50. <th>图标</th>
  51. <th>选项</th>
  52. <tbody id="menuBody">
  53. {% for row in menu_list %}
  54. {% if row.id|safe == mid %}
  55. <tr class="active">
  56. {% else %}
  57. <tr>
  58. {% endif %}
  59. <td>
  60. <a href="?mid={{ row.id }}">{{ row.title }}</a>
  61. </td>
  62. <td>
  63. <!-- 展示图标 -->
  64. <i class="fa {{ row.icon }}" aria-hidden="true"></i>
  65. </td>
  66. <td>
  67. <!-- 编辑和删除 -->
  68. <a href="{% url 'rbac:menu_edit' pk=row.id %}"><i class="fa fa-edit"
  69. aria-hidden="true"></i></a>
  70. <span style="padding: 0 5px;display: inline-block">|</span>
  71. <a href="{% url 'rbac:menu_del' pk=row.id %}"><i class="fa fa-trash-o"
  72. aria-hidden="true"></i></a>
  73. </td>
  74. </tr>
  75. {% endfor %}
  76. </tbody>
  77. </table>
  78. </div>
  79. </div>
  80. <div class="col-sm-9">
  81. <div class="panel panel-default">
  82. <!-- Default panel contents -->
  83. <div class="panel-heading">
  84. <i class="fa fa-cubes" aria-hidden="true"></i>
  85. 权限管理
  86. <a href="{% url 'rbac:menu_add' %}" class="btn btn-xs btn-success right"
  87. style="padding: 2px 8px;margin: -3px;">
  88. <i class="fa fa-plus-circle" aria-hidden="true"></i>
  89. 新建
  90. </a>
  91. </div>
  92. <!-- Table -->
  93. <table class="table">
  94. <th>名称</th>
  95. <th>URL</th>
  96. <th>CODE</th>
  97. <th>菜单</th>
  98. <th>所属菜单</th>
  99. <th>选项</th>
  100. <tbody>
  101. {% for row in root_permission_list %}
  102. {# 判断菜单id是否为空 #}
  103. {% if row.menu__title %}
  104. <tr class="root">
  105. {% else %}
  106. <tr style="display: none">
  107. {% endif %}
  108. <td>
  109. <i class="fa fa-caret-down" aria-hidden="true"></i>
  110. {{ row.title }}
  111. </td>
  112. <td>{{ row.url }}</td>
  113. <td>{{ row.name }}</td>
  114. <td>
  115. {# 判断菜单id是否为空 #}
  116. {% if row.menu__title %}
  117. {% endif %}
  118. </td>
  119. <td>
  120. {# 判断菜单id是否为空 #}
  121. {% if row.menu__title %}
  122. {{ row.menu__title }}
  123. {% endif %}
  124. </td>
  125. <td>
  126. <!-- 编辑和删除 -->
  127. <a href="{% url 'rbac:permission_edit' pk=row.id %}"><i class="fa fa-edit"
  128. aria-hidden="true"></i></a>
  129. <span style="padding: 0 5px;display: inline-block">|</span>
  130. <a href="{% url 'rbac:permission_del' pk=row.id %}"><i class="fa fa-trash-o"
  131. aria-hidden="true"></i></a>
  132. </td>
  133. </tr>
  134. <tbody class="two">
  135. {% for child in row.children %}
  136. <tr pid="{{ row.id }}">
  137. <td class="title">{{ child.title }}</td>
  138. <td>{{ child.url }}</td>
  139. <td>{{ child.name }}</td>
  140. <td></td>
  141. <td>
  142. {% if child.menu__title %}
  143. {{ row.menu__title }}
  144. {% endif %}
  145. </td>
  146. <td>
  147. <a href="{% url 'rbac:permission_edit' pk=row.id %}" style="color:#333333;">
  148. <i class="fa fa-edit" aria-hidden="true"></i></a>
  149. <a href="{% url 'rbac:permission_del' pk=row.id %}" style="color:#d9534f;">
  150. <i class="fa fa-trash-o" aria-hidden="true"></i>
  151. </a>
  152. </td>
  153. </tr>
  154. {% endfor %}
  155. </tbody>
  156. {% endfor %}
  157. </tbody>
  158. </table>
  159. </div>
  160. </div>
  161. </div>
  162. {% endblock %}
  163. {% block js %}
  164. <script>
  165. //默认隐藏
  166. {#$('.aa').prev().next().hide();#}
  167. $('.two').prev().click(function () {
  168. console.log('点击了');
  169. //卷帘门切换
  170. $(this).next().slideToggle(300);
  171. })
  172. </script>
  173. {% endblock %}

访问页面:http://127.0.0.1:8000/rbac/menu/list,效果如下:

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图4

点击左边的信息管理,效果如下:

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图5

二、Django表单集合Formset

什么是Formset

Formset(表单集)是多个表单的集合。Formset在Web开发中应用很普遍,它可以让用户在同一个页面上提交多张表单,一键添加多个数据,比如一个页面上添加多个用户信息

为什么要使用Django Formset

我们先来下看下Django中不使用Formset情况下是如何在同一页面上一键提交2张或多张表单的。我们在模板中给每个表单取不同的名字,如form1和form2(如下面代码所示)。注: form1和form2分别对应forms.py里的Form1()和Form2()。

  1. <form >
  2. {{ form1.as_p }}
  3. {{ form2.as_p }}
  4. </form>

用户点击提交后,我们就可以在视图里了对用户提交的数据分别处理。

  1. if request.method == 'POST':
  2. form1 = Form1( request.POST,prefix="form1")
  3. form2 = Form2( request.POST,prefix="form2")
  4. if form1.is_valid() or form2.is_valid():
  5. pass
  6. else:
  7. form1 = Form1(prefix="form1")
  8. form2 = Form2(prefix="form2")

这段代码看似并不复杂,然而当表单数量很多或不确定时,这个代码会非常冗长。我们希望能控制表单的数量,这是我们就可以用Formset了。

Formset的分类

Django针对不同的formset提供了3种方法: formset_factory, modelformset_factory和inlineformset_factory。我们接下来分别看下如何使用它们。

如何使用formset_factory

对于继承forms.Form的自定义表单,我们可以使用formset_factory。我们可以通过设置extra和max_num属性来确定我们想要展示的表单数量。注意: max_num优先级高于extra。比如下例中,我们想要显示3个空表单(extra=3),但最后只会显示2个空表单,因为max_num=2。

  1. from django import forms
  2. class BookForm(forms.Form):
  3. name = forms.CharField(max_length=100)
  4. title = forms.CharField()
  5. pub_date = forms.DateField(required=False)
  6. # forms.py - build a formset of books
  7. from django.forms import formset_factory
  8. from .forms import BookForm
  9. # extra: 想要显示空表单的数量
  10. # max_num: 表单显示最大数量,可选,默认1000
  11. BookFormSet = formset_factory(BookForm, extra=3, max_num=2)

在视图文件views.py里,我们可以像使用form一样使用formset。

# views.py - formsets example.
from .forms import BookFormSet
from django.shortcuts import render

def manage_books(request):
    if request.method == 'POST':
        formset = BookFormSet(request.POST, request.FILES)
        if formset.is_valid():
            # do something with the formset.cleaned_data
            pass
    else:
        formset = BookFormSet()
    return render(request, 'manage_books.html', {'formset': formset})

模板里可以这样使用formset

<form action=”.” method=”POST”>
{{ formset }}
</form>

也可以这样使用

<form method="post">
    {{ formset.management_form }}
    <table>
        {% for form in formset %}
        {{ form }}
        {% endfor %}
    </table>
</form>

关于其他更多信息,请参考链接:

https://blog.csdn.net/weixin_42134789/article/details/81505983

举例

新建一个django项目,注意:django版本为1.11

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图6

修改urls.py

from django.conf.urls import url
from django.contrib import admin

from app01 import views
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', views.index),
    url(r'^index/', views.index),
]

修改views.py

from django.shortcuts import render

# Create your views here.
def index(request):
    return render(request,"index.html")

在templates目录下创建文件index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .hide{
            display: none;
        }
    </style>
</head>
<body>
    <form method="post">
        {{ formset.management_form }}
        {% csrf_token %}
        <table border="1">
            <tr>
                <th>用户名</th>
                <th>密码</th>
                <th>邮箱</th>
            </tr>
            <tr>
                <td><input type="text" name="user" placeholder="请输入用户名"></td>
                <td><input type="text" name="pwd" placeholder="请输入密码"></td>
                <td><input type="text" name="email" placeholder="请输入邮箱"></td>
            </tr>
            <tr>
                <td><input type="text" name="user" placeholder="请输入用户名"></td>
                <td><input type="text" name="pwd" placeholder="请输入密码"></td>
                <td><input type="text" name="email" placeholder="请输入邮箱"></td>
            </tr>
            <tr>
                <td><input type="text" name="user" placeholder="请输入用户名"></td>
                <td><input type="text" name="pwd" placeholder="请输入密码"></td>
                <td><input type="text" name="email" placeholder="请输入邮箱"></td>
            </tr>
        </table>
        <input type="submit" value="提交">
    </form>
</body>
</html>

启动项目,访问页面: http://127.0.0.1:8000/

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图7

点击提交按钮,需要一次性写入3条信息到数据库中

如果使用post提交,那么后台需要做form表单验证。之前学习的form组件,一次验证一行数据。

使用form

修改views.py

from django.shortcuts import render
from django import forms

# Create your views here.
class UserForm(forms.Form):
    user = forms.CharField()
    pwd = forms.CharField()
    email = forms.CharField()

def index(request):
    if request.method == 'GET':
        form = UserForm()
        return render(request,"index.html",{'form':form})

修改index.html

由于一个form对象,只能生成一行。那么3行,得需要复制3次

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .hide{
            display: none;
        }
    </style>
</head>
<body>
    <form method="post">
        {{ formset.management_form }}
        {% csrf_token %}
        <table border="1">
            <tr>
                <th>用户名</th>
                <th>密码</th>
                <th>邮箱</th>
            </tr>
            <tr>
                {% for field in form %}
                    <td>{{ field }}</td>
                {% endfor %}
            </tr>
            <tr>
                {% for field in form %}
                    <td>{{ field }}</td>
                {% endfor %}
            </tr>
            <tr>
                {% for field in form %}
                    <td>{{ field }}</td>
                {% endfor %}
            </tr>
        </table>
        <input type="submit" value="提交">
    </form>
</body>
</html>

刷新页面,效果如下:

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图8

假设不知道有多少条数据呢?得使用formset

使用formset

修改views.py

extra设置展示的表单数量,如果是0,则不会生成!

min_num=1,第一个form表单,必须是完整的。否则提示This field is required

from django.shortcuts import render
from django import forms

# Create your views here.
class UserForm(forms.Form):
    user = forms.CharField()
    pwd = forms.CharField()
    email = forms.CharField()

def index(request):
    # 生成一个类,它是form集合。extra设置展示的表单数量
    # min_num至少提交一个完整的form表单
    UserFormSet = forms.formset_factory(UserForm,extra=2,min_num=1)
    if request.method == 'GET':
        formset = UserFormSet()
        return render(request,"index.html",{'formset':formset})

    formset = UserFormSet(request.POST)
    if formset.is_valid():
        print(formset.cleaned_data)  # 验证通过的数据
        print('验证成功')

    return render(request, "index.html", {'formset': formset})

修改index.html

它必须使用2层for循环。第一次是fromset,它是一个集合,集合每一个元素都是form对象。

第二次是form对象

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .hide{
            display: none;
        }
    </style>
</head>
<body>
    <form method="post">
        {{ formset.management_form }}
        {% csrf_token %}
        <table border="1">
            <tr>
                <th>用户名</th>
                <th>密码</th>
                <th>邮箱</th>
            </tr>
            {% for form in formset %}
            <tr>
                {% for field in form %}
                    <td>{{ field }}{{ field.errors.0 }}</td>
                {% endfor %}
            </tr>
            {% endfor %}
        </table>
        <input type="submit" value="提交">
    </form>
</body>
</html>

关闭网页,重新访问,效果如下:

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图9

它有3个form对象,分别是extra和min_num设置的总和。min_num就是第一个!

直接提交一个空的表单,它会提示This field is required

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图10

提交数据

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图11

查看Pycharm控制台输出:

[{'pwd': '11', 'email': '11', 'user': '11'}, {'pwd': '22', 'email': '22', 'user': '22'}, {}]
验证成功

可以看出formset.cleaned_data是一个列表,列表的每一个元素,都是字典。

由于第3行没有填写,它是一个空字典

批量保存数据

修改models.py

from django.db import models

# Create your models here.
class User(models.Model):
    """
    用户表
    """
    username = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=32)
    email = models.EmailField(verbose_name='邮箱', max_length=32)

    def __str__(self):
        return self.username

使用2个命令生成表

python manage.py makemigrations
python manage.py migrate

修改views.py,使用**ModelForm做添加,并且对数据做校验**

from django.shortcuts import render,HttpResponse
from django import forms
from app01 import models

# Create your views here.
class UserForm(forms.ModelForm):
    class Meta:
        model = models.User  # user表
        fields = '__all__'  # 所有字段

def index(request):
    # 生成一个类,它是form集合。extra设置展示的表单数量
    # min_num至少提交一个完整的form表单
    UserFormSet = forms.formset_factory(UserForm,extra=2,min_num=1)
    if request.method == 'GET':
        formset = UserFormSet()
        return render(request,"index.html",{'formset':formset})

    formset = UserFormSet(request.POST)
    if formset.is_valid():
        # print(formset.cleaned_data)  # 验证通过的数据
        flag = False  # 标志位
        for row in formset.cleaned_data:
            if row:  # 判断数据不为空
                # print(row)  # 它是一个字典
                # **表示将字典扩展为关键字参数
                res = models.User.objects.create(**row)
                if res:  # 判断返回信息
                    flag = True

        if flag:
            return HttpResponse('添加成功')
        else:
            return HttpResponse('添加失败')

    return render(request, "index.html", {'formset': formset})

注意:表示将字典扩展为关键字参数**

问题来了,为什么要用**呢?

先来看一下,使用create的添加示例

models.User.objects.create(username='xiao',password='123',email='123@qq.com')

由于row是一个字典,而create需要关键字参数。那么就可以完美解决这个问题!论Python基础的重要性!**

重启django,刷新页面。

如果添加不合法的数据,会有提示!

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图12

输入2条正确的值

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图13

提示添加成功

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图14

查看用户表,发现有2条数据了!

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图15

批量修改数据

修改urls.py,增加路径

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', views.index),
    url(r'^index/', views.index),
    url(r'^batch_update/', views.batch_update),
]

修改views.py,增加视图函数

from django.shortcuts import render,HttpResponse
from django import forms
from app01 import models

# Create your views here.
class UserForm(forms.ModelForm):
    id = forms.IntegerField()
    class Meta:
        model = models.User  # user表
        fields = '__all__'  # 所有字段

def index(request):
    # 生成一个类,它是form集合。extra设置展示的表单数量
    # min_num至少提交一个完整的form表单
    UserFormSet = forms.formset_factory(UserForm,extra=2,min_num=1)
    if request.method == 'GET':
        formset = UserFormSet()
        return render(request,"index.html",{'formset':formset})

    formset = UserFormSet(request.POST)
    if formset.is_valid():
        # print(formset.cleaned_data)  # 验证通过的数据
        flag = False  # 标志位
        for row in formset.cleaned_data:
            if row:  # 判断数据不为空
                # print(row)  # 它是一个字典
                # **表示将字典扩展为关键字参数
                res = models.User.objects.create(**row)
                if res:  # 判断返回信息
                    flag = True

        if flag:
            return HttpResponse('添加成功')
        else:
            return HttpResponse('添加失败')

    return render(request, "index.html", {'formset': formset})

def batch_update(request):
    """
    批量更新
    :param request:
    :return:
    """
    queryset = models.User.objects.all().values()  # 查询表的所有记录
    # extra=0表示不渲染form表单。如果指定为0,页面会多一个空的form表单。强迫症者表示不爽!
    UserFormSet = forms.formset_factory(UserForm, extra=0)
    if request.method =='GET':
        # initial 参数用来给 ModelForm 定义初始值。注意:同名 Field 会覆盖 instance 的值
        formset = UserFormSet(initial=queryset)
        # print(queryset)
        return render(request,'batch_update.html',{'formset':formset})
    else:
        formset = UserFormSet(request.POST)
        # print(request.POST)
        if formset.is_valid(): # 判断数据
            flag = False  # 标志位
            for row in formset.cleaned_data:
                # pop() 方法删除字典给定键 key 及对应的值,返回值为被删除的值
                id = row.pop('id')  # 获取id
                if id:  # 判断数据不为空
                    # print(row)  # 它是一个字典
                    # **表示将字典扩展为关键字参数
                    res = models.User.objects.filter(id=id).update(**row)
                    if res:  # 判断返回信息
                        flag = True

            if flag:
                return HttpResponse('修改成功')
            else:
                return HttpResponse('修改失败')
        else:
            return render(request, "batch_update.html", {'formset': formset})

在templates目录新建文件batch_update.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .hide{
            display: none;
        }
    </style>
</head>
<body>
    <form method="post">
        {{ formset.management_form }}
        {% csrf_token %}
        <table border="1">
            <tr>
                <th>用户名</th>
                <th>密码</th>
                <th>邮箱</th>
            </tr>
            {% for form in formset %}
            <tr>
                {% for field in form %}
                    {#判断最后一个字段,也就是id#}
                    {% if forloop.last %}
                        {#隐藏id#}
                        <td class="hide">{{ field }}</td>
                    {% else %}
                        {#显示其他字段#}
                        <td>{{ field }}{{ field.errors.0 }}</td>
                    {% endif %}
                {% endfor %}
            </tr>
            {% endfor %}
        </table>
        <input type="submit" value="提交">
    </form>
</body>
</html>

访问页面:http://127.0.0.1:8000/batch_update/

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图16

修改密码

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图17

点击提交,效果如下:

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图18

查看表记录

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图19

注释事项

在更新的时候,需要有一个id。由于id字段是AutoField,该 Field 不会出现在 ModelForm 表单中。

因此,在views.py里面的UserForm中,定义了一个id字段。

那么在batch_update.html渲染时,就会显示这个id字段。但是更新时,这个id是主键,是不允许更改的。

所以用了一个很巧妙的办法,使用css样式,来隐藏它。

使用ModelForm渲染表单时,自定义的字段它是排在最后面的。所以使用forloop.last就可以定位到ID!

最后点击提交时,request.POST就带有id数据。那么后端,就可以通过id进行更新了!

关于其他更多的ModelForm信息,请参考链接:

https://blog.csdn.net/Leo062701/article/details/80963625

三、ORM之limit_choices_to

limit_choices_to介绍

它是在Admin或ModelForm中显示关联数据时,提供的条件

比如:

- limit_choices_to={'nid__gt': 5}
- limit_choices_to=lambda : {'nid__gt': 5}

from django.db.models import Q
- limit_choices_to=Q(nid__gt=10)
- limit_choices_to=Q(nid=8) | Q(nid__gt=10)
- limit_choices_to=lambda : Q(Q(nid=8) | Q(nid__gt=10)) & Q(caption='root')

举例

打开权限管理项目,访问url: http://127.0.0.1:8000/rbac/menu/list/

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图20

点击删除账单后面的编辑按钮,进入之后,点击父权限

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图21

注意:这里的父权限,展示不对。为什么?因为它必须是能作为菜单权限的!

而这里却展示了所有url。

怎么解决呢?有2个办法

第一:页面渲染时,对数据做判断。

第二:使用limit_choices_to(推荐)

修改 rbac—>models.py,找到字段

parent = models.ForeignKey(verbose_name='父权限',to='Permission',null=True,blank=True)

更改为

parent = models.ForeignKey(verbose_name='父权限',to='Permission',null=True,blank=True,limit_choices_to={'menu__isnull': False})

这个时候,不需要执行那2个命令

直接刷新页面,效果如下:

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图22

补充另外一个选项help_text,它是用来提示帮助信息

比如:

help_text="对于无法作为菜单的URL,可以为其选择一个可以作为菜单的权限,那么访问时,则默认选中此权限",

parent 字段完整信息如下:

parent = models.ForeignKey(verbose_name='父权限', to='Permission', null=True, blank=True,
                               limit_choices_to={'menu__isnull': False},
                               help_text="对于无法作为菜单的URL,可以为其选择一个可以作为菜单的权限,那么访问时,则默认选中此权限")

在输入框的右侧,就可以显示信息

在模板中,使用{{ field.help_text }}渲染,效果如下:

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图23

由于父权限和菜单是二选一的,前端无法做验证,需要在后端做验证。

使用全局钩子,来做判断!

def clean(self):
    menu = self.cleaned_data.get('menu_id')
    pid = self.cleaned_data.get('pid_id')
    if menu and pid:
        self.add_error('menu','菜单和根权限同时只能选择一个')

四、构造家族结构

先来看一下Laravel官网的评论区,这是一个评论树形结构

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图24

其中可以针对任何一个人进行回复.说白一点就是多叉树,类似的结构如下:

Day111 客户管理之 编辑权限(二),Django表单集合Formset,ORM之limit_choices_to,构造家族结构 - 图25

文章的id就是根节点,每一个子节点都保存着上一级节点的id,同级节点之间使用根据创建时间进行先后排序。

评论数型结构

根评论

comment_list = [
    {'id':1, 'title':'写的不错', 'pid':None}
]

子评论

comment_list = [
    {'id':1, 'title':'写的不错', 'pid':None},
    {'id':2, 'title':'还不错', 'pid':1}
]

三级评论

comment_list = [
    {'id':1, 'title':'写的不错', 'pid':None},
    {'id':2, 'title':'还不错', 'pid':1},
    {'id':3, 'title':'什么玩意', 'pid':2}
]

后面的层级,依次类推。

最终数据如下:

comment_list = [
    {'id':1, 'title':'写的不错', 'pid':None},
    {'id':2, 'title':'还不错', 'pid':1},
    {'id':3, 'title':'什么玩意', 'pid':2},
    {'id':4, 'title':'q1', 'pid':2},
    {'id':5, 'title':'666', 'pid':1},
    {'id':6, 'title':'去你的吧', 'pid':3},
]

层级关系应该是这个样子的

{'id':1, 'title':'写的不错', 'pid':None},
    {'id':2, 'title':'还不错', 'pid':1},
        {'id':3, 'title':'什么玩意', 'pid':2}
            {'id':6, 'title':'去你的吧', 'pid':3},

        {'id':4, 'title':'q1', 'pid':2}

    {'id':5, 'title':'666', 'pid':1},

如何构造

转换字典

先将数据转换成字典

comment_dict = {}
for item in comment_list:
    comment_dict[item['id']] = item

执行之后,结果是一个大字典

comment_dict = {
    1: {'title': '写的不错', 'id': 1, 'pid': None},
    2: {'title': '还不错', 'id': 2, 'pid': 1}, 
    3: {'title': '什么玩意', 'id': 3, 'pid': 2}, 
    4: {'title': 'q1', 'id': 4, 'pid': 2},
    5: {'title': '666', 'id': 5, 'pid': 1}, 
    6: {'title': '去你的吧', 'id': 6, 'pid': 3}
}

注意:这里面的每一个小字典,和comment_list里面的字典,用的是同一个内存地址

证明一下,改个数据

comment_list = [
    {'id':1, 'title':'写的不错', 'pid':None},
    {'id':2, 'title':'还不错', 'pid':1},
    {'id':3, 'title':'什么玩意', 'pid':2},
    {'id':4, 'title':'q1', 'pid':2},
    {'id':5, 'title':'666', 'pid':1},
    {'id':6, 'title':'去你的吧', 'pid':3},
]

comment_dict = {}
for item in comment_list:
    comment_dict[item['id']] = item

print('字典更改前',comment_dict[1])
comment_list[0]['title'] = '写的不错~~~'
print('字典更改后',comment_dict[1])

执行输出:

字典更改前 {'title': '写的不错', 'pid': None, 'id': 1}
字典更改后 {'title': '写的不错~~~', 'pid': None, 'id': 1}

发现了吧!明明更改的是大列表,但是大字典里面的数据,也随之变动!

加children

加children的目的,是为了存放除了根评论之外的,比如:二级和三级评论!

comment_list = [
    {'id':1, 'title':'写的不错', 'pid':None},
    {'id':2, 'title':'还不错', 'pid':1},
    {'id':3, 'title':'什么玩意', 'pid':2},
    {'id':4, 'title':'q1', 'pid':2},
    {'id':5, 'title':'666', 'pid':1},
    {'id':6, 'title':'去你的吧', 'pid':3},
]

comment_dict = {}
for item in comment_list:
    # 增加新的key为children,值为空列表
    item['children'] = []
    comment_dict[item['id']] = item

for row in comment_list:
    if not row['pid']:
        continue

执行之后,comment_dict结构如下:

comment_dict = {
    1: {'title': '写的不错', 'id': 1, 'pid': None,'children':[]},
    2: {'title': '还不错', 'id': 2, 'pid': 1,'children':[]},
    3: {'title': '什么玩意', 'id': 3, 'pid': 2,'children':[]},
    4: {'title': 'q1', 'id': 4, 'pid': 2,'children':[]},
    5: {'title': '666', 'id': 5, 'pid': 1,'children':[]},
    6: {'title': '去你的吧', 'id': 6, 'pid': 3,'children':[]}
}

最加到children

判断是根评论,跳过循环。否则最加到children

comment_list = [
    {'id':1, 'title':'写的不错', 'pid':None},
    {'id':2, 'title':'还不错', 'pid':1},
    {'id':3, 'title':'什么玩意', 'pid':2},
    {'id':4, 'title':'q1', 'pid':2},
    {'id':5, 'title':'666', 'pid':1},
    {'id':6, 'title':'去你的吧', 'pid':3},
]

comment_dict = {}
for item in comment_list:
    # 增加新的key为children,值为空列表
    item['children'] = []
    comment_dict[item['id']] = item

for row in comment_list:
    if not row['pid']:  # 判断根评论
        continue  # 跳过此次循环
    pid = row['pid']  # 获取pid
    # 最加到children中
    comment_dict[pid]['children'].append(row)

执行之后,comment_dict结构如下:

{
    "1": {
        "pid": null,
        "id": 1,
        "title": "写的不错",
        "children": [{
            "pid": 1,
            "id": 2,
            "title": "还不错",
            "children": [{
                "pid": 2,
                "id": 3,
                "title": "什么玩意",
                "children": [{
                    "pid": 3,
                    "id": 6,
                    "title": "去你的吧",
                    "children": []
                }]
            }, {
                "pid": 2,
                "id": 4,
                "title": "q1",
                "children": []
            }]
        }, {
            "pid": 1,
            "id": 5,
            "title": "666",
            "children": []
        }]
    },
    "2": {
        "pid": 1,
        "id": 2,
        "title": "还不错",
        "children": [{
            "pid": 2,
            "id": 3,
            "title": "什么玩意",
            "children": [{
                "pid": 3,
                "id": 6,
                "title": "去你的吧",
                "children": []
            }]
        }, {
            "pid": 2,
            "id": 4,
            "title": "q1",
            "children": []
        }]
    },
    "3": {
        "pid": 2,
        "id": 3,
        "title": "什么玩意",
        "children": [{
            "pid": 3,
            "id": 6,
            "title": "去你的吧",
            "children": []
        }]
    },
    "4": {
        "pid": 2,
        "id": 4,
        "title": "q1",
        "children": []
    },
    "5": {
        "pid": 1,
        "id": 5,
        "title": "666",
        "children": []
    },
    "6": {
        "pid": 3,
        "id": 6,
        "title": "去你的吧",
        "children": []
    }
}

上面的结构,是用json格式化工具之后的!

这里面的字典,虽然有重复的,但是它们是同一个内存地址!

可以看到children的数据,都加上了

终极版

import json
comment_list = [
    {'id': 1, 'title': '写的不错', 'pid': None},
    {'id': 2, 'title': '还不错', 'pid': 1},
    {'id': 3, 'title': '什么玩意', 'pid': 2},
    {'id': 4, 'title': 'q1', 'pid': 2},
    {'id': 5, 'title': '666', 'pid': 1},
    {'id': 6, 'title': '去你的吧', 'pid': 3},
]

comment_dict = {}
for item in comment_list:
    # 增加新的key为children,值为空列表
    item['children'] = []
    comment_dict[item['id']] = item

result = []  # 空列表
for row in comment_list:
    if not row['pid']:  # 判断根评论
        result.append(row)  # 添加到列表
    else:
        pid = row['pid']  # 获取pid
        # 最加到children中
        comment_dict[pid]['children'].append(row)

print(json.dumps(result))

执行输出:

注意:这个是使用网页版json工具,进行排版的!!!

[{
    "id": 1,
    "children": [{
        "id": 2,
        "children": [{
            "id": 3,
            "children": [{
                "id": 6,
                "children": [],
                "pid": 3,
                "title": "去你的吧"
            }],
            "pid": 2,
            "title": "什么玩意"
        }, {
            "id": 4,
            "children": [],
            "pid": 2,
            "title": "q1"
        }],
        "pid": 1,
        "title": "还不错"
    }, {
        "id": 5,
        "children": [],
        "pid": 1,
        "title": "666"
    }],
    "pid": null,
    "title": "写的不错"
}]

关于客户权限管理系统,详细步骤没有时间写了,附上终极版本

链接:https://pan.baidu.com/s/1uVM5iAl4lCqAhdPZSSyd_A 密码:yj4z

未完待续…