后端开发简介

后端开发框架:

  • Java: Spring
  • Python: Django, Flask, tornado

MVC 框架(核心思想:解耦。):

89-1.png

Web MVC 框架模块功能:

89-2.png

M: Model,模型,和数据库进行交互。
V: View,视图,产生 html 页面。
C: Controller,控制器,接收请求,进行处理,与 M 和 V 进行交互,返回应答。

Django 一般前后端不分离,虽然也可以分离。

Django 遵循 MVC 思想,但是有自己的一个名词,叫做 MVT。Django 遵循快速开发DRY(Do not repeat yourself) 原则,不要自己去重复一些工作。

MVT 各部分功能:

89-3.png

M: Model,模型,和 MVC 中 M 功能相同,和数据库进行交互。
V: View,视图,和 MVC 中 C 功能相同,接收请求,进行处理,与 M 和 T 进行交互,返回应答。
T: Template,模板,和 MVC 中 V 功能相同,产生 html 页面。

配置虚拟环境

虚拟环境是真实 python 环境的复制版本。

在虚拟环境中使用的 python 是复制的 python,安装 python 包也是安装在复制的 python 中。

安装虚拟环境工具:

1
sudo apt install python3-venv

创建虚拟环境:

1
python3 -m venv myenv

激活虚拟环境:

1
source myenv/bin/activate

想退出虚拟环境,可以运行以下命令:

1
deactivate

退出了虚拟环境,如何再次进入?首先,导航到虚拟环境所在的目录。

在该目录下,运行以下命令来激活虚拟环境:

1
source bin/activate

现在,尝试在虚拟环境里面下载东西。注意,即使配置了翻墙工具,虚拟环境中的 pip 请求可能无法正确通过代理。解决方案(port 需要视情况改动):

1
pip install jieba --proxy="http://127.0.0.1:7897"

安装完成后,我们可以:

1
pip freeze > requirements.txt

查看内容:

1
2
(myenv) zhiyue@168:~/myenv$ cat requirements.txt 
jieba==0.42.1

这个 txt 文件的作用是,我们可以使用它方便地创建需要的环境

1
pip install -r requirements.txt

在虚拟环境中安装 Django :

1
pip install django==4.2 --proxy="http://127.0.0.1:7897"

项目创建

创建 Django 项目

注意:创建应用必须先进入虚拟环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(myenv) zhiyue@168:~/myenv$ django-admin startproject day1010
(myenv) zhiyue@168:~/myenv$ ls
bin day1010 include lib lib64 pyvenv.cfg
(myenv) zhiyue@168:~/myenv$ cd day1010/
(myenv) zhiyue@168:~/myenv/day1010$ tree
.
├── day1010
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

2 directories, 6 files

说明:

  • __init__.py: 说明 day1010 是一个 python 包。
  • settings.py: 项目的配置文件。
  • urls.py: 进行 url 路由的配置。
  • wsgi.py: (对接某种类似协议的东西)web 服务器和 Django 交互的入口。
  • manage.py: 项目的管理文件。

创建 Django 应用

89-4.png

一个项目由很多个应用组成的,每一个应用完成一个功能模块。

注意,创建应用时需要先进入项目目录:

1
python3 manage.py startapp booktest

效果如下:

89-5.png

对于 booktest 文件夹下的文件:

  • __init__.py: 说明目录是一个 Python 模块。
  • models.py: 写和数据库项目的内容,设计模型类。
  • views.py: 接收请求,进行处理,与 M 和 T 进行交互,返回应答。定义处理函数,视图函数
  • tests.py: 写测试代码的文件
  • admin.py: 网站后台管理相关的文件。
  • migrations: (作用后面讲解)

应用注册

建立应用和项目之间的联系,需要对应用进行注册。

在 day1010/settings.py 中 INSTALLED_APPS 下添加应用的名称就可以完成安装:

1
2
3
4
5
6
7
8
9
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'booktest', # 新增内容
]

启动项目

在开发阶段,为了能够快速预览到开发的效果,django 提供了一个纯 python 编写的轻量级 web 服务器,仅在开发阶段使用。

运行服务器的命令:

1
python3 manage.py runserver

模型类

ORM

89-6.png

ORM 框架帮我们把类和数据表进行了映射,可以让我们通过类和类对象操作它所对应的表格(数据库)中的数据。ORM 框架还可以根据我们设计的类自动生成数据库中的表格,省去了我们自己建表的过程。

使用 django 进行数据库开发的步骤如下:

  1. models.py 中定义模型类
  2. 迁移去数据库中看表是否生成
  3. 通过类和对象完成数据增删改查操作

模型类设计

在 models.py 中定义模型类如下:

1
2
3
4
5
6
7
from django.db import models

# Create your models here.

class BookInfo(models.Model):
btitle = models.CharField(max_length= 20)
bpub_data = models.DateField()

模型类生成表

生成迁移文件,根据模型类生成创建表的迁移文件:

1
python3 manage.py makemigrations

Django 框架根据我们设计的模型类生成了迁移文件,在迁移文件中可以看到 fields 列表中每一个元素跟 BookInfo 类属性名以及属性的类型是一致的。同时我们发现多了一个 id 项,这一项是 Django 框架帮我们自动生成的,在创建表的时候 id 就会作为对应表的主键列,并且主键列自动增长。

执行迁移,根据第一步生成的迁移文件在数据库中创建表:

1
python3 manage.py migrate

执行迁移命令后,Django 框架会读取迁移文件自动帮我们在数据库中生成对应的表格。

Django 默认采用 sqlite3 数据库,db.sqlite3 就是 Django 框架帮我们自动生成的数据库文件。 sqlite3 是一个很小的数据库,通常用在手机中,它跟 mysql 一样,我们也可以通过 sql 语句来操作它。

在 vscode 安装 SQLite Viewer 插件,可以查看此文件。

89-7.png

上面生成的表的名字叫做 booktest_bookinfo,booktest 是应用的名字,bookinfo 是模型类的名字。

通过模型类操作数据表

进入项目 shell 的命令:

1
python3 manage.py shell

在 shell 终端中演示的例子:

1
2
3
4
5
6
>>> from booktest.models import BookInfo
>>> b = BookInfo()
>>> b.btitle = '转生成为雷电将军然后天下无敌'
>>> from datetime import date
>>> b.bpub_data = date(2022,11,11)
>>> b.save()

注意我之前把 date 拼成了 data ,神智不清了一会。现在将错就错。

89-8.png

b.save() 之后才会将数据保存进数据库。

查询:

1
2
3
4
5
6
7
8
>>> from booktest.models import BookInfo
>>> b = BookInfo.objects.get(id=1)
>>> b
<BookInfo: BookInfo object (1)>
>>> b.btitle
'转生成为雷电将军然后天下无敌'
>>> b.bpub_data
datetime.date(2022, 11, 11)

修改:

1
2
3
>>> from datetime import date
>>> b.bpub_data = date(1999,9,9)
>>> b.save()

刷新,可以看到数据已经被修改。

删除:

1
2
>>> b.delete()
(1, {'booktest.BookInfo': 1})

刷新,可以看到数据已经被删除。

现在,在 models.py 中设计一个新的类:

1
2
3
4
5
6
7
class HeroInfo(models.Model):
hname = models.CharField(max_length=20)
hgender = models.BooleanField(default=False)
hcomment = models.CharField(max_length=100)
# on_delete=models.CASCADEon_delete=models.CASCADE
# 删除 BookInfo 里面的书籍时,会自动删除依赖该书籍的英雄信息
hbook = models.ForeignKey('BookInfo',on_delete=models.CASCADE,)

执行:

1
2
python3 manage.py makemigrations
python3 manage.py migrate

可以看到生成了一张新表。

重新在 BookInfo 中插入一条数据,可以看到 id =2 ,过程略。

在 heroinfo 中插入数据,并尝试关联两张表:

1
2
3
4
5
6
7
>>> from booktest.models import HeroInfo
>>> h = HeroInfo()
>>> h.hname = '雷电将军'
>>> h.hcomment = '梦想一心'
>>> b = BookInfo.objects.get(id = 2)
>>> h.hbook = b
>>> h.save()

89-9.png

查询操作:

1
2
3
4
5
6
>>> HeroInfo.objects.all()
<QuerySet [<HeroInfo: HeroInfo object (1)>, <HeroInfo: HeroInfo object (2)>]>
>>> HeroInfo.objects.all()[0]
<HeroInfo: HeroInfo object (1)>
>>> HeroInfo.objects.all()[0].hname
'雷电将军'

关联操作

省流:由一查多,由多查一。

目前的数据库:

89-10.jpeg

查询:

1
2
>>> h.hbook.btitle
'三哼经'
1
2
3
4
5
6
7
8
9
10
>>> b = BookInfo.objects.get(btitle = '转生成为雷电将军然后天下无敌'
... )
>>> b
<BookInfo: BookInfo object (2)>
>>> b.bpub_data
datetime.date(2022, 11, 11)
>>> b.heroinfo_set.all()
<QuerySet [<HeroInfo: HeroInfo object (1)>, <HeroInfo: HeroInfo object (2)>]>
>>> b.heroinfo_set.all()[0].hname
'雷电将军'

后台管理

假设我们要设计一个新闻网站,我们需要编写展示给用户的页面,网页上展示的新闻信息是从哪里来的呢?是从数据库中查找到新闻的信息,然后把它展示在页面上。但是我们的网站上的新闻每天都要更新,这就意味着对数据库的增、删、改、查操作,那么我们需要每天写 sql 语句操作数据库吗? 这样会非常繁琐,所以我们可以设计一个页面,通过对这个页面的操作来实现对新闻数据库的增删改查。那么问题来了,老板说我们需要在建立一个新网站,是不是还要设计一个页面来实现对新网站数据库的增删改查操作?但是这样的页面有很大的重复性,那有没有一种方法能够很快生成管理数据库表的页面呢?有,那就是 Django 的后台管理。Django 能够根据定义的模型类自动地生成管理页面。使用 Django 的管理模块,需要按照如下步骤操作:

  1. 管理界面本地化
  2. 创建管理员
  3. 注册模型类
  4. 自定义管理页面

本地化,打开 day1010/settings.py ,找到语言编码、时区的设置项,将内容改为如下:

1
2
3
LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

创建管理员:

1
python3 manage.py createsuperuser

根据提示操作即可。

启动 server:

1
python3 manage.py runserver

在浏览器中进入:

1
http://127.0.0.1:8000/admin/

注册模型类:
登录后台管理后,默认没有我们创建的应用中定义的模型类,需要在自己应用中的 admin.py 文件中注册,才可以在后台管理中看到,并进行增删改查操作。

在 booktest/admin.py 中,编写代码:

1
2
3
4
5
6
7
8
from django.contrib import admin

# Register your models here.

from booktest.models import BookInfo,HeroInfo

