Django实战总结
一, 在模板和视图中反解Form组件中的choices.
// 前端中获得FORM中choices选项的对应值
obj.get_字段名称_display
// PY代码中获得FORM中choices选项的对应值
obj.get_字段名称_display()
二, 分页
方式一
# 先来一个视图
class CustomerList(View):
def get(self, request):
# 获取所有的数据
resp = models.Customer.objects.all()
# 每页显示20条数据
paginator = Paginator(resp, 20)
page = request.GET.get('page', 1)
all_customer = paginator.page(page)
return render(request, 'customer_list.html', {'all_customer': all_customer})
// 前端代码如下
<nav aria-label="Page navigation" class="text-right">
<ul class="pagination">
{% if all_customer.has_previous %}
<li>
<a href="?page=1" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<li><a href="?page={{ all_customer.previous_page_number }}">上一页</a></li>
{% endif %}
<li>
<a>
第 {{ all_customer.number }}页 共 {{ all_customer.paginator.num_pages }}页
</a>
</li>
{% if all_customer.has_next %}
<li><a href="?page={{ all_customer.next_page_number }}">下一页</a></li>
<li>
<a href="?page={{ all_customer.paginator.num_pages }}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% endif %}
</ul>
</nav>
方式二
# 视图函数
# 测试分页
class Pagination(View):
def get(self, request):
# 所有数据(模拟309条数据)
all_data = [{'name': f'user{i}', 'pwd': '123'} for i in range(1, 310)]
max_data_count = len(all_data)
# 每页显示的数据条数
every_page_count = 10
# 最大分页数
page_count_max, more = divmod(max_data_count, every_page_count)
if more:
page_count_max += 1
# 请求的页数(当前页)
page = request.GET.get('page')
try:
page = int(page)
if page < 1:
page = 1
elif page > page_count_max:
page = page_count_max
except Exception:
page = 1
# 起始数据
start_count = (page - 1) * every_page_count
# 结束数据
end_count = page * every_page_count
# 每页最大展示页数按钮数
page_show_count = 9
page_show_count_half = page_show_count // 2
# 如果每页展示的页数按钮大于等于总共的页数按钮
if page_show_count >= page_count_max:
start_page = 1
end_page = page_count_max
else:
# 起始页
start_page = page - page_show_count_half
# 终止页
end_page = page + page_show_count_half
if start_page < 1:
start_page = 1
end_page = page_show_count
elif end_page > page_count_max:
end_page = page_count_max
start_page = end_page - page_show_count + 1
# 生成按钮
button_list = []
if page <= 1:
button_list.append(
f'<li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">«</span></a></li>')
else:
button_list.append(
f'<li><a href="?page={page - 1}" aria-label="Previous"><span aria-hidden="true">«</span></a></li>')
for i in range(start_page, end_page + 1):
if i == page:
button_list.append(f'<li class="active"><a href="?page={i}">{i}</a></li>')
else:
button_list.append(f'<li><a href="?page={i}">{i}</a></li>')
if page >= page_count_max:
button_list.append(
f'<li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">»</span></a></li>')
else:
button_list.append(
f'<li><a href="?page={page + 1}" aria-label="Previous"><span aria-hidden="true">»</span></a></li>')
button_html = mark_safe(''.join(button_list))
return render(request, 'pagination.html', {'all_data': all_data[start_count:end_count],
'button_html': button_html
})
// 前端
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>用户名</th>
<th>密码</th>
</tr>
</thead>
<tbody>
{% for data in all_data %}
<tr>
<td>{{ data.name }}</td>
<td>{{ data.pwd }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<nav aria-label="Page navigation" class="text-right">
<ul class="pagination">
{{ button_html }}
</ul>
</nav>
方式三
# 将方式二提取出一个类
from django.utils.safestring import mark_safe
class MyPagination:
def __init__(self, max_data_count, page, every_page_count=10, page_show_count=9):
# 所有数据条数
max_data_count = max_data_count
# 每页显示的数据条数
every_page_count = every_page_count
# 最大分页数
self.page_count_max, more = divmod(max_data_count, every_page_count)
if more:
self.page_count_max += 1
# 请求的页数(当前页)
try:
self.page = int(page)
if self.page < 1:
self.page = 1
elif self.page_count_max and self.page > self.page_count_max:
self.page = self.page_count_max
except Exception:
self.page = 1
# 起始数据
self.start_count = (self.page - 1) * every_page_count
# 结束数据
self.end_count = self.page * every_page_count
# 每页最大展示页数按钮数
page_show_count = page_show_count
page_show_count_half = page_show_count // 2
# 如果每页展示的页数按钮大于等于总共的页数按钮
if page_show_count >= self.page_count_max:
self.start_page = 1
self.end_page = self.page_count_max
else:
# 起始页
self.start_page = self.page - page_show_count_half
# 终止页
self.end_page = self.page + page_show_count_half
if self.start_page < 1:
self.start_page = 1
self.end_page = page_show_count
elif self.end_page > self.page_count_max:
self.end_page = self.page_count_max
self.start_page = self.end_page - page_show_count + 1
@property
def show_html(self):
# 生成按钮
button_list = []
if self.page <= 1:
button_list.append(
f'<li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">«</span></a></li>')
else:
button_list.append(
f'<li><a href="?page={self.page - 1}" aria-label="Previous"><span aria-hidden="true">«</span></a></li>')
for i in range(self.start_page, self.end_page + 1):
if i == self.page:
button_list.append(f'<li class="active"><a href="?page={i}">{i}</a></li>')
else:
button_list.append(f'<li><a href="?page={i}">{i}</a></li>')
if self.page >= self.page_count_max:
button_list.append(
f'<li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">»</span></a></li>')
else:
button_list.append(
f'<li><a href="?page={self.page + 1}" aria-label="Previous"><span aria-hidden="true">»</span></a></li>')
return mark_safe(''.join(button_list))
# 视图函数中
# 测试分页
class Pagination(View):
def get(self, request):
# 所有数据(模拟309条数据)
all_data = [{'name': f'user{i}', 'pwd': '123'} for i in range(1, 310)]
max_data_count = len(all_data)
page = MyPagination(max_data_count, request.GET.get('page', 1))
return render(request, 'pagination.html', {'all_data': all_data[page.start_count:page.end_count],
'button_html': page.show_html
})
进阶: 分页保留原有参数
前提知识: QueryDict的方法
# request.POST获得包含所有POST数据的QueryDict对象
# request.GET获得包含所有Url中携带的参数的QueryDict对象
# 假设参数有search(搜索条件)和page(页码)
# {'search': 'xxx', 'page': 'xxx'}
query_dict = request.GET # 默认不可修改
query_dict._mutable = True # 设置成可修改
query_dict['page'] = 1
query_dict.copy() # 深拷贝一份QueryDict对象,并且改为可修改类型
query_dict.urlencode() # ==> 字符串 search=xxx&page=xxx
from django.utils.s9afestring import mark_safe
from django.http.request import QueryDict
class MyPagination:
def __init__(self, max_data_count, page, query_dict=None, every_page_count=10, page_show_count=9):
if not query_dict:
# 实例化一个可编辑的QueryDict
query_dict = QueryDict(mutable=True)
self.query_dict = query_dict
# 所有数据条数
max_data_count = max_data_count
# 每页显示的数据条数
every_page_count = every_page_count
# 最大分页数
self.page_count_max, more = divmod(max_data_count, every_page_count)
if more:
self.page_count_max += 1
# 请求的页数(当前页)
try:
self.page = int(page)
if self.page < 1:
self.page = 1
elif self.page_count_max and self.page > self.page_count_max:
self.page = self.page_count_max
except Exception:
self.page = 1
# 起始数据
self.start_count = (self.page - 1) * every_page_count
# 结束数据
self.end_count = self.page * every_page_count
# 每页最大展示页数按钮数
page_show_count = page_show_count
page_show_count_half = page_show_count // 2
# 如果每页展示的页数按钮大于等于总共的页数按钮
if page_show_count >= self.page_count_max:
self.start_page = 1
self.end_page = self.page_count_max
else:
# 起始页
self.start_page = self.page - page_show_count_half
# 终止页
self.end_page = self.page + page_show_count_half
if self.start_page < 1:
self.start_page = 1
self.end_page = page_show_count
elif self.end_page > self.page_count_max:
self.end_page = self.page_count_max
self.start_page = self.end_page - page_show_count + 1
@property
def show_html(self):
# 生成按钮
button_list = []
# 获取完整的参数显示
parameters = self.query_dict
if self.page <= 1:
button_list.append(
f'<li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">«</span></a></li>')
else:
parameters['page'] = self.page - 1
button_list.append(
f'<li><a href="?{parameters.urlencode()}" aria-label="Previous"><span aria-hidden="true">«</span></a></li>')
for i in range(self.start_page, self.end_page + 1):
parameters['page'] = i
if i == self.page:
button_list.append(f'<li class="active"><a href="?{parameters.urlencode()}">{i}</a></li>')
else:
button_list.append(f'<li><a href="?{parameters.urlencode()}">{i}</a></li>')
if self.page >= self.page_count_max:
button_list.append(
f'<li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">»</span></a></li>')
else:
parameters['page'] = self.page + 1
button_list.append(
f'<li><a href="?{parameters.urlencode()}" aria-label="Previous"><span aria-hidden="true">»</span></a></li>')
return mark_safe(''.join(button_list))
三, 在模板中使用Model From
<form action="" method="post" class="form-horizontal col-md-8 col-md-offset-1" novalidate>
{% csrf_token %}
{% for field in form_obj %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}"
class="col-md-4 control-label {% if not field.field.required %}not_required{% endif %}">
{{ field.label }}</label>
<div class="col-md-8">
{{ field }}
<span class="help-block">{{ field.errors.0 }}</span>
</div>
</div>
{% endfor %}
<p class="text-right">
<button type="reset" class="btn btn-default">重置</button>
<button type="submit" class="btn btn-primary">提交</button>
</p>
</form>
在模板中可以直接使用 request 对象
四, 使用Model Form进行编辑
# 编辑客户
class CustomerEdit(View):
def get(self, request, pk):
customer = models.Customer.objects.filter(pk=pk).first()
if not customer:
return HttpResponse('要编辑的数据不存在')
form_obj = CustomerForm(instance=customer)
return render(request, 'customer_edit.html', {'form_obj': form_obj})
def post(self, request, pk):
customer = models.Customer.objects.filter(pk=pk).first()
form_obj = CustomerForm(data=request.POST, instance=customer)
if form_obj.is_valid():
form_obj.save() # 编辑
return redirect('crm:customer_list')
return render(request, 'customer_edit.html', {'form_obj': form_obj})
进阶:
Model Form的保存流程:
instance: 如果提供instance则使用传入的instance,如果未提供,根据meta中的model生成对应的model对象.
form_obj.save(): 遍历更新instance的字段(fileds中指定的字段),然后调用instance对象的save()方法
所以,可以在is_valid()方法后,通过form_obj.instance新增字段信息(exclude中排除的字段),然后调用save()
也可以在实例化Model Form时给instance传入新创建的model对象(可以添加想要的字段),依据instance,在init方法中修改字段的相关选项等待.
五, 验证登录状态
class MyMiddleware(MiddlewareMixin):
# 验证登录状态
def process_request(self, request):
url = request.path_info
# 白名单
if url in [reverse('crm:login'), reverse('crm:register')]:
return None
# admin也要免验证
if url.startswith('/admin'):
return None
is_login = request.session.get('is_login')
if not is_login:
return redirect(f"{reverse('crm:login')}?next={url}")
user_id = request.session.get('user_id')
request.user_obj = models.UserProfile.objects.filter(pk=user_id).first()
六, CBV之反射
# 展示客户(公户/私户)/模糊查询/批量操作
class CustomerList(View):
def get(self, request):
url = request.path_info
q = self.search_q(['qq', 'qq_name', 'name', 'phone'])
if url == reverse('crm:customer_list'):
all_customer = models.Customer.objects.filter(q, consultant_id=None)
else:
all_customer = models.Customer.objects.filter(q, consultant_id=request.user_obj)
page = MyPagination(all_customer.count(), request.GET.get('page', 1), request.GET.copy())
return render(request, 'customer_list.html', {
'all_customer': all_customer[page.start_count:page.end_count],
'button_html': page.show_html,
})
def post(self, request):
action = request.POST.get('action')
# 通过反射获取对应的方法
if not hasattr(self, action):
return HttpResponse('请求不合法')
getattr(self, action)()
# 仍然返回当前页面
return self.get(request)
# 转换为公户
def to_public(self):
pk_list = self.request.POST.getlist('customer_pk')
customers = models.Customer.objects.filter(pk__in=pk_list)
# 方式一
customers.update(consultant=None)
# 方式二
# self.request.user_obj.customers.remove(*customers)
# 转换为私户
def to_private(self):
pk_list = self.request.POST.getlist('customer_pk')
customers = models.Customer.objects.filter(pk__in=pk_list)
# 方式一
customers.update(consultant=self.request.user_obj)
# 方式二
# self.request.user_obj.customers.add(*customers)
# 生成查询语句
def search_q(self, field_list):
search = self.request.GET.get('search', '')
q = Q()
q.connector = 'OR'
for field in field_list:
q.children.append((f'{field}__contains', search))
return q
七, Q查询的用法
q = self.search_q(['qq', 'qq_name', 'name', 'phone'])
# 生成查询语句
def search_q(self, field_list):
search = self.request.GET.get('search', '')
q = Q()
q.connector = 'OR'
for field in field_list:
q.children.append((f'{field}__contains', search))
return q
八, 修改/新增后仍然返回当前页面
思路: 将本页面的完整url传入修改/新增页面,在完成修改/新增后重定向到此url
难点: url中有多个参数时,&等特殊字符会影响参数的获取
解决: QueryDict的urlencode()方法
创建一个自定义方法:
from django import template
from django.urls import reverse
from django.http.request import QueryDict
register = template.Library()
@register.simple_tag
def url_tag(request, url_name, *args, **kwargs):
ulr = reverse(url_name, args=args, kwargs=kwargs)
next_url = request.get_full_path()
query_dict = QueryDict(mutable=True)
query_dict['next'] = next_url
return f'{ulr}?{query_dict.urlencode()}'
模板中使用:
{% load mytags %}
<a href="{% url_tag request 'crm:customer_add' %}">新增</a>
<a href="{% url_tag request 'crm:customer_edit' customer.pk %}">编辑</a>
九, MySql行级锁
# mysql中
begin;
select * from app01_user where id=1 for update; # 加行级锁
commit;
# django中
from django.db import transaction
from django.conf import settings
def to_private(self):
pk_list = self.request.POST.getlist('customer_pk')
if len(pk_list) + models.Customer.objects.filter(
# 在settings.py中设置MAX_CUSTOMER_NUM客户上限
consultant=self.request.user_obj).count() > settings.MAX_CUSTOMER_NUM客户上线:
return HttpResponse('太多了')
try:
with transaction.atomic():
customers = models.Customer.objects.filter(pk__in=pk_list, consultant=None).select_for_update()
if len(pk_list) == customers.count():
customers.update(consultant=self.request.user_obj)
else:
return HttpResponse('手速太慢了')
except Exception:
pass
十, 批量操作数据
# 批量插入
def make_study(self):
course_pk_list = self.request.POST.getlist('course_pk')
for course_pk in course_pk_list:
course_obj = models.CourseRecord.objects.filter(pk=course_pk).first()
class_obj = course_obj.re_class
study_record_list = []
for student in class_obj.customer_set.filter(status='studying'):
if not models.StudyRecord.objects.filter(course_record=course_obj, student=student).exists():
study_record_list.append(models.StudyRecord(course_record=course_obj, student=student))
models.StudyRecord.objects.bulk_create(study_record_list) # 批量插入
modelformset:
from django.forms import modelformset_factory
# 展示学习记录
class StudyList(MyView):
def get(self, request, course_id=None, *args, **kwargs):
# 工厂模型,针对model(StudyRecord)的每条数据,生成一个instance传入form中,最终生成一个form表单
form_set = modelformset_factory(model=models.StudyRecord, form=StudyForm, extra=0)
queryset = models.StudyRecord.objects.filter(course_record_id=course_id).order_by('pk')
page = MyPagination(queryset.count(), request.GET.get('page', 1), request.GET.copy())
formset_obj = form_set(queryset=queryset[page.start_count:page.end_count])
return render(request, 'teacher/study_record.html', {
'formset_obj': formset_obj,
'button_html': page.show_html
})
def post(self, request, course_id=None, *args, **kwargs):
form_set = modelformset_factory(model=models.StudyRecord, form=StudyForm, extra=0)
queryset = models.StudyRecord.objects.filter(course_record_id=course_id).order_by('pk')
page = MyPagination(queryset.count(), request.GET.get('page', 1), request.GET.copy())
formset_obj = form_set(data=request.POST)
# 依次验证每个form表单的数据,并更新,在from中可以用instance拿到对应的数据
if formset_obj.is_valid():
formset_obj.save()
return self.get(request, course_id, *args, **kwargs)
return render(request, 'teacher/study_record.html', {
'formset_obj': formset_obj,
'button_html': page.show_html
})
# 模板中
<div class="table-responsive">
<form action="" method="post">
{% csrf_token %}
{{ formset_obj.management_form }}
<button class="btn btn-primary"><span><i
class="fa fa-address-book"></i></span>
保存
</button>
<table class="table table-bordered table-condensed text-center" style="font-size: 12px;">
<thead>
<tr>
<th>序号</th>
<th>学生姓名</th>
<th>考勤</th>
<th>成绩</th>
<th>批语</th>
</tr>
</thead>
<tbody>
{% for form in formset_obj %}
<tr>
{{ form.id }}
<td>{{ forloop.counter }}</td>
<td>{{ form.instance.student }}</td>
<td>{{ form.attendance }}</td>
<td>{{ form.score }}</td>
<td>{{ form.homework_note }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
</div>
<nav aria-label="Page navigation" class="text-right">
<ul class="pagination">
{{ button_html }}
</ul>
</nav>