Merge pull request #3 from jackfrued/master

拉取最新代码
This commit is contained in:
fanbiao 2020-07-05 09:16:04 +08:00 committed by GitHub
commit 830167eafa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
170 changed files with 2479 additions and 3562 deletions

View File

@ -32,7 +32,7 @@
| \| | 分支 | foo\|bar | 可以匹配foo或者bar |
| (?#) | 注释 | | |
| (exp) | 匹配exp并捕获到自动命名的组中 | | |
| (? <name>exp) | 匹配exp并捕获到名为name的组中 | | |
| (?<name>exp) | 匹配exp并捕获到名为name的组中 | | |
| (?:exp) | 匹配exp但是不捕获匹配的文本 | | |
| (?=exp) | 匹配exp前面的位置 | \\b\\w+(?=ing) | 可以匹配I'm dancing中的danc |
| (?<=exp) | 匹配exp后面的位置 | (?<=\\bdanc)\\w+\\b | 可以匹配I love dancing and reading中的第一个ing |

View File

@ -566,7 +566,7 @@
@singleton
class President():
class President:
"""总统(单例类)"""
pass
```

View File

@ -68,7 +68,7 @@
</li>
</ul>
<div>
<input @keydown.enter="addItem()" type="text" id="fname" v-model="fname">
<input @keydown.enter="addItem()" type="text" id="fname" v-model.trim="fname">
<button id="ok" @click="addItem()">确定</button>
</div>
</div>
@ -82,8 +82,8 @@
},
methods: {
addItem() {
if (this.fname.trim().length > 0) {
this.fruits.push(this.fname.trim())
if (this.fname.length > 0) {
this.fruits.push(this.fname)
}
this.fname = ''
},

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -65,7 +65,7 @@
<div class="search">
<!-- type属性是text的input标签代表文本框 可以接收用户输入 -->
<!-- placeholder是文本框的输入提示 -->
<input type="text" placeholder="请输入垃圾名字" v-model="word" @keydown.enter="search()">
<input type="text" placeholder="请输入垃圾名字" v-model.trim="word" @keydown.enter="search()">
<!-- button代表按钮 点击可以开始查询 -->
<button @click="search()">查询</button>
</div>
@ -75,9 +75,9 @@
<div v-for="result in results">
<p>
<!-- img是图像标签 可以用来实现图片-->
<img :src="pictures[result.type]" width="56" :alt="types[result.type]">
<img :src="'images/' + pictures[result.type]" width="56" :alt="types[result.type]">
&nbsp;&nbsp;
<!-- span是跨度标签 代表一个逻辑区域(不分段)-->
<!-- span是跨度标签 代表一个逻辑区域-->
<span>{{ result.name }}</span>
&nbsp;&nbsp;
<span class="pre" v-if="result.aipre == 1">(预测结果)</span>
@ -102,7 +102,7 @@
// 查询垃圾分类的函数
search() {
if (this.word.trim().length > 0) {
let key = '9aeb28ee8858a167c1755f856f830e22'
let key = '9636cec76ee2593ba6b195e5b770b394'
let url = `http://api.tianapi.com/txapi/lajifenlei/?key=${key}&word=${this.word}`
fetch(url)
.then(resp => resp.json())

View File

@ -74,6 +74,8 @@ MySQL在过去由于性能高、成本低、可靠性好已经成为最流行
```Shell
rpm -ivh mysql-community-common-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-libs-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-libs-compat-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-devel-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-client-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-server-5.7.26-1.el7.x86_64.rpm
```

View File

@ -1,5 +1,5 @@
drop database if exists hrs;
create database hrs default charset utf8;
create database hrs default charset utf8mb4;
use hrs;
@ -14,6 +14,8 @@ dloc varchar(20) not null comment '所在地',
primary key (dno)
);
-- alter table tb_dept add constraint pk_dept_dno primary key(dno);
insert into tb_dept values
(10, '会计部', '北京'),
(20, '研发部', '成都'),
@ -29,11 +31,13 @@ mgr int comment '主管编号',
sal int not null comment '员工月薪',
comm int comment '每月补贴',
dno int comment '所在部门编号',
primary key (eno)
primary key (eno),
foreign key (dno) references tb_dept(dno),
foreign key (mgr) references tb_emp(eno)
);
alter table tb_emp add constraint fk_emp_mgr foreign key (mgr) references tb_emp (eno);
alter table tb_emp add constraint fk_emp_dno foreign key (dno) references tb_dept (dno);
-- alter table tb_emp add constraint fk_emp_mgr foreign key (mgr) references tb_emp (eno);
-- alter table tb_emp add constraint fk_emp_dno foreign key (dno) references tb_dept (dno);
insert into tb_emp values
(7800, '张三丰', '总裁', null, 9000, 1200, 20),
@ -70,4 +74,4 @@ insert into tb_emp values
-- 查询主管的姓名和职位
-- 查询月薪排名4~6名的员工姓名和月薪
-- 查询月薪排名4~6名的员工排名、姓名和月薪

View File