admin.site.register(BookInfo)
admin.site.register(HeroInfo)

到浏览器中刷新页面,可以看到模型类 BookInfo 和 HeroInfo 的管理了。

出现如下问题:

89-11.png

为什么没有直接显示书名呢?因为是 str(object) 的返回值。我们可以重写 str 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# models.py
from django.db import models

# Create your models here.

class BookInfo(models.Model):
btitle = models.CharField(max_length= 20)
bpub_data = models.DateField()

# 重写 str 后,打印对象会得到 return 返回的内容
def __str__(self) -> str:
return self.btitle

class HeroInfo(models.Model):
hname = models.CharField(max_length=20)
hgender = models.BooleanField(default=False)
hcomment = models.CharField(max_length=100)
# on_delete=models.CASCADEon_delete=models.CASCADE
# 删除 BookInfo 里面的书籍时,会自动删除依赖该书籍的英雄信息
hbook = models.ForeignKey('BookInfo',on_delete=models.CASCADE,)

# 暂定
def __str__(self) -> str:
return self.hname

在列表页只显示出了 BookInfo object,对象的其它属性并没有列出来,查看非常不方便。Django 提供了自定义管理页面的功能,比如列表页要显示哪些值。

修改 booktest/admin.py 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.contrib import admin

# Register your models here.

from booktest.models import BookInfo,HeroInfo


class BookInfoAdmin(admin.ModelAdmin):
list_display = ['id', 'btitle', 'bpub_data']

class HeroInfoAdmin(admin.ModelAdmin):
list_display = ['id', 'hname', 'hgender', 'hcomment']


admin.site.register(BookInfo, BookInfoAdmin)
admin.site.register(HeroInfo, HeroInfoAdmin)

刷新,发现功能实现。

视图

什么是视图:
一个 url 首先到达路由(这里的“路由”和计算机网络中的“路由”概念不同),路由会分配到对应的视图函数,不同的网址路由会分配到不同的视图函数。

定义视图函数

视图就是一个 Python 函数,被定义在 views.py 中。

打开 booktest/views.py 文件,定义视图 index 如下:

1
2
3
4
5
6
7
8
from django.shortcuts import render

# Create your views here.

from django.http import HttpResponse

def index(request):
return HttpResponse('hello python')

url 配置语法

一个简单的示例:

1
2
3
4
5
6
7
8
9
# views.py
from django.shortcuts import render

# Create your views here.

from django.http import HttpResponse

def index(request):
return HttpResponse('hello python')
1
2
3
4
5
6
7
8
9
# urls.py
from django.contrib import admin
from django.urls import path
from booktest.views import index

urlpatterns = [
path('admin/', admin.site.urls),
path('', index) # 仅做教学示例
]

回到主页,浏览器显示 hello python

模板

模板不仅仅是一个 html 文件。

模板文件的使用

创建模板文件夹,名字为 templates,与 booktest 在同一个路径级别。

settings.py 中,添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [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',
],
},
},
]

在 templates 下新建一个 index.html,并写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>

<body>
<h1>这是一个模板文件</h1>
</body>

</html>

views.pyurls.py 作如下改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# views.py
from django.shortcuts import render

# Create your views here.

from django.http import HttpResponse
from django.template import loader,RequestContext

def index(request):
# return HttpResponse('hello python')
return myrender(request, 'index.html')

def index2(request):
return HttpResponse('hello python')

def myrender(request, template_path, context_dict={}):
# 1.加载模板文件, 模板对象
temp = loader.get_template(template_path)

# 2.定义模板上下文:给模板文件传递数据,模板渲染:产生标准的 html 内容
res_html = temp.render(context_dict)

# 3.返回给浏览器
return HttpResponse(res_html)
1
2
3
4
5
6
7
8
9
10
# urls.py
from django.contrib import admin
from django.urls import path
from booktest import views

urlpatterns = [
path('admin/', admin.site.urls),
path('index/', views.index),
path('index2/', views.index2),
]

刷新页面,可以看到效果。

现在,对 index.html 作如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>

<body>
<h1>这是一个模板文件</h1>
使用模板变量:<br>
{{ content }}<br>
</body>

</html>

其实我们是不需要 myrender 的,上面写 myrender 的目的是为了理解 render 帮我们做了什么。我们可以直接改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# views.py
from django.shortcuts import render

# Create your views here.

from django.http import HttpResponse
# from django.template import loader,RequestContext

def index(request):
# return HttpResponse('hello python')
return render(request, 'index.html', {'content':'hello world'})

def index2(request):
return HttpResponse('hello python')

# def myrender(request, template_path, context_dict={}):
# # 1.加载模板文件, 模板对象
# temp = loader.get_template(template_path)
#
# # 2.定义模板上下文:给模板文件传递数据,模板渲染:产生标准的 html 内容
# res_html = temp.render(context_dict)
#
# # 3.返回给浏览器
# return HttpResponse(res_html)

效果:

89-12.png

模板文件进阶用法

下面实现了一个 for 循环,直接看例子,简洁明了。

1
2
3
4
5
6
7
8
# urls.py
# 前略
urlpatterns = [
path('admin/', admin.site.urls),
path('index/', views.index),
path('index2/', views.index2),
path('books/', views.show_books),
]
1
2
3
4
5
# views.py
# 前略
def show_books(request):
books = BookInfo.objects.all()
return render(request, 'showbooks.html', {'books': books})

在 templates 文件夹下新增 showbooks.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>显示图书信息</title>
</head>

<body>
<ul>
{% for book in books %}
<li>{{ book.btitle }}</li>
{% endfor %}
</ul>
</body>

</html>

效果:

89-13.png

现在,我们尝试实现更高级的功能:把这两个文本做成超链接,点开之后可以查看详情。

为了做成超链接,首先要修改 showbooks.html 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>显示图书信息</title>
</head>

<body>
<ul>
{% for book in books %}
<li><a href="/books/{{ book.id }}"> {{ book.btitle }} </a></li>
{% endfor %}
</ul>
</body>

</html>

然后,在 urls.py 中增加路由信息:

1
2
3
4
5
6
7
8
# 前略
urlpatterns = [
path('admin/', admin.site.urls),
path('index/', views.index),
path('index2/', views.index2),
path('books/', views.show_books),
path('books/<int:bid>',views.detail)
]

我们将 detail 函数设计为:

1
2
3
4
5
6
# views.py
# 前略
def detail(request, bid):
book = BookInfo.objects.get(id = bid)
heros = book.heroinfo_set.all()
return render(request, 'detail.html', {'book':book, 'heros':heros})

最后,detail.html 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>图书详情</title>
</head>

<body>
<h1>{{ book.btitle }}</h1>
英雄信息如下: <br/>
<ul>
{% for hero in heros %}
<li> {{hero.hname}} --- {{hero.hcomment}} </li>
{% empty %}
<li> 没有英雄信息 </li>
{% endfor %}
</ul>
</body>

</html>

效果如下:

89-14.jpeg

{% empty %} 有什么用?我们删除”应龙“的数据,然后点击”三哼经“的链接,就会显示”没有英雄信息“。

数据库配置

现在我们将 sqlite 切换为 mysql.

在 settings.py 中修改:

1
2
3
4
5
6
7
8
9
10
11
12
DATABASES = {
'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
'ENGINE': 'django.db.backends.mysql',
'NAME': 'test2', # 使用的数据库的名字,数据库必须手动创建
'USER': 'root', # 链接 mysql 的用户名
'PASSWORD': 'jtsws', # 用户对应的密码
'HOST': 'localhost', # 指定 mysql 数据库所在电脑 ip
'PORT': 3306, # mysql 服务的端口号
}
}

接下来安装(我不确定这两个是否有用,有可能你只需要执行接下来的一个命令):

1
2
sudo apt install libssl-dev
sudo apt install libmysqlclient-dev

执行:

1
sudo apt-get install pkg-config python3-dev default-libmysqlclient-dev build-essential

然后:

1
pip install mysqlclient

相关讨论参见:
https://stackoverflow.com/questions/76585758/mysqlclient-cannot-install-via-pip-cannot-find-pkg-config-name-in-ubuntu

生成迁移文件、执行迁移:

1
2
python3 manage.py makemigrations
python3 manage.py migrate

我们可以看到,test2(原本为空)中多出了很多 table :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql> show tables;
+----------------------------+
| Tables_in_test2 |
+----------------------------+
| auth_group |
| auth_group_permissions |
| auth_permission |
| auth_user |
| auth_user_groups |
| auth_user_user_permissions |
| booktest_bookinfo |
| booktest_heroinfo |
| django_admin_log |
| django_content_type |
| django_migrations |
| django_session |
+----------------------------+
12 rows in set (0.00 sec)

mysql> select * from booktest_bookinfo
-> ;
Empty set (0.00 sec)

为了演示方便,我们接下来仍然使用 sqlite.

更多细节的演示

btw, 不使用 SQLite Viewer 了,使用更强大的 vscode 插件 SQLite3 Editor(by yy0931).

实现效果:

89-15.png

点击“新增”会新增一本指定的书《C 语言开发宝典》(同时数据库中也删除);点击书名背后的删除会删除该书(同时数据库中也删除)。

models.py 更新为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from django.db import models

# Create your models here.

class BookInfo(models.Model):
btitle = models.CharField(max_length= 20)
bpub_data = models.DateField()
# 阅读量,default 是在 django 的逻辑层(模型类层),而不是数据库中
bread = models.IntegerField(default=0)

# 价格,最大位数为 10,小数为 2
bprice = models.DecimalField(max_digits=10, decimal_places=2, default=0, null=True)

# 评论量
bcomment = models.IntegerField(default=0)

# 删除标记
isDelete = models.BooleanField(default=False)

# 重写 str 后,打印对象会得到 return 返回的内容
def __str__(self) -> str:
return self.btitle

class HeroInfo(models.Model):
hname = models.CharField(max_length=20)
hgender = models.BooleanField(default=False)
hcomment = models.CharField(max_length=100)
isDelete = models.BooleanField(default=False)
# on_delete=models.CASCADEon_delete=models.CASCADE
# 删除 BookInfo 里面的书籍时,会自动删除依赖该书籍的英雄信息
hbook = models.ForeignKey('BookInfo',on_delete=models.CASCADE,)

# 暂定
def __str__(self) -> str:
return self.hname

修改 models 文件后需要重新 migrate.

修改 showbooks.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>显示图书信息</title>
</head>

<body>
<a href="/create">新增</a>
<ul>
{% for book in books %}
<li><a href="/books/{{ book.id }}"> {{ book.btitle }} </a>---<a href="/delete{{ book.id }}">删除</a></li>
{% endfor %}
</ul>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# urls.py
from django.contrib import admin
from django.urls import path
from booktest import views

urlpatterns = [
path('admin/', admin.site.urls),
path('index/', views.index),
path('index2/', views.index2),
path('books/', views.show_books),
path('books/<int:bid>', views.detail),
path('create/', views.create),
path('delete<int:bid>', views.delete),
]

