目录
Django是目前比较火爆的框架,之前有在知乎刷到,很多毕业生进入大厂实习后因为不会git和Django框架3天就被踢掉了,因为他们很难把自己的工作融入到整个组的工作中。因此,我尝试自学Django并整理出如下笔记。
原理部分包括Ajax请求简介,实战部分围绕完成任务管理界面进行,包括Ajax请求前后端传输数据,数据库建立(关联外键 级联删除),ModelForm完成输入框样式,输入数据校验,错误信息返回,数据展示页面分页展示。
0. Why Ajax?
之前我们的浏览器向网页发送请求都是以URL和表单的形式提交的(POST, GET),基本上都能实现我们想要的功能。那为什么要用Ajax请求?之前的方法不是挺好的嘛。
之前的方法在每次提交请求的时候都会刷新页面,这是一个弊端。举个例子,在做验证码的时候,如果我们还是用原来的方法发送请求,就会导致每次换验证码的时候,都要重新输入账号和密码。
1. 简单应用
一般写Ajax请求都会配上jQuery,这样方便一点。先放上两段比较常用的写法:
2.1. GET请求
前端代码:
$.ajax({
url: '地址',
type: 'get',
data: {
x: 123456,
y: '123456',
},
dataType: 'JSON',
success: function(res){
console.log(res);
}
})
后端代码:
def aaa(request):
print(request.GET)
2.2. POST请求
我们知道平时POST请求是需要csrf验证的,我们这里暂时绕过这个,直接采用装饰器里的csrf_exempt
免除这个校验。
前端代码:
$.ajax({
url: '地址',
type: 'post',
data: {
x: 123456,
y: '123456',
},
dataType: 'JSON',
success: function(res){
console.log(res);
}
})
后端代码:
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def aaa(request):
print(request.POST)
2. 实战
我们已经有了管理员账户、部门管理、用户管理、靓号管理4块管理界面。在此之前,这四个界面都是通过刷新页面传递参数的,也就是说都未用到Ajax请求。
在这里我再创建一个任务管理,使用Ajax完成数据的传递。
我的思路是利用task_list
函数产生一个网页页面,网页页面中动态生成部分靠传入的form
和模板语言完成利用按钮响应事件将数据从前端读取过来传到task/add
网页中,这个网页对应的函数是task_add
,在此函数中,我们完成数据校验(决定校验成功和失败时的返回值)和数据的保存。
注意点有:
- 数据表关联外表,级联删除
on_delete=models.CASCADE
- 任务详情采用输入框样式,空间更大更方便
widgets = {'detail': forms.Textarea}
- Ajax传输数据,不需要每次刷新页面
location.reload();
- 自制分页类
PAGE
,比Django自带的分页更方便 - 每次接收Ajax请求后我们采用
is_valid()
自主完成信息校验 - Ajax请求时不可以直接返回redirect的,会直接被忽略,建议用json格式
HttpResponse(json.dumps(data_dict, ensure_ascii=False))
- Ajax请求不需要csrf校验,我们手动免除掉
@csrf_exempt
2.1. 创建数据表
在开始之前,我们需要先创建Task数据表,在之后的工作中将数据存入这个数据表。
application01/models.py
class Task(models.Model):
title = models.CharField(verbose_name='任务名', max_length=64)
# 选择
level_choices = {
(1, '紧急'),
(2, '必要'),
(3, '重要'),
(4, '无ddl'),
(5, '可搁置')
}
level = models.SmallIntegerField(verbose_name='级别', choices=level_choices, default=3)
# 关联外表,级联删除
user = models.ForeignKey(verbose_name='负责人(管理员)', to='Admin', on_delete=models.CASCADE)
detail = models.TextField(verbose_name='详细信息')
2.2. ModelForm结合Bootstrap
数据表的前端样式将在这里生成。
class Task(models.Model):
title = models.CharField(verbose_name='任务名', max_length=64)
# 选择
level_choices = {
(1, '紧急'),
(2, '必要'),
(3, '重要'),
(4, '无ddl'),
(5, '可搁置')
}
level = models.SmallIntegerField(verbose_name='级别', choices=level_choices, default=3)
# 关联外表,级联删除
user = models.ForeignKey(verbose_name='负责人(管理员)', to='Admin', on_delete=models.CASCADE)
detail = models.TextField(verbose_name='详细信息')
2.3. 前端界面
这部分由模板语言配合html完成。
模板layout.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Olsen</title>
<link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1-dist/css/bootstrap-datetimepicker.min.css' %}">
<style>
.navbar{
border-radius: 0;
}
</style>
</head>
<body>
<nav class="navbar navbar-default">
<!-- <div class="container-fluid">-->
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Olsen用户管理系统</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a href="/admin/list">管理员账户</a></li>
<li><a href="/depart/list">部门管理</a></li>
<li><a href="/user/list">用户管理</a></li>
<li><a href="/num/list">靓号管理</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<!-- <li><a href="#">登录</a></li>-->
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false">{{ request.session.info.username }} <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">个人资料</a></li>
<li><a href="#">我的信息</a></li>
<li role="separator" class="divider"></li>
<li><a href="/logout">注销</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<div>
{% block content %}
{% endblock %}
</div>
<script src="{% static 'js/jquery.js' %}"></script>
<script src="{% static 'plugins/bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
{% block js %}
{% endblock %}
</body>
</html>
这部分分为输入面板、展示面板、分页三部分。其中输入面板主要完成Ajax指令,最为重要;展示面板跟之前的差不多,就是将接受的json数据展示出来,分页是我们之前完成的分页类。
{% extends 'layout.html' %}
{% block content %}
<div class="container">
<!-- 输入面板-->
<div class="panel panel-default">
<div class="panel-heading">新建任务</div>
<div class="panel-body">
<!-- id定位,从form中获取输入内容-->
<form id="addForm" novalidate>
<div class="clearfix">
<!-- 循环生成输入框,用if判断是不是面积最大的‘详细信息’-->
{% for field in form %}
{% if field.label == '详细信息' %}
<div class="col-xs-12">
<div class="form-group">
<label>{{ field.label }}</label>
{{ field }}
<!-- 自制错误信息类,用于返回错误信息-->
<span class="error-msg" style="color: red; position: absolute;"></span>
</div>
</div>
{% else %}
<div class="col-xs-6">
<div class="form-group" style="position: relative; margin-bottom: 20px">
<label>{{ field.label }}</label>
{{ field }}
<span class="error-msg" style="color: red; position: absolute;"></span>
</div>
</div>
{% endif %}
{% endfor %}
<p></p>
<!-- 不能用submit-->
<div class="col-xs-12">
<button id="btnAdd" type="button" , class="btn btn-primary">提 交</button>
</div>
</div>
</form>
</div>
</form>
</div>
<!--展示面板-->
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-th-list" aria-hidden="true"></span>
任务列表
</div>
<table class="table table-bordered">
<thead>
<tr>
<th>ID</th>
<th>任务名</th>
<th>优先级</th>
<th>负责人</th>
</tr>
</thead>
<tbody>
{% for obj in queryset %}
<tr>
<th>{{ obj.id }}</th>
<td>{{ obj.title }}</td>
<td>{{ obj.get_level_display }}</td>
<td>{{ obj.user.username }}</td>
<td>
<a class="btn btn-primary btn-xs" href="/admin/{{ obj.id }}/edit">编辑</a>
<a class="btn btn-danger btn-xs" href="/admin/{{ obj.id }}/del">删除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!--分页-->
<ul class="pagination" style="width:100%;">
{{ page_string }}
{{ search_page }}
</ul>
</div>
{% endblock %}
{% block js %}
<script type="text/javascript">
$(function () {
bindBtnAddEvent();
})
function bindBtnAddEvent(){
// 找到btnAdd
$("#btnAdd").click(function (){
// 每次使用的时候清空,防止上次的报错这次还显示
$(".error-msg").empty();
$.ajax({
url: "/task/add",
type: "post",
data: $("#addForm").serialize(),
dataType: "JSON",
success: function (res) {
if(res.status){
alert('添加成功')
// 每次添加成功后刷新界面,即可在下方列表立刻更新数据
location.reload();
}else{
// 通过循环匹配错误信息,并输出
$.each(res.error, function(name, data){
$('#id_' + name).next().text(data[0]);
})
}
}
})
})
}
</script>
{% endblock %}
2.4. 分页类
application01/utils/page.py
# -*- coding:utf-8 -*-
# 自定义分页主键
from django.utils.safestring import mark_safe
class PAGE(object):
def __init__(self, request, queryset, page_size=10, plus=5, page_param="page"):
"""
:param request: 进入这个页面时的request,请求对象
:param queryset: 符合条件的对象,就是经过搜索筛选之后仍符合要求的数据
:param page_size: 每页发的那个多少行数据
:param plus: 分页时显示本页前后plus页的页码
:param page_param: 再url中传递的获取分页的参数
"""
# 获取当前页码
page = request.GET.get(page_param, "1")
# 防止页码为字母等等
if page.isdecimal():
page = int(page)
else:
page = 1
# 获取总样本数
total_count = queryset.count()
total_page_count, div = divmod(total_count, page_size)
if div:
total_page_count += 1
self.total_page_count = total_page_count
self.page = page
# 防止搜索的时候超限
if self.page < 1:
self.page = 1
if self.page > self.total_page_count:
self.page = self.total_page_count
self.plus = plus
self.page_param = page_param
self.page_size = page_size
self.start = (self.page - 1) * self.page_size * (self.page != 0)
self.end = self.page * self.page_size
# 搜索的时候分页会把原来搜索的条件替换掉,用这四行代码解决
import copy
querydict = copy.deepcopy(request.GET) # 把包含原来所有参数的网址给弄过来
querydict.mutable = True # 这句话要看源码去找_mutable
self.query_dict = querydict
self.page_queryset = queryset[self.start: self.end]
"""生成html代码"""
def create_html(self):
# 显示当前页的前plus页和后plus页
if self.total_page_count <= 2 * self.plus + 1:
start_page = 1
end_page = self.total_page_count
else:
if self.page <= self.plus:
start_page = 1
end_page = 2 * self.plus + 1
else:
if (self.page + self.plus) > self.total_page_count:
start_page = self.total_page_count - 2 * self.plus
end_page = self.total_page_count
else:
start_page = self.page - self.plus
end_page = self.page + self.plus
page_str_list = list()
self.query_dict.setlist(self.page_param, [1])
# 首页
page_str_list.append('<li><a href="?{}">首页</a></li>'.format(self.query_dict.urlencode()))
# 上一页
if self.page > 1:
self.query_dict.setlist(self.page_param, [self.page - 1])
prev = '<li><a href="?{}">上一页</a></li>'.format(self.query_dict.urlencode())
else:
self.query_dict.setlist(self.page_param, [1])
prev = '<li><a href="?{}">上一页</a></li>'.format(self.query_dict.urlencode())
page_str_list.append(prev)
# 展示页
for i in range(start_page, end_page + 1):
self.query_dict.setlist(self.page_param, [i])
if i == self.page:
ele = '<li class="active"><a href="?{}">{}</a></li>'.format(self.query_dict.urlencode(), i)
else:
ele = '<li><a href="?{}">{}</a></li>'.format(self.query_dict.urlencode(), i)
page_str_list.append(ele)
# 下一页
if self.page < self.total_page_count:
self.query_dict.setlist(self.page_param, [self.page + 1])
nex = '<li><a href="?{}">下一页</a></li>'.format(self.query_dict.urlencode())
else:
self.query_dict.setlist(self.page_param, [self.total_page_count])
nex = '<li><a href="?{}">下一页</a></li>'.format(self.query_dict.urlencode())
page_str_list.append(nex)
# 尾页
self.query_dict.setlist(self.page_param, [self.total_page_count])
page_str_list.append('<li><a href="?{}">尾页</a></li>'.format(self.query_dict.urlencode()))
page_string = mark_safe("".join(page_str_list))
return page_string
"""搜索页码"""
def search(self):
search_string = """
<form method="get">
<div class="input-group" style="width: 200px; float: right;">
<input name = "page" type="text" class="form-control" style="width: 200px;" placeholder="页码">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">跳转</button>
</span>
</div>
</form>
"""
return mark_safe(search_string)
2.5. views部分连接前后端
task_list用于是展示页面,当json数据从前端发来时task_add负责校验、保存数据。
# -*- coding:utf-8 -*-
import json
from django.shortcuts import render, HttpResponse
from application01.utils.form import TaskModelForm
from django.views.decorators.csrf import csrf_exempt
from application01 import models
from application01.utils.page import PAGE
def task_list(request):
form = TaskModelForm()
queryset_list = models.Task.objects.all().order_by('-id')
page_obj = PAGE(request, queryset_list, page_size=5)
context = {
'form': form,
'queryset': page_obj.page_queryset,
'page_string': page_obj.create_html(),
"search_page": page_obj.search()
}
return render(request, 'task_list.html', context)
@csrf_exempt
def task_add(request):
# 校验用户发来的数据(是否为空,是否符合格式等等)
form = TaskModelForm(data=request.POST)
if form.is_valid():
form.save()
# 注意这里是不能直接return redirect的,不会执行这个跳转,只能返回json
data_dict = {'status': True}
return HttpResponse(json.dumps(data_dict))
data_dict = {'status': False, 'error': form.errors}
return HttpResponse(json.dumps(data_dict, ensure_ascii=False))