@ -0,0 +1,339 @@
## Django快速上手
Web开发的早期阶段开发者需要手动编写每个页面例如一个新闻门户网站每天都要修改它的HTML页面随着网站规模和体量的增大这种做法一定是非常糟糕的。为了解决这个问题开发人员想到了用程序来为Web服务器生成动态内容也就是说网页中的动态内容不再通过手动编写而是通过程序自动生成。最早的时候这项技术被称为CGI公共网关接口当然随着时间的推移CGI暴露出的问题也越来越多例如大量重复的样板代码总体性能较为低下等。在时代呼唤新英雄的背景下PHP、ASP、JSP这类Web应用开发技术在上世纪90年代中后期如雨后春笋般涌现。通常我们说的Web应用是指通过浏览器来访问网络资源的应用程序因为浏览器的普及性以及易用性Web应用使用起来方便简单免除了安装和更新应用程序带来的麻烦站在开发者的角度也不用关心用户使用什么样的操作系统甚至不用区分是PC端还是移动端。
### Web应用机制和术语
下图向我们展示了Web应用的工作流程其中涉及到的术语如下表所示。
![](./res/web-application.png)
> 说明:相信有经验的读者会发现,这张图中其实还少了很多东西,例如反向代理服务器、数据库服务器、防火墙等,而且图中的每个节点在实际项目部署时可能是一组节点组成的集群。当然,如果你对这些没有什么概念也不要紧,继续下去就行了,后面会给大家一一讲解的。
| 术语 | 解释 |
| ------------- | ------------------------------------------------------------ |
| **URL/URI** | 统一资源定位符/统一资源标识符,网络资源的唯一标识 |
| **域名** | 与Web服务器地址对应的一个易于记忆的字符串名字 |
| **DNS** | 域名解析服务可以将域名转换成对应的IP地址 |
| **IP地址** | 网络上的主机的身份标识通过IP地址可以区分不同的主机 |
| **HTTP** | 超文本传输协议构建在TCP之上的应用级协议万维网数据通信的基础 |
| **反向代理** | 代理客户端向服务器发出请求,然后将服务器返回的资源返回给客户端 |
| **Web服务器** | 接受HTTP请求然后返回HTML文件、纯文本文件、图像等资源给请求者 |
| **Nginx** | 高性能的Web服务器也可以用作[反向代理](https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86)[负载均衡](https://zh.wikipedia.org/wiki/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1) 和 [HTTP缓存](https://zh.wikipedia.org/wiki/HTTP%E7%BC%93%E5%AD%98) |
#### HTTP协议
这里我们先费一些笔墨来说说HTTP这个协议。HTTP超文本传输协议是构建于TCP传输控制协议之上应用级协议它利用了TCP提供的可靠的传输服务实现了Web应用中的数据交换。按照维基百科上的介绍设计HTTP最初的目的是为了提供一种发布和接收[HTML](https://zh.wikipedia.org/wiki/HTML)页面的方法也就是说这个协议是浏览器和Web服务器之间传输的数据的载体。关于这个协议的详细信息以及目前的发展状况大家可以阅读[《HTTP 协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)、[《互联网协议入门》](http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html)系列以及[《图解HTTPS协议》](http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html)这几篇文章进行了解。下图是我在四川省网络通信技术重点实验室学习和工作期间使用开源协议分析工具Ethereal抓包工具WireShark的前身截取的访问百度首页时的HTTP请求和响应的报文协议数据由于Ethereal截取的是经过网络适配器的数据因此可以清晰的看到从物理链路层到应用层的协议数据。
HTTP请求请求行+请求头+空行+[消息体]
![](./res/http-request.png)
HTTP响应响应行+响应头+空行+消息体):
![](./res/http-response.png)
> **说明**这两张图是在2009年9月10日凌晨获得的但愿这两张如同泛黄的照片般的截图能帮助你了解HTTP到底是什么样子的。当然如果没有专业的抓包工具也可以通过浏览器提供的“开发者工具”来查看HTTP请求和响应的数据格式。
### Django概述
Python的Web框架有上百个比它的关键字还要多。所谓Web框架就是用于开发Web服务器端应用的基础设施说得通俗一点就是一系列封装好的模块和工具。事实上即便没有Web框架我们仍然可以通过socket或[CGI](https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E7%BD%91%E5%85%B3%E6%8E%A5%E5%8F%A3)来开发Web服务器端应用但是这样做的成本和代价在商业项目中通常是不能接受的。通过Web框架我们可以化繁为简降低创建、更新、扩展应用程序的工作量。刚才我们说到Python有上百个Web框架这些框架包括Django、Flask、Tornado、Sanic、Pyramid、Bottle、Web2py、web.py等。
在上述Python的Web框架中Django无疑是最有代表性的重量级选手开发者可以基于Django快速的开发可靠的Web应用程序因为它减少了Web开发中不必要的开销对常用的设计和开发模式进行了封装并对MVC架构提供了支持Django中称之为MTV架构。MVC是软件系统开发领域中一种放之四海而皆准的架构它将系统中的组件分为模型Model、视图View和控制器Controller三个部分并借此实现模型数据和视图显示的解耦合。由于模型和视图进行了分离所以需要一个中间人将解耦合的模型和视图联系起来扮演这个角色的就是控制器。稍具规模的软件系统都会使用MVC架构或者是从MVC演进出的其他架构Django项目中我们称之为MTVMTV中的M跟MVC中的M没有区别就是代表数据的模型T代表了网页模板显示数据的视图而V代表了视图函数在Django框架中视图函数和Django框架本身一起扮演了MVC中C的角色。
![](./res/mvc.png)
Django框架诞生于2003年它是一个在真正的应用中成长起来的项目由劳伦斯出版集团旗下在线新闻网站的内容管理系统CMS研发团队主要是Adrian Holovaty和Simon Willison开发以比利时的吉普赛爵士吉他手Django Reinhardt来命名。Django框架在2005年夏天作为开源框架发布使用Django框架能用很短的时间构建出功能完备的网站因为它代替程序员完成了那些重复乏味的劳动剩下真正有意义的核心业务给程序员来开发这一点就是对DRYDon't Repeat Yourself理念的最好践行。许多成功的网站和应用都是基于Python语言进行开发的国内比较有代表性的网站包括知乎、豆瓣网、果壳网、搜狐闪电邮箱、101围棋网、海报时尚网、背书吧、堆糖、手机搜狐网、咕咚、爱福窝、果库等其中不乏使用了Django框架的产品。
### 快速上手
#### 第一个Django项目
1. 检查Python环境Django 1.11需要Python 2.7或Python 3.4以上的版本Django 2.0需要Python 3.4以上的版本Django 2.1和2.2需要Python 3.5以上的版本Django 3.0需要Python 3.6以上版本。
> **说明**Django框架不同版本所需的Python解释器环境可以在Django官方文档的[FAQ](https://docs.djangoproject.com/zh-hans/3.0/faq/install/#faq-python-version-support)中找到。
可以在macOS的终端中输入下面的命令检查Python解释器版本Windows系统可以在命令行提示符中输入`python --version`。
```Bash
python3 --version
```
也可以在Python的交互式环境中执行下面的代码来查看Python解释器的版本。
```Shell
import sys
sys.version
sys.version_info
```
2. 更新包管理工具并安装Django环境用于创建Django项目
> **说明**在更新这个文档时Django最新的正式版本是3.0.7Django 3.0提供了对ASGI的支持可以实现全双工的异步通信但是目前的使用体验一般所以暂时不推荐大家使用Django 3.0下面我们安装的是Django 2.2.13版本。使用`pip`安装三方库和工具时,可以通过`==`来指定安装的版本。
```Bash
pip3 install -U pip
pip3 install django==2.2.13
```
3. 检查Django环境并使用`django-admin`命令创建Django项目项目名称为hellodjango
```Shell
django-admin --version
django-admin startproject hellodjango
```
4. 用PyCharm打开创建好的Djang项目并为其添加虚拟环境。
![](res/pycharm-django-project.png)
如上图所示PyCharm的项目浏览器中最顶层的文件夹`hellodjango`是Python项目文件夹这个文件夹的名字并不重要Django项目也不关心这个文件夹叫什么名字。该文件夹下有一个同名的文件夹它是Django项目文件夹其中包含了`__init__.py`、`settings.py`、`urls.py`、`wsgi.py`四个文件,与名为`hellodjango`的Django项目文件夹同级的还有一个名为`manage.py` 的文件,这些文件的作用如下所示:
- `hellodjango/__init__.py`空文件告诉Python解释器这个目录应该被视为一个Python的包。
- `hellodjango/settings.py`Django项目的配置文件。
- `hellodjango/urls.py`Django项目的URL映射声明就像是网站的“目录”。
- `hellodjango/wsgi.py`项目运行在WSGI兼容Web服务器上的入口文件。
- `manage.py` 管理Django项目的脚本程序。
> 说明WSGI全称是Web服务器网关接口维基百科上给出的解释是“为Python语言定义的[Web服务器](https://zh.wikipedia.org/wiki/%E7%B6%B2%E9%A0%81%E4%BC%BA%E6%9C%8D%E5%99%A8)和[Web应用程序](https://zh.wikipedia.org/wiki/%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F)或框架之间的一种简单而通用的接口”。
创建虚拟环境的界面如下图所示。
![pycharm-django-virtual-environment](res/pycharm-django-virtual-environment.png)
5. 安装项目依赖项。
方法一打开PyCharm的终端在终端中通过`pip`命令安装Django项目的依赖项。
> **说明**由于已经基于Python 3解释器环境为项目创建了虚拟环境所以虚拟环境中的`python`命令对应的是Python 3的解释器而`pip`命令对应的是Python 3的包管理工具。
```Shell
pip install django==2.2.13
```
方法二在PyCharm的偏好设置中可以找到项目的解释器环境和已经安装的三方库可以通过点击添加按钮来安装新的依赖项需要提醒大家的是在安装Django依赖项时需要指定版本号否则将默认安装更新本文时最新的3.0.7版本。
![](res/pycharm-install-django.png)
下图展示了Django版本和Python版本的对应关系请大家自行对号入座。
| Django版本 | Python版本 |
| ---------- | ----------------------------------------- |
| 1.8 | 2.7、3.2、3.3、3.4、3.5 |
| 1.9、1.10 | 2.7、3.4、3.5 |
| 1.11 | 2.7、3.4、3.5、3.6、3.7Django 1.11.17 |
| 2.0 | 3.4、3.5、3.6、3.7 |
| 2.1 | 3.5、3.6、3.7 |
| 2.2 | 3.5、3.6、3.7、3.8Django 2.2.8 |
| 3.0 | 3.6、3.7、3.8 |
6. 启动Django自带的服务器运行项目。
方法一在“Run”菜单选择“Edit Configuration”配置“Django server”运行项目适用于专业版PyCharm
![](res/pycharm-django-server.png)
方法二在“Run”菜单选择“Edit Configuration”配置运行“Python”程序运行项目适用于专业版和社区版PyCharm
![](res/pycharm-python-manage.png)
方法三在PyCharm的终端Terminal中通过命令运行项目适用于专业版和社区版PyCharm
```Shell
python manage.py runserver
```
7. 查看运行效果。
在浏览器中输入`http://127.0.0.1:8000`访问我们的服务器,效果如下图所示。
![](./res/django-index-1.png)
> **说明**
>
> 1. 刚刚启动的Django自带的服务器只能用于开发和测试环境因为这个服务器是纯Python编写的轻量级Web服务器不适合在生产环境中使用。
> 2. 如果修改了代码不需要为了让修改的代码生效而重新启动Django自带的服务器。但是在添加新的项目文件时该服务器不会自动重新加载这个时候就得手动重启服务器。
> 3. 可以在终端中通过`python manage.py help`命令查看Django管理脚本程序可用的命令参数。
> 4. 使用`python manage.py runserver`启动服务器时可以在后面添加参数来指定IP地址和端口号默认情况下启动的服务器将运行在本机的`8000`端口。
> 5. 在终端中运行的服务器可以通过Ctrl+C来停止它 。通过PyCharm的“运行配置”运行的服务器直接点击窗口上的关闭按钮就可以终止服务器的运行。
> 6. 不能在同一个端口上启动多个服务器因为会导致地址的冲突端口是对IP地址的扩展也是计算机网络地址的一部分
8. 修改项目的配置文件`settings.py`。
Django是一个支持国际化和本地化的框架因此刚才我们看到的Django项目的默认首页也是支持国际化的我们可以通过修改配置文件将默认语言修改为中文时区设置为东八区。
找到修改前的配置(在`settings.py`文件第100行以后
```Python
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
```
修改为以下内容。
```Python
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Chongqing'
```
刷新刚才的页面,可以看到修改语言代码和时区之后的结果。
![](./res/django-index-2.png)
#### 创建自己的应用
如果要开发自己的Web应用需要先在Django项目中创建“应用”一个Django项目可以包含一个或多个应用。
1. 在PyCharm的终端中执行下面的命令创建名为`first`的应用。
```Shell
python manage.py startapp first
```
执行上面的命令会在当前路径下创建`first`目录,其目录结构如下所示:
- `__init__.py`一个空文件告诉Python解释器这个目录应该被视为一个Python的包。
- `admin.py`可以用来注册模型用于在Django框架自带的管理后台中管理模型。
- `apps.py`:当前应用的配置文件。
- `migrations`:存放与模型有关的数据库迁移信息。
- `__init__.py`一个空文件告诉Python解释器这个目录应该被视为一个Python的包。
- `models.py`存放应用的数据模型MTV中的M
- `tests.py`:包含测试应用各项功能的测试类和测试函数。
- `views.py`处理用户HTTP请求并返回HTTP响应的函数或类MTV中的V
2. 修改应用目录下的视图文件`views.py`。
```Python
from django.http import HttpResponse
def show_index(request):
return HttpResponse('<h1>Hello, Django!</h1>')
```
4. 修改Django项目目录下的`urls.py`文件,将视图函数和用户在浏览器中请求的路径对应。
```Python
from django.contrib import admin
from django.urls import path, include
from first.views import show_index
urlpatterns = [
path('admin/', admin.site.urls),
path('hello/', show_index),
]
```
5. 重新运行项目,并打开浏览器中访问`http://127.0.0.1:8000/hello/`。
5. 上面我们通过代码为浏览器生成了内容,但仍然是静态内容,如果要生成动态内容,可以修改`views.py`文件并添加如下所示的代码。
```Python
from random import sample
from django.http import HttpResponse
def show_index(request):
fruits = [
'Apple', 'Orange', 'Pitaya', 'Durian', 'Waxberry', 'Blueberry',
'Grape', 'Peach', 'Pear', 'Banana', 'Watermelon', 'Mango'
]
selected_fruits = sample(fruits, 3)
content = '<h3>今天推荐的水果是:</h3>'
content += '<hr>'
content += '<ul>'
for fruit in selected_fruits:
content += f'<li>{fruit}</li>'
content += '</ul>'
return HttpResponse(content)
```
6. 刷新页面查看程序的运行结果,看看每次刷新的网页的时候,是不是可以看到不一样的内容。
#### 使用模板
上面通过拼接HTML代码的方式为浏览器生成动态内容的做法在实际开发中是无能接受的因为实际项目中的前端页面可能非常复杂无法用这种拼接动态内容的方式来完成这一点大家一定能够想到。为了解决这个问题我们可以提前准备一个模板页MTV中的T所谓模板页就是一个带占位符和模板指令的HTML页面。
Django框架中有一个名为`render`的便捷函数可以来完成渲染模板的操作。所谓的渲染就是用数据替换掉模板页中的模板指令和占位符当然这里的渲染称为后端渲染即在服务器端完成页面的渲染再输出到浏览器中。后端渲染的做法在Web应用的访问量较大时会让服务器承受较大的负担所以越来越多的Web应用会选择前端渲染的方式即服务器只提供页面所需的数据通常是JSON格式在浏览器中通过JavaScript代码获取这些数据并渲染页面上。关于前端渲染的内容我们会在后续的课程中为大家讲解目前我们使用的是通过模板页进行后端渲染的做法具体步骤如下所示。
使用模板页的步骤如下所示。
1. 在项目目录下创建名为templates文件夹。
![](res/pycharm-django-template.png)
2. 添加模板页`index.html`。
> **说明**实际项目开发中静态页由前端开发者提供后端开发者需要将静态页修改为模板页以便通过Python程序对其进行渲染这种做法就是上面提到的后端渲染。
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<style>
#fruits {
font-size: 1.25em;
}
</style>
</head>
<body>
<h1>今天推荐的水果是:</h1>
<hr>
<ul id="fruits">
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% endfor %}
</ul>
</body>
</html>
```
在上面的模板页中我们使用了`{{ fruit }}`这样的模板占位符语法,也使用了`{% for %}`这样的模板指令这些都是Django模板语言DTL的一部分。关于模板语法和指令大家可以看看官方文档相信这些内容还是很容易理解的并不需要过多的赘述大家也可以参考[官方文档]()了解模板指令和语法。
3. 修改`views.py`文件,调用`render`函数渲染模板页。
```Python
from random import sample
from django.shortcuts import render
def show_index(request):
fruits = [
'Apple', 'Orange', 'Pitaya', 'Durian', 'Waxberry', 'Blueberry',
'Grape', 'Peach', 'Pear', 'Banana', 'Watermelon', 'Mango'
]
selected_fruits = sample(fruits, 3)
return render(request, 'index.html', {'fruits': selected_fruits})
```
`render`函数的第一个参数是请求对象request第二个参数是我们要渲染的模板页的名字第三个参数是要渲染到页面上的数据我们通过一个字典将数据交给模板页字典中的键就是模板页中使用的模板指令或占位符中的变量名。
4. 到此为止,视图函数中的`render`还无法找到模板文件`index.html`,需要修改`settings.py`文件,配置模板文件所在的路径。修改`settings.py`文件,找到`TEMPLATES`配置,修改其中的`DIRS`配置。
```Python
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates'), ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
```
5. 重新运行项目或直接刷新页面查看结果。
### 总结
至此我们已经利用Django框架完成了一个非常小的Web应用虽然它并没有任何的实际价值但是可以通过这个项目对Django框架有一个感性的认识。学习Django最好的资料肯定是它的[官方文档](https://docs.djangoproject.com/zh-hans/2.0/)官方文档提供了对多国语言的支持而且有新手教程引导初学者学习使用Django框架建议大家通过阅读Django的官方文档来学习和使用这个框架。当然图灵社区出版的[《Django基础教程》](http://www.ituring.com.cn/book/2630)也是非常适合初学者的入门级读物,有兴趣的读者可以点击链接进行购买。

View File

@ -1,393 +0,0 @@
## 快速上手
Web开发的早期阶段开发者需要手动编写每个页面例如一个新闻门户网站每天都要修改它的HTML页面随着网站规模和体量的增大这种方式就变得极度糟糕。为了解决这个问题开发人员想到了用外部程序来为Web服务器生成动态内容也就是说HTML页面以及页面中的动态内容不再通过手动编写而是通过程序自动生成。最早的时候这项技术被称为CGI公共网关接口当然随着时间的推移CGI暴露出的问题也越来越多例如大量重复的样板代码总体性能较为低下等因此在时代呼唤新英雄的背景下PHP、ASP、JSP这类Web应用开发技术在上世纪90年代中后期如雨后春笋般涌现。通常我们说的Web应用是指通过浏览器来访问网络资源的应用程序因为浏览器的普及性以及易用性Web应用使用起来方便简单免除了安装和更新应用程序带来的麻烦而且也不用关心用户到底用的是什么操作系统甚至不用区分是PC端还是移动端。
### Web应用机制和术语
下图向我们展示了Web应用的工作流程其中涉及到的术语如下表所示。
![](./res/web-application.png)
> 说明:相信有经验的读者会发现,这张图中其实还少了很多东西,例如反向代理服务器、数据库服务器、防火墙等,而且图中的每个节点在实际项目部署时可能是一组节点组成的集群。当然,如果你对这些没有什么概念也不要紧,继续下去就行了,后面会给大家一一讲解的。
| 术语 | 解释 |
| ------------- | ------------------------------------------------------------ |
| **URL/URI** | 统一资源定位符/统一资源标识符,网络资源的唯一标识 |
| **域名** | 与Web服务器地址对应的一个易于记忆的字符串名字 |
| **DNS** | 域名解析服务可以将域名转换成对应的IP地址 |
| **IP地址** | 网络上的主机的身份标识通过IP地址可以区分不同的主机 |
| **HTTP** | 超文本传输协议构建在TCP之上的应用级协议万维网数据通信的基础 |
| **反向代理** | 代理客户端向服务器发出请求,然后将服务器返回的资源返回给客户端 |
| **Web服务器** | 接受HTTP请求然后返回HTML文件、纯文本文件、图像等资源给请求者 |
| **Nginx** | 高性能的Web服务器也可以用作[反向代理](https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86)[负载均衡](https://zh.wikipedia.org/wiki/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1) 和 [HTTP缓存](https://zh.wikipedia.org/wiki/HTTP%E7%BC%93%E5%AD%98) |
#### HTTP协议
这里我们稍微费一些笔墨来谈谈上面提到的HTTP。HTTP超文本传输协议是构建于TCP传输控制协议之上应用级协议它利用了TCP提供的可靠的传输服务实现了Web应用中的数据交换。按照维基百科上的介绍设计HTTP最初的目的是为了提供一种发布和接收[HTML](https://zh.wikipedia.org/wiki/HTML)页面的方法也就是说这个协议是浏览器和Web服务器之间传输的数据的载体。关于这个协议的详细信息以及目前的发展状况大家可以阅读阮一峰老师的[《HTTP 协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)、[《互联网协议入门》](http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html)系列以及[《图解HTTPS协议》](http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html)进行了解。下图是我在四川省网络通信技术重点实验室学习和工作期间使用开源协议分析工具Ethereal抓包工具WireShark的前身截取的访问百度首页时的HTTP请求和响应的报文协议数据由于Ethereal截取的是经过网络适配器的数据因此可以清晰的看到从物理链路层到应用层的协议数据。
HTTP请求请求行+请求头+空行+[消息体]
![](./res/http-request.png)
HTTP响应响应行+响应头+空行+消息体):
![](./res/http-response.png)
> 说明这两张图是在2009年9月10日凌晨获得的但愿这两张如同泛黄的照片般的截图能帮助你了解HTTP到底是什么样子的。
### Django概述
Python的Web框架有上百个比它的关键字还要多。所谓Web框架就是用于开发Web服务器端应用的基础设施说得通俗一点就是一系列封装好的模块和工具。事实上即便没有Web框架我们仍然可以通过socket或[CGI](https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E7%BD%91%E5%85%B3%E6%8E%A5%E5%8F%A3)来开发Web服务器端应用但是这样做的成本和代价在商业项目中通常是不能接受的。通过Web框架我们可以化繁为简降低创建、更新、扩展应用程序的工作量。刚才我们说到Python有上百个Web框架这些框架包括Django、Flask、Tornado、Sanic、Pyramid、Bottle、Web2py、web.py等。
在上述Python的Web框架中Django无疑是最有代表性的重量级选手开发者可以基于Django快速的开发可靠的Web应用程序因为它减少了Web开发中不必要的开销对常用的设计和开发模式进行了封装并对MVC架构提供了支持Django中称之为MTV架构。许多成功的网站和应用都是基于Django框架构建的国内比较有代表性的网站包括知乎、豆瓣网、果壳网、搜狐闪电邮箱、101围棋网、海报时尚网、背书吧、堆糖、手机搜狐网、咕咚、爱福窝、果库等。
![](./res/mvc.png)
Django诞生于2003年它是一个在真正的应用中成长起来的项目由劳伦斯出版集团旗下在线新闻网站的内容管理系统CMS研发团队编写主要是Adrian Holovaty和Simon Willison以比利时的吉普赛爵士吉他手Django Reinhardt来命名在2005年夏天作为开源框架发布。使用Django能用很短的时间构建出功能完备的网站因为它代替程序员完成了所有乏味和重复的劳动剩下真正有意义的核心业务给程序员这一点就是对DRYDon't Repeat Yourself理念的最好践行。
### 快速上手
#### 准备工作
1. 检查Python环境Django 1.11需要Python 2.7或Python 3.4以上的版本Django 2.0需要Python 3.4以上的版本Django 2.1需要Python 3.5以上的版本。
> 说明我自己平时使用macOS和Linux系统做开发macOS和Linux系统在命令的使用上跟Windows系统还是有一些差别如果使用Windows平台做开发要使用Windows平台对应的命令。
```Shell
$ python3 --version
```
```Shell
$ python3
>>> import sys
>>> sys.version
>>> sys.version_info
```
2. 更新包管理工具并安装Django管理工具。
```Shell
$ pip3 install -U pip
$ pip3 install django
```
3. 使用Django管理工具创建Django项目项目名称为hellodjango
```Shell
$ django-admin startproject hellodjango
```
> 说明上面使用了Python自带的venv模块完成了虚拟环境的创建当然也可以使用virtualenv或pipenv这样的工具。要激活虚拟环境在Windows环境下可以通过"venv/Scripts/activate"执行批处理文件来实现。
4. 进入项目文件夹,创建并激活虚拟环境。
```Shell
$ cd hellodjango
$ python3 -m venv venv
$ source venv/bin/activate
```
> **提示**上面使用了Python 3自带的`venv`模块来创建虚拟环境,当然也可以使用如`virtualenv`这样的三方工具来创建虚拟环境激活虚拟环境后请注意终端中提示符的变化在虚拟环境下使用Python解释器和包管理工具时对应的命令是`python`和`pip`,而不再需要键入`python3`和`pip3`。
5. 在虚拟环境中安装项目依赖项。
```Shell
(venv)$ pip install django mysqlclient django-redis pillow requests
```
> **提示**:使用`pip`安装三方库时,可以通过如`django==1.11.27`的方式来指定三方库的版本。
下图展示了Django版本和Python版本的对应关系如果在安装时没有指定版本号将自动选择最新的版本在写作这段内容时Django最新的版本是2.2)。
| Django版本 | Python版本 |
| ---------- | ----------------------- |
| 1.8 | 2.7、3.2、3.3、3.4、3.5 |
| 1.9、1.10 | 2.7、3.4、3.5 |
| 1.11 | 2.7、3.4、3.5、3.6、3.7 |
| 2.0 | 3.4、3.5、3.6、3.7 |
| 2.1、2.2 | 3.5、3.6、3.7 |
刚才创建的Django项目其文件和文件夹如下所示
- `manage.py` 一个让你可以管理Django项目的工具程序。
- `hellodjango/__init__.py`一个空文件告诉Python解释器这个目录应该被视为一个Python的包。
- `hellodjango/settings.py`Django项目的配置文件。
- `hellodjango/urls.py`Django项目的URL声明URL映射就像是你的网站的“目录”。
- `hellodjango/wsgi.py`项目运行在WSGI兼容Web服务器上的接口文件。
> 说明WSGI全称是Web服务器网关接口维基百科上给出的解释是“为Python语言定义的[Web服务器](https://zh.wikipedia.org/wiki/%E7%B6%B2%E9%A0%81%E4%BC%BA%E6%9C%8D%E5%99%A8)和[Web应用程序](https://zh.wikipedia.org/wiki/%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F)或框架之间的一种简单而通用的接口”。
6. 启动Django自带的服务器运行项目。
```Shell
(venv)$ python manage.py runserver
```
在浏览器中输入<http://127.0.0.1:8000>访问我们的服务器,效果如下图所示。
> **说明1**刚刚启动的是Django自带的用于开发和测试的服务器它是一个用纯Python编写的轻量级Web服务器但它并不是真正意义上的生产级别的服务器千万不要将这个服务器用于和生产环境相关的任何地方。
>
> **说明2**用于开发的服务器在需要的情况下会对每一次的访问请求重新载入一遍Python代码。所以你不需要为了让修改的代码生效而频繁的重新启动服务器。然而一些动作比如添加新文件将不会触发自动重新加载这时你得自己手动重启服务器。
>
> **说明3**:可以通过`python manage.py help`命令查看可用命令列表;在启动服务器时,也可以通过`python manage.py runserver 1.2.3.4:5678`来指定将服务器运行于哪个IP地址和端口。
>
> **说明4**可以通过Ctrl+C来终止服务器的运行。
![](./res/django-index-1.png)
7. 修改项目的配置文件settings.pyDjango是一个支持国际化和本地化的框架因此刚才我们看到的默认首页也是支持国际化的我们将默认语言修改为中文时区设置为东八区。
```Shell
(venv)$ vim hellodjango/settings.py
```
```Python
# 此处省略上面的内容
# 设置语言代码
LANGUAGE_CODE = 'zh-hans'
# 设置时区
TIME_ZONE = 'Asia/Chongqing'
# 此处省略下面的内容
```
刷新刚才的页面,可以看到修改语言代码和时区之后的结果。
![](./res/django-index-2.png)
#### 动态页面
1. 创建名为hrs人力资源系统的应用一个Django项目可以包含一个或多个应用。
```Shell
(venv)$ python manage.py startapp hrs
```
执行上面的命令会在当前路径下创建hrs目录其目录结构如下所示
- `__init__.py`一个空文件告诉Python解释器这个目录应该被视为一个Python的包。
- `admin.py`可以用来注册模型用于在Django的管理界面管理模型。
- `apps.py`:当前应用的配置文件。
- `migrations`:存放与模型有关的数据库迁移信息。
- `__init__.py`一个空文件告诉Python解释器这个目录应该被视为一个Python的包。
- `models.py`存放应用的数据模型即实体类及其之间的关系MVC/MTV中的M
- `tests.py`:包含测试应用各项功能的测试类和测试函数。
- `views.py`处理请求并返回响应的函数MVC中的CMTV中的V
2. 修改应用目录下的视图文件views.py。
```Shell
(venv)$ vim hrs/views.py
```
```Python
from django.http import HttpResponse
def index(request):
return HttpResponse('<h1>Hello, Django!</h1>')
```
3. 在应用目录创建一个urls.py文件并映射URL。
```Shell
(venv)$ touch hrs/urls.py
(venv)$ vim hrs/urls.py
```
```Python
from django.urls import path
from hrs import views
urlpatterns = [
path('', views.index, name='index'),
]
```
> 说明:上面使用的`path`函数是Django 2.x中新添加的函数除此之外还可以使用支持正则表达式的URL映射函数`re_path`函数Django 1.x中是用名为`url`函数来设定URL映射。
4. 修改项目目录下的urls.py文件对应用中设定的URL进行合并。
```Shell
(venv) $ vim oa/urls.py
```
```Python
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('hrs/', include('hrs.urls')),
]
```
> 说明:上面的代码通过`include`函数将hrs应用中配置URL的文件包含到项目的URL配置中并映射到`hrs/`路径下。
5. 重新运行项目,并打开浏览器中访问<http://localhost:8000/hrs>
```Shell
(venv)$ python manage.py runserver
```
6. 修改views.py生成动态内容。
```Shell
(venv)$ vim hrs/views.py
```
```Python
from io import StringIO
from django.http import HttpResponse
depts_list = [
{'no': 10, 'name': '财务部', 'location': '北京'},
{'no': 20, 'name': '研发部', 'location': '成都'},
{'no': 30, 'name': '销售部', 'location': '上海'},
]
def index(request):
output = StringIO()
output.write('<html>\n')
output.write('<head>\n')
output.write('\t<meta charset="utf-8">\n')
output.write('\t<title>首页</title>')
output.write('</head>\n')
output.write('<body>\n')
output.write('\t<h1>部门信息</h1>\n')
output.write('\t<hr>\n')
output.write('\t<table>\n')
output.write('\t\t<tr>\n')
output.write('\t\t\t<th width=120>部门编号</th>\n')
output.write('\t\t\t<th width=180>部门名称</th>\n')
output.write('\t\t\t<th width=180>所在地</th>\n')
output.write('\t\t</tr>\n')
for dept in depts_list:
output.write('\t\t<tr>\n')
output.write(f'\t\t\t<td align=center>{dept["no"]}</td>\n')
output.write(f'\t\t\t<td align=center>{dept["name"]}</td>\n')
output.write(f'\t\t\t<td align=center>{dept["location"]}</td>\n')
output.write('\t\t</tr>\n')
output.write('\t</table>\n')
output.write('</body>\n')
output.write('</html>\n')
return HttpResponse(output.getvalue())
```
7. 刷新页面查看程序的运行结果。
![](./res/show-depts.png)
#### 使用视图模板
上面通过拼接HTML代码的方式生成动态视图的做法在实际开发中是无能接受的这一点大家一定能够想到。为了解决这个问题我们可以提前准备一个模板页所谓模板页就是一个带占位符的HTML页面当我们将程序中获得的数据替换掉页面中的占位符时一个动态页面就产生了。
我们可以用Django框架中template模块的Template类创建模板对象通过模板对象的render方法实现对模板的渲染在Django框架中还有一个名为`render`的便捷函数可以来完成渲染模板的操作。所谓的渲染就是用数据替换掉模板页中的占位符当然这里的渲染称为后端渲染即在服务器端完成页面的渲染再输出到浏览器中这种做法的主要坏处是当并发访问量较大时服务器会承受较大的负担所以今天有很多的Web应用都使用了前端渲染即服务器只提供所需的数据通常是JSON格式在浏览器中通过JavaScript获取这些数据并渲染到页面上这个我们在后面的内容中会讲到。
1. 先回到manage.py文件所在的目录创建名为templates文件夹。
```Shell
(venv)$ mkdir templates
```
2. 创建模板页index.html。
```Shell
(venv)$ touch templates/index.html
(venv)$ vim templates/index.html
```
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>部门信息</h1>
<hr>
<table>
<tr>
<th>部门编号</th>
<th>部门名称</th>
<th>所在地</th>
</tr>
{% for dept in depts_list %}
<tr>
<td>{{ dept.no }}</td>
<td>{{ dept.name }}</td>
<td>{{ dept.location }}</td>
<tr>
{% endfor %}
</table>
</body>
</html>
```
在上面的模板页中我们使用了`{{ greeting }}`这样的模板占位符语法,也使用了`{% for %}`这样的模板指令这些都是Django模板语言DTL的一部分。如果对此不熟悉并不要紧我们会在后续的内容中进一步的讲解而且我们刚才也说到了渲染页面还有更好的选择就是使用前端渲染当然这是后话。
3. 回到应用目录修改views.py文件。
```Shell
(venv)$ vim hrs/views.py
```
```Python
from django.shortcuts import render
depts_list = [
{'no': 10, 'name': '财务部', 'location': '北京'},
{'no': 20, 'name': '研发部', 'location': '成都'},
{'no': 30, 'name': '销售部', 'location': '上海'},
]
def index(request):
return render(request, 'index.html', {'depts_list': depts_list})
```
> 说明Django框架通过shortcuts模块的便捷函数`render`简化了渲染模板的操作,有了这个函数,就不用先创建`Template`对象再去调用`render`方法。。
到此为止我们还没有办法让views.py中的`render`函数找到模板文件index.html为此我们需要修改settings.py文件配置模板文件所在的路径。
4. 切换到项目目录修改settings.py文件。
```Shell
(venv)$ vim oa/settings.py
```
```Python
# 此处省略上面的内容
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# 此处省略下面的内容
```
5. 重新运行项目或直接刷新页面查看结果。
```Shell
(venv)$ python manage.py runserver
```
### 总结
至此我们已经利用Django框架完成了一个非常小的Web应用虽然它并没有任何的实际价值但是可以通过这个项目对Django框架有一个感性的认识。当然实际开发中我们可以用PyCharm来创建项目如果使用专业版的PyCharm可以直接创建Django项目。使用PyCharm的好处在于编写代码时可以获得代码提示、错误修复、自动导入等功能从而提升开发效率但是专业版的PyCharm需要按年支付相应的费用社区版的PyCharm中并未包含对Django框架直接的支持但是我们仍然可以使用它来创建Django项目只是在使用上没有专业版的方便。关于PyCharm的使用可以参考[《玩转PyCharm》](../玩转PyCharm.md)一文。此外Django最好的学习资料肯定是它的[官方文档](https://docs.djangoproject.com/zh-hans/2.0/),当然图灵社区出版的[《Django基础教程》](http://www.ituring.com.cn/book/2630)也是非常适合初学者的入门级读物。

File diff suppressed because it is too large Load Diff

View File

@ -1,216 +1,60 @@
## 静态资源和Ajax请求
基于前面两个章节讲解的知识我们已经可以使用Django框架来完成Web应用的开发了。接下来我们就尝试实现一个投票应用具体的需求是用户进入应用首先查看到“学科介绍”页面该页面显示了一个学校所开设的所有学科通过点击某个学科可以进入“老师介绍”页面该页面展示了该学科所有老师的详细情况可以在该页面上给老师点击“好评”或“差评”如果用户没有登录在投票时会先跳转到“登录页”要求用户登录登录成功才能投票对于未注册的用户可以在“登录页”点击“新用户注册”进入“注册页”完成用户注册操作注册成功后会跳转到“登录页”注册失败会获得相应的提示信息。
### 准备工作
由于之前已经详细的讲解了如何创建Django项目以及项目的相关配置因此我们略过这部分内容唯一需要说明的是从上面对投票应用需求的描述中我们可以分析出三个业务实体学科、老师和用户。学科和老师之间通常是一对多关联关系一个学科有多个老师一个老师通常只属于一个学科用户因为要给老师投票所以跟老师之间是多对多关联关系一个用户可以给多个老师投票一个老师也可以收到多个用户的投票。首先修改应用下的models.py文件来定义数据模型先给出学科和老师的模型。
```Python
from django.db import models
class Subject(models.Model):
"""学科"""
no = models.IntegerField(primary_key=True, verbose_name='编号')
name = models.CharField(max_length=20, verbose_name='名称')
intro = models.CharField(max_length=511, default='', verbose_name='介绍')
create_date = models.DateField(null=True, verbose_name='成立日期')
is_hot = models.BooleanField(default=False, verbose_name='是否热门')
def __str__(self):
return self.name
class Meta:
db_table = 'tb_subject'
verbose_name = '学科'
verbose_name_plural = '学科'
class Teacher(models.Model):
"""老师"""
no = models.AutoField(primary_key=True, verbose_name='编号')
name = models.CharField(max_length=20, verbose_name='姓名')
detail = models.CharField(max_length=1023, default='', blank=True, verbose_name='详情')
photo = models.CharField(max_length=1023, default='', verbose_name='照片')
good_count = models.IntegerField(default=0, verbose_name='好评数')
bad_count = models.IntegerField(default=0, verbose_name='差评数')
subject = models.ForeignKey(to=Subject, on_delete=models.PROTECT, db_column='sno', verbose_name='所属学科')
class Meta:
db_table = 'tb_teacher'
verbose_name = '老师'
verbose_name_plural = '老师'
```
模型定义完成后可以通过“生成迁移”和“执行迁移”来完成关系型数据库中二维表的创建当然这需要提前启动数据库服务器并创建好对应的数据库同时我们在项目中已经安装了PyMySQL而且完成了相应的配置这些内容此处不再赘述。
```Shell
(venv)$ python manage.py makemigrations vote
...
(venv)$ python manage.py migrate
...
```
> 注意为了给vote应用生成迁移文件需要修改Django项目settings.py文件在INSTALLED_APPS中添加vote应用。
完成模型迁移之后我们可以直接使用Django提供的后台管理来添加学科和老师信息这需要先注册模型类和模型管理类。
```SQL
from django.contrib import admin
from poll2.forms import UserForm
from poll2.models import Subject, Teacher
class SubjectAdmin(admin.ModelAdmin):
list_display = ('no', 'name', 'create_date', 'is_hot')
ordering = ('no', )
class TeacherAdmin(admin.ModelAdmin):
list_display = ('no', 'name', 'detail', 'good_count', 'bad_count', 'subject')
ordering = ('subject', 'no')
admin.site.register(Subject, SubjectAdmin)
admin.site.register(Teacher, TeacherAdmin)
```
接下来我们就可以修改views.py文件通过编写视图函数先实现“学科介绍”页面。
```Python
def show_subjects(request):
"""查看所有学科"""
subjects = Subject.objects.all()
return render(request, 'subject.html', {'subjects': subjects})
```
至此,我们还需要一个模板页,模板的配置以及模板页中模板语言的用法在之前已经进行过简要的介绍,如果不熟悉可以看看下面的代码,相信这并不是一件困难的事情。
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>所有学科信息</title>
<style>/* 此处略去了层叠样式表的选择器 */</style>
</head>
<body>
<h1>所有学科</h1>
<hr>
{% for subject in subjects %}
<div>
<h3>
<a href="/teachers/?sno={{ subject.no }}">{{ subject.name }}</a>
{% if subject.is_hot %}
<img src="/static/images/hot.png" width="32" alt="">
{% endif %}
</h3>
<p>{{ subject.intro }}</p>
</div>
{% endfor %}
</body>
</html>
```
在上面的模板中,我们为每个学科添加了一个超链接,点击超链接可以查看该学科的讲师信息,为此需要再编写一个视图函数来处理查看指定学科老师信息。
```Python
def show_teachers(request):
"""显示指定学科的老师"""
try:
sno = int(request.GET['sno'])
subject = Subject.objects.get(no=sno)
teachers = subject.teacher_set.all()
return render(request, 'teachers.html', {'subject': subject, 'teachers': teachers})
except (KeyError, ValueError, Subject.DoesNotExist):
return redirect('/')
```
显示老师信息的模板页。
```HTML
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>老师</title>
<style>/* 此处略去了层叠样式表的选择器 */</style>
</head>
<body>
<h1>{{ subject.name }}学科老师信息</h1>
<hr>
{% if teachers %}
{% for teacher in teachers %}
<div>
<div>
<img src="{% static teacher.photo %}" alt="">
</div>
<div>
<h3>{{ teacher.name }}</h3>
<p>{{ teacher.detail }}</p>
<p class="comment">
<a href="">好评</a>
(<span>{{ teacher.good_count }}</span>)
<a href="">差评</a>
(<span>{{ teacher.bad_count }}</span>)
</p>
</div>
</div>
{% endfor %}
{% else %}
<h3>暂时没有该学科的老师信息</h3>
{% endif %}
<p>
<a href="/">返回首页</a>
</p>
</body>
</html>
```
### 加载静态资源
在上面的模板页面中,我们使用了`<img>`标签来加载老师的照片,其中使用了引用静态资源的模板指令`{% static %}`,要使用该指令,首先要使用`{% load static %}`指令来加载静态资源我们将这段代码放在了页码开始的位置。在上面的项目中我们将静态资源置于名为static的文件夹中在该文件夹下又创建了三个文件夹css、js和images分别用来保存外部层叠样式表、外部JavaScript文件和图片资源。为了能够找到保存静态资源的文件夹我们还需要修改Django项目的配置文件settings.py如下所示
如果要在Django项目中使用静态资源可以先创建一个用于保存静态资源的目录。在`vote`项目中,我们将静态资源置于名为`static`的文件夹中在该文件夹包含了三个子文件夹css、js和images分别用来保存外部CSS文件、外部JavaScript文件和图片资源如下图所示。
![](res/pycharm-django-static.png)
为了能够找到保存静态资源的文件夹我们还需要修改Django项目的配置文件`settings.py`,如下所示:
```Python
# 此处省略上面的代码
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'), ]
STATIC_URL = '/static/'
# 此处省略下面的代码
```
接下来修改urls.py文件配置用户请求的URL和视图函数的对应关系
配置好静态资源之后大家可以运行项目然后看看之前我们写的页面上的图片是否能够正常加载出来。需要说明的是在项目正式部署到线上环境后我们通常会把静态资源交给专门的静态资源服务器如Nginx、Apache来处理而不是有运行Python代码的服务器来管理静态资源所以上面的配置并不适用于生产环境仅供项目开发阶段测试使用。使用静态资源的正确姿势我们会在后续的章节为大家讲解。
```Python
from django.contrib import admin
from django.urls import path
### Ajax概述
from vote import views
接下来就可以实现“好评”和“差评”的功能了,很明显如果能够在不刷新页面的情况下实现这两个功能会带来更好的用户体验,因此我们考虑使用[Ajax](https://zh.wikipedia.org/wiki/AJAX)技术来实现“好评”和“差评”。Ajax是Asynchronous Javascript And XML的缩写 , 简单的说使用Ajax技术可以在不重新加载整个页面的情况下对页面进行局部刷新。
urlpatterns = [
path('', views.show_subjects),
path('teachers/', views.show_teachers),
path('admin/', admin.site.urls),
]
对于传统的Web应用每次页面上需要加载新的内容都需要重新请求服务器并刷新整个页面如果服务器短时间内无法给予响应或者网络状况并不理想那么可能会造成浏览器长时间的空白并使得用户处于等待状态在这个期间用户什么都做不了如下图所示。很显然这样的Web应用并不能带来很好的用户体验。
![](res/synchronous-web-request.png)
对于使用Ajax技术的Web应用浏览器可以向服务器发起异步请求来获取数据。异步请求不会中断用户体验当服务器返回了新的数据我们可以通过JavaScript代码进行DOM操作来实现对页面的局部刷新这样就相当于在不刷新整个页面的情况下更新了页面的内容如下图所示。
![](res/asynchronous-web-request.png)
在使用Ajax技术时浏览器跟服务器通常会交换XML或JSON格式的数据XML是以前使用得非常多的一种数据格式近年来几乎已经完全被JSON取代下面是两种数据格式的对比。
XML格式
```XML
<?xml version="1.0" encoding="utf-8"?>
<message>
<from>Alice</from>
<to>Bob</to>
<content>Dinner is on me!</content>
</message>
```
启动服务器运行项目,进入首页查看学科信息。
JSON格式
![](./res/show_subjects.png)
```JSON
{
"from": "Alice",
"to": "Bob",
"content": "Dinner is on me!"
}
```
点击学科查看老师信息。
通过上面的对比明显JSON格式的数据要紧凑得多所以传输效率更高而且JSON本身也是JavaScript中的一种对象表达式语法在JavaScript代码中处理JSON格式的数据更加方便。
![](./res/show_teachers.png)
### 用Ajax实现投票功能
### Ajax请求
接下来就可以实现“好评”和“差评”的功能了,很明显如果能够在不刷新页面的情况下实现这两个功能会带来更好的用户体验,因此我们考虑使用[Ajax](https://zh.wikipedia.org/wiki/AJAX)技术来实现“好评”和“差评”Ajax技术我们在Web前端部分已经介绍过了此处不再赘述。
首先修改项目的urls.py文件为“好评”和“差评”功能映射对应的URL。
下面我们使用Ajax技术来实现投票的功能首先修改项目的`urls.py`文件为“好评”和“差评”功能映射对应的URL。
```Python
from django.contrib import admin
@ -233,16 +77,18 @@ urlpatterns = [
def praise_or_criticize(request):
"""好评"""
try:
tno = int(request.GET['tno'])
tno = int(request.GET.get('tno'))
teacher = Teacher.objects.get(no=tno)
if request.path.startswith('/praise'):
teacher.good_count += 1
count = teacher.good_count
else:
teacher.bad_count += 1
count = teacher.bad_count
teacher.save()
data = {'code': 200, 'hint': '操作成功'}
except (KeyError, ValueError, Teacher.DoseNotExist):
data = {'code': 404, 'hint': '操作失败'}
data = {'code': 20000, 'mesg': '操作成功', 'count': count}
except (ValueError, Teacher.DoseNotExist):
data = {'code': 20001, 'mesg': '操作失败'}
return JsonResponse(data)
```
@ -250,54 +96,90 @@ def praise_or_criticize(request):
```HTML
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>老师</title>
<style>/* 此处略去了层叠样式表的选择器 */</style>
<title>老师信息</title>
<style>
#container {
width: 80%;
margin: 10px auto;
}
.teacher {
width: 100%;
margin: 0 auto;
padding: 10px 0;
border-bottom: 1px dashed gray;
overflow: auto;
}
.teacher>div {
float: left;
}
.photo {
height: 140px;
border-radius: 75px;
overflow: hidden;
margin-left: 20px;
}
.info {
width: 75%;
margin-left: 30px;
}
.info div {
clear: both;
margin: 5px 10px;
}
.info span {
margin-right: 25px;
}
.info a {
text-decoration: none;
color: darkcyan;
}
</style>
</head>
<body>
<h1>{{ subject.name }}学科老师信息</h1>
<hr>
{% if teachers %}
{% for teacher in teachers %}
<div class="teacher">
<div class="photo">
<img src="{% static teacher.photo %}" height="140" alt="">
<div id="container">
<h1>{{ subject.name }}学科的老师信息</h1>
<hr>
{% if not teachers %}
<h2>暂无该学科老师信息</h2>
{% endif %}
{% for teacher in teachers %}
<div class="teacher">
<div class="photo">
<img src="/static/images/{{ teacher.photo }}" height="140" alt="">
</div>
<div class="info">
<div>
<span><strong>姓名:{{ teacher.name }}</strong></span>
<span>性别:{{ teacher.sex | yesno:'男,女' }}</span>
<span>出生日期:{{ teacher.birth }}</span>
</div>
<div class="intro">{{ teacher.intro }}</div>
<div class="comment">
<a href="/praise/?tno={{ teacher.no }}">好评</a>&nbsp;&nbsp;
(<strong>{{ teacher.good_count }}</strong>)
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="/criticize/?tno={{ teacher.no }}">差评</a>&nbsp;&nbsp;
(<strong>{{ teacher.bad_count }}</strong>)
</div>
</div>
</div>
<div class="info">
<h3>{{ teacher.name }}</h3>
<p>{{ teacher.detail }}</p>
<p class="comment">
<a href="/praise/?tno={{ teacher.no }}">好评</a>
(<span>{{ teacher.good_count }}</span>)
&nbsp;&nbsp;
<a href="/criticize/?tno={{ teacher.no }}">差评</a>
(<span>{{ teacher.bad_count }}</span>)
</p>
</div>
</div>
{% endfor %}
{% else %}
<h3>暂时没有该学科的老师信息</h3>
{% endif %}
<p>
{% endfor %}
<a href="/">返回首页</a>
</p>
</div>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
$(() => {
$('.comment>a').on('click', (evt) => {
evt.preventDefault()
let anchor = $(evt.target)
let url = anchor.attr('href')
let url = $(evt.target).attr('href')
$.getJSON(url, (json) => {
if (json.code == 10001) {
let span = anchor.next()
span.text(parseInt(span.text()) + 1)
if (json.code == 20000) {
$(evt.target).next().text(json.count)
} else {
alert(json.hint)
alert(json.mesg)
}
})
})
@ -307,6 +189,8 @@ def praise_or_criticize(request):
</html>
```
上面的前端代码中使用了jQuery库封装的`getJSON`方法向服务器发送异步请求如果不熟悉前端的jQuery库可以参考[《jQuery API手册》](https://www.runoob.com/manual/jquery/)。
### 小结
到此为止,这个投票项目的核心功能已然完成,在下面的章节中我们会要求用户必须登录才能投票,没有账号的用户可以通过注册功能注册一个账号。

View File

@ -0,0 +1,581 @@
## Cookie和Session
我们继续来完成上一章节中的项目,实现“用户登录”的功能,并限制只有登录的用户才能投票。
### 用户登录的准备工作
我们先为实现用户登录做一些准备工作。
1. 创建用户模型。之前我们讲解过如果通过Django的ORM实现从二维表到模型的转换反向工程这次我们尝试把模型变成二维表正向工程
```Python
class User(models.Model):
"""用户"""
no = models.AutoField(primary_key=True, verbose_name='编号')
username = models.CharField(max_length=20, unique=True, verbose_name='用户名')
password = models.CharField(max_length=32, verbose_name='密码')
tel = models.CharField(max_length=20, verbose_name='手机号')
reg_date = models.DateTimeField(auto_now_add=True, verbose_name='注册时间')
last_visit = models.DateTimeField(null=True, verbose_name='最后登录时间')
class Meta:
db_table = 'tb_user'
verbose_name = '用户'
verbose_name_plural = '用户'
```
2. 使用下面的命令生成迁移文件并执行迁移,将`User`模型直接变成关系型数据库中的二维表`tb_user`。
```Bash
python manage.py makemigrations polls
python manage.py migrate polls
```
3. 用下面的SQL语句直接插入两条测试数据通常不能讲用户的密码直接保存在数据库中因此我们将用户密码处理成对应的MD5摘要。MD5消息摘要算法是一种被广泛使用的密码哈希函数散列函数可以产生出一个128位比特的哈希值散列值用于确保信息传输完整一致。在使用哈希值时通常会将哈希值表示为16进制字符串因此128位的MD5摘要通常表示为32个十六进制符号。
```SQL
insert into `tb_user`
(`username`, `password`, `tel`, `reg_date`)
values
('wangdachui', '1c63129ae9db9c60c3e8aa94d3e00495', '13122334455', now()),
('hellokitty', 'c6f8cf68e5f68b0aa4680e089ee4742c', '13890006789', now());
```
> **说明**:上面创建的两个用户`wangdachui`和`hellokitty`密码分别是`1qaz2wsx`和`Abc123!!`。
4. 我们在应用下增加一个名为`utils.py`的模块用来保存需要使用的工具函数。Python标准库中的`hashlib`模块封装了常用的哈希算法包括MD5、SHA1、SHA256等。下面是使用`hashlib`中的`md5`类将字符串处理成MD5摘要的函数如下所示。
```Python
import hashlib
def gen_md5_digest(content):
return hashlib.md5(content.encode()).hexdigest()
```
5. 编写用户登录的视图函数和模板页。
添加渲染登录页面的视图函数:
```Python
def login(request: HttpRequest) -> HttpResponse:
hint = ''
return render(request, 'login.html', {'hint': hint})
```
增加`login.html`模板页:
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
<style>
#container {
width: 520px;
margin: 10px auto;
}
.input {
margin: 20px 0;
width: 460px;
height: 40px;
}
.input>label {
display: inline-block;
width: 140px;
text-align: right;
}
.input>img {
width: 150px;
vertical-align: middle;
}
input[name=captcha] {
vertical-align: middle;
}
form+div {
margin-top: 20px;
}
form+div>a {
text-decoration: none;
color: darkcyan;
font-size: 1.2em;
}
.button {
width: 500px;
text-align: center;
margin-top: 20px;
}
.hint {
color: red;
font-size: 12px;
}
</style>
</head>
<body>
<div id="container">
<h1>用户登录</h1>
<hr>
<p class="hint">{{ hint }}</p>
<form action="/login/" method="post">
{% csrf_token %}
<fieldset>
<legend>用户信息</legend>
<div class="input">
<label>用户名:</label>
<input type="text" name="username">
</div>
<div class="input">
<label>密码:</label>
<input type="password" name="password">
</div>
<div class="input">
<label>验证码:</label>
<input type="text" name="captcha">
<img id="code" src="/captcha/" alt="" width="150" height="40">
</div>
</fieldset>
<div class="button">
<input type="submit" value="登录">
<input type="reset" value="重置">
</div>
</form>
<div>
<a href="/">返回首页</a>
<a href="/register/">注册新用户</a>
</div>
</div>
</body>
</html>
```
注意,在上面的表单中,我们使用了模板指令`{% csrf_token %}`为表单添加一个隐藏域(大家可以在浏览器中显示网页源代码就可以看到这个指令生成的`type`属性为`hidden`的`input`标签它的作用是在表单中生成一个随机令牌token来防范[跨站请求伪造](<https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0>)简称为CSRF这也是Django在提交表单时的硬性要求。如果我们的表单中没有这样的令牌那么提交表单时Django框架会产生一个响应状态码为`403`的响应禁止访问除非我们设置了免除CSRF令牌。下图是一个关于CSRF简单生动的例子。
![](./res/csrf-simple.png)
接下来我们可以编写提供验证码和实现用户登录的视图函数在此之前我们先说说一个Web应用实现用户跟踪的方式以及Django框架对实现用户跟踪所提供的支持。对一个Web应用来说用户登录成功后必然要让服务器能够记住该用户已经登录这样服务器才能为这个用户提供更好的服务而且上面说到的CSRF也是通过钓鱼网站来套取用户登录信息进行恶意操作的攻击手段这些都是以用户跟踪技术为基础的。在理解了这些背景知识后我们就清楚用户登录时到底需要执行哪些操作。
### 实现用户跟踪
如今,一个网站如果不通过某种方式记住你是谁以及你之前在网站的活动情况,失去的就是网站的可用性和便利性,继而很有可能导致网站用户的流式,所以记住一个用户(更专业的说法叫**用户跟踪**对绝大多数Web应用来说都是必需的功能。
在服务器端我们想记住一个用户最简单的办法就是创建一个对象通过这个对象就可以把用户相关的信息都保存起来这个对象就是我们常说的session用户会话对象。那么问题来了HTTP本身是一个**无连接**(每次请求和响应的过程中,服务器一旦完成对客户端请求的响应之后就断开连接)、**无状态**客户端再次发起对服务器的请求时服务器无法得知这个客户端之前的任何信息的协议即便服务器通过session对象保留了用户数据还得通过某种方式来确定当前的请求与之前保存过的哪一个session是有关联的。相信很多人都能想到我们可以给每个session对象分配一个全局唯一的标识符来识别session对象我们姑且称之为sessionid每次客户端发起请求时只要携带上这个sessionid就有办法找到与之对应的session对象从而实现在两次请求之间记住该用户的信息也就是我们之前说的用户跟踪。
要让客户端记住并在每次请求时带上sessionid又有以下几种做法
1. URL重写。所谓URL重写就是在URL中携带sessionid例如`http://www.example.com/index.html?sessionid=123456`服务器通过获取sessionid参数的值来取到与之对应的session对象。
2. 隐藏域(隐式表单域)。在提交表单的时候,可以通过在表单中设置隐藏域向服务器发送额外的数据。例如:`<input type="hidden" name="sessionid" value="123456">`。
3. 本地存储。现在的浏览器都支持多种本地存储方案包括cookie、localStorage、sessionStorage、IndexedDB等。在这些方案中cookie是历史最为悠久也是被诟病得最多的一种方案也是我们接下来首先为大家讲解的一种方案。简单的说cookie是一种以键值对方式保存在浏览器临时文件中的数据每次请求时请求头中会携带本站点的cookie到服务器那么只要将sessionid写入cookie下次请求时服务器只要读取请求头中的cookie就能够获得这个sessionid如下图所示。
![](./res/sessionid_from_cookie.png)
在HTML5时代要除了cookie还可以使用新的本地存储API来保存数据就是刚才提到的localStorage、sessionStorage、IndexedDB等技术如下图所示。
![](./res/cookie_xstorage_indexeddb.png)
**总结一下**要实现用户跟踪服务器端可以为每个用户会话创建一个session对象并将session对象的ID写入到浏览器的cookie中用户下次请求服务器时浏览器会在HTTP请求头中携带该网站保存的cookie信息这样服务器就可以从cookie中找到session对象的ID并根据此ID获取到之前创建的session对象由于session对象可以用键值对的方式保存用户数据这样之前保存在session对象中的信息可以悉数取出服务器也可以根据这些信息判定用户身份和了解用户偏好为用户提供更好的个性化服务。
### Django框架对session的支持
在创建Django项目时默认的配置文件`settings.py`文件中已经激活了一个名为`SessionMiddleware`的中间件(关于中间件的知识我们在后面的章节做详细讲解,这里只需要知道它的存在即可),因为这个中间件的存在,我们可以直接通过请求对象的`session`属性来操作会话对象。前面我们说过,`session`属性是一个像字典一样可以读写数据的容器对象,因此我们可以使用“键值对”的方式来保留用户数据。与此同时,`SessionMiddleware`中间件还封装了对cookie的操作在cookie中保存了sessionid这一点我们在上面已经提到过了。
在默认情况下Django将session的数据序列化后保存在关系型数据库中在Django 1.6以后的版本中默认的序列化数据的方式是JSON序列化而在此之前一直使用Pickle序列化。JSON序列化和Pickle序列化的差别在于前者将对象序列化为字符串字符形式而后者将对象序列化为字节串二进制形式因为安全方面的原因JSON序列化成为了目前Django框架默认序列化数据的方式这就要求在我们保存在session中的数据必须是能够JSON序列化的否则就会引发异常。还有一点需要说明的是使用关系型数据库保存session中的数据在大多数时候并不是最好的选择因为数据库可能会承受巨大的压力而成为系统性能的瓶颈在后面的章节中我们会告诉大家如何将session保存到缓存服务中以提升系统的性能。
### 实现用户登录验证
首先,我们在刚才的`polls/utils.py`文件中编写生成随机验证码的函数`gen_random_code`,内容如下所示。
```Python
import random
ALL_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
def gen_random_code(length=4):
return ''.join(random.choices(ALL_CHARS, k=length))
```
编写生成验证码图片的类`Captcha`。
```Python
"""
图片验证码
"""
import os
import random
from io import BytesIO
from PIL import Image
from PIL import ImageFilter
from PIL.ImageDraw import Draw
from PIL.ImageFont import truetype
class Bezier:
"""贝塞尔曲线"""
def __init__(self):
self.tsequence = tuple([t / 20.0 for t in range(21)])
self.beziers = {}
def make_bezier(self, n):
"""绘制贝塞尔曲线"""
try:
return self.beziers[n]
except KeyError:
combinations = pascal_row(n - 1)
result = []
for t in self.tsequence:
tpowers = (t ** i for i in range(n))
upowers = ((1 - t) ** i for i in range(n - 1, -1, -1))
coefs = [c * a * b for c, a, b in zip(combinations,
tpowers, upowers)]
result.append(coefs)
self.beziers[n] = result
return result
class Captcha:
"""验证码"""
def __init__(self, width, height, fonts=None, color=None):
self._image = None
self._fonts = fonts if fonts else \
[os.path.join(os.path.dirname(__file__), 'fonts', font)
for font in ['Arial.ttf', 'Georgia.ttf', 'Action.ttf']]
self._color = color if color else random_color(0, 200, random.randint(220, 255))
self._width, self._height = width, height
@classmethod
def instance(cls, width=200, height=75):
"""用于获取Captcha对象的类方法"""
prop_name = f'_instance_{width}_{height}'
if not hasattr(cls, prop_name):
setattr(cls, prop_name, cls(width, height))
return getattr(cls, prop_name)
def _background(self):
"""绘制背景"""
Draw(self._image).rectangle([(0, 0), self._image.size],
fill=random_color(230, 255))
def _smooth(self):
"""平滑图像"""
return self._image.filter(ImageFilter.SMOOTH)
def _curve(self, width=4, number=6, color=None):
"""绘制曲线"""
dx, height = self._image.size
dx /= number
path = [(dx * i, random.randint(0, height))
for i in range(1, number)]
bcoefs = Bezier().make_bezier(number - 1)
points = []
for coefs in bcoefs:
points.append(tuple(sum([coef * p for coef, p in zip(coefs, ps)])
for ps in zip(*path)))
Draw(self._image).line(points, fill=color if color else self._color, width=width)
def _noise(self, number=50, level=2, color=None):
"""绘制扰码"""
width, height = self._image.size
dx, dy = width / 10, height / 10
width, height = width - dx, height - dy
draw = Draw(self._image)
for i in range(number):
x = int(random.uniform(dx, width))
y = int(random.uniform(dy, height))
draw.line(((x, y), (x + level, y)),
fill=color if color else self._color, width=level)
def _text(self, captcha_text, fonts, font_sizes=None, drawings=None, squeeze_factor=0.75, color=None):
"""绘制文本"""
color = color if color else self._color
fonts = tuple([truetype(name, size)
for name in fonts
for size in font_sizes or (65, 70, 75)])
draw = Draw(self._image)
char_images = []
for c in captcha_text:
font = random.choice(fonts)
c_width, c_height = draw.textsize(c, font=font)
char_image = Image.new('RGB', (c_width, c_height), (0, 0, 0))
char_draw = Draw(char_image)
char_draw.text((0, 0), c, font=font, fill=color)
char_image = char_image.crop(char_image.getbbox())
for drawing in drawings:
d = getattr(self, drawing)
char_image = d(char_image)
char_images.append(char_image)
width, height = self._image.size
offset = int((width - sum(int(i.size[0] * squeeze_factor)
for i in char_images[:-1]) -
char_images[-1].size[0]) / 2)
for char_image in char_images:
c_width, c_height = char_image.size
mask = char_image.convert('L').point(lambda i: i * 1.97)
self._image.paste(char_image,
(offset, int((height - c_height) / 2)),
mask)
offset += int(c_width * squeeze_factor)
@staticmethod
def _warp(image, dx_factor=0.3, dy_factor=0.3):
"""图像扭曲"""
width, height = image.size
dx = width * dx_factor
dy = height * dy_factor
x1 = int(random.uniform(-dx, dx))
y1 = int(random.uniform(-dy, dy))
x2 = int(random.uniform(-dx, dx))
y2 = int(random.uniform(-dy, dy))
warp_image = Image.new(
'RGB',
(width + abs(x1) + abs(x2), height + abs(y1) + abs(y2)))
warp_image.paste(image, (abs(x1), abs(y1)))
width2, height2 = warp_image.size
return warp_image.transform(
(width, height),
Image.QUAD,
(x1, y1, -x1, height2 - y2, width2 + x2, height2 + y2, width2 - x2, -y1))
@staticmethod
def _offset(image, dx_factor=0.1, dy_factor=0.2):
"""图像偏移"""
width, height = image.size
dx = int(random.random() * width * dx_factor)
dy = int(random.random() * height * dy_factor)
offset_image = Image.new('RGB', (width + dx, height + dy))
offset_image.paste(image, (dx, dy))
return offset_image
@staticmethod
def _rotate(image, angle=25):
"""图像旋转"""
return image.rotate(random.uniform(-angle, angle),
Image.BILINEAR, expand=1)
def generate(self, captcha_text='', fmt='PNG'):
"""生成验证码(文字和图片)
:param captcha_text: 验证码文字
:param fmt: 生成的验证码图片格式
:return: 验证码图片的二进制数据
"""
self._image = Image.new('RGB', (self._width, self._height), (255, 255, 255))
self._background()
self._text(captcha_text, self._fonts,
drawings=['_warp', '_rotate', '_offset'])
self._curve()
self._noise()
self._smooth()
image_bytes = BytesIO()
self._image.save(image_bytes, format=fmt)
return image_bytes.getvalue()
def pascal_row(n=0):
"""生成毕达哥拉斯三角形(杨辉三角)"""
result = [1]
x, numerator = 1, n
for denominator in range(1, n // 2 + 1):
x *= numerator
x /= denominator
result.append(x)
numerator -= 1
if n & 1 == 0:
result.extend(reversed(result[:-1]))
else:
result.extend(reversed(result))
return result
def random_color(start=0, end=255, opacity=255):
"""获得随机颜色"""
red = random.randint(start, end)
green = random.randint(start, end)
blue = random.randint(start, end)
if opacity is None:
return red, green, blue
return red, green, blue, opacity
```
> **说明**:上面的代码中用到了三个字体文件,字体文件位于`polls/fonts`目录下大家可以自行添加字体文件但是需要注意字体文件的文件名跟上面代码的第45行保持一致。
接下来,我们先完成提供验证码的视图函数。
```Python
def get_captcha(request: HttpRequest) -> HttpResponse:
"""验证码"""
captcha_text = gen_random_code()
request.session['captcha'] = captcha_text
image_data = Captcha.instance().generate(captcha_text)
return HttpResponse(image_data, content_type='image/png')
```
注意上面代码中的第4行我们将随机生成的验证码字符串保存到session中稍后用户登录时我们要将保存在session中的验证码字符串和用户输入的验证码字符串进行比对如果用户输入了正确的验证码才能够执行后续的登录流程代码如下所示。
```Python
def login(request: HttpRequest) -> HttpResponse:
hint = ''
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
if username and password:
password = gen_md5_digest(password)
user = User.objects.filter(username=username, password=password).first()
if user:
request.session['userid'] = user.no
request.session['username'] = user.username
return redirect('/')
else:
hint = '用户名或密码错误'
else:
hint = '请输入有效的用户名和密码'
return render(request, 'login.html', {'hint': hint})
```
>**说明**:上面的代码没有对用户名和密码没有进行验证,实际项目中建议使用正则表达式验证用户输入信息,否则有可能将无效的数据交给数据库进行处理或者造成其他安全方面的隐患。
上面的代码中我们设定了登录成功后会在session中保存用户的编号`userid`)和用户名(`username`页面会重定向到首页。接下来我们可以稍微对首页的代码进行调整在页面的右上角显示出登录用户的用户名。我们将这段代码单独写成了一个名为header.html的HTML文件首页中可以通过在`<body>`标签中添加`{% include 'header.html' %}`来包含这个页面,代码如下所示。
```HTML
<div class="user">
{% if request.session.userid %}
<span>{{ request.session.username }}</span>
<a href="/logout">注销</a>
{% else %}
<a href="/login">登录</a>&nbsp;&nbsp;
{% endif %}
<a href="/register">注册</a>
</div>
```
如果用户没有登录页面会显示登录和注册的超链接而用户登录成功后页面上会显示用户名和注销的链接注销链接对应的视图函数如下所示URL的映射与之前讲过的类似不再赘述。
```Python
def logout(request):
"""注销"""
request.session.flush()
return redirect('/')
```
上面的代码通过session对象`flush`方法来销毁session一方面清除了服务器上session对象保存的用户数据一方面将保存在浏览器cookie中的sessionid删除掉稍后我们会对如何读写cookie的操作加以说明。
我们可以通过项目使用的数据库中名为`django_session` 的表来找到所有的session该表的结构如下所示
| session_key | session_data | expire_date |
| -------------------------------- | ------------------------------- | -------------------------- |
| c9g2gt5cxo0k2evykgpejhic5ae7bfpl | MmI4YzViYjJhOGMyMDJkY2M5Yzg3... | 2019-05-25 23:16:13.898522 |
其中第1列就是浏览器cookie中保存的sessionid第2列是经过BASE64编码后的session中的数据如果使用Python的`base64`对其进行解码,解码的过程和结果如下所示。
```Python
import base64
base64.b64decode('MmI4YzViYjJhOGMyMDJkY2M5Yzg3ZWIyZGViZmUzYmYxNzdlNDdmZjp7ImNhcHRjaGEiOiJzS3d0Iiwibm8iOjEsInVzZXJuYW1lIjoiamFja2ZydWVkIn0=')
```
第3列是session的过期时间session过期后浏览器保存的cookie中的sessionid就会失效但是数据库中的这条对应的记录仍然会存在如果想清除过期的数据可以使用下面的命令。
```Shell
python manage.py clearsessions
```
Django框架默认的session过期时间为两周1209600秒如果想修改这个时间可以在项目的配置文件中添加如下所示的代码。
```Python
# 配置会话的超时时间为1天86400秒
SESSION_COOKIE_AGE = 86400
```
有很多对安全性要求较高的应用都必须在关闭浏览器窗口时让会话过期不再保留用户的任何信息如果希望在关闭浏览器窗口时就让会话过期cookie中的sessionid失效可以加入如下所示的配置。
```Python
# 设置为True在关闭浏览器窗口时session就过期
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
```
如果不希望将session的数据保存在数据库中可以将其放入缓存中对应的配置如下所示缓存的配置和使用我们在后面讲解。
```Python
# 配置将会话对象放到缓存中存储
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
# 配置使用哪一组缓存来保存会话
SESSION_CACHE_ALIAS = 'default'
```
如果要修改session数据默认的序列化方式可以将默认的`JSONSerializer`修改为`PickleSerializer`。
```Python
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
```
接下来,我们就可以限制只有登录用户才能为老师投票,修改后的`praise_or_criticize`函数如下所示,我们通过从`request.session`中获取`userid`来判定用户是否登录。
```Python
def praise_or_criticize(request: HttpRequest) -> HttpResponse:
if request.session.get('userid'):
try:
tno = int(request.GET.get('tno'))
teacher = Teacher.objects.get(no=tno)
if request.path.startswith('/praise/'):
teacher.good_count += 1
count = teacher.good_count
else:
teacher.bad_count += 1
count = teacher.bad_count
teacher.save()
data = {'code': 20000, 'mesg': '投票成功', 'count': count}
except (ValueError, Teacher.DoesNotExist):
data = {'code': 20001, 'mesg': '投票失败'}
else:
data = {'code': 20002, 'mesg': '请先登录'}
return JsonResponse(data)
```
当然,在修改了视图函数后,`teachers.html`也需要进行调整,用户如果没有登录,就将用户引导至登录页,登录成功再返回到投票页,此处不再赘述。
### 在视图函数中读写cookie
下面我们对如何使用cookie做一个更为细致的说明以便帮助大家在Web项目中更好的使用这项技术。Django封装的`HttpRequest`和`HttpResponse`对象分别提供了读写cookie的操作。
HttpRequest封装的属性和方法
1. `COOKIES`属性 - 该属性包含了HTTP请求携带的所有cookie。
2. `get_signed_cookie`方法 - 获取带签名的cookie如果签名验证失败会产生`BadSignature`异常。
HttpResponse封装的方法
1. `set_cookie`方法 - 该方法可以设置一组键值对并将其最终将写入浏览器。
2. `set_signed_cookie`方法 - 跟上面的方法作用相似但是会对cookie进行签名来达到防篡改的作用。因为如果篡改了cookie中的数据在不知道[密钥](<https://zh.wikipedia.org/wiki/%E5%AF%86%E9%92%A5>)和[盐](<https://zh.wikipedia.org/wiki/%E7%9B%90_(%E5%AF%86%E7%A0%81%E5%AD%A6)>)的情况下是无法生成有效的签名这样服务器在读取cookie时会发现数据与签名不一致从而产生`BadSignature`异常。需要说明的是这里所说的密钥就是我们在Django项目配置文件中指定的`SECRET_KEY`,而盐是程序中设定的一个字符串,你愿意设定为什么都可以,只要是一个有效的字符串。
上面提到的方法如果不清楚它们的具体用法可以自己查阅一下Django的[官方文档](<https://docs.djangoproject.com/en/2.1/ref/request-response/>),没有什么资料比官方文档能够更清楚的告诉你这些方法到底如何使用。
刚才我们说过了,激活`SessionMiddleware`之后,每个`HttpRequest`对象都会绑定一个session属性它是一个类似字典的对象除了保存用户数据之外还提供了检测浏览器是否支持cookie的方法包括
1. `set_test_cookie`方法 - 设置用于测试的cookie。
2. `test_cookie_worked`方法 - 检测测试cookie是否工作。
3. `delete_test_cookie`方法 - 删除用于测试的cookie。
4. `set_expiry`方法 - 设置会话的过期时间。
5. `get_expire_age`/`get_expire_date`方法 - 获取会话的过期时间。
6. `clear_expired`方法 - 清理过期的会话。
下面是在执行登录之前检查浏览器是否支持cookie的代码。通常情况下浏览器默认开启了对cookie的支持但是可能因为某种原因用户禁用了浏览器的cookie功能遇到这种情况我们可以在视图函数中提供一个检查功能如果检查到用户浏览器不支持cookie可以给出相应的提示。
```Python
def login(request):
if request.method == 'POST':
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
# Add your code to perform login process here
else:
return HttpResponse("Please enable cookies and try again.")
request.session.set_test_cookie()
return render_to_response('login.html')
```
### Cookie的替代品
之前我们说过了cookie的名声一直都不怎么好当然我们在实际开发中是不会在cookie中保存用户的敏感信息如用户的密码、信用卡的账号等而且保存在cookie中的数据一般也会做好编码和签名的工作。对于支持HTML5的浏览器来说可以使用localStorage和sessionStorage做为cookie的替代方案相信从名字上你就能听出二者的差别存储在`localStorage`的数据可以长期保留;而存储在`sessionStorage`的数据会在浏览器关闭时会被清除 。关于这些cookie替代品的用法建议大家查阅[MDN](<https://developer.mozilla.org/zh-CN/docs/Web>)来进行了解。

View File

@ -1,492 +0,0 @@
## 表单的应用
我们继续来完成上一章节中的项目实现“用户注册”和“用户登录”的功能并限制只有登录的用户才能为老师投票。Django框架中提供了对表单的封装而且提供了多种不同的使用方式。
首先添加用户模型。
```Python
class User(models.Model):
"""用户"""
no = models.AutoField(primary_key=True, verbose_name='编号')
username = models.CharField(max_length=20, unique=True, verbose_name='用户名')
password = models.CharField(max_length=32, verbose_name='密码')
regdate = models.DateTimeField(auto_now_add=True, verbose_name='注册时间')
class Meta:
db_table = 'tb_user'
verbose_name_plural = '用户'
```
通过生成迁移和执行迁移操作,在数据库中创建对应的用户表。
```Shell
(venv)$ python manage.py makemigrations vote
...
(venv)$ python manage.py migrate
...
```
定制一个非常简单的注册模板页面。
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
<style>/* 此处省略层叠样式表选择器 */</style>
</head>
<body>
<h1>用户注册</h1>
<hr>
<p class="hint">{{ hint }}</p>
<form action="/register/" method="post">
{% csrf_token %}
<div class="input">
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
</div>
<div class="input">
<label for="password">密码:</label>
<input type="password" id="password" name="password">
</div>
<div class="input">
<label for="repassword">确认密码:</label>
<input type="password" id="repassword" name="repassword">
</div>
<div class="input">
<input type="submit" value="注册">
<input type="reset" value="重置">
</div>
</form>
<a href="/login">返回登录</a>
</body>
</html>
```
注意,在上面的表单中,我们使用了模板指令`{% csrf_token %}`为表单添加一个隐藏域type属性值为hidden的input标签它的作用是在表单中生成一个随机令牌token来防范[跨站请求伪造](<https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0>)通常简称为CSRF这也是Django在提交表单时的硬性要求除非我们设置了免除CSRF令牌。下图是一个关于CSRF简单生动的例子它来自于[维基百科](<https://zh.wikipedia.org/wiki/Wikipedia:%E9%A6%96%E9%A1%B5>)。
![](./res/CSRF.png)
用户在提交注册表单时我们还需要对用户的输入进行验证例如我们的网站要求用户名必须由字母、数字、下划线构成且长度在4-20个字符之间密码的长度为8-20个字符确认密码必须跟密码保持一致。这些验证操作首先可以通过浏览器中的JavaScript代码来完成但是即便如此在服务器端仍然要对用户输入再次进行验证来避免将无效的数据库交给数据库因为用户可能会禁用浏览器的JavaScript功能也有可能绕过浏览器的输入检查将注册数据提交给服务器所以服务器端的用户输入检查仍然是必要的。
我们可以利用Django框架封装的表单功能来对用户输入的有效性进行检查虽然Django封装的表单还能帮助我们定制出页面上的表单元素但这显然是一种灵活性很差的设计这样的功能在实际开发中基本不考虑所以表单主要的作用就在于数据验证具体的做法如下所示。
```Python
USERNAME_PATTERN = re.compile(r'\w{4,20}')
class RegisterForm(forms.ModelForm):
repassword = forms.CharField(min_length=8, max_length=20)
def clean_username(self):
username = self.cleaned_data['username']
if not USERNAME_PATTERN.fullmatch(username):
raise ValidationError('用户名由字母、数字和下划线构成且长度为4-20个字符')
return username
def clean_password(self):
password = self.cleaned_data['password']
if len(password) < 8 or len(password) > 20:
raise ValidationError('无效的密码密码长度为8-20个字符')
return to_md5_hex(self.cleaned_data['password'])
def clean_repassword(self):
repassword = to_md5_hex(self.cleaned_data['repassword'])
if repassword != self.cleaned_data['password']:
raise ValidationError('密码和确认密码不一致')
return repassword
class Meta:
model = User
exclude = ('no', 'regdate')
```
上面我们定义了一个与User模型绑定的表单继承自ModelForm我们排除了用户编号no和注册日期regdate这两个属性并添加了一个repassword属性用来接收从用户表单传给服务器的确认密码。我们在定义User模型时已经对用户名的最大长度进行了限制上面我们又对确认密码的最小和最大长度进行了限制但是这些都不足以完成我们对用户输入的验证。上面以`clean_`打头的方法就是我们自定义的验证规则。很明显,`clean_username`是对用户名的检查,而`clean_password`是对密码的检查。由于数据库二维表中不应该保存密码的原文所以对密码做了一个简单的MD5摘要处理实际开发中如果只做出这样的处理还不太够因为即便使用了摘要仍然有利用彩虹表反向查询破解用户密码的风险如何做得更好我们会在后续的内容中讲到。为字符串生成MD5摘要的代码如下所示。
```Python
def to_md5_hex(message):
return hashlib.md5(message.encode()).hexdigest()
```
新增一个视图函数实现用户注册的功能。
```Python
def register(request):
page, hint = 'register.html', ''
if request.method == 'POST':
form = RegisterForm(request.POST)
if form.is_valid():
form.save()
page = 'login.html'
hint = '注册成功,请登录'
else:
hint = '请输入有效的注册信息'
return render(request, page, {'hint': hint})
```
如果用户发起GET请求将直接跳转到注册的页面如果用户以POST方式提交注册表单则创建自定义的注册表单对象并获取用户输入。可以通过表单对象的`is_valid`方法对表单进行验证如果用户输入没有问题该方法返回True否则返回False由于我们定义的RegisterForm继承自ModelForm因此也可以直接使用表单对象的`save`方法来保存模型。下面是注册请求的URL配置。
```Python
from django.contrib import admin
from django.urls import path
from vote import views
urlpatterns = [
# 此处省略上面的代码
path('register/', views.register, name='register'),
# 此处省略下面的代码
]
```
> 说明:`path`函数可以通过name参数给URL绑定一个逆向解析的名字也就是说如果需要可以从后面给的名字逆向解析出对应的URL。
我们再来定制一个非常简单的登录页。
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
<style>/* 此处省略层叠样式表选择器 */</style>
</head>
<body>
<h1>用户登录</h1>
<hr>
<p class="hint">{{ hint }}</p>
<form action="/login/" method="post">
{% csrf_token %}
<div class="input">
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
</div>
<div class="input">
<label for="password">密码:</label>
<input type="password" id="password" name="password">
</div>
<div class="input captcha">
<label for="captcha">验证码:</label>
<input type="text" id="captcha" name="captcha">
<img src="/captcha/" width="120">
</div>
<div class="input">
<input type="submit" value="登录">
<input type="reset" value="重置">
</div>
</form>
<a href="/register">注册新用户</a>
</body>
</html>
```
上面的登录页中,我们要求用户提供验证码,验证码全称是**全自动区分计算机和人类的公开图灵测试**,它是一种用来区分系统的使用者是计算机还是人类的程序。简单的说就是程序出一个只有人类能够回答的问题,由系统使用者来解答,由于计算机理论上无法解答程序提出的问题,所以回答出问题的用户就可以被认为是人类。大多数的网站都使用了不同类型的验证码技术来防范用程序自动注册用户或模拟用户登录(暴力破解用户密码),因为验证码具有一次消费性,而没有通过图灵测试的程序是不能够完成注册或登录的。
在Python程序中生成验证码并不算特别复杂但需要三方库Pillow的支持PIL的分支因为要对验证码图片进行旋转、扭曲、拉伸以及加入干扰信息来防范那些用OCR光学文字识别破解验证码的程序。下面的代码封装了生成验证码图片的功能大家可以直接用这些代码来生成图片验证码不要“重复发明轮子”。
```Python
"""
图片验证码
"""
import os
import random
from io import BytesIO
from PIL import Image
from PIL import ImageFilter
from PIL.ImageDraw import Draw
from PIL.ImageFont import truetype
class Bezier(object):
"""贝塞尔曲线"""
def __init__(self):
self.tsequence = tuple([t / 20.0 for t in range(21)])
self.beziers = {}
def make_bezier(self, n):
"""绘制贝塞尔曲线"""
try:
return self.beziers[n]
except KeyError:
combinations = pascal_row(n - 1)
result = []
for t in self.tsequence:
tpowers = (t ** i for i in range(n))
upowers = ((1 - t) ** i for i in range(n - 1, -1, -1))
coefs = [c * a * b for c, a, b in zip(combinations,
tpowers, upowers)]
result.append(coefs)
self.beziers[n] = result
return result
class Captcha(object):
"""验证码"""
def __init__(self, width, height, fonts=None, color=None):
self._image = None
self._fonts = fonts if fonts else \
[os.path.join(os.path.dirname(__file__), 'fonts', font)
for font in ['ArialRB.ttf', 'ArialNI.ttf', 'Georgia.ttf', 'Kongxin.ttf']]
self._color = color if color else random_color(0, 200, random.randint(220, 255))
self._width, self._height = width, height
@classmethod
def instance(cls, width=200, height=75):
prop_name = f'_instance_{width}_{height}'
if not hasattr(cls, prop_name):
setattr(cls, prop_name, cls(width, height))
return getattr(cls, prop_name)
def background(self):
"""绘制背景"""
Draw(self._image).rectangle([(0, 0), self._image.size],
fill=random_color(230, 255))
def smooth(self):
"""平滑图像"""
return self._image.filter(ImageFilter.SMOOTH)
def curve(self, width=4, number=6, color=None):
"""绘制曲线"""
dx, height = self._image.size
dx /= number
path = [(dx * i, random.randint(0, height))
for i in range(1, number)]
bcoefs = Bezier().make_bezier(number - 1)
points = []
for coefs in bcoefs:
points.append(tuple(sum([coef * p for coef, p in zip(coefs, ps)])
for ps in zip(*path)))
Draw(self._image).line(points, fill=color if color else self._color, width=width)
def noise(self, number=50, level=2, color=None):
"""绘制扰码"""
width, height = self._image.size
dx, dy = width / 10, height / 10
width, height = width - dx, height - dy
draw = Draw(self._image)
for i in range(number):
x = int(random.uniform(dx, width))
y = int(random.uniform(dy, height))
draw.line(((x, y), (x + level, y)),
fill=color if color else self._color, width=level)
def text(self, captcha_text, fonts, font_sizes=None, drawings=None, squeeze_factor=0.75, color=None):
"""绘制文本"""
color = color if color else self._color
fonts = tuple([truetype(name, size)
for name in fonts
for size in font_sizes or (65, 70, 75)])
draw = Draw(self._image)
char_images = []
for c in captcha_text:
font = random.choice(fonts)
c_width, c_height = draw.textsize(c, font=font)
char_image = Image.new('RGB', (c_width, c_height), (0, 0, 0))
char_draw = Draw(char_image)
char_draw.text((0, 0), c, font=font, fill=color)
char_image = char_image.crop(char_image.getbbox())
for drawing in drawings:
d = getattr(self, drawing)
char_image = d(char_image)
char_images.append(char_image)
width, height = self._image.size
offset = int((width - sum(int(i.size[0] * squeeze_factor)
for i in char_images[:-1]) -
char_images[-1].size[0]) / 2)
for char_image in char_images:
c_width, c_height = char_image.size
mask = char_image.convert('L').point(lambda i: i * 1.97)
self._image.paste(char_image,
(offset, int((height - c_height) / 2)),
mask)
offset += int(c_width * squeeze_factor)
@staticmethod
def warp(image, dx_factor=0.3, dy_factor=0.3):
"""图像扭曲"""
width, height = image.size
dx = width * dx_factor
dy = height * dy_factor
x1 = int(random.uniform(-dx, dx))
y1 = int(random.uniform(-dy, dy))
x2 = int(random.uniform(-dx, dx))
y2 = int(random.uniform(-dy, dy))
warp_image = Image.new(
'RGB',
(width + abs(x1) + abs(x2), height + abs(y1) + abs(y2)))
warp_image.paste(image, (abs(x1), abs(y1)))
width2, height2 = warp_image.size
return warp_image.transform(
(width, height),
Image.QUAD,
(x1, y1, -x1, height2 - y2, width2 + x2, height2 + y2, width2 - x2, -y1))
@staticmethod
def offset(image, dx_factor=0.1, dy_factor=0.2):
"""图像偏移"""
width, height = image.size
dx = int(random.random() * width * dx_factor)
dy = int(random.random() * height * dy_factor)
offset_image = Image.new('RGB', (width + dx, height + dy))
offset_image.paste(image, (dx, dy))
return offset_image
@staticmethod
def rotate(image, angle=25):
"""图像旋转"""
return image.rotate(random.uniform(-angle, angle),
Image.BILINEAR, expand=1)
def generate(self, captcha_text='', fmt='PNG'):
"""生成验证码(文字和图片)"""
self._image = Image.new('RGB', (self._width, self._height), (255, 255, 255))
self.background()
self.text(captcha_text, self._fonts,
drawings=['warp', 'rotate', 'offset'])
self.curve()
self.noise()
self.smooth()
image_bytes = BytesIO()
self._image.save(image_bytes, format=fmt)
return image_bytes.getvalue()
def pascal_row(n=0):
"""生成Pascal三角第n行"""
result = [1]
x, numerator = 1, n
for denominator in range(1, n // 2 + 1):
x *= numerator
x /= denominator
result.append(x)
numerator -= 1
if n & 1 == 0:
result.extend(reversed(result[:-1]))
else:
result.extend(reversed(result))
return result
def random_color(start=0, end=255, opacity=255):
"""获得随机颜色"""
red = random.randint(start, end)
green = random.randint(start, end)
blue = random.randint(start, end)
if opacity is None:
return red, green, blue
return red, green, blue, opacity
```
> 说明上面的代码在生成验证码图片时用到了三种字体文件使用上面的代码时需要添加字体文件到应用目录下的fonts目录中。
下面的视图函数用来生成验证码并通过HttpResponse对象输出到用户浏览器中。
```Python
ALL_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
def get_captcha_text(length=4):
selected_chars = random.choices(ALL_CHARS, k=length)
return ''.join(selected_chars)
def get_captcha(request):
"""获得验证码"""
captcha_text = get_captcha_text()
image = Captcha.instance().generate(captcha_text)
return HttpResponse(image, content_type='image/png')
```
生成的验证码如下图所示。
![](./res/captcha.png)
为了验证用户提交的登录表单,我们再定义个表单类。
```Python
class LoginForm(forms.Form):
username = forms.CharField(min_length=4, max_length=20)
password = forms.CharField(min_length=8, max_length=20)
captcha = forms.CharField(min_length=4, max_length=4)
def clean_username(self):
username = self.cleaned_data['username']
if not USERNAME_PATTERN.fullmatch(username):
raise ValidationError('无效的用户名')
return username
def clean_password(self):
return to_md5_hex(self.cleaned_data['password'])
```
跟之前我们定义的注册表单类略有区别登录表单类直接继承自Form没有跟模型绑定定义了三个字段分别对应登录表单中的用户名、密码和验证码。接下来是处理用户登录的视图函数。
```Python
def login(request):
hint = ''
if request.method == 'POST':
form = LoginForm(request.POST)
if form.is_valid():
username = form.cleaned_data['username']
password = form.cleaned_data['password']
user = User.objects.filter(username=username, password=password).first()
if user:
return redirect('/')
else:
hint = '用户名或密码错误'
else:
hint = '请输入有效的登录信息'
return render(request, 'login.html', {'hint': hint})
```
映射URL。
```Python
from django.contrib import admin
from django.urls import path
from vote import views
urlpatterns = [
# 此处省略上面的代码
path('login/', views.login, name='login'),
# 此处省略下面的代码
]
```
需要指出上面我们设定用户登录成功时直接返回首页而且在用户登录时并没有验证用户输入的验证码是否正确这些我们留到下一个单元再为大家讲解。另外如果要在Django自带的管理后台中进行表单验证可以在admin.py的模型管理类中指定`form`属性为自定义的表单即可,例如:
```Python
class UserForm(forms.ModelForm):
password = forms.CharField(min_length=8, max_length=20,
widget=forms.PasswordInput, label='密码')
def clean_username(self):
username = self.cleaned_data['username']
if not USERNAME_PATTERN.fullmatch(username):
raise ValidationError('用户名由字母、数字和下划线构成且长度为4-20个字符')
return username
def clean_password(self):
password = self.cleaned_data['password']
return to_md5_hex(self.cleaned_data['password'])
class Meta:
model = User
exclude = ('no', )
class UserAdmin(admin.ModelAdmin):
list_display = ('no', 'username', 'password', 'email', 'tel')
ordering = ('no', )
form = UserForm
list_per_page = 10
admin.site.register(User, UserAdmin)
```

View File

@ -1,185 +0,0 @@
## Cookie和Session
### 实现用户跟踪
如今,一个网站如果不通过某种方式记住你是谁以及你之前在网站的活动情况,失去的就是网站的可用性和便利性,继而很有可能导致网站用户的流式,所以记住一个用户(更专业的说法叫**用户跟踪**对绝大多数Web应用来说都是必需的功能。
在服务器端我们想记住一个用户最简单的办法就是创建一个对象通过这个对象就可以把用户相关的信息都保存起来这个对象就是我们常说的session用户会话对象。那么问题来了HTTP本身是一个无连接每次请求和响应的过程中服务器一旦完成对客户端请求的响应之后就断开连接、无状态客户端再次发起对服务器的请求时服务器无法得知这个客户端之前的任何信息的协议即便服务器通过session对象保留了用户数据还得通过某种方式来确定当前的请求与之前保存过的哪一个session是有关联的。相信很多人都能想到我们可以给每个session对象分配一个全局唯一的标识符来识别session对象我们姑且称之为sessionid每次客户端发起请求时只要携带上这个sessionid就有办法找到与之对应的session对象从而实现在两次请求之间记住该用户的信息也就是我们之前说的用户跟踪。
要让客户端记住并在每次请求时带上sessionid又有以下几种做法
1. URL重写。所谓URL重写就是在URL中携带sessionid例如`http://www.example.com/index.html?sessionid=123456`服务器通过获取sessionid参数的值来取到与之对应的session对象。
2. 隐藏域(隐式表单域)。在提交表单的时候,可以通过在表单中设置隐藏域向服务器发送额外的数据。例如:`<input type="hidden" name="sessionid" value="123456">`。
3. 本地存储。现在的浏览器都支持多种本地存储方案包括cookie、localStorage、sessionStorage、IndexedDB等。在这些方案中cookie是历史最为悠久也是被诟病得最多的一种方案也是我们接下来首先为大家讲解的一种方案。简单的说cookie是一种以键值对方式保存在浏览器临时文件中的数据每次请求时请求头中会携带本站点的cookie到服务器那么只要将sessionid写入cookie下次请求时服务器只要读取请求头中的cookie就能够获得这个sessionid如下图所示。
![](./res/sessionid_from_cookie.png)
在HTML5时代要除了cookie还可以使用新的本地存储API来保存数据就是刚才提到的localStorage、sessionStorage、IndexedDB等技术如下图所示。
![](./res/cookie_xstorage_indexeddb.png)
### Django框架对session的支持
在创建Django项目时默认的配置文件`settings.py`文件中已经激活了一个名为`SessionMiddleware`的中间件(关于中间件的知识我们在下一个章节做详细的讲解,这里只需要知道它的存在即可),因为这个中间件的存在,我们可以直接通过请求对象的`session`属性来操作会话对象。`session`属性是一个像字典一样可以读写数据的容器对象,因此我们可以使用“键值对”的方式来保留用户数据。与此同时,`SessionMiddleware`中间件还封装了对cookie的操作在cookie中保存了sessionid就如同我们之前描述的那样。
在默认情况下Django将session的数据序列化后保存在关系型数据库中在Django 1.6以后的版本中默认的序列化数据的方式是JSON序列化而在此之前一直使用Pickle序列化。JSON序列化和Pickle序列化的差别在于前者将对象序列化为字符串字符形式而后者将对象序列化为字节串二进制形式因为安全方面的原因JSON序列化成为了目前Django框架默认序列化数据的方式这就要求在我们保存在session中的数据必须是能够JSON序列化的否则就会引发异常。还有一点需要说明的是使用关系型数据库保存session中的数据在大多数时候并不是最好的选择因为数据库可能会承受巨大的压力而成为系统性能的瓶颈在后面的章节中我们会告诉大家如何将session的数据保存到缓存服务中。
我们继续完善之前的投票应用,前一个章节中我们实现了用户的登录和注册,下面我们首先完善登录时对验证码的检查。
```Python
def get_captcha(request):
"""验证码"""
captcha_text = random_captcha_text()
request.session['captcha'] = captcha_text
image_data = Captcha.instance().generate(captcha_text)
return HttpResponse(image_data, content_type='image/png')
```
注意上面代码中的第4行我们将随机生成的验证码字符串保存到session中稍后用户登录时我们要将保存在session中的验证码字符串和用户输入的验证码字符串进行比对如果用户输入了正确的验证码才能够执行后续的登录流程代码如下所示。
```Python
def login(request: HttpRequest):
"""登录"""
hint = ''
if request.method == 'POST':
form = LoginForm(request.POST)
if form.is_valid():
# 对验证码的正确性进行验证
captcha_from_user = form.cleaned_data['captcha']
captcha_from_sess = request.session.get('captcha', '')
if captcha_from_sess.lower() != captcha_from_user.lower():
hint = '请输入正确的验证码'
else:
username = form.cleaned_data['username']
password = form.cleaned_data['password']
user = User.objects.filter(username=username, password=password).first()
if user:
# 登录成功后将用户编号和用户名保存在session中
request.session['userid'] = user.no
request.session['username'] = user.username
return redirect('/')
else:
hint = '用户名或密码错误'
else:
hint = '请输入有效的登录信息'
return render(request, 'login.html', {'hint': hint})
```
上面的代码中我们设定了登录成功后会在session中保存用户的编号`userid`)和用户名(`username`页面会重定向到首页。接下来我们可以稍微对首页的代码进行调整在页面的右上角显示出登录用户的用户名。我们将这段代码单独写成了一个名为header.html的HTML文件首页中可以通过在`<body>`标签中添加`{% include 'header.html' %}`来包含这个页面,代码如下所示。
```HTML
<div class="user">
{% if request.session.userid %}
<span>{{ request.session.username }}</span>
<a href="/logout">注销</a>
{% else %}
<a href="/login">登录</a>&nbsp;&nbsp;
{% endif %}
<a href="/register">注册</a>
</div>
```
如果用户没有登录页面会显示登录和注册的超链接而用户登录成功后页面上会显示用户名和注销的链接注销链接对应的视图函数如下所示URL的映射与之前讲过的类似不再赘述。
```Python
def logout(request):
"""注销"""
request.session.flush()
return redirect('/')
```
上面的代码通过session对象`flush`方法来销毁session一方面清除了服务器上session对象保存的用户数据一方面将保存在浏览器cookie中的sessionid删除掉稍后我们会对如何读写cookie的操作加以说明。
我们可以通过项目使用的数据库中名为`django_session` 的表来找到所有的session该表的结构如下所示
| session_key | session_data | expire_date |
| -------------------------------- | ------------------------------- | -------------------------- |
| c9g2gt5cxo0k2evykgpejhic5ae7bfpl | MmI4YzViYjJhOGMyMDJkY2M5Yzg3... | 2019-05-25 23:16:13.898522 |
其中第1列就是浏览器cookie中保存的sessionid第2列是经过BASE64编码后的session中的数据如果使用Python的`base64`对其进行解码,解码的过程和结果如下所示。
```Python
>>> import base64
>>> base64.b64decode('MmI4YzViYjJhOGMyMDJkY2M5Yzg3ZWIyZGViZmUzYmYxNzdlNDdmZjp7ImNhcHRjaGEiOiJzS3d0Iiwibm8iOjEsInVzZXJuYW1lIjoiamFja2ZydWVkIn0=')
'2b8c5bb2a8c202dcc9c87eb2debfe3bf177e47ff:{"captcha":"sKwt","no":1,"username":"jackfrued"}'
```
第3列是session的过期时间session过期后浏览器保存的cookie中的sessionid就会失效但是数据库中的这条对应的记录仍然会存在如果想清除过期的数据可以使用下面的命令。
```Shell
python manage.py clearsessions
```
Django框架默认的session过期时间为两周1209600秒如果想修改这个时间可以在项目的配置文件中添加如下所示的代码。
```Python
# 配置会话的超时时间为1天86400秒
SESSION_COOKIE_AGE = 86400
```
有很多对安全性要求较高的应用都必须在关闭浏览器窗口时让会话过期不再保留用户的任何信息如果希望在关闭浏览器窗口时就让会话过期cookie中的sessionid失效可以加入如下所示的配置。
```Python
# 设置为True在关闭浏览器窗口时session就过期
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
```
如果不希望将session的数据保存在数据库中可以将其放入缓存中对应的配置如下所示缓存的配置和使用我们在后面讲解。
```Python
# 配置将会话对象放到缓存中存储
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
# 配置使用哪一组缓存来保存会话
SESSION_CACHE_ALIAS = 'default'
```
如果要修改session数据默认的序列化方式可以将默认的`JSONSerializer`修改为`PickleSerializer`。
```Python
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
```
### 在视图函数中读写cookie
Django封装的`HttpRequest`和`HttpResponse`对象分别提供了读写cookie的操作。
HttpRequest封装的属性和方法
1. `COOKIES`属性 - 该属性包含了HTTP请求携带的所有cookie。
2. `get_signed_cookie`方法 - 获取带签名的cookie如果签名验证失败会产生`BadSignature`异常。
HttpResponse封装的方法
1. `set_cookie`方法 - 该方法可以设置一组键值对并将其最终将写入浏览器。
2. `set_signed_cookie`方法 - 跟上面的方法作用相似但是会对cookie进行签名来达到防篡改的作用。因为如果篡改了cookie中的数据在不知道[密钥](<https://zh.wikipedia.org/wiki/%E5%AF%86%E9%92%A5>)和[盐](<https://zh.wikipedia.org/wiki/%E7%9B%90_(%E5%AF%86%E7%A0%81%E5%AD%A6)>)的情况下是无法生成有效的签名这样服务器在读取cookie时会发现数据与签名不一致从而产生`BadSignature`异常。需要说明的是这里所说的密钥就是我们在Django项目配置文件中指定的`SECRET_KEY`,而盐是程序中设定的一个字符串,你愿意设定为什么都可以,只要是一个有效的字符串。
上面提到的方法如果不清楚它们的具体用法可以自己查阅一下Django的[官方文档](<https://docs.djangoproject.com/en/2.1/ref/request-response/>),没有什么资料比官方文档能够更清楚的告诉你这些方法到底如何使用。
刚才我们说过了,激活`SessionMiddleware`之后,每个`HttpRequest`对象都会绑定一个session属性它是一个类似字典的对象除了保存用户数据之外还提供了检测浏览器是否支持cookie的方法包括
1. `set_test_cookie`方法 - 设置用于测试的cookie。
2. `test_cookie_worked`方法 - 检测测试cookie是否工作。
3. `delete_test_cookie`方法 - 删除用于测试的cookie。
4. `set_expiry`方法 - 设置会话的过期时间。
5. `get_expire_age`/`get_expire_date`方法 - 获取会话的过期时间。
6. `clear_expired`方法 - 清理过期的会话。
下面是在执行登录之前检查浏览器是否支持cookie的代码。
```Python
def login(request):
if request.method == 'POST':
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
# Add your code to perform login process here
else:
return HttpResponse("Please enable cookies and try again.")
request.session.set_test_cookie()
return render_to_response('login.html')
```
### Cookie的替代品
之前我们说过了cookie的名声一直都不怎么好当然我们在实际开发中是不会在cookie中保存用户的敏感信息如用户的密码、信用卡的账号等而且保存在cookie中的数据一般也会做好编码和签名的工作。即便如此HTML5中还是给出了用于替代cookie的技术方案其中使用得最为广泛的就是localStorage和sessionStorage相信从名字上你就能听出二者的差别存储在`localStorage`的数据可以长期保留;而存储在`sessionStorage`的数据会在浏览器关闭时会被清除 。关于这些cookie替代品的用法建议大家查阅[MDN](<https://developer.mozilla.org/zh-CN/docs/Web>)来进行了解。

151
Day41-55/45.制作报表.md Normal file
View File

@ -0,0 +1,151 @@
## 制作报表
### 导出Excel报表
报表就是用表格、图表等格式来动态显示数据,所以有人用这样的公式来描述报表:
```
报表 = 多样的格式 + 动态的数据
```
有很多的三方库支持在Python程序中写Excel文件包括[xlwt](<https://xlwt.readthedocs.io/en/latest/>)、[xlwings](<https://docs.xlwings.org/en/latest/quickstart.html>)、[openpyxl](<https://openpyxl.readthedocs.io/en/latest/>)、[xlswriter](<https://xlsxwriter.readthedocs.io/>)、[pandas](<http://pandas.pydata.org/>)等其中的xlwt虽然只支持写xls格式的Excel文件但在性能方面的表现还是不错的。下面我们就以xlwt为例来演示如何在Django项目中导出Excel报表例如导出一个包含所有老师信息的Excel表格。
```Python
def export_teachers_excel(request):
# 创建工作簿
wb = xlwt.Workbook()
# 添加工作表
sheet = wb.add_sheet('老师信息表')
# 查询所有老师的信息
queryset = Teacher.objects.all()
# 向Excel表单中写入表头
colnames = ('姓名', '介绍', '好评数', '差评数', '学科')
for index, name in enumerate(colnames):
sheet.write(0, index, name)
# 向单元格中写入老师的数据
props = ('name', 'detail', 'good_count', 'bad_count', 'subject')
for row, teacher in enumerate(queryset):
for col, prop in enumerate(props):
value = getattr(teacher, prop, '')
if isinstance(value, Subject):
value = value.name
sheet.write(row + 1, col, value)
# 保存Excel
buffer = BytesIO()
wb.save(buffer)
# 将二进制数据写入响应的消息体中并设置MIME类型
resp = HttpResponse(buffer.getvalue(), content_type='application/vnd.ms-excel')
# 中文文件名需要处理成百分号编码
filename = quote('老师.xls')
# 通过响应头告知浏览器下载该文件以及对应的文件名
resp['content-disposition'] = f'attachment; filename*=utf-8''{filename}'
return resp
```
映射URL。
```Python
urlpatterns = [
# 此处省略上面的代码
path('excel/', views.export_teachers_excel),
# 此处省略下面的代码
]
```
### 导出PDF报表
在Django项目中如果需要导出PDF报表可以借助三方库reportlab来生成PDF文件的内容再将文件的二进制数据输出给浏览器并指定MIME类型为`application/pdf`,具体的代码如下所示。
```Python
def export_pdf(request: HttpRequest) -> HttpResponse:
buffer = io.BytesIO()
pdf = canvas.Canvas(buffer)
pdf.setFont("Helvetica", 80)
pdf.setFillColorRGB(0.2, 0.5, 0.3)
pdf.drawString(100, 550, 'hello, world!')
pdf.showPage()
pdf.save()
resp = HttpResponse(buffer.getvalue(), content_type='application/pdf')
resp['content-disposition'] = 'inline; filename="demo.pdf"'
return resp
```
关于如何用reportlab定制PDF报表的内容可以参考reportlab的[官方文档](https://www.reportlab.com/docs/reportlab-userguide.pdf)。
### 生成前端统计图表
如果项目中需要生成前端统计图表,可以使用百度的[ECharts](<https://echarts.baidu.com/>)。具体的做法是后端通过提供数据接口返回统计图表所需的数据前端使用ECharts来渲染出柱状图、折线图、饼图、散点图等图表。例如我们要生成一个统计所有老师好评数和差评数的报表可以按照下面的方式来做。
```Python
def get_teachers_data(request):
queryset = Teacher.objects.all()
names = [teacher.name for teacher in queryset]
good_counts = [teacher.good_count for teacher in queryset]
bad_counts = [teacher.bad_count for teacher in queryset]
return JsonResponse({'names': names, 'good': good_counts, 'bad': bad_counts})
```
映射URL。
```Python
urlpatterns = [
path('teachers_data/', views.export_teachers_excel),
]
```
使用ECharts生成柱状图。
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>老师评价统计</title>
</head>
<body>
<div id="main" style="width: 600px; height: 400px"></div>
<p>
<a href="/">返回首页</a>
</p>
<script src="https://cdn.bootcss.com/echarts/4.2.1-rc1/echarts.min.js"></script>
<script>
var myChart = echarts.init(document.querySelector('#main'))
fetch('/teachers_data/')
.then(resp => resp.json())
.then(json => {
var option = {
color: ['#f00', '#00f'],
title: {
text: '老师评价统计图'
},
tooltip: {},
legend: {
data:['好评', '差评']
},
xAxis: {
data: json.names
},
yAxis: {},
series: [
{
name: '好评',
type: 'bar',
data: json.good
},
{
name: '差评',
type: 'bar',
data: json.bad
}
]
}
myChart.setOption(option)
})
</script>
</body>
</html>
```
运行效果如下图所示。
![](./res/echarts_bar_graph.png)

View File

@ -1,351 +0,0 @@
## 报表和日志
### 导出Excel报表
报表就是用表格、图表等格式来动态显示数据,所以有人用这样的公式来描述报表:
```
报表 = 多样的格式 + 动态的数据
```
有很多的三方库支持在Python程序中写Excel文件包括[xlwt](<https://xlwt.readthedocs.io/en/latest/>)、[xlwings](<https://docs.xlwings.org/en/latest/quickstart.html>)、[openpyxl](<https://openpyxl.readthedocs.io/en/latest/>)、[xlswriter](<https://xlsxwriter.readthedocs.io/>)、[pandas](<http://pandas.pydata.org/>)等其中的xlwt虽然只支持写xls格式的Excel文件但在性能方面的表现还是不错的。下面我们就以xlwt为例来演示如何在Django项目中导出Excel报表例如导出一个包含所有老师信息的Excel表格。
```Python
def export_teachers_excel(request):
# 创建工作簿
wb = xlwt.Workbook()
# 添加工作表
sheet = wb.add_sheet('老师信息表')
# 查询所有老师的信息(注意:这个地方稍后需要优化)
queryset = Teacher.objects.all()
# 向Excel表单中写入表头
colnames = ('姓名', '介绍', '好评数', '差评数', '学科')
for index, name in enumerate(colnames):
sheet.write(0, index, name)
# 向单元格中写入老师的数据
props = ('name', 'detail', 'good_count', 'bad_count', 'subject')
for row, teacher in enumerate(queryset):
for col, prop in enumerate(props):
value = getattr(teacher, prop, '')
if isinstance(value, Subject):
value = value.name
sheet.write(row + 1, col, value)
# 保存Excel
buffer = BytesIO()
wb.save(buffer)
# 将二进制数据写入响应的消息体中并设置MIME类型
resp = HttpResponse(buffer.getvalue(), content_type='application/vnd.ms-excel')
# 中文文件名需要处理成百分号编码
filename = quote('老师.xls')
# 通过响应头告知浏览器下载该文件以及对应的文件名
resp['content-disposition'] = f'attachment; filename="{filename}"'
return resp
```
映射URL。
```Python
urlpatterns = [
# 此处省略上面的代码
path('excel/', views.export_teachers_excel),
# 此处省略下面的代码
]
```
### 生成前端统计图表
如果项目中需要生成前端统计图表,可以使用百度的[ECharts](<https://echarts.baidu.com/>)。具体的做法是后端通过提供数据接口返回统计图表所需的数据前端使用ECharts来渲染出柱状图、折线图、饼图、散点图等图表。例如我们要生成一个统计所有老师好评数和差评数的报表可以按照下面的方式来做。
```Python
def get_teachers_data(request):
# 查询所有老师的信息(注意:这个地方稍后也需要优化)
queryset = Teacher.objects.all()
# 用生成式将老师的名字放在一个列表中
names = [teacher.name for teacher in queryset]
# 用生成式将老师的好评数放在一个列表中
good = [teacher.good_count for teacher in queryset]
# 用生成式将老师的差评数放在一个列表中
bad = [teacher.bad_count for teacher in queryset]
# 返回JSON格式的数据
return JsonResponse({'names': names, 'good': good, 'bad': bad})
```
映射URL。
```Python
urlpatterns = [
# 此处省略上面的代码
path('teachers_data/', views.export_teachers_excel),
# 此处省略下面的代码
]
```
使用ECharts生成柱状图。
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>老师评价统计</title>
</head>
<body>
<div id="main" style="width: 600px; height: 400px"></div>
<p>
<a href="/">返回首页</a>
</p>
<script src="https://cdn.bootcss.com/echarts/4.2.1-rc1/echarts.min.js"></script>
<script>
var myChart = echarts.init(document.querySelector('#main'))
fetch('/teachers_data/')
.then(resp => resp.json())
.then(json => {
var option = {
color: ['#f00', '#00f'],
title: {
text: '老师评价统计图'
},
tooltip: {},
legend: {
data:['好评', '差评']
},
xAxis: {
data: json.names
},
yAxis: {},
series: [
{
name: '好评',
type: 'bar',
data: json.good
},
{
name: '差评',
type: 'bar',
data: json.bad
}
]
}
myChart.setOption(option)
})
</script>
</body>
</html>
```
运行效果如下图所示。
![](./res/echarts_bar_graph.png)
### 配置日志
项目开发阶段显示足够的调试信息以辅助开发人员调试代码还是非常必要的项目上线以后将系统运行时出现的警告、错误等信息记录下来以备相关人员了解系统运行状况并维护代码也是很有必要的。要做好这两件事件我们需要为Django项目配置日志。
Django的日志配置基本可以参照官方文档再结合项目实际需求来进行这些内容基本上可以从官方文档上复制下来然后进行局部的调整即可下面给出一些参考配置。
```Python
LOGGING = {
'version': 1,
# 是否禁用已经存在的日志器
'disable_existing_loggers': False,
# 日志格式化器
'formatters': {
'simple': {
'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
},
'verbose': {
'format': '%(asctime)s %(levelname)s [%(process)d-%(threadName)s] '
'%(module)s.%(funcName)s line %(lineno)d: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
}
},
# 日志过滤器
'filters': {
# 只有在Django配置文件中DEBUG值为True时才起作用
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
# 日志处理器
'handlers': {
# 输出到控制台
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'filters': ['require_debug_true'],
'formatter': 'simple',
},
# 输出到文件(每周切割一次)
'file1': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'access.log',
'when': 'W0',
'backupCount': 12,
'formatter': 'simple',
'level': 'INFO',
},
# 输出到文件(每天切割一次)
'file2': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'error.log',
'when': 'D',
'backupCount': 31,
'formatter': 'verbose',
'level': 'WARNING',
},
},
# 日志器记录器
'loggers': {
'django': {
# 需要使用的日志处理器
'handlers': ['console', 'file1', 'file2'],
# 是否向上传播日志信息
'propagate': True,
# 日志级别(不一定是最终的日志级别)
'level': 'DEBUG',
},
}
}
```
大家可能已经注意到了上面日志配置中的formatters是**日志格式化器**,它代表了如何格式化输出日志,其中格式占位符分别表示:
1. %(name)s - 记录器的名称
2. %(levelno)s - 数字形式的日志记录级别
3. %(levelname)s - 日志记录级别的文本名称
4. %(filename)s - 执行日志记录调用的源文件的文件名称
5. %(pathname)s - 执行日志记录调用的源文件的路径名称
6. %(funcName)s - 执行日志记录调用的函数名称
7. %(module)s - 执行日志记录调用的模块名称
8. %(lineno)s - 执行日志记录调用的行号
9. %(created)s - 执行日志记录的时间
10. %(asctime)s - 日期和时间
11. %(msecs)s - 毫秒部分
12. %(thread)d - 线程ID整数
13. %(threadName)s - 线程名称
14. %(process)d - 进程ID (整数)
日志配置中的handlers用来指定**日志处理器**,简单的说就是指定将日志输出到控制台还是文件又或者是网络上的服务器,可用的处理器包括:
1. logging.StreamHandler(stream=None) - 可以向类似与sys.stdout或者sys.stderr的任何文件对象输出信息
2. logging.FileHandler(filename, mode='a', encoding=None, delay=False) - 将日志消息写入文件
3. logging.handlers.DatagramHandler(host, port) - 使用UDP协议将日志信息发送到指定主机和端口的网络主机上
4. logging.handlers.HTTPHandler(host, url) - 使用HTTP的GET或POST方法将日志消息上传到一台HTTP 服务器
5. logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False) - 将日志消息写入文件如果文件的大小超出maxBytes指定的值那么将重新生成一个文件来记录日志
6. logging.handlers.SocketHandler(host, port) - 使用TCP协议将日志信息发送到指定主机和端口的网络主机上
7. logging.handlers.SMTPHandler(mailhost, fromaddr, toaddrs, subject, credentials=None, secure=None, timeout=1.0) - 将日志输出到指定的邮件地址
8. logging.MemoryHandler(capacity, flushLevel=ERROR, target=None, flushOnClose=True) - 将日志输出到内存指定的缓冲区中
上面每个日志处理器都指定了一个名为“level”的属性它代表了日志的级别不同的日志级别反映出日志中记录信息的严重性。Python中定义了六个级别的日志按照从低到高的顺序依次是NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL。
最后配置的**日志记录器**是用来真正输出日志的Django框架提供了如下所示的内置记录器
1. django - 在Django层次结构中的所有消息记录器
2. django.request - 与请求处理相关的日志消息。5xx响应被视为错误消息4xx响应被视为为警告消息
3. django.server - 与通过runserver调用的服务器所接收的请求相关的日志消息。5xx响应被视为错误消息4xx响应被记录为警告消息其他一切都被记录为INFO
4. django.template - 与模板渲染相关的日志消息
5. django.db.backends - 有与数据库交互产生的日志消息如果希望显示ORM框架执行的SQL语句就可以使用该日志记录器。
日志记录器中配置的日志级别有可能不是最终的日志级别,因为还要参考日志处理器中配置的日志级别,取二者中级别较高者作为最终的日志级别。
### 配置Django-Debug-Toolbar
Django-Debug-Toolbar是项目开发阶段辅助调试和优化的神器只要配置了它就可以很方便的查看到如下表所示的项目运行信息这些信息对调试项目和优化Web应用性能都是至关重要的。
| 项目 | 说明 |
| ----------- | --------------------------------- |
| Versions | Django的版本 |
| Time | 显示视图耗费的时间 |
| Settings | 配置文件中设置的值 |
| Headers | HTTP请求头和响应头的信息 |
| Request | 和请求相关的各种变量及其信息 |
| StaticFiles | 静态文件加载情况 |
| Templates | 模板的相关信息 |
| Cache | 缓存的使用情况 |
| Signals | Django内置的信号信息 |
| Logging | 被记录的日志信息 |
| SQL | 向数据库发送的SQL语句及其执行时间 |
1. 安装Django-Debug-Toolbar。
```Shell
pip install django-debug-toolbar
```
2. 配置 - 修改settings.py。
```Python
INSTALLED_APPS = [
'debug_toolbar',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
DEBUG_TOOLBAR_CONFIG = {
# 引入jQuery库
'JQUERY_URL': 'https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js',
# 工具栏是否折叠
'SHOW_COLLAPSED': True,
# 是否显示工具栏
'SHOW_TOOLBAR_CALLBACK': lambda x: True,
}
```
3. 配置 - 修改urls.py。
```Python
if settings.DEBUG:
import debug_toolbar
urlpatterns.insert(0, path('__debug__/', include(debug_toolbar.urls)))
```
4. 使用 - 如下图所示在配置好Django-Debug-Toolbar之后页面右侧会看到一个调试工具栏上面包括了如前所述的各种调试信息包括执行时间、项目设置、请求头、SQL、静态资源、模板、缓存、信号等查看起来非常的方便。
### 优化ORM代码
在配置了日志或Django-Debug-Toolbar之后我们可以查看一下之前将老师数据导出成Excel报表的视图函数执行情况这里我们关注的是ORM框架生成的SQL查询到底是什么样子的相信这里的结果会让你感到有一些意外。执行`Teacher.objects.all()`之后我们可以注意到在控制台看到的或者通过Django-Debug-Toolbar输出的SQL是下面这样的
```SQL
SELECT `tb_teacher`.`no`, `tb_teacher`.`name`, `tb_teacher`.`detail`, `tb_teacher`.`photo`, `tb_teacher`.`good_count`, `tb_teacher`.`bad_count`, `tb_teacher`.`sno` FROM `tb_teacher`; args=()
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,)
```
这里的问题通常被称为“1+N查询”或“N+1查询”原本获取老师的数据只需要一条SQL但是由于老师关联了学科当我们查询到N条老师的数据时Django的ORM框架又向数据库发出了N条SQL去查询老师所属学科的信息。每条SQL执行都会有较大的开销而且会给数据库服务器带来压力如果能够在一条SQL中完成老师和学科的查询肯定是更好的做法这一点也很容易做到相信大家已经想到怎么做了。是的我们可以使用连接查询但是在使用Django的ORM框架时如何做到这一点呢对于多对一关联如投票应用中的老师和学科我们可以使用`QuerySet`的用`select_related()`方法来加载关联对象;而对于多对多关联(如电商网站中的订单和商品),我们可以使用`prefetch_related()`方法来加载关联对象。
在导出老师Excel报表的视图函数中我们可以按照下面的方式优化代码。
```Python
queryset = Teacher.objects.all().select_related('subject')
```
事实上用ECharts生成前端报表的视图函数中查询老师好评和差评数据的操作也能够优化因为在这个例子中我们只需要获取老师的姓名、好评数和差评数这三项数据但是在默认的情况生成的SQL会查询老师表的所有字段。可以用`QuerySet`的`only()`方法来指定需要查询的属性,也可以用`QuerySet`的`defer()`方法来指定暂时不需要查询的属性这样生成的SQL会通过投影操作来指定需要查询的列从而改善查询性能代码如下所示
```Python
queryset = Teacher.objects.all().only('name', 'good_count', 'bad_count')
```
当然如果要统计出每个学科的老师好评和差评的平均数利用Django的ORM框架也能够做到代码如下所示
```Python
queryset = Teacher.objects.values('subject').annotate(
good=Avg('good_count'), bad=Avg('bad_count'))
```
这里获得的`QuerySet`中的元素是字典对象,每个字典中有三组键值对,分别是代表学科编号的`subject`、代表好评数的`good`和代表差评数的`bad`。如果想要获得学科的名称而不是编号,可以按照如下所示的方式调整代码:
```Python
queryset = Teacher.objects.values('subject__name').annotate(
good=Avg('good_count'), bad=Avg('bad_count'))
```
可见Django的ORM框架允许我们用面向对象的方式完成关系数据库中的分组和聚合查询。

View File

@ -0,0 +1,217 @@
## 日志和调试工具栏
### 配置日志
项目开发阶段,显示足够的调试信息以辅助开发人员调试代码还是非常必要的;项目上线以后,将系统运行时出现的警告、错误等信息记录下来以备相关人员了解系统运行状况并维护代码也是很有必要的。与此同时,采集日志数据也是为网站做数字化运营奠定一个基础,通过对系统运行日志的分析,我们可以监测网站的流量以及流量分布,同时还可以挖掘出用户的使用习惯和行为模式。
接下来我们先看看如何通过Django的配置文件来配置日志。Django的日志配置基本可以参照官方文档再结合项目实际需求来进行这些内容基本上可以从官方文档上复制下来然后进行局部的调整即可下面给出一些参考配置。
```Python
LOGGING = {
'version': 1,
# 是否禁用已经存在的日志器
'disable_existing_loggers': False,
# 日志格式化器
'formatters': {
'simple': {
'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
},
'verbose': {
'format': '%(asctime)s %(levelname)s [%(process)d-%(threadName)s] '
'%(module)s.%(funcName)s line %(lineno)d: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
}
},
# 日志过滤器
'filters': {
# 只有在Django配置文件中DEBUG值为True时才起作用
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
# 日志处理器
'handlers': {
# 输出到控制台
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'filters': ['require_debug_true'],
'formatter': 'simple',
},
# 输出到文件(每周切割一次)
'file1': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'access.log',
'when': 'W0',
'backupCount': 12,
'formatter': 'simple',
'level': 'INFO',
},
# 输出到文件(每天切割一次)
'file2': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'error.log',
'when': 'D',
'backupCount': 31,
'formatter': 'verbose',
'level': 'WARNING',
},
},
# 日志器记录器
'loggers': {
'django': {
# 需要使用的日志处理器
'handlers': ['console', 'file1', 'file2'],
# 是否向上传播日志信息
'propagate': True,
# 日志级别(不一定是最终的日志级别)
'level': 'DEBUG',
},
}
}
```
大家可能已经注意到了,上面日志配置中的`formatters`是**日志格式化器**,它代表了如何格式化输出日志,其中格式占位符分别表示:
1. `%(name)s` - 记录器的名称
2. `%(levelno)s` - 数字形式的日志记录级别
3. `%(levelname)s` - 日志记录级别的文本名称
4. `%(filename)s` - 执行日志记录调用的源文件的文件名称
5. `%(pathname)s` - 执行日志记录调用的源文件的路径名称
6. `%(funcName)s` - 执行日志记录调用的函数名称
7. `%(module)s` - 执行日志记录调用的模块名称
8. `%(lineno)s` - 执行日志记录调用的行号
9. `%(created)s` - 执行日志记录的时间
10. `%(asctime)s` - 日期和时间
11. `%(msecs)s` - 毫秒部分
12. `%(thread)d` - 线程ID整数
13. `%(threadName)s` - 线程名称
14. `%(process)d` - 进程ID (整数)
日志配置中的handlers用来指定**日志处理器**,简单的说就是指定将日志输出到控制台还是文件又或者是网络上的服务器,可用的处理器包括:
1. `logging.StreamHandler(stream=None)` - 可以向类似与`sys.stdout`或者`sys.stderr`的任何文件对象输出信息
2. `logging.FileHandler(filename, mode='a', encoding=None, delay=False)` - 将日志消息写入文件
3. `logging.handlers.DatagramHandler(host, port)` - 使用UDP协议将日志信息发送到指定主机和端口的网络主机上
4. `logging.handlers.HTTPHandler(host, url)` - 使用HTTP的GET或POST方法将日志消息上传到一台HTTP 服务器
5. `logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)` - 将日志消息写入文件,如果文件的大小超出`maxBytes`指定的值,那么将重新生成一个文件来记录日志
6. `logging.handlers.SocketHandler(host, port)` - 使用TCP协议将日志信息发送到指定主机和端口的网络主机上
7. `logging.handlers.SMTPHandler(mailhost, fromaddr, toaddrs, subject, credentials=None, secure=None, timeout=1.0)` - 将日志输出到指定的邮件地址
8. `logging.MemoryHandler(capacity, flushLevel=ERROR, target=None, flushOnClose=True)` - 将日志输出到内存指定的缓冲区中
上面每个日志处理器都指定了一个名为`level`的属性它代表了日志的级别不同的日志级别反映出日志中记录信息的严重性。Python中定义了六个级别的日志按照从低到高的顺序依次是NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL。
最后配置的**日志记录器**是用来真正输出日志的Django框架提供了如下所示的内置记录器
1. `django` - 在Django层次结构中的所有消息记录器
2. `django.request` - 与请求处理相关的日志消息。5xx响应被视为错误消息4xx响应被视为为警告消息
3. `django.server` - 与通过runserver调用的服务器所接收的请求相关的日志消息。5xx响应被视为错误消息4xx响应被记录为警告消息其他一切都被记录为INFO
4. `django.template` - 与模板渲染相关的日志消息
5. `django.db.backends` - 有与数据库交互产生的日志消息如果希望显示ORM框架执行的SQL语句就可以使用该日志记录器。
日志记录器中配置的日志级别有可能不是最终的日志级别,因为还要参考日志处理器中配置的日志级别,取二者中级别较高者作为最终的日志级别。
### 配置Django-Debug-Toolbar
如果想调试你的Django项目你一定不能不过名为Django-Debug-Toolbar的神器它是项目开发阶段辅助调试和优化的必备工具只要配置了它就可以很方便的查看到如下表所示的项目运行信息这些信息对调试项目和优化Web应用性能都是至关重要的。
| 项目 | 说明 |
| ----------- | --------------------------------- |
| Versions | Django的版本 |
| Time | 显示视图耗费的时间 |
| Settings | 配置文件中设置的值 |
| Headers | HTTP请求头和响应头的信息 |
| Request | 和请求相关的各种变量及其信息 |
| StaticFiles | 静态文件加载情况 |
| Templates | 模板的相关信息 |
| Cache | 缓存的使用情况 |
| Signals | Django内置的信号信息 |
| Logging | 被记录的日志信息 |
| SQL | 向数据库发送的SQL语句及其执行时间 |
1. 安装Django-Debug-Toolbar。
```Shell
pip install django-debug-toolbar
```
2. 配置 - 修改settings.py。
```Python
INSTALLED_APPS = [
'debug_toolbar',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
DEBUG_TOOLBAR_CONFIG = {
# 引入jQuery库
'JQUERY_URL': 'https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js',
# 工具栏是否折叠
'SHOW_COLLAPSED': True,
# 是否显示工具栏
'SHOW_TOOLBAR_CALLBACK': lambda x: True,
}
```
3. 配置 - 修改urls.py。
```Python
if settings.DEBUG:
import debug_toolbar
urlpatterns.insert(0, path('__debug__/', include(debug_toolbar.urls)))
```
4. 在配置好Django-Debug-Toolbar之后页面右侧会看到一个调试工具栏如下图所示上面包括了如前所述的各种调试信息包括执行时间、项目设置、请求头、SQL、静态资源、模板、缓存、信号等查看起来非常的方便。
![](res/django-debug-toolbar.png)
#### 优化ORM代码
在配置了日志或Django-Debug-Toolbar之后我们可以查看一下之前将老师数据导出成Excel报表的视图函数执行情况这里我们关注的是ORM框架生成的SQL查询到底是什么样子的相信这里的结果会让你感到有一些意外。执行`Teacher.objects.all()`之后我们可以注意到在控制台看到的或者通过Django-Debug-Toolbar输出的SQL是下面这样的
```SQL
SELECT `tb_teacher`.`no`, `tb_teacher`.`name`, `tb_teacher`.`detail`, `tb_teacher`.`photo`, `tb_teacher`.`good_count`, `tb_teacher`.`bad_count`, `tb_teacher`.`sno` FROM `tb_teacher`; args=()
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,)
```
这里的问题通常被称为“1+N查询”或“N+1查询”原本获取老师的数据只需要一条SQL但是由于老师关联了学科当我们查询到N条老师的数据时Django的ORM框架又向数据库发出了N条SQL去查询老师所属学科的信息。每条SQL执行都会有较大的开销而且会给数据库服务器带来压力如果能够在一条SQL中完成老师和学科的查询肯定是更好的做法这一点也很容易做到相信大家已经想到怎么做了。是的我们可以使用连接查询但是在使用Django的ORM框架时如何做到这一点呢对于多对一关联如投票应用中的老师和学科我们可以使用`QuerySet`的用`select_related()`方法来加载关联对象;而对于多对多关联(如电商网站中的订单和商品),我们可以使用`prefetch_related()`方法来加载关联对象。
在导出老师Excel报表的视图函数中我们可以按照下面的方式优化代码。
```Python
queryset = Teacher.objects.all().select_related('subject')
```
事实上用ECharts生成前端报表的视图函数中查询老师好评和差评数据的操作也能够优化因为在这个例子中我们只需要获取老师的姓名、好评数和差评数这三项数据但是在默认的情况生成的SQL会查询老师表的所有字段。可以用`QuerySet`的`only()`方法来指定需要查询的属性,也可以用`QuerySet`的`defer()`方法来指定暂时不需要查询的属性这样生成的SQL会通过投影操作来指定需要查询的列从而改善查询性能代码如下所示
```Python
queryset = Teacher.objects.all().only('name', 'good_count', 'bad_count')
```
当然如果要统计出每个学科的老师好评和差评的平均数利用Django的ORM框架也能够做到代码如下所示
```Python
queryset = Teacher.objects.values('subject').annotate(
good=Avg('good_count'), bad=Avg('bad_count'))
```
这里获得的`QuerySet`中的元素是字典对象,每个字典中有三组键值对,分别是代表学科编号的`subject`、代表好评数的`good`和代表差评数的`bad`。如果想要获得学科的名称而不是编号,可以按照如下所示的方式调整代码:
```Python
queryset = Teacher.objects.values('subject__name').annotate(
good=Avg('good_count'), bad=Avg('bad_count'))
```
可见Django的ORM框架允许我们用面向对象的方式完成关系数据库中的分组和聚合查询。

View File

@ -1,55 +1,6 @@
## 中间件的应用
### 实现登录验证
我们继续来完善投票应用。在上一个章节中我们在用户登录成功后通过session保留了用户信息接下来我们可以应用做一些调整要求在为老师投票时必须要先登录登录过的用户可以投票否则就将用户引导到登录页面为此我们可以这样修改视图函数。
```Python
def praise_or_criticize(request: HttpRequest):
"""投票"""
if 'username' in request.session:
try:
tno = int(request.GET.get('tno', '0'))
teacher = Teacher.objects.get(no=tno)
if request.path.startswith('/praise'):
teacher.good_count += 1
else:
teacher.bad_count += 1
teacher.save()
data = {'code': 200, 'message': '操作成功'}
except (ValueError, Teacher.DoesNotExist):
data = {'code': 404, 'message': '操作失败'}
else:
data = {'code': 401, 'message': '请先登录'}
return JsonResponse(data)
```
前端页面在收到`{'code': 401, 'message': '请先登录'}`后可以将用户引导到登录页面修改后的teacher.html页面的JavaScript代码部门如下所示。
```HTML
<script>
$(() => {
$('.comment > a').on('click', (evt) => {
evt.preventDefault()
let a = $(evt.target)
$.getJSON(a.attr('href'), (json) => {
if (json.code == 200) {
let span = a.next()
span.text(parseInt(span.text()) + 1)
} else if (json.code == 401) {
location.href = '/login/?backurl=' + location.href
} else {
alert(json.message)
}
})
})
})
</script>
```
> 注意:为了在登录成功之后能够回到刚才投票的页面,我们在跳转登录时设置了一个`backurl`参数把当前浏览器中的URL作为返回的页面地址。
这样我们已经实现了用户必须登录才能投票的限制但是一个新的问题来了。如果我们的应用中有很多功能都需要用户先登录才能执行例如将前面导出Excel报表和查看统计图表的功能都加以登录限制那么我们是不是需要在每个视图函数中添加代码来检查session中是否包含了登录用户的信息呢答案是否定的如果这样做了我们的视图函数中必然会充斥着大量的重复代码。编程大师*Martin Fowler*曾经说过:**代码有很多种坏味道,重复是最坏的一种**。在Django项目中我们可以把验证用户是否登录这样的重复性代码放到中间件中。
之前我们已经实现了用户必须登录才能投票的限制但是一个新的问题来了。如果我们的应用中有很多功能都需要用户先登录才能执行例如将前面导出Excel报表和查看统计图表的功能都做了必须登录才能访问的限制那么我们是不是需要在每个视图函数中添加代码来检查session中是否包含`userid`的代码呢?答案是否定的,如果这样做了,我们的视图函数中必然会充斥着大量的重复代码。编程大师*Martin Fowler*曾经说过:**代码有很多种坏味道,重复是最坏的一种**。在Python程序中我们可以通过装饰器来为函数提供额外的能力在Django项目中我们可以把类似于验证用户是否登录这样的重复性代码放到**中间件**中。
### Django中间件概述
@ -71,20 +22,20 @@ MIDDLEWARE = [
我们稍微为大家解释一下这些中间件的作用:
1. CommonMiddleware - 基础设置中间件,可以处理以下一些配置参数。
1. `CommonMiddleware` - 基础设置中间件,可以处理以下一些配置参数。
- DISALLOWED_USER_AGENTS - 不被允许的用户代理(浏览器)
- APPEND_SLASH - 是否追加`/`
- USE_ETAG - 浏览器缓存相关
2. SecurityMiddleware - 安全相关中间件,可以处理和安全相关的配置项。
2. `SecurityMiddleware` - 安全相关中间件,可以处理和安全相关的配置项。
- SECURE_HSTS_SECONDS - 强制使用HTTPS的时间
- SECURE_HSTS_INCLUDE_SUBDOMAINS - HTTPS是否覆盖子域名
- SECURE_CONTENT_TYPE_NOSNIFF - 是否允许浏览器推断内容类型
- SECURE_BROWSER_XSS_FILTER - 是否启用跨站脚本攻击过滤器
- SECURE_SSL_REDIRECT - 是否重定向到HTTPS连接
- SECURE_REDIRECT_EXEMPT - 免除重定向到HTTPS
3. SessionMiddleware - 会话中间件。
4. CsrfViewMiddleware - 通过生成令牌,防范跨请求份伪的造中间件。
5. XFrameOptionsMiddleware - 通过设置请求头参数,防范点击劫持攻击的中间件。
3. `SessionMiddleware` - 会话中间件。
4. `CsrfViewMiddleware` - 通过生成令牌,防范跨请求份伪的造中间件。
5. `XFrameOptionsMiddleware` - 通过设置请求头参数,防范点击劫持攻击的中间件。
在请求的过程中上面的中间件会按照书写的顺序从上到下执行然后是URL解析最后请求才会来到视图函数在响应的过程中上面的中间件会按照书写的顺序从下到上执行与请求时中间件执行的顺序正好相反。
@ -144,7 +95,3 @@ MIDDLEWARE = [
注意上面这个中间件列表中元素的顺序,当收到来自用户的请求时,中间件按照从上到下的顺序依次执行,这行完这些中间件以后,请求才会最终到达视图函数。当然,在这个过程中,用户的请求可以被拦截,就像上面我们自定义的中间件那样,如果用户在没有登录的情况下访问了受保护的资源,中间件会将请求直接重定向到登录页,后面的中间件和视图函数将不再执行。在响应用户请求的过程中,上面的中间件会按照从下到上的顺序依次执行,这样的话我们还可以对响应做进一步的处理。
中间件执行的顺序是非常重要的,对于有依赖关系的中间件必须保证被依赖的中间件要置于依赖它的中间件的前面,就好比我们刚才自定义的中间件要放到`SessionMiddleware`的后面,因为我们要依赖这个中间件为请求绑定的`session`对象才能判定用户是否登录。
### 小结
至此,除了对用户投票数量加以限制的功能外,这个投票应用就算基本完成了,整个项目的完整代码请参考<https://github.com/jackfrued/django1902>,其中用户注册时使用的手机验证码功能请大家使用自己注册的短信平台替代它。如果需要投票应用完整的视频讲解,可以在首页扫码打赏后留言联系作者获取视频下载地址,谢谢大家的理解和支持。

View File

@ -161,4 +161,4 @@ class SubjectMapper(ModelMapper):
前后端分离的开发需要将前端页面作为静态资源进行部署项目实际上线的时候我们会对整个Web应用进行动静分离静态资源通过Nginx或Apache服务器进行部署生成动态内容的Python程序部署在uWSGI或者Gunicorn服务器上对动态内容的请求由Nginx或Apache路由到uWSGI或Gunicorn服务器上。
在开发阶段我们通常会使用Django自带的测试服务器如果要尝试前后端分离可以先将静态页面放在之前创建的放静态资源的目录下具体的做法可以参考[项目完整代码](<https://github.com/jackfrued/django1902_vue/>)。
在开发阶段我们通常会使用Django自带的测试服务器如果要尝试前后端分离可以先将静态页面放在之前创建的放静态资源的目录下具体的做法可以参考[项目完整代码]()。

View File

@ -0,0 +1,4 @@
## 文件上传

View File

@ -1,4 +0,0 @@
## 文件上传和富文本编辑

View File

@ -1,4 +0,0 @@
## 短信和邮件

View File

@ -0,0 +1,3 @@
## 项目上线

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class FirstConfig(AppConfig):
name = 'first'

View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,11 @@
from random import sample
from django.shortcuts import render
def show_index(request):
fruits = [
'Apple', 'Orange', 'Pitaya', 'Durian', 'Waxberry', 'Blueberry',
'Grape', 'Peach', 'Pear', 'Banana', 'Watermelon', 'Mango'
]
return render(request, 'index.html', {'fruits': sample(fruits, 3)})

View File

@ -1,13 +1,13 @@
"""
Django settings for shop project.
Django settings for hellodjango project.
Generated by 'django-admin startproject' using Django 2.0.5.
Generated by 'django-admin startproject' using Django 2.2.13.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/topics/settings/
https://docs.djangoproject.com/en/2.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
https://docs.djangoproject.com/en/2.2/ref/settings/
"""
import os
@ -17,10 +17,10 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '+gqc54!5+uhvc^o0)fjvihmg&5uu^u+#s5m*fc+e+@bw*(+!w*'
SECRET_KEY = 'x)q$(0m0^ttqii@^zn^9bdbh&%l$)wzjm=nv&_y+^y9e!37=-z'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -37,7 +37,6 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'cart',
]
MIDDLEWARE = [
@ -50,13 +49,12 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'shop.urls'
ROOT_URLCONF = 'hellodjango.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
,
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -69,26 +67,22 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = 'shop.wsgi.application'
WSGI_APPLICATION = 'hellodjango.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'Shop',
'HOST': 'localhost',
'PORT': 3306,
'USER': 'yourname',
'PASSWORD': 'yourpass',
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
@ -107,11 +101,11 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Asia/Chongqing'
TIME_ZONE = 'UTC'
USE_I18N = True
@ -121,6 +115,6 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/'

View File

@ -1,7 +1,7 @@
"""shop URL Configuration
"""hellodjango URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.0/topics/http/urls/
https://docs.djangoproject.com/en/2.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
@ -16,11 +16,9 @@ Including another URLconf
from django.contrib import admin
from django.urls import path
from cart import views
from first.views import show_index
urlpatterns = [
path('', views.index),
path('show_cart', views.show_cart),
path('add_to_cart/<int:no>', views.add_to_cart),
path('admin/', admin.site.urls),
path('hello/', show_index),
]

View File

@ -1,16 +1,16 @@
"""
WSGI config for shop project.
WSGI config for hellodjango project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings")
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hellodjango.settings')
application = get_wsgi_application()

View File

@ -1,9 +1,11 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings")
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hellodjango.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@ -13,3 +15,7 @@ if __name__ == "__main__":
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<style>
#fruits {
font-size: 1.25em;
}
</style>
</head>
<body>
<h1>今天推荐的水果是:</h1>
<hr>
<ul id="fruits">
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% endfor %}
</ul>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

View File

@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
<style>
#container {
width: 400px;
margin: 10px auto;
}
.input, form+a {
margin: 20px 20px;
width: 250px;
}
.input>label {
display: inline-block;
width: 70px;
text-align: right;
}
.input:last-child {
text-align: center;
}
input[type=text], input[type=password] {
outline: none;
}
input[type=submit], input[type=reset] {
width: 60px;
}
form+a {
text-decoration: none;
color: blue;
}
.captcha {
width: 380px;
}
.captcha>img {
vertical-align: middle;
}
.hint {
margin-left: 20px;
color: red;
font-size: 0.8em;
height: 20px;
}
</style>
</head>
<body>
<div id="container">
<h1>用户登录</h1>
<hr>
<form action="" method="post">
<div class="input">
<label>用户名:</label>
<input type="text" name="username">
</div>
<div class="input">
<label>密码:</label>
<input type="password" name="password">
</div>
<div class="input captcha">
<label>验证码:</label>
<input type="text" name="captcha">
<img id="code" src="images/captcha.jpg" width="120">
</div>
<div class="input">
<input type="submit" value="登录">
<input type="reset" value="重置">
</div>
</form>
<a href="register.html">注册新用户</a>
</div>
</body>
</html>

View File

@ -1,87 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
<style>
#container {
width: 400px;
margin: 0 auto;
}
.input, form+a {
margin: 20px 20px;
width: 250px;
}
.input>label {
display: inline-block;
width: 80px;
text-align: right;
}
.input:last-child {
text-align: center;
}
input[type=text], input[type=password] {
outline: none;
}
input[type=submit], input[type=reset] {
width: 60px;
}
form+a {
text-decoration: none;
color: blue;
}
.hint {
margin-left: 20px;
color: red;
font-size: 0.8em;
height: 20px;
}
.mobile {
width: 300px;
}
.photo {
width: 350px;
}
.preview {
margin: 10px 50px;
}
.intro {
width: 720px;
}
</style>
</head>
<body>
<div id="container">
<h1>用户注册</h1>
<hr>
<form action="" method="post">
<div class="input">
<label>用户名:</label>
<input type="text" name="username" required minlength="4" maxlength="20">
</div>
<div class="input">
<label>密码:</label>
<input type="password" name="password">
</div>
<div class="input">
<label>确认密码:</label>
<input type="password" name="repassword">
</div>
<div class="input mobile">
<label>手机号:</label>
<input type="tel" name="tel">
<input type="button" id="sendBtn" value="发送验证码">
</div>
<div class="input">
<label>验证码:</label>
<input type="text" name="mobilecode">
</div>
<div class="input">
<input type="submit" value="注册">
<input type="reset" value="重置">
</div>
</form>
<a href="login.html">返回登录</a>
</div>
</body>
</html>

View File

@ -1,67 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>学科信息</title>
<style>
#container {
width: 80%;
margin: 10px auto;
}
#main dt {
font-size: 1.5em;
}
#main dd {
font-size: 1.2em;
}
#main a {
text-decoration: none;
color: blue;
}
</style>
</head>
<body>
<div id="container">
<h1>千锋教育成都校区所有学科</h1>
<hr>
<div id="main">
<dl>
<dt>
<a href="teachers.html">Python全栈+人工智能</a>
<img src="images/hot-icon-small.png">
</dt>
<dd>
Python英国发音/ˈpaɪθən/ 美国发音:/ˈpaɪθɑːn/)是一种广泛使用的解释型、高级编程、通用型编程语言,
由吉多·范罗苏姆创造第一版发布于1991年。可以视之为一种改良加入一些其他编程语言的优点如面向对象的LISP。
Python的设计哲学强调代码的可读性和简洁的语法尤其是使用空格缩进划分代码块而非使用大括号或者关键词
相比于C++或JavaPython让开发者能够用更少的代码表达想法。不管是小型还是大型程序该语言都试图让程序的结构清晰明了。
</dd>
</dl>
<dl>
<dt>
<a href="">全栈软件测试</a>
</dt>
<dd>
软件测试在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程。
软件测试有许多方法,但对复杂的产品运行有效测试不仅仅是研究过程,更是创造并严格遵守某些呆板步骤的大事。
测试的其中一个定义:为了评估而质疑产品的过程;这里的“质疑”是测试员试着对产品做的事,而产品以测试者脚本行为反应作为回答。
</dd>
</dl>
<dl>
<dt>
<a href="">JavaEE+分布式开发</a>
<img src="images/hot-icon-small.png">
</dt>
<dd>
Java是一种广泛使用的计算机编程语言拥有跨平台、面向对象、泛型编程的特性广泛应用于企业级Web应用开发和移动应用开发。
该语言由当时任职于太阳微系统的詹姆斯·高斯林等人于1990年代初开发Java语言的雏形最初被命名为Oak
目标设置在家用电器等小型系统的编程语言,应用在电视机、电话、闹钟、烤面包机等家用电器的控制和通信。
由于这些智能化家电的市场需求没有预期的高太阳计算机系统Sun公司放弃了该项计划。
随着1990年代互联网的发展Sun公司看见Oak在互联网上应用的前景于是改造了Oak于1995年5月以Java的名称正式发布。
Java伴随着互联网的迅猛发展而发展逐渐成为重要的网络编程语言。
</dd>
</dl>
</div>
</div>
</body>
</html>

View File

@ -1,110 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>老师信息</title>
<style>
#container {
width: 80%;
margin: 10px auto;
}
.teacher {
width: 100%;
margin: 0 auto;
padding: 10px 0;
border-bottom: 1px dashed gray;
overflow: auto;
}
.teacher div {
float: left;
}
.photo {
height: 140px;
border-radius: 75px;
overflow: hidden;
margin-left: 20px;
}
.info {
width: 75%;
margin-left: 30px;
}
.info div {
clear: both;
margin: 5px 10px;
}
.info span {
margin-right: 25px;
}
.info a {
text-decoration: none;
color: blue;
}
.user {
float: right;
margin: 10px 10px;
}
.user+h1 {
clear: both;
}
#photo {
border-radius: 32px 32px 32px 32px;
}
</style>
</head>
<body>
<div id="container">
<h1>Python全栈+人工智能学科的老师信息</h1>
<hr>
<div class="teacher">
<div class="photo">
<img src="images/luohao.png" height="140" alt="">
</div>
<div class="info">
<div>
<span><strong>姓名:骆昊</strong></span>
<span>性别:男</span>
<span>出生日期1980年11月28日</span>
</div>
<div class="intro">
10年以上软硬件产品和系统设计、研发、架构和管理经验2003年毕业于四川大学四川大学Java技术俱乐部创始人
四川省优秀大学毕业生在四川省网络通信技术重点实验室工作期间参与了2项国家自然科学基金项目、
1项中国科学院中长期研究项目和多项四川省科技攻关项目在国际会议和国内顶级期刊上发表多篇论文1篇被SCI收录3篇被EI收录
大规模网络性能测量系统DMC-TS的设计者和开发者perf-TTCN语言的发明者。国内最大程序员社区CSDN的博客专家
在Github上参与和维护了多个高质量开源项目精通C/C++、Java、Python、R、Swift、JavaScript等编程语言
擅长OOAD、系统架构、算法设计、协议分析和网络测量主持和参与过电子政务系统、KPI考核系统、P2P借贷平台等产品的研发
一直践行“用知识创造快乐”的教学理念,善于总结,乐于分享。
</div>
<div class="comment">
<a href="">好评</a>&nbsp;&nbsp;(<strong>100</strong>)
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="">差评</a>&nbsp;&nbsp;(<strong>50</strong>)
</div>
</div>
</div>
<div class="teacher">
<div class="photo">
<img src="images/yuting.png" height="140" alt="">
</div>
<div class="info">
<div>
<span><strong>姓名:余婷</strong></span>
<span>性别:女</span>
<span>出生日期1992年9月20日</span>
</div>
<div class="intro">
5年以上移动互联网项目开发经验和教学经验曾担任上市游戏公司高级软件研发工程师和移动端iOS技术负责人
参了多个企业级应用和游戏类应用的移动端开发和后台服务器开发,拥有丰富的开发经验和项目管理经验,
以个人开发者和协作开发者的身份在苹果的AppStore上发布过多款App。精通Python、C、Objective-C、Swift等开发语言
熟悉iOS原生App开发、RESTful接口设计以及基于Cocos2d-x的游戏开发。授课条理清晰、细致入微
性格活泼开朗、有较强的亲和力,教学过程注重理论和实践的结合,在学员中有良好的口碑。
</div>
<div class="comment">
<a href="">好评</a>&nbsp;&nbsp;(<strong>50</strong>)
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="">差评</a>&nbsp;&nbsp;(<strong>100</strong>)
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,12 +0,0 @@
from django.contrib import admin
from cart.models import Goods
class GoodsAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'price', 'image')
search_fields = ('name', )
admin.site.register(Goods, GoodsAdmin)

View File

@ -1,5 +0,0 @@
from django.apps import AppConfig
class CartConfig(AppConfig):
name = 'cart'

View File

@ -1,27 +0,0 @@
# Generated by Django 2.0.5 on 2018-05-25 06:28
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Goods',
fields=[
('id', models.AutoField(db_column='gid', primary_key=True, serialize=False)),
('name', models.CharField(db_column='gname', max_length=50)),
('price', models.DecimalField(db_column='gprice', decimal_places=2, max_digits=10)),
('image', models.CharField(db_column='gimage', max_length=255)),
],
options={
'db_table': 'tb_goods',
'ordering': ('id',),
},
),
]

View File

@ -1,15 +0,0 @@
from django.db import models
class Goods(models.Model):
"""商品模型类"""
id = models.AutoField(primary_key=True, db_column='gid')
name = models.CharField(max_length=50, db_column='gname')
price = models.DecimalField(max_digits=10, decimal_places=2, db_column='gprice')
image = models.CharField(max_length=255, db_column='gimage')
class Meta:
db_table = 'tb_goods'
ordering = ('id', )

View File

@ -1,77 +0,0 @@
from django.core import serializers
from django.shortcuts import render, redirect
from cart.models import Goods
def index(request):
goods_list = list(Goods.objects.all())
return render(request, 'goods.html', {'goods_list': goods_list})
class CartItem(object):
"""购物车中的商品项"""
def __init__(self, goods, amount=1):
self.goods = goods
self.amount = amount
@property
def total(self):
return self.goods.price * self.amount
class ShoppingCart(object):
"""购物车"""
def __init__(self):
self.items = {}
self.index = 0
def add_item(self, item):
if item.goods.id in self.items:
self.items[item.goods.id].amount += item.amount
else:
self.items[item.goods.id] = item
def remove_item(self, id):
if id in self.items:
self.items.remove(id)
def clear_all_items(self):
self.items.clear()
@property
def cart_items(self):
return self.items.values()
@property
def total(self):
val = 0
for item in self.items.values():
val += item.total
return val
def add_to_cart(request, id):
goods = Goods.objects.get(pk=id)
# 通过request对象的session属性可以获取到session
# session相当于是服务器端用来保存用户数据的一个字典
# session利用了Cookie保存sessionid
# 通过sessionid就可以获取与某个用户对应的会话(也就是用户数据)
# 如果在浏览器中清除了Cookie那么也就清除了sessionid
# 再次访问服务器时服务器会重新分配新的sessionid这也就意味着之前的用户数据无法找回
# 默认情况下Django的session被设定为持久会话而非浏览器续存期会话
# 通过SESSION_EXPIRE_AT_BROWSER_CLOSE和SESSION_COOKIE_AGE参数可以修改默认设定
# Django中的session是进行了持久化处理的因此需要设定session的序列化方式
# 1.6版开始Django默认的session序列化器是JsonSerializer
# 可以通过SESSION_SERIALIZER来设定其他的序列化器(例如PickleSerializer)
cart = request.session.get('cart', ShoppingCart())
cart.add_item(CartItem(goods))
request.session['cart'] = cart
return redirect('/')
def show_cart(request):
cart = serializers.deserialize(request.session.get('cart'))
return render(request, 'cart.html', {'cart': cart})

View File

@ -1,15 +0,0 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)

View File

@ -1,3 +0,0 @@
import pymysql
pymysql.install_as_MySQLdb()

View File

@ -1,129 +0,0 @@
"""
Django settings for shop project.
Generated by 'django-admin startproject' using Django 2.0.5.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '3(n^av%_kt*^2zhz0!iwkxv6_wp^ed7-dpow*vqr7ck0_6=9^e'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'cart.apps.CartConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'shop.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'shop.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'shop',
'HOST': 'localhost',
'PORT': 3306,
'USER': 'yourname',
'PASSWORD': 'yourpass',
}
}
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Chongqing'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'),]
STATIC_URL = '/static/'

View File

@ -1,26 +0,0 @@
"""shop URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from cart import views
urlpatterns = [
path('', views.index),
path('add_to_cart/<int:id>', views.add_to_cart),
path('show_cart', views.show_cart),
path('admin/', admin.site.urls),
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,55 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
th, td { margin: 0; padding: 0; width: 180px; text-align: left; }
.name { font-size: 14px; font-weight: bolder; width: 280px; }
.price { color: red; font-size: 18px; }
a { display: inline-block; text-align: center; background-color: red; }
.back { width: 120px; height: 30px; line-height: 30px; }
.del { width: 60px; height: 20px; line-height: 20px; }
a:link, a:visited { color: white; text-decoration: none; }
.left { float: left; width: 1000px;}
.right { float: right; }
.total { text-align: right; }
</style>
</head>
<body>
<div class="left">
<h1>购物车列表</h1>
<hr>
</div>
<div class="right">
<a href="/" class="back">返回</a>
</div>
{% if cart %}
<table style="clear: both;">
<tr>
<th>商品名称</th>
<th>商品单价</th>
<th>商品数量</th>
<th>商品总价</th>
<th>操作</th>
</tr>
{% for item in cart %}
<tr>
<td class="name">{{ item.goods.name }}</td>
<td class="price">&yen;{{ item.goods.price }}</td>
<td>{{ item.amount }}</td>
<td class="price">&yen;{{ item.total }}</td>
<td>
<a href="" class="del">删除</a>
</td>
</tr>
{% endfor %}
<tr>
<td colspan="5" class="total price">&yen;{{ cart.total }}元</td>
</tr>
</table>
<a href="" class="back">清空购物车</a>
{% else %}
<h3 style="clear: both;">购物车中暂时没有商品!</h3>
{% endif %}
</body>
</html>

View File

@ -1,46 +0,0 @@
<!DOCTYPE html>
{% load staticfiles %}
<html>
<head>
<meta charset="utf-8">
<style>
img { display: inline-block; width: 150px; height: 150px; border: 1px solid gray; }
th, td { margin: 0; padding: 0; width: 250px; text-align: left; }
.name { font-size: 14px; font-weight: bolder; }
.price { color: red; font-size: 18px; }
a { display: inline-block; width: 120px; height: 30px; line-height: 30px; text-align: center; background-color: red; }
a:link, a:visited { color: white; text-decoration: none; }
.left { float: left; width: 1000px;}
.right { float: right; }
</style>
</head>
<body>
<div class="left">
<h1>商品列表</h1>
<hr>
</div>
<div class="right">
<a href="/show_cart">查看购物车</a>
</div>
<table style="clear:both;">
<tr>
<th>商品名称</th>
<th>商品价格</th>
<th>商品图片</th>
<th>操作</th>
</tr>
{% for goods in goods_list %}
<tr>
<td class="name">{{ goods.name }}</td>
<td class="price">&yen;{{ goods.price }}</td>
<td>
<img src="{% static goods.image %}" alt="{{ goods.name }}">
</td>
<td>
<a href="/add_to_cart/{{ goods.id }}">加入购物车</a>
</td>
</tr>
{% endfor %}
</table>
</body>
</html>

View File

@ -1,11 +0,0 @@
from django.contrib import admin
from cart.models import Goods
class GoodsAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'price', 'image')
admin.site.register(Goods, GoodsAdmin)

View File

@ -1,5 +0,0 @@
from django.apps import AppConfig
class CartConfig(AppConfig):
name = 'cart'

View File

@ -1,27 +0,0 @@
# Generated by Django 2.0.5 on 2018-05-25 05:11
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Goods',
fields=[
('id', models.AutoField(db_column='gid', primary_key=True, serialize=False)),
('name', models.CharField(db_column='gname', max_length=50)),
('price', models.DecimalField(db_column='gprice', decimal_places=2, max_digits=10)),
('image', models.CharField(db_column='gimage', max_length=255)),
],
options={
'db_table': 'tb_goods',
'ordering': ('id',),
},
),
]

View File

@ -1,13 +0,0 @@
from django.db import models
class Goods(models.Model):
id = models.AutoField(primary_key=True, db_column='gid')
name = models.CharField(max_length=50, db_column='gname')
price = models.DecimalField(max_digits=10, decimal_places=2, db_column='gprice')
image = models.CharField(max_length=255, db_column='gimage')
class Meta:
db_table = 'tb_goods'
ordering = ('id',)

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,16 +0,0 @@
from django.shortcuts import render
from cart.models import Goods
def index(request):
goods_list = list(Goods.objects.all())
return render(request, 'goods.html', {'goods_list': goods_list})
def show_cart(request):
return render(request, 'cart.html')
def add_to_cart(request, no):
pass

View File

@ -1,3 +0,0 @@
import pymysql
pymysql.install_as_MySQLdb()

View File

@ -1,16 +0,0 @@
"""
WSGI config for shop project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings")
application = get_wsgi_application()

View File

@ -1,7 +0,0 @@
insert into tb_goods values
(default, '乐事Lays无限薯片', 8.2, 'images/lay.jpg'),
(default, '旺旺 仙贝 加量装 540g', 18.5, 'images/wang.jpg'),
(default, '多儿比Dolbee黄桃水果罐头', 6.8, 'images/dolbee.jpg'),
(default, '王致和 精制料酒 500ml', 7.9, 'images/wine.jpg'),
(default, '陈克明 面条 鸡蛋龙须挂面', 1.0, 'images/noodle.jpg'),
(default, '鲁花 菜籽油 4L', 69.9, 'images/oil.jpg');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,55 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
th, td { margin: 0; padding: 0; width: 180px; text-align: left; }
.name { font-size: 14px; font-weight: bolder; width: 280px; }
.price { color: red; font-size: 18px; }
a { display: inline-block; text-align: center; background-color: red; }
.back { width: 120px; height: 30px; line-height: 30px; }
.del { width: 60px; height: 20px; line-height: 20px; }
a:link, a:visited { color: white; text-decoration: none; }
.left { float: left; width: 1000px;}
.right { float: right; }
.total { text-align: right; }
</style>
</head>
<body>
<div class="left">
<h1>购物车列表</h1>
<hr>
</div>
<div class="right">
<a href="list_goods" class="back">返回</a>
</div>
{% if cart_items %}
<table style="clear: both;">
<tr>
<th>商品名称</th>
<th>商品单价</th>
<th>商品数量</th>
<th>商品总价</th>
<th>操作</th>
</tr>
{% for item in cart_items %}
<tr>
<td class="name">{{ item.name }}</td>
<td class="price">&yen;{{ item.unit_price }}</td>
<td>{{ item.amount }}</td>
<td class="price">&yen;{{ item.total_price }}</td>
<td>
<a href="" class="del">删除</a>
</td>
</tr>
{% endfor %}
<tr>
<td colspan="5" class="total price">&yen;{{ cart.total }}元</td>
</tr>
</table>
<a href="clear_cart" class="back">清空购物车</a>
{% else %}
<h3 style="clear: both;">购物车中暂时没有商品!</h3>
{% endif %}
</body>
</html>

View File

@ -1,46 +0,0 @@
<!DOCTYPE html>
{% load staticfiles %}
<html>
<head>
<meta charset="utf-8">
<style>
img { display: inline-block; width: 150px; height: 150px; border: 1px solid gray; }
th, td { margin: 0; padding: 0; width: 250px; text-align: left; }
.name { font-size: 14px; font-weight: bolder; }
.price { color: red; font-size: 18px; }
a { display: inline-block; width: 120px; height: 30px; line-height: 30px; text-align: center; background-color: red; }
a:link, a:visited { color: white; text-decoration: none; }
.left { float: left; width: 1000px;}
.right { float: right; }
</style>
</head>
<body>
<div class="left">
<h1>商品列表</h1>
<hr>
</div>
<div class="right">
<a href="/show_cart">查看购物车</a>
</div>
<table style="clear:both;">
<tr>
<th>商品名称</th>
<th>商品价格</th>
<th>商品图片</th>
<th>操作</th>
</tr>
{% for goods in goods_list %}
<tr>
<td class="name">{{ goods.name }}</td>
<td class="price">&yen;{{ goods.price }}</td>
<td>
<img src="{% static goods.image %}" alt="{{ goods.name }}">
</td>
<td>
<a href="/add_to_cart/{{ goods.id }}">加入购物车</a>
</td>
</tr>
{% endfor %}
</table>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Some files were not shown because too many files have changed in this diff Show More