最后,views.py 中也要添加相应的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from django.shortcuts import render

# Create your views here.

from django.http import HttpResponse, HttpResponseRedirect
from booktest.models import BookInfo
from datetime import date

def index(request):
# return HttpResponse('hello python')
return render(request, 'index.html', {'content':'hello world'})

def index2(request):
return HttpResponse('hello python')

def show_books(request):
books = BookInfo.objects.all()
return render(request, 'showbooks.html', {'books': books})

def detail(request, bid):
book = BookInfo.objects.get(id = bid)
heros = book.heroinfo_set.all()
return render(request, 'detail.html', {'book':book, 'heros':heros})

def create(request):
'''新增一本图书'''
# 1.创建 BookInfo 对象
b = BookInfo()
b.btitle = 'C 语言开发宝典'
b.bpub_data = date(2019,10,1)
# 2.保存进数据库
b.save()
# 3.返回应答,让浏览器再访问/books,重定向
return HttpResponseRedirect('/books')

def delete(request, bid):
'''删除点击的图书'''
book = BookInfo.objects.get(id = bid)
book.delete()
return HttpResponseRedirect('/books')

字段属性和选项

模型类属性命名限制

  • 不能是 python 的保留关键字。
  • 不允许使用连续的下划线,这是由 django 的查询方式决定的。比如 Books__Info 是不可以的。
  • 定义属性时需要指定字段类型,通过字段类型的参数指定选项,语法如下:
    属性名=models.字段类型(选项)

字段类型

官方文档:
https://docs.djangoproject.com/en/4.2/ref/models/fields/#field-types

使用时需要引入 django.db.models 包,几个常用的如下:

  • AutoField 自动增长的 IntegerField,通常不用指定,不指定时 Django 会自动创建属性名为 id 的自动增长属性。
  • BooleanField 布尔字段,值为 True 或 False。
  • NullBooleanField 支持 Null、True、False 三种值。
  • CharField(max_length=最大长度) 字符串。参数 max_length 表示最大字符个数。
  • TextField 大文本字段,一般超过 4000 个字符时使用。
  • IntegerField 整数。
  • DecimalField(max_digits=None,decimal_places=None) 十进制浮点数。参数 max_digits 表示总位。参数 decimal_places 表示小数位数。(精度较高,建议用这个)
  • FloatField 浮点数。参数同上(精度不够)。
  • DateFieldTimeFieldDateTimeField
  • FileField 上传文件字段。
  • ImageField 继承于 FileField,对上传的内容进行校验,确保是有效的图片。

选项

通过选项实现对字段的约束。

官网可查:
https://docs.djangoproject.com/en/4.2/ref/models/fields/

对比: null 是数据库范畴的概念,blank 是后台管理页面表单验证范畴的。

查询

查询函数

通过 模型类.objects 属性可以调用如下函数,实现对模型类对应的数据表的查询。

get 函数:

  • 返回表中满足条件的一条且只能有一条数据
  • 返回值是一个模型类对象
  • 参数中写查询条件
    • 如果查到多条数据,则抛异常 MultipleObjectsReturned
    • 查询不到数据,则抛异常 DoesNotExist

all 函数:

  • 返回模型类对应表格中的所有数据
  • 返回值是 QuerySet 类型
  • 查询集,可以拿出来进行遍历

filter 函数:

  • 返回满足条件的数据
  • 返回值是QuerySet类型
  • 参数写查询条件

exclude 函数:

  • 返回不满足条件的数据

order_by 函数:

  • 对查询结果进行排序
  • 返回值是 QuerySet
  • 参数中写根据哪些字段进行排序

下面展示一些例子:

1
2
3
4
>>> from booktest.models import *
>>> c = BookInfo.objects.filter(bcomment=34)
>>> c
<QuerySet [<BookInfo: 射雕英雄传>]>

模糊查询:

1
2
3
4
5
6
7
>>> b = BookInfo.objects.filter(btitle__contains='传')
>>> b
<QuerySet [<BookInfo: 射雕英雄传>]>

>>> b = BookInfo.objects.filter(btitle__endswith='部')
>>> b
<QuerySet [<BookInfo: 天龙八部>]>

空查询:

1
2
3
4
5
# 刚刚给这本书加了点价格

>>> b= BookInfo.objects.filter(bprice__isnull=False)
>>> b
<QuerySet [<BookInfo: C 语言开发宝典>]>

范围查询:

1
2
3
>>> b = BookInfo.objects.filter(id__in = [1,3,5])
>>> b
<QuerySet [<BookInfo: 射雕英雄传>, <BookInfo: 笑傲江湖>, <BookInfo: C 语言开发宝典>]>

比较查询:

1
2
3
>>> b = BookInfo.objects.filter(id__gt=3)
>>> b
<QuerySet [<BookInfo: 雪山飞狐>, <BookInfo: C 语言开发宝典>]>

F 对象

作用:用于对象属性之间的比较。

1
2
3
4
>>> from django.db.models import F
>>> from booktest.models import *
>>> BookInfo.objects.filter(bread__gt=F('bcomment'))
<QuerySet [<BookInfo: 雪山飞狐>]>
1
2
>>> BookInfo.objects.filter(bread__gt=F('bcomment')*2)
<QuerySet [<BookInfo: 雪山飞狐>]>

没有问题:

89-16.png

Q 对象

作用:用于查询时条件之间的逻辑关系。not、and、or,可以对 Q 对象进行 &|~ 操作(和 C 语言对应的运算符)。

我们改变一下演示的方式。

1
2
3
4
5
6
7
8
# views.py
# 前略
from django.db.models import F,Q
def index2(request):
# 练习 Q 对象
print(BookInfo.objects.filter(Q(id__gt=2) & Q(bread__gt=19)))
return HttpResponse('hello python')
# 后略

访问 /index2 , 我们可以在终端中的一堆打印信息中看到:

1
<QuerySet [<BookInfo: 笑傲江湖>, <BookInfo: 雪山飞狐>]>

聚合函数

作用:对查询结果进行聚合操作。

我们在 views.py 中演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 前略
from django.db.models import Sum,Count,Max,Min,Avg

def use_aggregate(request):
print(BookInfo.objects.all().aggregate(Count('id')))
print(BookInfo.objects.aggregate(Sum('bread')))

# count() 的特殊待遇
print(BookInfo.objects.all().count())
print(BookInfo.objects.count())

print(BookInfo.objects.filter(id__gt=3).count())

return HttpResponse('ok')

同时 urls.py 中添加路由:

1
2
3
4
5
6
7
8
9
10
11
# 前略
urlpatterns = [
path('admin/', admin.site.urls),
path('index/', views.index),
path('index2/', views.index2),
path('books/', views.show_books),
path('books/<int:bid>', views.detail),
path('create/', views.create),
path('delete<int:bid>', views.delete),
path('aggregate/',views.use_aggregate), # 新增
]

页面 http://127.0.0.1:8000/aggregate/ 返回 ok;终端中看到:

1
2
3
4
5
{'id__count': 5}
{'bread__sum': 126}
5
5
2

多对多关系

models.py 中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 前略
class NewsType(models.Model):
# 类型名
type_name = models.CharField(max_length=20)
# 关系属性,代表类型下面的信息
type_news = models.ManyToManyField('NewsInfo')
# 新闻类

class NewsInfo(models.Model):
# 新闻标题
title = models.CharField(max_length=128)
# 发布时间
pub_date = models.DateTimeField(auto_now_add=True)
# 信息内容
content = models.TextField()
# 关系属性, 代表信息所属的类型,注意不能和上面的同时开启
#news_type = models.ManyToManyField('NewsType')

多对多会生成三张表。

迁移之后,查看数据库:

89-17.jpeg

建表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> from booktest.models import *
>>> n = NewsType()
>>> n.type_name = 'IT'
>>> n.save()
>>> n = NewsType()
>>> n.type_name = '考研'
>>> n.save()
>>> i = NewsInfo()
>>> i.title = '408上热搜'
>>> i.content = '计算机卷疯了'
>>> i.save()
>>> i = NewsInfo()
>>> i.title = '金毛师王兴趣转移'
>>> i.content = '将诺贝尔奖挂在嘴边'
>>> i.save()
>>> i = NewsInfo()
>>> i.title = '神秘男子在武当山接引雷劫'
>>> i.content = '雷击木附近出现神秘舍利子'
>>> i.save()

在第三张表中添加多对多关系:

1
2
3
>>> n = NewsType.objects.get(id = 1)
>>> n.type_news.add(2)
>>> n.type_news.remove(2)

(现在第三张表为空)

第二种方法:

1
2
3
4
5
6
# n 还是之前的 n

>>> i = NewsInfo.objects.filter(id__lt=3)
>>> i
<QuerySet [<NewsInfo: NewsInfo object (1)>, <NewsInfo: NewsInfo object (2)>]>
>>> n.type_news.set(i)

89-18.png

关联查询

89-19.png

自关联

89-20.png

我们设计一个类:

1
2
3
4
5
6
7
8
9
# models.py

# 自关联的模型类设计
class Areas(models.Model):
'''地区模型类'''
# 地区名称
atitle = models.CharField(max_length=20)
# 关系属性,代表当前地区的父级地区
aParent = models.ForeignKey('self', null=True,blank=True,on_delete=models.CASCADE,)

迁移之后,我们尝试在表中导入数据(一个 sql 文件)。

安装 sqlite3 命令行工具:

1
sudo apt install sqlite3

进入虚拟环境,进入包含 db.sqlite3(即 django 自带的那个数据库) 的目录。

执行:

1
sqlite3 db.sqlite3

执行上面的命令后,会进入 SQLite 命令行中。

在 SQLite 命令行中,运行以下命令:

1
.read /home/zhiyue/Downloads/area.sql

可能会出现 “database is locked” 错误,因为 SQLite 数据库文件被锁定。这个时候可以多试几次上面的命令,会补全的。

一睹芳容:

89-21.png

上面这张图也可以帮助理解什么叫自关联。

编写模板页面 area.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en"></html>
<head>
<meta charset="UTF-8">
<title>自关联案例</title>
</head>
<body>
<h1>当前地区</h1>
{{ area.atitle }}<br/>
<h1>父级地区</h1>
{{ parent.atitle }}<br/>
<h1>下级地址</h1>
<ul>
{% for child in children %}
<li>{{ child.atitle }}</li>
{% endfor %}
</ul>
</body>
</html>

编写 views 函数:

1
2
3
4
5
6
7
8
9
10
def areas(request):
'''获取广州市的上级地区和下级地区'''
# 1.获取广州市的信息
area = Areas.objects.get(atitle='广州市')
# 2.查询广州市的上级地区
parent = area.aParent
# 3.查询广州市的下级地址
children = area.areas_set.all()
# 使用模板
return render(request, 'area.html', {'area':area,'parent':parent, 'children':children})

