客户管理
# 关联数据的处理
客户表Customer有个ForeignKey的外键关联字段level..
注意, 客户是可以被删除的! 客户的active默认为1, 逻辑删除后, 设置为0..
Ps: 有些业务逻辑会逻辑删除到回车站, 然后再确认是否进一步的进行物理删除..
思考题: 级别逻辑删除后, 与之关联的客户如何处理?!!
若此处的级别是物理删除,还好办,删除级别后,可使该级别关联的客户级联删除.
但订单系统中的级别删除是逻辑删除,仅仅是将该级别/该记录的active字段至为0.那此时该级别下关联的客户如何处理?
▲ 解决思路一:修改级别删除的逻辑
在前面的级别关联中,针对级别的删除,只执行了该语句 `models.Level.objects.filter(id=pk).update(active=0)`
我们不直接逻辑删除,应该先查询是否有关联数据,若有关联数据,则不允许被删除!
exists = models.Customer.objects.filter(level_id=pk).exists()
if not exists:
models.Level.objects.filter(id=pk).update(active=0)
▲ 解决思路二:级别被逻辑删除了,给与之关联的客户记录的ForeignKey外键关联字段level.设置默认值. 例如:NULL
注意!若设置默认值为null,那么Customer表的level字段得允许为空!! null=True,blank=True
models.Customer.objects.filter(level_id=pk).update(level=None)
models.Level.objects.filter(id=pk).update(active=0)
▲ 解决思路三:不做任何处理.
在前面的级别关联中,针对级别的删除,依旧只执行该语句 `models.Level.objects.filter(id=pk).update(active=0)`
但在后续的客户查询,就得这么写:
queryset = models.Customer.objects.filter(active=1, level__active=1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
订单系统中, 我们选择的是 方案一的解决思路.
# 客户列表
客户列表 跟 级别列表的 前端html展示页面的代码差不多!!
def customer_list(request):
# 1.获取所有的客户数据
queryset = models.Customer.objects.filter(active=1).select_related('level', 'creator')
context = {
"queryset": queryset,
}
return render(request, 'customer_list.html', context)
1
2
3
4
5
6
7
2
3
4
5
6
7
1> 想要解决 用户列表中 "级别"和"创建者" 显示的是对象的问题. - 主动跨表!
2> 想要自定义 用户列表中 "创建时间" 的格式
[级别 和 创建者]
方案一: 在对象的model表中,加上__str__!!
方案二: 在customer_list.html中进行跨表查询. '★ 一定要主动跨表,能大大提高效率 '
首先要明白, `queryset = models.Customer.objects.filter(active=1)`该行代码实现的是对Customer表的单表查询!!
而在Django中模版中进行的跨表,会再次查询一次数据库!!
比如有300条客户数据,那么{{ row.level.title }} {{ row.level.percent }} 就会额外查询600次数据库.
{% for row in queryset %}
<td>{{ row.level.title }} ({{ row.level.percent }}%)</td>
<td>{{ row.create_date }}</td>
<td>{{ row.creator.username }}</td>
{% endfor %}
★ So!确认要在模版中做跨表查询!!记得在orm的查询语句里加上select_related("跨表字段1","跨表字段2")
下方的该语句,会进行一次联表查询!! 这样在模版中的跨表就不会做额外的查询啦!!
queryset = models.Customer.objects.filter(active=1).select_related('level', 'creator')
[创建时间]
在表模型中,表明创建时间的create_date字段是用的DateTimeField类型,在页面上展示时,默认就是 xx年xx月xx日 xx:xx
可以使用模版语言中的过滤器自定义在页面上显示的时间的格式
<td>{{ row.create_date|date:"Y-m-d H:i:s" }}</td>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 添加客户
# 界面展示
1.编写CustomerModelForm类使得添加客户页面自动生成字段.
1> 添加客户页面只用展示表中的 用户名、密码、手机号、级别 4个字段;
余额不用,因为新创建的用户余额默认为0. 创建日期不用管,会自动创建.
至于creator,应该将当前登陆的用户在后端自动添加进去!!
2> 针对密码, 页面上不仅要展示表中的字段password; 我们还得在CustomerModelForm类中手动添加一个重复密码.
confirm_password = forms.CharField(
label="重复密码",
widget=forms.PasswordInput
)
2.CustomerModelForm类继承的BootStrapForm使得页面变得好看!!
3.在添加客户页面展示的level级别字段是一个下拉选择框,其值是 Level object(1)、Level object(2)..
解决办法1:
在Customer模型里添加 __str__ 方法!
4.使用ModelForm展示的字段,会将所有与之关联的数据都展示出来. -- ModelForm显示关联数据.
So在添加客户页面展示的level级别字段还有一个问题,它将级别表中已经逻辑删除的也展示出来啦!!
你想定制它的显示的话,有两种方式.
- 解决办法1: 给模型Customer表里的level字段,添加一个limit_choices_to的属性.
在这里,它会去读取Level这张表里active等于1的记录.
★ So,基于modelform做添加时,若某些关联数据需要通过筛选进行展示.即可通过指定limit_choices_to进行展示!
▲ 注意,该条件是写死的,若需要用的参数跟当前的登陆用户/当前登陆用户传入的参数等有关.使用limit_choices_to是做不了的!!
level = models.ForeignKey(
verbose_name="级别",
to="Level",
on_delete=models.CASCADE,
limit_choices_to={"active": 1} # {"active": 1, "id__gt": 2} 条件可写多个.
)
- 解决办法2: 在CustomerModelForm类里重写__init__方法!
def __init__(self, request, *args, **kwargs):
# request参数是在 CustomerModelForm类实例化的时候传入的!! 视图函数里,form = CustomerModelForm(request)
super().__init__(*args, **kwargs)
self.fields['level'].queryset = models.Level.objects.filter(active=1)
5.在添加客户页面展示的level级别字段默认是一个下拉选择框,想要其变成redio单选框.
那么,就得进一步定制 level 字段的插件.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
关键代码
"""
bootstrap.py
"""
class BootStrapForm:
exclude_filed_list = [] # 指定字段不添加form-control的样式.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
if name in self.exclude_filed_list:
continue
field.widget.attrs['class'] = "form-control"
field.widget.attrs['placeholder'] = "请输入{}".format(field.label)
if name == "title":
field.widget.attrs['style'] = "width:10%;"
else:
field.widget.attrs['style'] = "width:25%;"
"""
customer.py
"""
from django.shortcuts import render
from web import models
from django import forms
from utils.bootstrap import BootStrapForm
class CustomerModelForm(BootStrapForm, forms.ModelForm):
exclude_filed_list = ["level"]
confirm_password = forms.CharField(
label="重复密码",
# 若提交返回错误信息,提交刷新页面,render_value=True使得密码保留原来的值.
widget=forms.PasswordInput(render_value=True),
)
class Meta:
model = models.Customer
fields = ["username", "mobile", "password", "confirm_password", "level"]
widgets = {
"password": forms.PasswordInput(render_value=True), # 也可通过重写password字段来实现
"level": forms.RadioSelect(attrs={'class': 'form-radio'}), # 自定义form-radio样式
}
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['level'].queryset = models.Level.objects.filter(active=1)
def customer_add(request):
if request.method == "GET":
form = CustomerModelForm(request)
return render(request, "form.html", {"form": form})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 验证保存
1.手机号格式的校验.
方式1> 在CustomerModelForm类里自定义mobile字段,添加覆盖默认的mobile字段.
mobile = forms.CharField(
label="手机号",
validators=[RegexValidator(r'^\d{11}$', '手机号格式错误.'), ]
)
方式2> 在CustomerModelForm类里定义mobile字段的钩子方法.
def clean_mobile(self):
mobile = self.cleaned_data['mobile']
import re
from django.core.exceptions import ValidationError
if not re.match(r'^\d{11}$', mobile):
raise ValidationError("手机格式错误.")
方式3> 若使用的是ModelForm,还可以在Customer这个model模型"数据库"的mobile字段中添加validators参数
mobile = models.CharField(
verbose_name="手机号",
max_length=11,
db_index=True,
validators=[RegexValidator(r'^\d{11}$', '手机号格式错误.'),]
)
2.新增客户的密码需要经过md5加密后再存储到数据库中.因为登陆的验证用到了md5加密. (我们采用的是方式1)
方式1> 写password字段的钩子函数,通过钩子函数的返回值控制
def clean_password(self):
password = self.cleaned_data['password']
return md5(password)
方式2> 在存入数据库之前稍微修改下存入的值
关键代码如下:
password = form.cleaned_data["password"]
form.instance.password = md5(password)
form.instance.creator_id = request.nb_user.id
form.save()
3.两次密码需填写一致的问题 通过写confirm_password字段的钩子函数解决.
def clean_confirm_password(self):
# 因为class Meta的fields列表中,confirm_password在password字段后面
# So,若password字段校验成功,就能在执行confirm_password的钩子函数时拿到
# 注意:此处的password的值是通过md5加密过的.
password = self.cleaned_data.get("password")
confirm_password = md5(self.cleaned_data.get("confirm_password",""))
if password != confirm_password:
raise ValidationError("两次密码输入不一致.")
return confirm_password
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 编辑客户
编辑客户的页面相对于添加客户的页面, 少了密码相关的内容.
class CustomerEditModelForm(BootStrapForm, forms.ModelForm):
exclude_filed_list = ["level"]
class Meta:
model = models.Customer
fields = ["username", "mobile", "level"]
widgets = {
"level": forms.RadioSelect(attrs={'class': 'form-radio'}), # 自定义form-radio样式
}
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['level'].queryset = models.Level.objects.filter(active=1)
def customer_edit(request, pk):
instance = models.Customer.objects.filter(pk=pk, active=1).first()
if request.method == "GET":
form = CustomerEditModelForm(request, instance=instance)
return render(request, "form.html", {'form': form})
form = CustomerEditModelForm(request, data=request.POST, instance=instance)
if not form.is_valid():
return render(request, "form.html", {'form': form})
# 不用再添加数据,因为instance里有数据,此次只是更新.
form.save()
return redirect(reverse("customer_list"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 重置密码
在 客户列表的页面 添加一个重置密码的功能. 点击, 跳转重置密码的页面.
注: 在重置密码的页面, 用户名字段是不能编辑的. - 用 disabled
和 initial
来实现的!!
class CustomerResetModelForm(BootStrapForm, forms.ModelForm):
confirm_password = forms.CharField(
label="重复密码",
widget=forms.PasswordInput(render_value=True), # 定义插件的方式一
)
class Meta:
model = models.Customer
fields = ["username", "password", "confirm_password"]
widgets = { # 定义插件的方式二
"username": forms.TextInput(attrs={"disabled": True}),
"password": forms.PasswordInput(render_value=True),
}
def clean_password(self):
password = self.cleaned_data['password']
return md5(password)
def clean_confirm_password(self):
password = self.cleaned_data.get("password")
confirm_password = md5(self.cleaned_data.get("confirm_password", ""))
if password != confirm_password:
raise ValidationError("两次密码输入不一致.")
return confirm_password
def customer_reset(request, pk):
""" 重置客户密码 """
instance = models.Customer.objects.filter(pk=pk, active=1).first()
if request.method == "GET":
form = CustomerResetModelForm(initial={'username': instance.username})
return render(request, "form.html", {"form": form})
form = CustomerResetModelForm(data=request.POST, instance=instance)
if not form.is_valid():
return render(request, "form.html", {'form': form})
form.save()
return redirect(reverse("customer_list"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 删除客户
点击删除按钮, 弹出一个模态框.
# 弹出对话框
弹出对话框的两种方式
在BootStrap官网的JavaScript插件里找到模态框. copy复制粘贴.
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
... ...
</div>
让模态框显示,有两种方式.
方式一:通过bootstrap的标签属性
# data-target="#deleteModal" 找到id=“deleteModal”的盒子触发显示模态框的点击事件. bootstrap里面已经写好了.
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#deleteModal">
Launch demo modal
</button>
方式二:给盒子绑定一个点击事件,自己写js让其展示出来.
具体步骤,给客户列表展示页面的删除按钮都加上名为btn-delete的类选择器;编写js代码.
<script>
$(function () {
bindDeleteEvent();
});
function bindDeleteEvent() {
$(".btn-delete").click(function () {
$("#deleteModal").modal("show");
});
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 业务逻辑
在删除按钮上 自定义属性,cid={{ row.id }}
在触发删除按钮的点击事件时,可以通过 DELETE_ID = $(this).attr("cid") 获取到当前删除客户的id,并赋值给js的全局变量DELETE_ID.
当点击删除按钮弹出的对话框中的确定按钮时,触发执行的函数中可以使用全局变量DELETE_ID!! 成功将客户的id进行了传递!
此处,点击确认按钮后,将通过全局变量DELETE_ID得到的客户ID传递到后端.
用什么方式传递数据呢? 表单 和 ajax. 因为表单会刷新.这里,我们使用ajax,将id偷偷的发送到后台,后台一接受一删除即可.
后端进行细致的判断. cid是否取到/url中有无cid参数;要删除的数据是否存在.. 并使用封装好的返回类返回json数据给前端.
前端根据res.status的真假来处理
若删除失败,展示错误信息;
若删除成功,不想刷新页面的话,就找到当前删除的行在页面上手动删除.(在tr标签上自定义了属性 row-id)
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
关键代码 web/views/customer.py
def customer_delete(request):
""" 删除数据 """
cid = request.GET.get('cid', '')
if not cid:
res = BaseResponse(status=False, detail="请选择要删除的数据")
return JsonResponse(res.dict)
exists = models.Customer.objects.filter(id=cid, active=1).exists()
if not exists:
res = BaseResponse(status=False, detail="要删除的数据不存在")
return JsonResponse(res.dict)
models.Customer.objects.filter(id=cid, active=1).update(active=0)
res = BaseResponse(status=True)
return JsonResponse(res.dict)
"""可以不做那么细致的判断,通过try..except..来实现!
try:
cid = request.GET.get("cid")
models.Customer.objects.filter(pk=cid, active=1).update(active=0)
res = BaseResponse(status=True)
return JsonResponse(res.dict)
except Exception as e:
res = BaseResponse(status=True, detail=e)
return JsonResponse(res.dict)
"""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
关键代码 web/templates/customer_list.html
# 删除按钮
<a href="#" cid="{{ row.id }}" class="btn btn-danger btn-xs btn-delete">删除</a>
# 对话框
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">...</div>
# tr标签
<tr row-id="{{ row.id }}">...</tr>
{% block js %}
<script>
let DELETE_ID;
$(function () {
bindDeleteEvent();
bindConfirmDeleteEvent(); // 确认删除
});
function bindDeleteEvent() {
// 触发点击事件,对话框显示
$(".btn-delete").click(function () {
$("#deleteError").empty(); // 先删除对话框里的错误信息
$("#deleteModal").modal("show");
DELETE_ID = $(this).attr("cid")
});
// 点击点击事件,对话框消失
$("#btnCancelDelete").click(function () {
$("#deleteModal").modal('hide');
});
}
function bindConfirmDeleteEvent() {
$("#btnConfirmDelete").click(function () {
// 举个例子,ajax发送get请求 /customer/delete/?cid=2
$.ajax({
url: "{% url 'customer_delete' %}",
type: "GET",
data: {cid: DELETE_ID},
dataType: "JSON",
success: function (res) {
if (res.status) {
// -- 删除成功
// 方式一:页面的刷新
// location.reload();
// 方式二:在tr标签上自定义row-id属性 便于找到当前数据行,删除标签
$("tr[row-id='" + DELETE_ID + "']").remove();
$("#deleteModal").modal('hide'); // 删除后,对话框消失
} else {
// -- 删除失败,错误信息在页面展示.
$("#deleteError").text(res.detail);
}
}
})
});
}
</script>
{% endblock %}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# request.is_ajax()
关于不同的请求(form / ajax),处理其返回值的问题.
我们先在settings里不给角色删除客户的权限.
当我们登陆后,点击删除客户的按钮,再在弹出的对话框里点击确认按钮,数据发过去了,但页面不会有没有任何的反应..
打开浏览器的开发者模式,看该ajax请求的返回是什么?! 是一堆html的代码.当然不会有反应啦!!
ajax请求里,dataType: "JSON",希望返回的是json数据!
而且可以观察到,ajax请求的请求头中,`X-Requested-With: XMLHttpRequest`,ajax在发送请求时底层用的就是该对象!
So,请求头里它就是ajax请求,没有就不是ajax请求.可以用form表单发送请求,查看请求头进行验证!!
为什么无权限,返回的是html代码呢? 因为在中间件里,我们进行了权限判定时的逻辑导致的!!
关键代码如下:
user_permission_dict = settings.NB_PERMISSION.get(request.nb_user.role)
if current_name not in user_permission_dict:
# 当无权时,返回的就是 一个无权访问的html页面!!
return render(request, "permission.html")
上述的代码只适用于form表单发送请求时无权访问 -- 返回html页面! ajax请求无权访问时 -- 应该返回json字符串!!!
关键代码的修改如下:
user_permission_dict = settings.NB_PERMISSION.get(request.nb_user.role)
if current_name not in user_permission_dict:
if request.is_ajax():
# ajax
return JsonResponse({"status": False, "detail": "您无权访问!"})
else:
# form
return render(request, "permission.html")
综上:(特别是针对权限判断时,应该根据请求方式的不对,无权访问时,做不同的处理)
- ajax请求,不应返回页面,应返回json数据
- form请求,重定向到无权访问的页面.
举一反三:
遇到请求相关的问题.或者request中包含某个值,忘记了,不要到处搜索,打印类型--找到类--看看包含了哪些数据/成员.
# <WSGIRequest: GET '/customer/list/'> <class 'django.core.handlers.wsgi.WSGIRequest'>
print(request,type(request))
from django.core.handlers.wsgi import WSGIRequest # 点进去查看WSGIRequest的源码!
WSGIRequest类中没有,就去它继承的父类HttpRequest中找. 找到了一个is_ajax的方法! 问题得以解决!
def is_ajax(self):
warnings.warn(
'request.is_ajax() is deprecated. See Django 3.1 release notes '
'for more details about this deprecation.',
RemovedInDjango40Warning,
stacklevel=2,
)
return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' # True or False
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45