最后,配置 urls.

效果:

89-22.png

管理器

BookInfo.objects.all()->objects 是一个什么东西呢?

  • objects 是 Django 帮我自动生成的管理器对象,通过这个管理器可以实现对数据的查询。

objects 是 models.Manger 类的一个对象,是 models.Model 的一个属性。

自定义管理器之后 Django 不再帮我们生成默认的 objects 管理器。

下面我们尝试自定义模型管理器类

需求
我们要实现软删除。并非在数据库中删除数据,而是将 isDelete 字段设为 1 ,这样的字段不会在 all 查询中被查询到。

models.py 中新增管理器类,然后在 BookInfo 类中重写 objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 新增
class BookInfoManager(models.Manager):
'''图书模型管理器类'''
# 1.改变原有查询的结果集
def all(self):
# 1.调用父类的 all 方法,获取所有数据
books = super().all() # QuerySet
# 2.对 books 中的数据进行过滤
books = books.filter(isDelete=False)
# 返回 books
return books

# ......

class BookInfo(models.Model):
# ......

# override 了 objects
objects = BookInfoManager()

# ......

# ......

我们将《C语言开发宝典》的 isDelete 设为 1,然后执行:

1
2
3
>>> from booktest.models import *
>>> BookInfo.objects.all()
<QuerySet [<BookInfo: 射雕英雄传>, <BookInfo: 天龙八部>, <BookInfo: 笑傲江湖>, <BookInfo: 雪山飞狐>]>

需求
我们希望改进新增数据的方法,之前在命令行里一个一个敲非常麻烦。

这里我直接更新一版 models.py ,新增的功能在里面了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from django.db import models

# Create your models here.

class BookInfoManager(models.Manager):
'''图书模型管理器类'''
# 1.改变原有查询的结果集
def all(self):
# 1.调用父类的 all 方法,获取所有数据
books = super().all() # QuerySet
# 2.对 books 中的数据进行过滤
books = books.filter(isDelete=False)
# 返回 books
return books

def create_book(self, btitle, bpub_data):
'''添加一本图书'''
# 1.创建一个图书对象
# 获取 self 所在的模型类
model_class = self.model
book = model_class()
# book = BookInfo()
book.btitle = btitle
book.bpub_data = bpub_data
# 2.添加进数据库
book.save()
# 3.返回 book
return book


class BookInfo(models.Model):
btitle = models.CharField(max_length= 20)
bpub_data = models.DateField()
# 阅读量,default 是在 django 的逻辑层(模型类层),而不是数据库中
bread = models.IntegerField(default=0)

# 价格,最大位数为 10,小数为 2
bprice = models.DecimalField(max_digits=10, decimal_places=2, default=0, null=True)

# 评论量
bcomment = models.IntegerField(default=0)

# 删除标记
isDelete = models.BooleanField(default=False)

# override 了 objects
objects = BookInfoManager()

# 重写 str 后,打印对象会得到 return 返回的内容
def __str__(self) -> str:
return self.btitle

class HeroInfo(models.Model):
hname = models.CharField(max_length=20)
hgender = models.BooleanField(default=False)
hcomment = models.CharField(max_length=100)
isDelete = models.BooleanField(default=False)
# on_delete=models.CASCADEon_delete=models.CASCADE
# 删除 BookInfo 里面的书籍时,会自动删除依赖该书籍的英雄信息
hbook = models.ForeignKey('BookInfo',on_delete=models.CASCADE,)

# 暂定
def __str__(self) -> str:
return self.hname

class NewsType(models.Model):
# 类型名
type_name = models.CharField(max_length=20)
# 关系属性,代表类型下面的信息
type_news = models.ManyToManyField('NewsInfo')
# 新闻类

def __str__(self):
return self.type_name

class NewsInfo(models.Model):
# 新闻标题
title = models.CharField(max_length=128)
# 发布时间,自动添加
pub_date = models.DateTimeField(auto_now_add=True)
# 信息内容
content = models.TextField()
# 关系属性, 代表信息所属的类型,注意不能和上面的同时开启
#news_type = models.ManyToManyField('NewsType')

# 自关联的模型类设计
class Areas(models.Model):
'''地区模型类'''
# 地区名称
atitle = models.CharField(max_length=20)
# 关系属性,代表当前地区的父级地区
aParent = models.ForeignKey('self', null=True,blank=True,on_delete=models.CASCADE,)

新增功能之后,可以:

1
2
3
4
>>> from booktest.models import *
>>> from datetime import date
>>> BookInfo.objects.create_book('日月前事', date(2019, 1, 1))
<BookInfo: 日月前事>

在数据库中,可以看到这本书已经添加了进去。

元选项

Django 默认生成的表名: 应用名小写_模型类名小写

元选项可以更改表名。

例如,我们在 BookInfo 模型类中增加如下代码:

1
2
3
4
class BookInfo(models.Model):
# ......
class Meta:
db_table = 'bookinfo' # 指定模型类对应表名

404 页面

我们设计一个 404 页面。需要在 settings.py 中把 DEBUG 改为 FALSE. 然后:

1
ALLOWED_HOST=['*'] # 允许绑定的 IP 地址列表

404 页面设计如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - 找不到页面</title>
<style>
body {
background-image: url('http://img.netbian.com/file/2017/0326/64aab4ae3e632dbcbf9223995c654317.jpg');
background-size: cover;
color: white;
font-family: 'Arial', sans-serif;
text-align: center;
padding: 200px;
}
h1 {
font-size: 3em;
}
p {
font-size: 1.5em;
}
a {
color: #ffcc00;
text-decoration: none;
font-weight: bold;
}
</style>
</head>
<body>
<h1>404 页面未找到</h1>
<p>找不到页面 {{ request_path }}</p>
<p><a href="/">返回主页</a></p>
</body>
</html>

我们仍回到 DEBUG 模式。

捕获 url 参数

官方文档:
https://docs.djangoproject.com/en/4.2/topics/http/urls/

例子:

1
2
3
4
5
6
7
8
9
10
from django.urls import path

from . import views

urlpatterns = [
path("articles/2003/", views.special_case_2003),
path("articles/<int:year>/", views.year_archive),
path("articles/<int:year>/<int:month>/", views.month_archive),
path("articles/<int:year>/<int:month>/<slug:slug>/", views.article_detail),
]

官方文档里写的很清楚了,不再搬运。

设计登录页面

实战

设计一个 login.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login page</title>
</head>
<body>
<form method="post" action="/login_check/">
用户名:<input type="text" name="username" value="{{ username }}"><br/>
密码:<input type="password" name="password"><br/>
<input type="checkbox" name="remember">记住用户名<br/>
<input type="submit" value="登录">
</form>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# urls.py
# ......
urlpatterns = [
path('admin/', admin.site.urls),
path('index/', views.index),
path('index2/', views.index2),
path('books/', views.show_books),
path('books/<int:bid>', views.detail),
path('create/', views.create),
path('delete<int:bid>', views.delete),
path('aggregate/',views.use_aggregate),
path('areas/', views.areas),
path('login/', views.login), # add
path('login_check/', views.login_check), # add
]
1
2
3
4
5
6
# views.py
def login(request):
return render(request, 'login.html')

def login_check(request):
pass # 暂时先这样,这里打了断点

填写数据后点击登录按钮:

89-23.png

避免 CSRF 报错的方法是注释掉 settings 中的校验

  • 仅出于演示目的。实际上线时为了安全,不能这样做。
1
2
3
4
5
6
7
8
9
10
# 演示目的
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',
]

HttpReqeust 对象

服务器接收到 http 协议的请求后,会根据报文创建 HttpRequest 对象,这个对象不需要我们创建,直接使用服务器构造好的对象就可以。视图的第一个参数必须是 HttpRequest 对象,在 django.http 模块中定义了 HttpRequest 对象的API。

属性

  • path: 一个字符串,表示请求的页面的完整路径,不包含域名和参数部分
  • method: 一个字符串,表示请求使用的 HTTP 方法,常用值包括:GETPOSTPUTDETELE
    • 在浏览器中发出地址请求采用 get 方式,如超链接
    • 在浏览器中点击表单的提交按钮发起请求,如果表单的 method 设置为 post, 则为 post 请求
  • encoding: 一个字符串,表示提交的数据的编码方式
    • 这个属性是可写的
  • GET: QueryDict 类型对象,类似于字典,包含 get 请求方式的所有参数
  • POST: QueryDict 类型对象,类似于字典,包含 post 请求方式的所有参数
  • FILES: 一个类似于字典的对象,包含所有的上传文件
  • COOKIES: 一个标准的 Python 字典,包含所有的 cookie,键和值都为字符串
  • session: 一个既可读又可写的类似于字典的对象,表示当前的会话,只有当 Django 启用会话的支持时才可用,详细内容见”状态保持”

关于 GET 和 POST 的更多细节:

89-24.jpeg

用调试模式演示一下。打上断点,在浏览器输入:

1
http://127.0.0.1:8000/login/?a=10&b=20&c=python

(实际上 ? 前面的 / 可能要去掉?它好像是之后生成的,存疑)

我们可以看到 GET 拿到了这些数据:

89-25.png

HttpResponse 对象

视图在接收请求并处理后,必须返回 HttpResponse 对象或子对象。

属性

  • content:表示返回的内容
  • charset:表示 response 采用的编码字符集,默认为 utf-8
  • status_code:返回的 HTTP 响应状态码
  • content-type:指定返回数据的的 MIME 类型,默认为’text/html’

实战(续)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# views.py
# 前略
def login(request):
# num = 1
return render(request, 'login.html')

def login_check(request):
'''登录校验视图'''
# request.POST 保存的是 post 方式提交的参数 QueryDict
# request.GET 保存是 get 方式提交的参数 类型也是 QueryDict
username=request.POST.get('username')
password=request.POST.get('password')
print(username+':'+password)
return HttpResponse('ok')

在前端点击提交按钮后,后端的终端显示:

1
2
3
asdf:asdf

# 这是我设置的用户名和密码

演示一个简单的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# views.py
# 前略
def login_check(request):
'''登录校验视图'''
# request.POST 保存的是 post 方式提交的参数 QueryDict
# request.GET 保存是 get 方式提交的参数 类型也是 QueryDict
username=request.POST.get('username')
password=request.POST.get('password')

# just an example
if username == 'akashi' and password == '123':
return HttpResponseRedirect('/index')
else:
return HttpResponseRedirect('/login')

Ajax

基本概念

异步的 javascript。在不全部加载某一个页面的情况下,对页面进行局部的刷新,ajax 请求都在后台。

图片,css 文件,js 文件都是静态文件。

89-26.png

大致流程:

  1. 发起 ajax 请求:jquery(某个老旧的前端框架) 发起
  2. 执行相应的视图函数,返回 json 内容
  3. 执行相应的回调函数。通过判断 json 内容,进行相应处理。

简单示例

在 templates 文件夹下新建 test_ajax.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ajax 页面</title>
<script src="/static/js/jquery-1.12.4.min.js"></script>
<script>
$(function () {
// 绑定 btnAjax 的 click 事件
// {#alert(1)#}
$('#btnAjax').click(function () {
$.ajax({
'url': '/ajax_handle',
'dataType': 'json',
'async': false, // 同步的 ajax 请求
}).success(function (data) {
// 进行处理
// {#alert(2)#}
if (data.res == 1){
$('#message').show().html('提示信息')
}
})
// {#alert(3)#}
})
})
</script>
<style>
#message {
display: none;
color: red;
}
</style>
</head>
<body>
<input type="button" id="btnAjax" value="ajax 请求">
<div id="message"></div>
</body>
</html>

建立 templates 的同级文件夹 static, 下面再设 js 文件夹。将 jquery-1.12.4.min.js 放入其中。

views.py 中新增:

1
2
3
def test_ajax(request):
'''显示 ajax 页面'''
return render(request, 'test_ajax.html')

同时在 urls.py 中配置好路由。

在 settings 中添加:

1
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # 静态文件的保存目录

这时运行,会提示 ajax_handle 404 Not Found,合理。

views.py 中新增:

1
2
3
4
5
6
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse

def ajax_handle(request):
'''ajax 请求处理'''
# 返回的 json 数据 {'res':1}
return JsonResponse({'res':1})

然后新增路由:

1
path('ajax_handle/', views.ajax_handle)

运行,在点击按钮后,页面不加载的情况下,按钮下方多出了红色的字“提示信息”。

89-27.png

关于:

1
'async': false, // 同步的 ajax 请求

这句代码,打开这个开关,在 html 文件中打开调试代码的注释,再运行。可以看到弹出窗口的消息提示顺序从“123”变成了“132”。

Ajax 登录案例

尝试用 ajax 做一个登录:若用户输入错误,则不刷新页面,提示错误。

增加路由:

1
path('login_ajax', views.login_ajax),

设计视图:

1
2
def login_ajax(request):
return render(request, 'login_ajax.html')

设计 html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ajax 登录</title>

<script src="/static/js/jquery-1.12.4.min.js"></script>
<script>
$(function () {
$('#btnLogin').click(function () {
// 1.获取用户名和密码
username = $('#username').val()
password = $('#password').val()
// 2.发起 post ajax 请求,/login_ajax_check, 携带用户名和密码
$.ajax({
'url':'/login_ajax_check/', // 当是 post 请求时默认多写一个/让 urls 匹配保持一致
'type': 'post',
'data': {'username':username,'password':password},
'dataType': 'json'
}).success(function (data) {
// 登录成功 {'res':1}
// 登录失败 {'res':0}
if (data.res == 0){
$('#errmsg').show().html(' 用 户 名 或 密码错误')
}
else{
// 跳转到首页
location.href = '/index'
}
})
})
})
</script>
<style>
#errmsg{
display: none;
color: red;
}
</style>
</head>

<body>
<div>
用户名:<input type="text" id="username"><br/>
密码:<input type="password" id="password"><br/>
<input type="button" id="btnLogin" value="登录">
<div id="errmsg"></div>
</div>
</body>

</html>

设计登录校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# views.py
def login_ajax_check(request):
'''ajax 登录校验'''
# 1.获取用户名和密码
username = request.POST.get('username')
password = request.POST.get('password')
# 2.进行校验,返回 json 数据
if username == 'admin' and password == '123':
# 用户名密码正确
return JsonResponse({'res':1})
# return redirect('/index') ajax 请求在后台,不要返回页面或者重定向,这样是不行的,一定要返回 Json!
else:
# 用户名或密码错误
return JsonResponse({'res':0})

不用忘了配置路由:

1
path('login_ajax_check/', views.login_ajax_check),

运行,成功。

状态保持

http 协议是无状态的。下一次去访问一个页面时并不知道上一次对这个页面做了什么。

基本知识

89-28.png

cookie 的特点:

  1. 键值对方式进行存储。
  2. 通过浏览器访问一个网站时,会将浏览器存储的跟网站相关的所有 cookie 信息发送给该网站的服务器。request.COOKIES
  3. cookie 是基于域名安全的。
  4. cookie 是有过期时间的,如果不指定,默认关闭浏览器之后 cookie 就会过期。

典型应用:记住用户名,网站的广告推送。

说明:这些广告推送的商品是基于你曾经在淘宝上点击的商品类别等条件筛选出来的,看上去这是在凤凰网上访问淘宝网的 Cookie,但是事实不是这样的,一般是采用 iframe 标签嵌套一个淘宝的广告页面到凤凰网的页面上,所以淘宝的 Cookie 并没有被凤凰网读取到,而是依然交给淘宝网读取的,可以通过”开发者工具”查看元素,如下图:

89-29.png

配置路由:

1
path('set_cookie/', views.set_cookie),

views.py 中:

1
2
3
4
5
6
7
8
9
10
11
12
from datetime import datetime,timedelta

def set_cookie(request):
'''设置 cookie 信息'''
response = HttpResponse('设置 cookie')
# 设置一个 cookie 信息,名字为 num, 值为 2
response.set_cookie('num', 2)
#下面是设置 cookie 在两周之后过期
# response.set_cookie('num', 2, max_age=14*24*3600)
# response.set_cookie('num', 1,expires=datetime.now()+timedelta(days=14))
# 返回 response
return response

运行,访问 /set_cookie 页面之后,再去该网站的其他页面,都可以在 F12 中看到多了我们自己添加的 cookie.

获取 cookie:

1
2
3
4
5
6
7
# views.py
def get_cookie(request):
'''获取 cookie 的信息'''
# 取出 cookie num 的值
num = request.COOKIES['num']
print(num)
return HttpResponse(num)

同时配置路由:

1
path('get_cookie/', views.get_cookie),

记住用户名案例

如果在前面的 login.html 我们勾选了记住用户名,那么如何实现下次 login 的时候,用户名在里边呢?

views.py 中对原来的函数作如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def login(request):
if 'username' in request.COOKIES:
username = request.COOKIES['username']
else:
username = ''

return render(request, 'login.html', {'username':username})

def login_check(request):
'''登录校验视图'''
# request.POST 保存的是 post 方式提交的参数 QueryDict
# request.GET 保存是 get 方式提交的参数 类型也是 QueryDict
username=request.POST.get('username')
password=request.POST.get('password')
remember = request.POST.get('remember') # add

# just an example
if username == 'akashi' and password == '123':
resp = HttpResponseRedirect('/index')
if remember == 'on':
resp.set_cookie('username', username, max_age=7*24*3600)
return resp

else:
return HttpResponseRedirect('/login')

运行,可以发现实现效果。

Session

基本介绍

89-30.png

session 存储在服务器端。在服务器端进行状态保持的方案就是 Session .

session 的特点

  1. session 是以键值对进行存储的。
  2. session 依赖于 cookie。唯一的标识码 sessionid 保存在 cookie 中。
  3. session 也是有过期时间,如果不指定,默认两周就会过期。
  4. session 与 cookie 的差异,cookie 无论保存什么值进去,取出来都是字符串,session 保存进去什么类型,取出来就是什么类型。

Django 项目默认启用 Session,可以在 MIDDLEWARE 配置里找到。

设置 SESSION_ENGINE 项,指定 Session 数据存储的方式,可以存储在数据库、django 的缓存、Redis 等。

默认存储方式,存储在数据库中,如下设置可以写,也可以不写:

1
SESSION_ENGINE='django.contrib.sessions.backends.db'

存储在缓存中

1
SESSION_ENGINE='django.contrib.sessions.backends.cache'

混合存储,优先从本机内存中存取,如果没有则从数据库中存取:

1
SESSION_ENGINE='django.contrib.sessions.backends.cached_db'

如果存储在数据库中,需要在项 INSTALLED_APPS 中安装 Session 应用。

在使用 Session 后,会在 Cookie 中存储一个 sessionid 的数据,每次请求时浏览器都会将这个数据发给服务器,服务器在接收到 sessionid 后,会根据这个值找出这个请求者的 Session。

存储 Session 时,键与 Cookie 中的 sessionid 相同,值是开发人员设置的键值对信息,进行了 base64 编码,过期时间由开发人员设置。

实战

1
2
3
4
5
6
7
8
9
10
11
12
13
# views.py
def set_session(request):
'''设置 session'''
request.session['username'] = 'yomiya'
request.session['age'] = 17
# request.session.set_expiry(5)
return HttpResponse('设置 session')

def get_session(request):
'''获取 session'''
username = request.session['username']
age = request.session['age']
return HttpResponse(username+':'+str(age))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# urls.py
# 前略
urlpatterns = [
path('admin/', admin.site.urls),
path('index/', views.index),
path('index2/', views.index2),
path('books/', views.show_books),
path('books/<int:bid>', views.detail),
path('create/', views.create),
path('delete<int:bid>', views.delete),
path('aggregate/',views.use_aggregate),
path('areas/', views.areas),
path('login/', views.login),
path('login_check/', views.login_check),
path('test_ajax/', views.test_ajax),
path('ajax_handle/', views.ajax_handle),
path('login_ajax/', views.login_ajax),
path('login_ajax_check/', views.login_ajax_check),
path('set_cookie/', views.set_cookie),
path('get_cookie/', views.get_cookie),
path('set_session/', views.set_session), # add
path('get_session/', views.get_session), # add
]

访问 /set_session , 在 F12 中可以看到:

1
sessionid=xsjm51tmhguylsknjf53nvvzsf83hgui;

与数据库中的一致,同时数据库中 session_data 字段:

1
.eJxVjDsOgzAQRO_iOrLW2HhJyvQ5A1rWayAfW-JToCh3j5EoknJm3ry3amldhnadZWrHoC7KqNNv1xE_JO1DuFPqs-aclmns9I7oY531LQd5Xg_2TzDQPJQ3NghWjDccHfuavQC4M9mGEDxLtAiurkMHleFKIDj0ZCMRVIwSyRbprkv0kmLb8mvcqHTUl2jw8wWmp0Dd:1t1I46:CMmhgMpr67_q-bZvtptryM6HddKavAQEW83ZsPD4TQI

访问 /get_session, 浏览器显示:

1
yomiya:17

清除 session:

1
2
3
4
5
6
7
# views.py

# def clear_session(request):
# '''清除 session 信息'''
# # request.session.clear()
# # request.session.flush()
# return HttpResponse('清除成功')

记住用户登录状态案例

需求:已登录的用户在访问 /login 时,直接访问首页,不需要再输入用户名和密码。

修改 views.py 中的 login、login_check 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def login(request):
# 判断用户是否登录
if request.session.has_key('islogin'):
# 用户已登录, 跳转到首页
return HttpResponseRedirect('/index')

if 'username' in request.COOKIES:
username = request.COOKIES['username']
else:
username = ''

return render(request, 'login.html', {'username':username})

def login_check(request):
'''登录校验视图'''
# request.POST 保存的是 post 方式提交的参数 QueryDict
# request.GET 保存是 get 方式提交的参数 类型也是 QueryDict
username=request.POST.get('username')
password=request.POST.get('password')
remember = request.POST.get('remember')

# just an example
if username == 'akashi' and password == '123':
resp = HttpResponseRedirect('/index')
request.session['islogin'] = True # add
if remember == 'on':
resp.set_cookie('username', username, max_age=7*24*3600)
return resp

else:
return HttpResponseRedirect('/login')

cookie: 记住用户名。安全性要求不高

session: 涉及到安全性要求比较高的数据。如用户名、余额、等级、验证码等。

深度延伸,如果用户禁用 cookie,如何使用 session :
https://www.cnblogs.com/ceceliahappycoding/p/10544075.html

模板进阶

模板变量

1
2
3
4
5
6
7
8
9
10
# views.py

def test_var(request):
'''模板变量'''
my_dict = {'title':'字典键值'}
my_list = [1,2,3]
book = BookInfo.objects.get(id=1)
# 定义模板上下文
context = {'my_dict':my_dict, 'my_list':my_list, 'book':book}
return render(request, 'test_var.html', context)

templates 文件夹下 test_var.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学习模板变量</title>
</head>
<body>
使用字典属性:{{ my_dict.title }}<br/>
使用列表元素:{{ my_list.1 }}<br/>
使用对象属性:{{ book.btitle }}
</body>
</html>

然后配置路由。转到浏览器对于路径可以看到效果。

模板标签

可以通过 {{ forloop.counter }} 得到 for 循环遍历到了第几次。

1
2
3
4
{% if 条件 %}
{% elif 条件 %}
{% else %}
{% endif %}

注意:进行比较操作时,比较操作符两边必须有空格。

过滤器

格式:模板变量|过滤器:参数

在 templates 文件夹下新建 test_filters.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
{% load filters %}
<head>
<meta charset="UTF-8">
<title>模板过滤器</title>
<style>
.red {
background-color: red;
}
.yellow {
background-color: yellow;
}
.green {
background-color: green;
}
</style>
</head>
<body>
<ul>
{% for book in books %}
{% if book.id|mod %}
<li class="red">{{ book.id }}--{{ book.btitle|length }}--{{ book.bpub_data|date:'Y 年-m 月-d 日' }}</li>
{% else %}
<li class="green">{{ book.btitle }}--{{ book.bpub_data }}</li>
{% endif %}
{% endfor %}
</ul>

default 过滤器:<br/>
{{ content|default:'没有数据' }}
</body>
</html>

新建 templatetags/filters.py 文件,其中 templatetags 文件夹与 models.py 文件同级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# filters.py

# 自定义过滤器
# 过滤器其实就是 python 函数
from django.template import Library

# 创建一个 Library 类的对象
register = Library()

# 自定义的过滤器函数,至少有一个参数,最多两个
@register.filter
def mod(num):
'''判断 num 是否为偶数'''
return num%2 == 0

@register.filter
def mod_val(num, val):
'''判断 num 是否能被 val 整除'''
return num%val == 0
1
2
3
4
# views.py
def test_filters(request):
books = BookInfo.objects.all()
return render(request, 'test_filters.html', {'books':books})

配置路由。

注意配置自定义过滤器不会自动加载,必须重启 Django 服务

效果:

89-31.png

模板继承

模板继承是为了重用 html 页面内容。比如很多网站的头部导航条和底部版权版权信息不变的。

89-32.png

在 templates 文件夹下创建 base.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}父模板文件{% endblock title %}</title>
</head>
<body>
<h1>导航条</h1>
{% block b1 %}
<h1>这是父模板 b1 块中的内容</h1>
{% endblock b1 %}
{% block b2 %}
<h1>这是父模板 b2 块中的内容</h1>
{% endblock b2 %}
<h1>版权信息</h1>
</body>
</html>
1
2
3
# views.py
def test_template_inhert(request):
return render(request, "base.html")

配置路由。

在 templates 文件夹下创建 child.html :

1
2
3
4
5
6
7
8
9
10
11
{% extends 'base.html' %}
{% block title %}子模板文件{% endblock title %}
{% block b1 %}
{{ block.super }}
<h1>这是子模板 b1 块中的内容</h1>
{% endblock b1 %}

{% block b2 %}
{{ block.super }}
<h1>这是子模板 b2 块中的内容</h1>
{% endblock b2 %}
1
2
3
4
# views.py
def test_template_inhert(request):
# return render(request, "base.html")
return render(request, "child.html")

运行即可对比前后效果。

html 转义

增加 html_escape.html 页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>html 转义</title>
</head>
<body>
html 转义:<br/>
{{ content }}<br/>
使用 safe 过滤器关闭转义:<br/>
{{ content|safe }}<br/>
使用 autoescape 关闭转义:<br/>
{% autoescape off %}
{{ content }}
{{ content }}
{% endautoescape %}<br/>
模板硬编码中的字符串默认不会经过转义:<br/>
{{ test|default:'<h1>hello</h1>' }}<br/>
手动进行转义:<br/>
{{ test|default:'&lt;h1&gt;hello&lt;/h1&gt;' }}
</body>
</html>

增加视图函数 html_escape:

1
2
3
4
5
# views.py

def html_escape(request):
'''html 转义'''
return render(request, 'html_escape.html',{'content':'<h1>hello</h1>'})

效果:

89-33.png

csrf 攻击

CSRF 全拼为 Cross Site Request Forgery,译为跨站请求伪造。CSRF 指攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF 能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。

89-34.png

接下来我们演示这一攻击。

设计一个修改密码的页面 change_pwd.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>修改密码页面</title>
</head>
<body>
<form method="post" action="/change_pwd_action/">
<!-- {% csrf_token %} -->
新密码:<input type="password" name="pwd">
<input type="submit" value="确认修改">
</form>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
# views.py

def change_pwd(request):
return render(request, 'change_pwd.html')

def change_pwd_action(request):
'''模拟修改密码处理'''
# 1.获取新密码
pwd = request.POST.get('pwd')
# 2.返回一个应答
return HttpResponse('修改密码为:%s'%pwd)

配置路由。

这个时候,我们修改密码,可以成功。

但是这个时候,假如有一个猥琐黑客,直接访问了 /change_pwd_action ,就可以通过一些手段修改我们的密码,怎么办?

答案是凉拌。

不过还好,Django 框架为我们提供了预防这种攻击的方法。

在 settings 中打开 CsrfViewMiddleware :

1
2
3
4
5
6
7
8
9
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',
]

这个时候,再回到 /change_pwd 修改密码,就会被阻止。

我们把之前 change_pwd.html 的注释打开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>修改密码页面</title>
</head>
<body>
<form method="post" action="/change_pwd_action/">
{% csrf_token %}
新密码:<input type="password" name="pwd">
<input type="submit" value="确认修改">
</form>
</body>
</html>

这时候就可以成功修改了!

防御的大致原理是,只有在本页面发出的请求可以访问到 /change_pwd_action

89-35.jpeg

验证码

在用户注册、登录页面,为了防止暴力请求,可以加入验证码功能,如果验证码错误,则不需要继续处理,可以减轻业务服务器、数据库服务器的压力。

安装 Pillow :

1
pip install Pillow --proxy="http://127.0.0.1:7897"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# views.py

# /verify_code
from PIL import Image, ImageDraw, ImageFont
# from django.utils.six import BytesIO #django 3 以后丢弃了
def verify_code(request):
# 引入随机函数模块
import random
# 定义变量,用于画面的背景色、宽、高 RGB
bgcolor = (random.randrange(20, 100), random.randrange(20, 100), 255)
width = 100
height = 25
# 创建画面对象
im = Image.new('RGB', (width, height), bgcolor)
# 创建画笔对象
draw = ImageDraw.Draw(im)
# 调用画笔的 point()函数绘制噪点
for i in range(0, 100):
xy = (random.randrange(0, width), random.randrange(0, height))
fill = (random.randrange(0, 255), 255, random.randrange(0, 255))
draw.point(xy, fill=fill)

# 定义验证码的备选值
str1 = 'ABCD123EFGHIJK456LMNOPQRS789TUVWXYZ0'
# 随机选取 4 个值作为验证码
rand_str = ''
for i in range(0, 4):
rand_str += str1[random.randrange(0, len(str1))]

# 构造字体对象,ubuntu 的字体路径为“/usr/share/fonts/truetype/freefont”
font = ImageFont.truetype('FreeMono.ttf', 23)
# 构造字体颜色
fontcolor = (255, random.randrange(0, 255), random.randrange(0,255))
# 绘制 4 个字
draw.text((5, 2), rand_str[0], font=font, fill=fontcolor)
draw.text((25, 2), rand_str[1], font=font, fill=fontcolor)
draw.text((50, 2), rand_str[2], font=font, fill=fontcolor)
draw.text((75, 2), rand_str[3], font=font, fill=fontcolor)
# 释放画笔
del draw
# 存入 session,用于做进一步验证
request.session['verifycode'] = rand_str
# 内存文件操作
import io
buf = io.BytesIO()
# 将图片保存在内存中,文件类型为 png
im.save(buf, 'png')
# 将内存中的图片数据返回给客户端,MIME 类型为图片 png
return HttpResponse(buf.getvalue(), 'image/png')

配置路由。

修改 login.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login page</title>
</head>
<body>
<form method="post" action="/login_check/">
{% csrf_token %}
用户名:<input type="text" name="username" value="{{ username }}"><br/>
密码:<input type="password" name="password"><br/>
<img src="/verify_code"><input type="text" name="vcode"><br/>
<input type="checkbox" name="remember">记住用户名<br/>
<input type="submit" value="登录">
</form>
</body>
</html>

修改一些逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# views.py

def login(request):
# # 判断用户是否登录
# if request.session.has_key('islogin'):
# # 用户已登录, 跳转到首页
# return HttpResponseRedirect('/index')

if 'username' in request.COOKIES:
username = request.COOKIES['username']
else:
username = ''

return render(request, 'login.html', {'username':username})

def login_check(request):
'''登录校验视图'''
# 获取用户输入验证码
vcode1 = request.POST.get('vcode')
# 获取 session 中保存的验证码
vcode2 = request.session.get('verifycode')
# 进行验证码校验
if vcode1 != vcode2:
# 验证码错误
return HttpResponseRedirect('/login')

# request.POST 保存的是 post 方式提交的参数 QueryDict
# request.GET 保存是 get 方式提交的参数 类型也是 QueryDict
username=request.POST.get('username')
password=request.POST.get('password')
remember = request.POST.get('remember') # add

# just an example
if username == 'akashi' and password == '123':
resp = HttpResponseRedirect('/index')
request.session['islogin'] = True
if remember == 'on':
resp.set_cookie('username', username, max_age=7*24*3600)
return resp

else:
return HttpResponseRedirect('/login')

现在可以实现效果:验证码不通过则无法登录。

反向解析

当某一个 url 配置的地址发生变化时,页面上使用反向解析生成地址的位置不需要发生变化。

新建 url_reverse.html 页面,里边加入首页超链接:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
index 链接:<br/>
<a href="/index">首页</a><br/>
</body>
</html>
1
2
3
4
# views.py

def url_reverse(request):
return render(request, 'url_reverse.html')

配置路由。

如果我们修改 urls.py :

1
2
path('index/', views.index),  # 去掉
path('index1/', views.index), # 改为

则原来能用的链接都失效了。

url_reverse.html 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<br>
index 链接:<br/>
<a href="/index">首页</a><br/>
url 反向解析生成 index 链接:<br/>
<a href="{% url 'index' %}">首页</a></br>

<!--
/show_args/1/2:<br/>
<a href="/show_args/1/2">/show_args/1/2</a><br/>
动态产生/show_args/1/2:<br/>
<a href="{% url 'show_args' 1 2 %}">/show_args/1/2</a><br/>

/show_kwargs/3/4:<br/>
<a href="/show_kwargs/3/4">/show_kwargs/3/4</a><br/>
动态产生/show_kwargs/3/4:<br/>
<a href="{% url 'show_kwargs' c=3 d=4 %}">/show_kwargs/3/4</a>
-->

</body>
</html>

同时在 urls.py 中新增(修改):

1
2
3
4
5
path('index1/', views.index), # 原本
path('index1/', views.index, name='index'), # 改为

path('show_args/<int:a>/<int:b>',views.show_args,name='show_args'),
path('show_kwargs/<int:c>/<int:d>',views.show_kwargs,name='show_kwargs'),
1
2
3
4
5
6
7
# views.py

def show_args(request, a, b):
return HttpResponse(str(a) + ':' + str(b))

def show_kwargs(request, c, d):
return HttpResponse(str(c) + ":" + str(d))

效果,第一个链接不可以访问,第二个可以:

89-36.png

下面我们做一些更复杂的操作。

在 booktest 文件夹下新增 urls.py . 注意现在我们有两个 urls.py :

  • day1010/day1010/urls.py (旧)
  • day1010/booktest/urls.py (新)

day1010/day1010/urls.py(旧)中,修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from django.contrib import admin
from django.urls import path, include
from booktest import views

urlpatterns = [
path('admin/', admin.site.urls),
path('index1/', views.index, name='index'),
path('index2/', views.index2),
path('books/', views.show_books),
path('books/<int:bid>', views.detail),
path('create/', views.create),
path('delete<int:bid>', views.delete),
path('aggregate/',views.use_aggregate),
path('areas/', views.areas),
path('login/', views.login),
path('login_check/', views.login_check),
path('test_ajax/', views.test_ajax),
path('ajax_handle/', views.ajax_handle),
path('login_ajax/', views.login_ajax),
path('login_ajax_check/', views.login_ajax_check),
path('set_cookie/', views.set_cookie),
path('get_cookie/', views.get_cookie),
path('set_session/', views.set_session),
path('get_session/', views.get_session),
# path('clear_session/', views.clear_session),
path('test_var/', views.test_var),
path('test_filters/', views.test_filters),
path('test_template_inhert/', views.test_template_inhert),
path('html_escape/', views.html_escape),
path('change_pwd/', views.change_pwd),
path('change_pwd_action/', views.change_pwd_action),
path('verify_code/', views.verify_code),

path('url_reverse/', views.url_reverse),
path('',include(('booktest.urls','booktest'),namespace = 'booktest')), # 将那个文件的内容合并过来
]

day1010/booktest/urls.py(新)中,修改:

1
2
3
4
5
6
7
from django.urls import path, include
from booktest import views

urlpatterns = [
path('show_args/<int:a>/<int:b>',views.show_args,name='show_args'),
path('show_kwargs/<int:c>/<int:d>',views.show_kwargs,name='show_kwargs'),
]

url_reverse.html 中打开注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<br>
index 链接:<br/>
<a href="/index">首页</a><br/>
url 反向解析生成 index 链接:<br/>
<a href="{% url 'index' %}">首页</a></br>

/show_args/1/2:<br/>
<a href="/show_args/1/2">/show_args/1/2</a><br/>
动态产生/show_args/1/2:<br/>
<a href="{% url 'booktest:show_args' 1 2 %}">/show_args/1/2</a><br/>

/show_kwargs/3/4:<br/>
<a href="/show_kwargs/3/4">/show_kwargs/3/4</a><br/>
动态产生/show_kwargs/3/4:<br/>
<a href="{% url 'booktest:show_kwargs' c=3 d=4 %}">/show_kwargs/3/4</a>

</body>
</html>

现在这些链接都可以访问:

89-37.png

现在,对 day1010/booktest/urls.py(新)作修改:

1
2
3
4
5
6
7
8
# 后面加了 1
from django.urls import path, include
from booktest import views

urlpatterns = [
path('show_args1/<int:a>/<int:b>',views.show_args,name='show_args'),
path('show_kwargs1/<int:c>/<int:d>',views.show_kwargs,name='show_kwargs'),
]

再次运行,结果:

89-38.png

静态文件

在 static 文件夹下新增 images 文件夹,里面放入 amber.png

templates 文件夹下新增 static_test.html 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>静态文件</title>
</head>
<body>
<img src="/static/images/amber.png"><br/>
<img src="/abc/images/amber.png"><br/>
动态获取 STATIC_URL,拼接静态文件路径:<br/>
<img src="{% static 'images/amber.png' %}">
</body>
</html>
1
2
3
4
# views.py

def static_test(request):
return render(request, "static_test.html")

配置路由(老方法)。

结果:

89-39.png

可以看到,中间那张无法加载。

如果在 settings 中,修改:

1
2
STATIC_URL = 'static/' # del
STATIC_URL = 'abc/' # add

那么结果变成:第一张无法加载,其他可以加载。

体现了动态获取 STATIC_URL 的好处

中间件

中间件函数是 django 框架给我们预留的函数接口,让我们可以干预请求和应答的过程。

89-40.png

需求:现在我们需要禁掉一些有恶意行为的 IP 访问网站。

一种方法,我们可以使用装饰器模式,在每一个视图函数前加装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EXCLUDE_IPS = [' 192.168.0.108']
def blocked_ips(view_func):
def wrapper(request, *view_args, **view_kwargs):
# 获取浏览器端的 ip 地址
user_ip = request.META['REMOTE_ADDR']
if user_ip in EXCLUDE_IPS:
return HttpResponse('<h1>Forbidden</h1>')
else:
return view_func(request, *view_args, **view_kwargs)
return wrapper

@blocked_ips
def index(request):
# ...
pass

但是这样有一个问题,如果要禁止某个 IP 访问所有页面,我们需要对所有的视图函数加装饰器。这样非常麻烦(提问:是否通过 vim 操作并不麻烦)。

第二个思路是使用中间件。

在 ./booktest/ 下新建 myMiddleware.py 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from django.http import HttpResponse

class BlockedIPSMiddleware(object):
def __init__( self, get_response ):
self.get_response = get_response

def __call__(self, request):
return self.get_response(request)

'''中间件类'''
EXCLUDE_IPS = ['127.0.0.1']

def process_view(self, request, view_func, *view_args, **view_kwargs):
'''视图函数调用之前会调用'''
user_ip = request.META['REMOTE_ADDR']
if user_ip in BlockedIPSMiddleware.EXCLUDE_IPS:
return HttpResponse('<h1>Forbidden</h1>')

class TestMiddleware(object):
'''中间件类'''
def __init__( self, get_response ):
print('---init---')
self.get_response = get_response

def __call__(self, request):
'''产生 request 对象之后,url 匹配之前调用'''
print('----process_request----')
# return HttpResponse('process_request me')
response=self.get_response(request)
# 视图函数调用之后,内容返回浏览器之前
print('------response------')
return response

def process_view(self, request, view_func, *view_args, **view_kwargs):
'''url 匹配之后,视图函数调用之前调用'''
print('----process_view----')
# view 视图函数没有得到执行,但是还是要走 process_response
# return HttpResponse('process_view')

class ExceptionTest1Middleware(object):
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
return self.get_response(request)

def process_exception(self, request, exception):
'''视图函数发生异常时调用'''
print('----process_exception1----')
print(exception)

class ExceptionTest2Middleware(object):
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
return self.get_response(request)

def process_exception(self, request, exception):
'''视图函数发生异常时调用'''
print('----process_exception2----')

在 settings 中注册:

1
2
3
4
5
6
7
8
9
10
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',
'booktest.myMiddleware.BlockedIPSMiddleware', # add
]

重启服务,发现无法访问(因为本机地址被禁了)。

后台管理(续)

一些零碎的知识点。

admin.py 新增:

1
2
3
4
5
6
7
8
9
10
11
class AreaInfoAdmin(admin.ModelAdmin):
'''地区模型管理类'''
list_per_page = 10 # 指定每页显示 10 条数据
#方法名也可以作为一列进行显示
list_display = ['id', 'atitle', 'parent']
actions_on_bottom = True # 底部显示动作窗口
actions_on_top = False #顶部不显示动作窗口
list_filter = ['atitle'] # 列表页右侧过滤栏
search_fields = ['atitle'] # 列表页上方的搜索框

admin.site.register(Areas, AreaInfoAdmin)

models.py 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 自关联的模型类设计
class Areas(models.Model):
'''地区模型类'''
# 地区名称
atitle = models.CharField(max_length=20)
# 关系属性,代表当前地区的父级地区
aParent = models.ForeignKey('self', null=True,blank=True,on_delete=models.CASCADE,)

def __str__(self):
return self.atitle

def parent(self):
if self.aParent is None:
return ''
return self.aParent.atitle

parent.short_description = '父级地区名称'

效果:

89-41.png

目前的这个管理页面的新增功能有点弱,作修改。

admin.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class AreaStackedInline(admin.StackedInline):
# 写多类的名字
model = Areas
extra = 2 #下面新增位置显示数目,默认显示 3 个

class AreaInfoAdmin(admin.ModelAdmin):
'''地区模型管理类'''
list_per_page = 10 # 指定每页显示 10 条数据
#方法名也可以作为一列进行显示
list_display = ['id', 'atitle', 'parent']
actions_on_bottom = True # 底部显示动作窗口
actions_on_top = False #顶部不显示动作窗口
list_filter = ['atitle'] # 列表页右侧过滤栏
search_fields = ['atitle'] # 列表页上方的搜索框

# add
fields = ['aParent', 'atitle']
inlines = [AreaStackedInline] #以块的形式

感觉没什么用。

上传图片

新建文件夹 ./static/media .

在 settings 中新增:

1
MEDIA_ROOT=os.path.join(BASE_DIR,'static/media')

./booktest/models.py 中新增:

1
2
3
class PicTest(models.Model):
'''上传图片'''
goods_pic = models.ImageField(upload_to='booktest')

迁移。

./booktest/admin.py 中新增:

1
2
3
from booktest.models import BookInfo,HeroInfo,Areas,PicTest

admin.site.register(PicTest)

在后台管理页面可以直接上传图片了。由于我们的配置,图片会被存放在 ./static/media/booktest/ 下面。

在数据库中,可以看到存储的是一个路径:

89-42.png

新增:

1
2
3
4
5
6
7
# views.py

from booktest.models import PicTest
def pic_show(request):
pic=PicTest.objects.get(id=1)
context={'pic':pic}
return render(request,'pic_show.html',context)

./templates/ 下新增 pic_show.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>显示图片</title>
</head>
<body>

<img src="/static/media/{{ pic.goods_pic }}"/>

</body>
</html>

./day1010/urls.py 中新增:

1
path('pic_show/', views.pic_show),

./templates/ 下新增 upload_pic.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传图片</title>
</head>
<body>
<form method="post" enctype="multipart/form-data" action="/upload_handle/">
{% csrf_token %}
<input type="file" name="pic"><br/>
<input type="submit" value="上传">
</form>
</body>
</html>

./booktest/views.py 中新增:

1
2
3
4
# /show_upload
def show_upload(request):
'''显示上传图片页面'''
return render(request, 'upload_pic.html')

配置路由。

接下来编写 upload_handle 的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# views.py

from day1010 import settings
def upload_handle(request):
'''上传图片处理'''
# 1.获取上传文件的处理对象
pic = request.FILES['pic']

# 2.创建一个文件
save_path = '%s/booktest/%s'%(settings.MEDIA_ROOT,pic.name)
with open(save_path, 'wb') as f:
# 3.获取上传文件的内容并写到创建的文件中
for content in pic.chunks():
f.write(content)

# 4.在数据库中保存上传记录
PicTest.objects.create(goods_pic='booktest/%s'%pic.name)

# 5.返回
return HttpResponse('ok')

配置路由。

此时,/show_upload/ 页面可以正常工作。

分页

./templates/ 下新建 show_area.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>显示地区</title>
</head>
<body>
{% for area in areas %}
<li>{{ area.atitle }} </li>
{% endfor %}
</body>
</html>

./booktest/views.py 中新增:

1
2
3
def show_area(request):
areas = Areas.objects.filter(aParent__isnull=True)
return render(request, 'show_area.html',{'areas': areas})

配置路由。

运行,在浏览器中查看。这个显示太长了,我们希望进行分页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 修改 views.py

from django.core.paginator import Paginator
def show_area(request, pindex=1):
areas = Areas.objects.filter(aParent__isnull=True)

# 2. 分页,每页显示 10 条
paginator = Paginator(areas, 10)

# 3. 获取第 pindex 页的内容
pindex = int(pindex)
# page 是 Page 类的实例对象
page = paginator.page(pindex)

return render(request, 'show_area.html',{'page': page})

修改 show_area.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>显示地区</title>
</head>
<body>
{% for area in page %}
<li>{{ area.atitle }} </li>
{% endfor %}
{% for pindex in page.paginator.page_range %}
<!-- {# 判断是否是当前页 #} -->
{% if pindex == page.number %}
{{ pindex }}
{% else %}
<a href="/show_area/{{ pindex }}">{{ pindex }}</a>
{% endif %}
{% endfor %}
</body>
</html>

配置路由:

1
2
path('show_area/', views.show_area), # do not remove
path('show_area/<int:pindex>', views.show_area),

运行,可以进行正常的分页、翻页操作。

增加“上一页” “下一页” 功能,修改 show_area.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>显示地区</title>
</head>
<body>
{% for area in page %}
<li>{{ area.atitle }} </li>
{% endfor %}

<!-- 判断是否有上一页 -->
{% if page.has_previous %}
<a href="/show_area/{{ page.previous_page_number }}">&lt;上一页</a>
{% endif %}

{% for pindex in page.paginator.page_range %}
<!-- {# 判断是否是当前页 #} -->
{% if pindex == page.number %}
{{ pindex }}
{% else %}
<a href="/show_area/{{ pindex }}">{{ pindex }}</a>
{% endif %}
{% endfor %}

<!-- 判断是否有下一页 -->
{% if page.has_next %}
<a href="/show_area/{{ page.next_page_number }}">下一页&gt;</a>
{% endif %}
</body>
</html>

效果:

89-43.png

省市县选择案例

这部分内容其实偏前端。

新建 ./templates/areas.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>收件地址选择</title>

<script src="/static/js/jquery-1.12.4.min.js"></script>
<script>
$(function () {
// 发起一个 ajax 请求 /prov,获取所有省级地区的信息
// 获取信息,使用 get
// 涉及到信息修改,使用 post
$.get('/prov', function (data) {
// 回调函数
// 获取返回的 json 数据
res = data.data
// 获取 prov 下拉列表框
prov = $('#prov')
// 变量 res 数组,获取每一个元素:[地区 id, 地区标题]
/*
for(i=0; i<res.length; i++){
id = res[i][0]
atitle = res[i][1]
option_str = '<option value="'+id + '">'+ atitle+ '</option>'
// 向 prov 下拉列表框中追加元素
prov.append(option_str)
}*/
$.each(res, function (index, item) {
id = item[0]
atitle = item[1]
option_str = '<option value="'+id + '">'+ atitle+ '</option>'
// 向 prov 下拉列表框中追加元素
prov.append(option_str)
})
})

// 绑定 prov 下拉列表框的 change 事件,获取省下面的市的信息
$('#prov').change(function () {
// 发起一个 ajax 请求 /city,获取省下面市级地区的信息
// 获取点击省的 id
prov_id=$(this).val()
$.get('/city/'+prov_id, function (data) {
// 获取返回的 json 数据
res = data.data
// 获取 city 下拉列表框
city = $('#city')
// 清空 city 下拉列表框
city.empty().append('<option>---请选择市---</option>')
// 获取 dis 下拉列表框
dis = $('#dis')
// 清空 dis 下拉列表框
dis.empty().append('<option>---请选择县---</option>')
// 变量 res 数组,获取每一个元素:[地区 id, 地区标题]
// 遍历取值添加到 city 下拉列表框中
$.each(res, function (index, item) {
id = item[0]
atitle = item[1]
option_str = '<option value="'+id + '">'+ atitle+
'</option>'
// 向 city 下拉列表框中追加元素
city.append(option_str)
})
})
})
// 绑定 city 下拉列表框的 change 事件,获取市下面的县的信息
$('#city').change(function () {
// 发起一个 ajax 请求 /dis,获取市下面县级地区的信息
// 获取点击市的 id
city_id=$(this).val()
$.get('/dis/'+city_id, function (data) {
// 获取返回的 json 数据
res = data.data
// 获取 dis 下拉列表框
dis = $('#dis')
// 清空 dis 下拉列表框
dis.empty().append('<option>---请选择县---</option>')
// 变量 res 数组,获取每一个元素:[地区 id, 地区标题]
// 遍历取值添加到 dis 下拉列表框中
$.each(res, function (index, item) {
id = item[0]
atitle = item[1]
option_str = '<option value="'+id + '">'+ atitle+'</option>'
// 向 dis 下拉列表框中追加元素
dis.append(option_str)
})
})
})
})
</script>
</head>

<body>
<select id="prov">
<option>---请选择省---</option>
</select>
<select id="city">
<option>---请选择市---</option>
</select>
<select id="dis">
<option>---请选择县---</option>
</select>
</body>
</html>

视图函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# views.py

def areas(request):
'''省市县选中案例'''
return render(request, 'areas.html')

def prov(request):
'''获取所有省级地区的信息'''
# 1.获取所有省级地区的信息
areas = Areas.objects.filter(aParent__isnull=True)
# 2.变量 areas 并拼接出 json 数据:atitle id
areas_list = []
for area in areas:
areas_list.append((area.id, area.atitle))
# 3.返回数据
return JsonResponse({'data':areas_list})

def city(request, pid=0):
'''获取 pid 的下级地区的信息'''
# 1.获取 pid 对应地区的下级地区
# area = AreaInfo.objects.get(id=pid)
# areas = area.areainfo_set.all()
areas = Areas.objects.filter(aParent__id=pid)
# 2.变量 areas 并拼接出 json 数据:atitle id
areas_list = []
for area in areas:
areas_list.append((area.id, area.atitle))
# 3.返回数据,返回给前端,对方得到的是数组
return JsonResponse({'data': areas_list})

配置路由:

1
2
3
4
path('areas/', views.areas),
path('prov/',views.prov),
path('city/<int:pid>',views.city),
path('dis/<int:pid>',views.city),

实现效果:

89-44.png

上一级区域选择之后,到下一级菜单中会自动显示该区域的下级单位。

实际上此类功能现在一般交由第三方处理。

富文本编辑器

安装:

1
pip install django-tinymce --proxy="http://127.0.0.1:7897"

在 settings 中新增:

1
2
3
4
5
6
7
8
9
10
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'booktest',
'tinymce', # add
]

继续在 settings 中增加配置:

1
2
3
4
5
TINYMCE_DEFAULT_CONFIG = {
'theme': 'silver',
'width': 600,
'height': 400,
}

配置 ./day1010/urls.py :

1
path('tinymce/',include('tinymce.urls')),

./booktest/models.py 新增:

1
2
3
from tinymce.models import HTMLField
class GoodsInfo(models.Model):
gcontent=HTMLField()

迁移。

./booktest/admin.py 中新增:

1
2
3
4
5
6
from booktest.models import BookInfo,HeroInfo,Areas,PicTest,GoodsInfo

class GoodsInfoAdmin(admin.ModelAdmin):
list_display = ['id']

admin.site.register(GoodsInfo,GoodsInfoAdmin)

可以在后台管理页面看到效果:

89-45.png

如何在前台看到呢?

新建 ./templates/show.html :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<title>展示富文本编辑器内容</title>
</head>
<body>
id:{{g.id}}
<hr>
{%autoescape off%}
{{g.gcontent}}
{%endautoescape%}
<hr>
<!-- {{g.gcontent|safe}} -->
</body>
</html>
1
2
3
4
5
6
# views.py

def show(request):
goods=GoodsInfo.objects.get(pk=1)
context={'g':goods}
return render(request,'show.html',context)

配置路由。运行成功。

如何在前台使用这个富文本编辑器呢? 关于这个,小编也很好奇呢

本文章的示例代码参见:
https://github.com/dropsong/py_webServer/tree/master/day1010