pythonDjango后端笔记

文章目录

软件开发架构

cs架构
bs架构
# 本质bs也是cs

纯手撸web框架

# HTTP协议
'''
HTTP协议			数据传输是明文
HTTPS协议			数据传输是密文
websocket协议		数据传输是密文

四大特征
	1. 基于请求响应
	2. 基于TCP、IP作用于应用层之上的协议
	3. 无状态
	4. 短/无链接

数据格式
	请求首行
	请求头
	
	请求体

响应状态码
	1xx
	2xx	200
	3xx
	4xx 403 404
	5xx 500
'''

代码:

import socket
import threading

data_endcoding = 'utf-8'
# 处理http协议发送过来的网站头部信息

def modify_recvHTTPData_returnHeadDatedict(HTTPData: str):
    HTTPData_dict = {}
    HTTPData_list = HTTPData.split('\r\n', 1)
    HTTPData_head = HTTPData_list[0].strip()
    HTTPData_head_list = HTTPData_head.split(' ')
    HTTPData_allLine_list = HTTPData_list[1].split('\r\n')
    for HTTPData_oneLine_str in HTTPData_allLine_list:
        if HTTPData_oneLine_str:
            HTTPData_oneLine_list = HTTPData_oneLine_str.split(':', 1)
            HTTPData_dict[HTTPData_oneLine_list[0].strip()] = HTTPData_oneLine_list[1].strip()
    return HTTPData_head_list, HTTPData_dict


# 你可以将web框架理解成服务端
def my_socket(server):
    global res

    server.bind(('127.0.0.1', 8080))
    server.listen(5)

    while True:
        socket_user, addr = server.accept()
        data = socket_user.recv(1024)
        # print(data)  # 这个数据为二进制数据,所以我们需要将它装换为字符类
        data = data.decode(data_endcoding)
        head_data,data_dict = modify_recvHTTPData_returnHeadDatedict(data)
        print(head_data[1])
        socket_user.send(b'HTTP/1.1 200 OK\r\n\r\n')
        if head_data[1] == '/index':
            with open('../Bootstrap/bootstrap的表格布局.html',mode='rb') as f1:
                # res = f1.read()
                socket_user.send(f1.read())
        else:
            socket_user.send(f'hello {head_data[1].rstrip("/")} internet'.encode(data_endcoding))  # 必须书写有关于http协议的内容才可以web后端
        socket_user.close()


def change_res(server):
    global res
    res = input('输入quit退出服务端')
    if res == 'quit':
        server.close()


if __name__ == '__main__':
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    res = ''
    change_res_thread = threading.Thread(target=change_res, args=(server,))
    change_res_thread.daemon = True
    change_res_thread.start()
    my_socket(server)
'''
# 不足之处
	1. 代码重复
	2. 手动处理http格式的数据,除了数据头之外的数据获取繁琐(数据的格式一样,处理的方式也差不多,重复写)
	3. 并发的问题
'''

借助于wsgiref模块

from wsgiref.simple_server import make_server


def run(env, response):
    '''
    :param env:请求相关的所有数据
    :param response:响应相关的所以数据
    :return:返回给浏览器的数据
    '''
    # print(env)  # 大字典 wsgiref模块帮你处理好了http格式的数据

    response('200 OK', [])  # 响应首行加上响应头
    url = env['PATH_INFO']
    metod = env['REQUEST_METHOD']
    if metod == 'GET':
        html_name_list = url.strip('/').split('/')
        if len(html_name_list) == 1:
            if html_name_list[0] == 'favicon.ico':
                with open('../ico/n7.ico', mode='rb') as f1:
                    return [f1.read()]
            else:
                try:
                    with open(f'../HTML5+Css+JS/{html_name_list[0]}.html', mode='rb') as f1:
                        return [f1.read()]
                except:
                    return [b'not find html']
        else:
            try:
                with open(f'../HTML5+Css+JS{url}', mode='rb') as f1:
                    return [f1.read()]
            except:
                return [b'not find that']
    elif metod == 'POST':
        for key in env:
            print(key,env[key])
        return ['123']
if __name__ == '__main__':
    '''
    def make_server(host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler):
    '''
    server = make_server('127.0.0.1', 8080, run)
    '''
    这句话的意思会实时监听127.0.0.1:8080地址 只要有客户端来了都会交给run函数处理(加括号触发run函数的运行)
    如果app这参数为对象的话那么客户端来了也会加括号不过调用的是对象的init方法
    flask启动源码
        make_server('127.0.0.1',8080,obj)
        这个源码后面启动的就是对象
    '''
    server.serve_forever()  # 启动服务端

动静态网页

  • 静态网页

    页面上的数据是写死的

  • 动态网页

    页面上的数据是实时获取的

动态网页有哪些

  1. 后端获取当前的事件或者新的数据展示到HTML页面上
  2. 数据是从数据库中获取的展示到HTML页面上

模块语法之Jinja2模块

pip3 install jinja2

# 模板语法(非常贴近Python语法,下面的语法都是写在HTML文件当中)
{{ user }}  # 获取user的数据
{{ user.get('username') }}  # 相当于使用Python字典里面的get函数获取key为username的数据
{{ user.age }}  # 直接获取字典里面的age对应的值
{{ user['hobby'] }}  # 直接获取字典里面的hobby对应的值

# 模板语法中的for循环
{% for userdict in user_list %}  # 这个user_list就是在使用jinja2模块的render函数里面的key值如:tmp.render(user_list=data_list)所以这里使用user_list来进行获取这个值
<tr>
    <td>{{ user_dict.id }}</td>
    <td>{{ user_dict.username }}</td>
    <td>{{ user_dict.pwd }}</td>
</tr>
{% endfor %}  #结束for循环

自定义简易版本web框架请求中各个模块的作用

'''
wsgiref模块
	1.请求来的时候解析http格式的数据 封装成大字典
	2.响应走的时候给数据打包成符合http格式 再返回给浏览器

'''

python3大主流web框架

'''
Django
	特点:
		大而全 自带的功能特别的多
	缺点:
		有时候过于笨重
		

flask
	特点:
		小而精 自带的功能特别少 第三方的模块特别多 如果将第三方的模块加起来完全可以盖过Django并且也越来越像Django
	缺点:
		比较依赖于第三方的开发者(很容易出现兼容性问题,因为flask框架更新了,第三方的作者没有更新就会到时版本不兼容)

tornado
	特点:
		异步非阻塞 支持高并发(可以支持开发游戏服务器)
'''
A:socket部分
B:路由与视图函数对应关系(路由匹配)
C:模板语法

Django
	A用的是别人的		wsgiref模块
    B用的是自己的
    C用的是自己的(没有jinja2好用 但是也很方便)

flask
	A用的是别人的		werkzeug(内部还是wsgiref模块)
    B用的是自己写的
    C用的别人的		jinja2模块

tornado
	A,B,C都是自己写的

注意事项

# 如何让计算机能够正常的启动Django项目
	1.计算机的名称不能有中文
    	右键计算机 点击显示设置 点击里面的关于就可以看到计算机名称了
    2.一个pycharm窗口只开一个项目
    3.项目里面所有的文件也尽量不要出现中文
    4.Python解释器尽量使用稳定版本
    	(如果项目报错 点击最后一个报错信息去源码中把逗号删掉)
# Django版本问题
	1.x 2.x 3.x(还在开发中,直接忽略)
    1.x和2.x本身差距也不大 我们讲解主要以1.x为例 会讲解2.x区别
    
# Django安装
	pip3 install django==1.11.28
    如果已经安装了其他版本 无需自己卸载
    直接安装 会自动卸载更新
    
    如果报错 看看是不是timeout 如果是 那么可能是网络出现了问题重新进行安装即可
    
    验证是否安装完成的方式1
    	终端输入django-admin看看有没有反应  # 注意这个需要配置django的环境变量

Django框架原理

将浏览器返回的url进行分装,使用一个URL对应着一个函数并且使用函数将访问网页进行封装

urls = [
    ('/index',index),
    ('/login',login)
]
'''
urls.py				路由与视图函数对应关系
views.py			视图函数(后端业务逻辑)
templates文件夹	  专门用来储存html文件
'''
#按照功能的不同拆分后 后续添加功能只需要在urls.py书写对应关系然后取views.py业务逻辑即可

Django

Django基本操作

# 命令行操作
	# 1.创建django项目
    	'''你可以切换到对应的盘符,然后再创建'''
    	django-admin startproject 项目名
        例:
        	django-admin startproject mysite
            
            创建的文件夹有:
            	mysite文件夹
                	manage.py
                    mysite文件夹
                    	__init__.py
                        settings.py
                        urls.py
                        wsgi.py
	# 2.启动django项目
    	'''
    	一定要先切换到项目目录下
    	cd mysite
    	'''
        python3 manage.py runserver
        进入浏览器输入对应的ip和端口,如果有返回的内容那么久表示django创建的没有问题
        # http://127.0.0.1:8000/
    # 3.创建应用
    '''
    Next, start your first app by running python manage.py startapp [app_label].
    '''
    	python3 manage.py startapp app名字
        应用名应该要做到见名知意
# pycharm操作
	# 1 new project 选择左侧第二个django即可
    	创建的时候选择Existing interpreter即可,不要选择第一个创建虚拟环境
    
    # 2 启动
    	1.使用命令行进行启动
        2.使用pycharm绿色小箭头进行启动,启动的时候要注意选中这个django文件
    # 3 创建应用
    	1.pycharm提供的终端直接输入完整命令
        2.在pycharm的django项目下点击Tools下面的Run manage.py Task...
        	输入:
            	startapp app名字
            这样即可创建好app,前期不能使用
        3.修改django项目的端口号
        	1.点击绿色小箭头的左边的项目
            2.选择Edit Configurations...
            3.找到Port 修改端口号
        4.当django的配置文件被删除了找不到
        	1.点击Add Configuration...
            2.点击+号
            3.选择Django Server,修改信息,点击Apply

应用

# 创建应用
python3 manage.py startapp app名字
# window将python3换成python
'''
django是一款专门用来开发app的web框架

django框架就类似于一所大学(空壳子,只提供场所)
app就类似于大学里面各个学院(app指的是具体的功能app)
	比如开发淘宝
		订单相关
		用户相关
		投诉相关
		创建不同的app对应不同的功能
	
	选课系统
		学生功能
		老师功能

一个app就是一个独立的功能模块
注意:
	创建的应用一定要去配置文件中注册
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01.apps.App01Config'  # 这个就是新创建的app,全写
]
'''
#  创建出来的应用第一步先去配置文件中注册
#  你在使用pycharm创建项目的时候 pycharm可以帮你创建一个app并且自动注册
	1.在上面使用pycharm创建项目的时候在More Settings中的Application name中写你的app名字然后在进行创建就可以了

主要文件介绍

-mysite项目文件夹
	--mysite文件夹
    	---setting.py	配置文件
        ---urls.py		路由与视图函数对应关系(路由层)
        ---wsgi.py		wsgiref模块(不考虑)
    --manage.py			django的入口文件
    --db.sqlite3		django自带的sqlite3数据库(小型数据库 功能不是很多还有bug)
    --app01文件夹		  创建的app文件
    	---admin.py		django后台管理
        --—apps.py		注册使用
        ---migrations文件夹	数据库迁移记录
        ---model.py		数据库相关的 模型类(orm)
        ---tests.py		测试文件
        ---views.py		视图函数(视图层)

命令行与pycharm创建的区别

# 1. 命令行创建不会自动有templatew文件夹 需要你自己动手创建而pycharm会自动帮你创建并且还会自动在配置文件中配置对应的路径
# 使用pycharm创建的django项目setting中配置文件的一个参数
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',
            ],
        },
    },
]
# 使用命令行创建的django项目的setting中配置文件的一个参数
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        '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',
            ],
        },
    },
]
# 它们区别于DIRS使用命令行创建的之后这个参数是空的,而pycharm直接给你配置好了
# 也就意味着你在用命令创建django项目的时候不单单需要创建templates文件夹还需要去配置文件中配置路径

配置文件讲解

BASE_DIR  # 这个是你当前的项目路径
SECRET_KEY  # 这个是加密相关的东西
DEBUG  # 上线之后改为False,一般是用来调试使用的
ALLOWED_HOSTS  # 这个表示的是允许访问的主机,'*'表示所有的注意都可以进行访问
INSTALLED_APPS  # 注册的app(app就是功能模块,django自带6个模块)
MIDDLEWARE  # 这个是django的中间件
ROOT_URLCONF  # 链接的urls路由
TEMPLATES  # html文件存放路径配置
WSGI_APPLICATION  # 链接的wigi路径
DATABASES  # 项目指定的数据库
LANGUAGE_CODE  # 文字编码
TIME_ZONE  # 时区

Django小白必会三板斧

'''
HttpResponse
	返回字符串类型的数据
	例:
		HttpResponse('你大爷的')
render
	返回HTML文件
	例:
		render(request,'firsthtml.html')
redirect
	重定向(跳转到指定的网页),也可以跳转自己的页面只写后缀
	例:
		跳转其他网页:
			redirect('https://www.baidu.com/')
		跳转自己的网页:
			redirect('/index/')
'''

render函数作用单独讲解

# return render(request, 'firsthtml.html')
# 第一种传值方式:更加的精准 节省资源
# user_list = [{'username': 'mubai', 'age': 18, 'gender': '男'}, ]
# return render(request, 'firsthtml.html', {'data': user_dict})  # 这个表示的是使用页面通过data来进行接收数据
# 第二种传值方式:当你要传的数据特别多的时候
'''locals会将所在的名称空间中所有的名字全部传递给html页面'''
username = 'mubai'
age = 18
gender = '男'
return render(request, 'secondhtml.html', locals())

静态文件配置和静态位置动态解析

# 登录功能

'''
我们将html文件默认都放在templates文件夹xai/
我们将网站所使用的静态文件默认都放在static文件下

静态文件
	前段已经写好了的 能够直接调用使用的文件
		网站写好的js文件
		网站写好的css文件
		网站用到的图片文件
		第三方前段框架
		。。。
		拿来就可以直接使用的

'''
# django默认是不会自动帮你创建static文件夹 需要你自己手动创建
一般情况下我们在static文件夹内还会做进一步的划分处理
	-static
    	--js
        --css
        --img
        其他第三方文件
目的:
	更加的方便管理
'''
在浏览器中输入url能够看到对应的资源
是因为后端提前开设了该资源的接口
如果访问不到资源 说明后端没有开设该资源的接口

'''

# 静态文件配置
STATICFILES_DIRS = [
    os.path.join(BASE_DIR,'static')
]
// 在setting里面设置了上面的东西了之后,可以直接在前段引入里面使用static开头引入
例:
	 <link rel="stylesheet" href="/static/bootstrap/bootstrap.min.css">
# 两个static的区别
STATIC_URL = '/static/'  # 类似于访问静态文件的令牌
'''如果你想要访问静态文件 你就必须以static开头'''
STATICFILES_DIRS = [
    os.path.join(BASE_DIR,'static'),
    os.path.join(BASE_DIR,'static1')
]  # 这个表示的是静态文件夹的路径
例:
	<link rel="stylesheet" href="/static/bootstrap/bootstrap.min.css">  # 注意static路径前面必须加/要不然不能进行处理
在这里面的static会被检测为静态文件夹访问,那么他会根据这个来和列表里面的路径进行拼接来进行查找数据文件,
如:
	os.path.join(BASE_DIR,'static')生成的路径结果为D:\Desktop\Python后端学习\后端代码存放处\myweb\static
    那么link最后导入的路径就是D:\Desktop\Python后端学习\后端代码存放处\myweb\static\bootstrap\bootstrap.min.css
    如果没有的话,那么就会从第二个路径os.path.join(BASE_DIR,'static1')(生成的路径结果为D:\Desktop\Python后端学习\后端代码存放处\myweb\static1)
    那么这次link导入的路径为D:\Desktop\Python后端学习\后端代码存放处\myweb\static1\bootstrap\bootstrap.min.css
    如果在上面的两个文件夹里面都没有查找这个数据那么将会报错
技巧:
	清除浏览器的缓存按f12进入控制台点击设置按钮看Network的Disable cache(while DevTools is open),勾选这个按钮,那么这个浏览器每次请求都会加载资源

静态位置动态解析:
	{% load static %}
    <link rel="stylesheet" href="{% static '/bootstrap/bootstrap.min.css' %}">
这样就可以动态解析
静态文件是从上到下进行加载的,如果第一个路径就找到了这个文件,那么将会直接退出
# CSRF verification failed. Request aborted 报错
第一种方法:
要将setting里面的'django.middleware.csrf.CsrfViewMiddleware'注释掉大概在47行在MIDDLEWARE参数里面
第二种方法:
	在html页面的form标签里面加上
    {%csrf_token%}
第三种方法:
	在views.py上导入装饰器方法
    from django.views.decorators.csrf import csrf_exempt
    在对应的函数上面添加
    @csrf_exempt

# form表单默认是get请求数据
	http://127.0.0.1:8000/login/?username=123&password=123
'''
from表单action参数
	1.不写 默认朝当前所在的url提交数据
	2.全写 指名道姓
	3.只写后缀	/login/

'''

request对象方法初识

request.method # 返回请求方式 并且是全大写的字符串形式  <class 'str'>
request.POST  # 获取用户post请求提交的普通数据不包含文件
	request.POST.get()  # 只获取列表最后一个元素
    request.POST.getlist()  # 直接将列表取出
request.GET  # 获取用户提交的get请求数据
	request.GET.get()  # 只获取列表最后一个元素
    request.GET.getlist()  # 直接将列表取出
'''
get请求携带的数据是有大小限制的 大概好像、只有4kb左右
而post请求则没有限制

'''
def login(request):
    '''
    get请求和post请求应该有不同的处理机制
    :param request: 请求相关的数据对象 里面有很多的简易方法
    :return:
    '''
    print(request.method,type(request.method))  # 返回请求方式 并且是全大写的字符串形式
    if request.method == 'GET':
        # 获取url后面携带的参数
        # http://127.0.0.1:8000/login/?username=123&password=123&hobby=%E5%90%83%E9%A5%AD&hobby=%E7%9D%A1%E8%A7%89&hobby=%E6%89%93%E8%B1%86%E8%B1%86
        print(request.GET)  # 获取用户提交的get请求数据
        # <QueryDict: {'username': ['123'], 'password': ['123'], 'hobby': ['吃饭', '睡觉', '打豆豆']}>
        gethobby = request.GET.get('hobby')
        getlisthobby = request.GET.getlist('hobby')
        print(gethobby,type(gethobby))
        print(getlisthobby,type(getlisthobby))

        return render(request,'login.html')
    elif request.method == 'POST':
        # 获取用户数据
        print(request.POST,type(request.POST))  # 获取用户提交的post请求数据(不包含文件)
        # <QueryDict: {'username': ['123'], 'password': ['123']}>
        # 返回的数据为一个字典,key值为前段里面的name值
        username = request.POST.get('username')
        print(username,type(username))  # 返回的数据为字符串
        hobbydata = request.POST.get('hobby')
        print(hobbydata,type(hobbydata))  # 及时是多个值,get获取的值始终是最后一个值,且始终为字符串
        '''
        get 方法只会获取列表最后一个元素
        '''
        # 获取列表所有的元素
        hobbydata_list = request.POST.getlist('hobby')  # 这个方法获取的值是前段返回给后端的一个列表
        print(hobbydata_list,type(hobbydata_list))
        return HttpResponse('恭喜你提交成功,你可以滚了孩子')
    # 一般的写法为:
    '''
    if request.method == 'POST':
        return HttpResponse('恭喜你提交成功,你可以滚了孩子')
    return render(request,'login.html')

pycharm链接数据库(MySQL)

'''
三个位置查找数据库相关
	右侧上方database
	左下方database
	配置(setting)里面的plugin是插件中选择installed搜索database
	
	还没有卸载重装pycharm
	再重复上面3步操作还没有则更换别的版本的pycharm包

pycharm可以充当很多款数据库软件的客户端
点击database里面的+号
选择MySQL
进入界面之后先查看下面的字看看有没有Download missing driver files(安装相关的驱动程序)
如果有那么点击Download进行下载
如果还是使用不了,那么点击General的Driver的MySQL选择MySQL5.1重新下载一下驱动
然后从上到下依次书写参数
HOST书写ip,测试阶段可以不写
user表示数据库用户名
password表示密码
Database表示使用的数据库名称
全部书写完毕那么点击Test Connection
如果出现对钩那么表示成功,如果不行换5.1,如果报错为timezone那么需要进入Advanced里面设置serverTimezone值,将时区改为UTC北京时间

操作mysql一般使用3个按钮
选择一个标,在上面有2个按钮+表示添加新的数据-表示删除一个数据
db表示同步到数据库里面
注意:pycharm操作数据库需要创建一个库才能进行操作
'''

django链接数据库(MySQL)

# 默认用的是sqkite3
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
# django链接MySQL
	1.第一步配置文件中配置
        DATABASES = {
            'default': {
                'ENGINE': 'django.db.backends.mysql',  # 连接的数据库类型
                'NAME': 'day60',  # 数据库的名字
                'USER': 'root',  # 数据库用户名称
                'PASSWORD': '1208',  # 数据库用户密码
                'HOST': '127.0.0.1',  # 数据库服务器电脑的ip
                'PORT': 3306,  # 连接数据库的端口号
                'CHARSET': 'utf8'  # 连接数据库的编码格式
            }
        }
    2.代码声明
    	django默认用的是mysqldb模块链接MySQL
        但是该模块的兼容性不好 需要手动改为用pymysql链接
        
        你需要让django使用pymysql来链接mysql数据库
        # 在项目名下的init或者任意的应用名下的init文件中书写以下代码都可以
        import pymysql
		pymysql.install_as_MySQLdb()

Django ORM

'''
ORM. 对象关系映射
作用:能够让一个不用sql语句的小白也能够通过Python 面向对象的代码简单快捷的操作数据库
不足之处:封装程度太高 有时候sql语句的效率偏低 需要你自己写sql语句

类				  映射			 表

对象				 映射				记录

对象属性			映射			记录某个字段对应的值

应用下面的model.py文件

'''
# 1.先去models.py中书写一个类
	class User(models.Model):
    id = models.AutoField(primary_key=True)
    # 等价于sql语句:id int primary_key auto_increment
    username = models.CharField(max_length=20)
    # 等价于sql语句:username varchar(20)
    password = models.IntegerField()
    # 等价于sql语句:password int
# 2.数据库迁移命令
python3 manage.py makemigrations
这个命令的作用:将操作记录记录到migrations文件夹里面
如果上面的指令没有反应那么输入python manage.py makemigrations试试
然后输入:
python3 manage.py migrate
这个命令的作用:将你的操作真正同步到数据库中
如果上面的指令没有反应那么输入python manage.py migrate

最后生成的库名有:
app01_user
auth_group
auth_group_permissions
auth_permission
auth_user
auth_user_groups
auth_user_user_permissions
django_admin_log
django_content_type
django_migrations
django_session
上面的库中除了app01_user以外的库都是django默认需要创建的表
app01_user是我们自己利用orm创建的表,加上app01的前缀是因为一个django项目可以有多个应用,那么多个应用之间可能出现表名出现冲突的情况,加上前缀就可以完全避免
# 只要你修改了model.py中跟数据库相关的代码 就必须重新执行上述的两条命令

class User(models.Model):
    id = models.AutoField(primary_key=True)
    # 等价于sql语句:id int primary_key auto_increment
    username = models.CharField(max_length=20,verbose_name='用户名')
    '''
    CharField必须要指定max_length参数 不指定会直接报错
    verbose_name该参数是所有字段都有的 就是用来对字段的解释
    '''
    # 等价于sql语句:username varchar(20)
    password = models.IntegerField()
    # 等价于sql语句:password int


class Author(models.Model):
    # 由于一张表中必须要有一个主键字段 并且一般情况下都叫id字段
    # 所以orm当你不定义主键字段的时候 orm会自动帮你创建一个int类型主键自增的名为id的字段
    # 也就意味着 后续我们在创建模型表的时候如果主键字段名没有额外的叫法 那么主键字段可以省略不写
    username = models.CharField(max_length=20)
    # 等价于sql语句:username varchar(20)
    password = models.IntegerField()
    # 等价于sql语句:password int
上面的操作可以直接点击run选择Run manage.py Task里面直接输入
那么Python3 manage.py makemigrations可以直接输入成makemigrations另外一个命令也可以直接输入migrate

字段的增删改查

#当你原来的表里面有值得时候修改model里面的数据,添加一个名为age的类的时候输入命令进行迁移数据库的时候会提示你你创建的字段没有默认值,第一个选项是输入一个值作为数据的默认值,第二个是在model.py创建的字段中添加一个默认值
# 字段的增加
	1.可以在终端内直接给出默认值
'''
You are trying to add a non-nullable field 'age' to user without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> 18
Migrations for 'app01':
  app01\migrations\0003_auto_20211123_1455.py
    - Add field age to user
    - Alter field password on author
    - Alter field username on author
    - Alter field password on user

   	'''
    2.直接允许该字段为空
		info = models.CharField(max_length=32,verbose_name='个人简介',null=True)
	3.直接给新添加的字段设置一个默认值
    	hobby = models.CharField(max_length=32,verbose_name='兴趣爱好',default='play game')
# 字段的修改
	直接修改代码,然后执行数据库迁移的命令即可
# 字段的删除(不要轻易的操作)
	直接注释对应的字段,然后执行数据库迁移的命令即可
    执行完毕之后字段对应的数据也都没有了
'''
在操作models.py的时候一定要细心
	千万不要注释一些字段
	执行迁移命令之前最好先检查一下自己写的代码

'''

数据的增删改查

# 先在views.py视图函数上面导入对应app的models,我的app名为app01所以导入为:
from app01 import models

# 查
	# 方法1:
        res = models.User.objects.filter(username=username)
        # 类似于的sql语句:select * from user where username='jason';
        print(res)  # <QuerySet [<User: User object>]>  相当一个列表里面多个数据对象:[数据对象1,数据对象2,数据对象3,...]
        user_obj= res[0]
        print(user_obj.username)  # 返回的结果为上面查找对应的username的值
        print(user_obj.password)  # 返回的结果为上面查找对应的password的值
        '''
        返回值你先看成是列表套数据对象的格式
        它也支持索引取值 切片操作 但是不支持负数索引
        同样它也不推荐你使用索引的方式取值,自带first方法,但是这个first方法内部就是使用索引的方式进行取值,只不过内置方法中,会考虑为空的情况
        '''
    # 方法2:
        user_obj = models.User.objects.filter(username=username).first()
        filter括号内可以携带多个参数,参数与参数之间默认是and关系,可以将filter当场mysql的where来记忆
    # 将数据库中数据全部展示到前段
    # 方法1(不推荐,语义不明确,了解即可):
        user_QuerySet = models.User.objects.filter()
    # 方法2(推荐操作,语义比较明确)
        user_QuerySet = models.User.objects.all()
# 增
	# 1.第一种方式
        res = models.User.objects.create(username=username, password=password)
        print(res, res.username, res.password)  # 返回值就是被创建的对象本身
    # 第二种添加数据的方式
        User_obj = models.User(username=username, password=password)
        User_obj.save()  # 保存数据
# 改
	# 修改数据方式1
        models.User.objects.filter(id=user_id).uptate(username=username, password=password)
        '''
        将filter查询出来的列表中所有额对象全部更新 批量更新操作
        只修改被修改的字段
        '''
    # 修改数据方式2
        edit_obj = models.User.objects.filter(id=user_id).first()
        edit_obj.uesrname = username
        edit_obj.password = password
        edit_obj.save()
        '''
            上述方法当字段特别的多的时候效率会特别的低
            从头到尾的所有字段全部更新一遍 无论该字段是否被修改
        '''
# 删
	# 直接去数据库中找到对应的数据即可
	    models.User.objects.filter(id=delete_id).delete()
    '''
        批量删除    
    '''

案例编辑功能书写(使用跳转网页的方式进行提交)

# 先将数据库中数据全部展示到前段 然后给每一个数据两个按钮 一个编辑一个删除
# 查看
    def userlist(request):
    # 查询用户表里面的所有的数据
    # 方式1(不推荐,语义不明确,了解即可)
    # user_QuerySet = models.User.objects.filter()
    # print(user_QuerySet)

    # 方式2
    user_QuerySet = models.User.objects.all()
    # print(user_QuerySet)  # 返回的结果和方式1的结果一样但是语义比较明确
    return render(request, 'userlist.html', locals())
# 编辑功能
	# 1.点击编辑按钮朝后端发送编辑数据的请求
    '''
    如果告诉后端用户想要编辑那条数据?
    	将编辑按钮所在的那一行数据的主键值发送给后端
    	利用url问号后面携带参数的方式
    '''
	{% for user_obj in user_QuerySet %}
		<tr>
            <td>{{ user_obj.id }}</td>
            <td>{{ user_obj.username }}</td>
            <td>{{ user_obj.password }}</td>
            <td>{{ user_obj.age }}</td>
            <td>
                <a href="/edit_user/?user_id={{ user_obj.id }}" class="btn btn-primary btn-xs">编辑</a>
                <a href="" class="btn btn-danger btn-xs">删除</a>
            </td>
        </tr>
    {% endfor %}
    # 2.后端查询出用户想要编辑的数据对象 展示到前段页面供用户查看和编辑
    def edit_user(request):
    # 获取url问号后面的参数
    user_id = request.GET.get('user_id')
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        # 去数据库中修改对应的数据内容
        # 修改数据方式1
        if not username or not password:
            return HttpResponse('输入的内容不能为空')
        # models.User.objects.filter(id=user_id).update(username=username, password=password)
        '''
            将filter查询出来的列表中所有额对象全部更新 批量更新操作
            只修改被修改的字段
        '''
        # 修改数据方式2
        edit_obj = models.User.objects.filter(id=user_id).first()
        edit_obj.uesrname = username
        edit_obj.password = password
        edit_obj.save()
        '''
            上述方法当字段特别的多的时候效率会特别的低
            从头到尾的所有字段全部更新一遍 无论该字段是否被修改
        '''
        # 跳转到数据的展示页面
        return redirect('/userlist/')
    # 查询当前用户想要编辑的数据对象
    edit_obj = models.User.objects.filter(id=user_id).first()
    # 将数据对象展示到页面上
    return render(request, 'edit_user.html', locals())
# 删除功能
	'''
	跟编辑功能逻辑类似
	def delete_user(request):
        # 获取用户想要删除的数据id值
        delete_id = request.GET.get('user_id')
        # 直接去数据库中找到对应的数据即可
        models.User.objects.filter(id=delete_id).delete()
        '''
            批量删除    
        '''
        # 跳转到展示页面
        return redirect('/userlist/')
	'''
# 真正删除功能应该需要二次确认
# 删除数据内部其实并不是真正的删除 我们会给数据添加一个标识字段用来表示当前数据是否被删除了,如果数据被删除了仅仅只是讲字段修改一个状态,例:
	username	password	isdelete
    mubai		123				0
    engon		123				1
# (因为数据很值钱所以一般情况下不会进行删除)

Django ORM中如何创建关系

"""
表与表之间的关系
	一对多
	
	多对多
	
	一对一

	没有关系
"""
图书表

出版社表


作者表

作者详情表
"""
图书和出版社是一对多的关系 外键字段建在多的那一方 book

图书和作者是多对多的关系 需要创建第三张表来专门存储

作者和作者详情表是一对一的关系
"""
from django.db import models


# Create your models here.


# 创建表关系先 将基表创建出来 然后再添加外键字段

class Book(models.Model):  # 书籍
    title = models.CharField(max_length=32, verbose_name='书名标题')
    price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='价格')  # 浮点数字段
    # 浮点数总共位数8位,小数部分保留2位
    '''
    图书和出版社是一对多 并且书是多的一方 所以外键字段放在书表里面    
    '''
    plublish = models.ForeignKey(to='Publish')  #
    '''
    如果字段对应的是ForeignKey 那么orm会自动字段后面加_id
    如果你自作聪明的加了_id那么orm还是会在后面继续加_id
    后面在定义ForeignKey的时候就不要自己加_id
    '''
    '''
    图书和作者是多对多的关系 外键字段在任意一方均可 但是推荐你建在查询频率较高的一方
    '''
    authors = models.ManyToManyField(to='Author')
    '''
    Author是一个虚拟字段 主要是用来告诉orm 书籍表和作者表示多对多关系
    让orm自动帮你创建第三张关系表
    '''


class Publish(models.Model):  # 出版社
    name = models.CharField(max_length=32, verbose_name='出版社名字')
    addr = models.CharField(max_length=32, verbose_name='地址')


class Author(models.Model):  # 作者
    name = models.CharField(max_length=32, verbose_name='作者名字')
    age = models.IntegerField(verbose_name='年龄')
    '''
    作者与作者详情是一对一的关系 外键字段在任意一方均可 但是推荐你建在查询频率较高的一表中
    '''
    Author_detail = models.OneToOneField(to='AuthorDetail')
    '''
    OneToOneField也会自动给字段加_id后缀
    所以你也不要自作聪明的自己加_id
    '''


class AuthorDetail(models.Model):  # 作者详情表
    phone = models.BigIntegerField(verbose_name='电话号码')  # 这里选择的是长整型也可以直接使用字符类型
    addr = models.CharField(max_length=32, verbose_name='地址')


"""
	orm中如何定义三种关系
		plublish = models.ForeignKey(to='Publish')  # 默认就是与出版社表的主键字段做外键关联
		
		authors = models.ManyToManyField(to='Author')

		Author_detail = models.OneToOneField(to='AuthorDetail')
		
		
		
		ForeignKey
		OneToOneField
			会自动在字段后面加_id后缀
"""
# 在django1.x版本中外键默认都是级联更新删除的
# 多对多的表关系可以有好几种创建方式 这里暂且先介绍一种

django请求生命周期流程图(必会)

# 扩展知识点
	"""
	缓存数据库
		提前已经将你想要的数据准备好了 你来直接拿就可以
		提高效率和响应事件
	缓存数据库理解:
        如果有缓存数据库,将请求来了之后,经过中间件不会立刻去请求django后端,会立刻去缓存数据库里面寻找有没有当前我需要的数据,如果有会直接在缓存数据库拿到我想要的页面,那么直接返回。如果没有我想要的数据经过后端处理然后再给你返回的时候会在经过中间件的时候会在缓存数据库里面储存一份数据,然后将你想要的数据发送给你,然后在你下一次的时候这个数据在缓存里面就有了,就不需要从后端里面拿取数据
	"""

路由层(urls.py)

路由匹配

# 路由匹配
url(r'test', views.test),
url(r'testadd', views.textadd),
"""
url方法第一个参数是正则表达式
	只要第一个参数正则表达式能够匹配到内容 那么就会立刻停止往下匹配
	直接执行对应的视图函数

你在输入url的时候默认加斜杠(/)
	django内部帮你做到重定向
		一次匹配不行
		url后面加斜杠再来一次
如何关闭这个功能:
	进入django的setting里面加入取消自动加斜杠的代码:
		APPEND_SLASH = False  # 这个参数默认为True(为自动加斜杠)
"""
# 避免报错首页url写法
url(r'^$', views.hom
# 尾页(当所有的url匹配不上那么走这个网页)
url(r'', views.error),
# 这种写尾页的方法会导致自动加斜杆的功能失效等后果

无名分组

"""
分组:就是给某一段正则表达式用小括号扩起来
"""
url(r'^test/(\d+)', views.test)

def test(request,number):
    print(number)
    return HttpResponse('test')

# 无名分组就是将括号内正则表达式匹配到的内容当作_位置参数_传递给后面的视图函数

有名分组

"""
可以给正则表达式起一个别名
"""
url(r'^testadd/(?P<year>\d+)', views.textadd)

def textadd(request, year):
    print(year)
    return HttpResponse('testadd')

# 无名分组就是将括号内正则表达式匹配到的内容当作_关键字参数_传递给后面的视图函数

无名分组和有名分组是否可以混合使用

'''
无名分组和有名分组不能混用
但是同一个分组可以使用N多次
当出现多个参数时,可以直接使用args和kwargs两个参数来进行接收
'''
# args接收
url(r'^index/(\d+)/(\d+)/(\d+)/',views.index)
def index(request,*args):
    print(args)
    return HttpResponse('index')
# kwargs接收
url(r'^index/(\d+)/(\d+)/(\d+)/',views.index)
def index(request,**kwargs):
    print(kwargs)
    return HttpResponse('index')

反向解析

# 通过一些方法得到一个结果 该结果可以直接访问对应的url触发视图函数

# 先给路由与视图函数起一个别名
    url(r'^func/',views.func,name= 'ooo')
# 反向解析
	# 后端反向解析
    	from django.shortcuts import reverse
        print(reverse('ooo'))
    # 前段反向解析
        {% url 'ooo' %}

有名无名分组的反向解析

# 无名分组反向解析
url(r'^index/(\d+)/', views.index, name='index')

# 前段
	{% url 'index' 123 %}
# 后端
	reverse('index',args=(1,))
"""
这个数字写代码的时候一般情况下是数据的主键值,用来做数据的编辑和删除
url(r'^edit/(\d+)/', views.edit, name='edit')

def home(request):
    user_all = models.User.objects.filter().all()
    return render(request, 'home.html', locals())

{% for user_obj in user_all %}
    <tr>
        <td>{{ user_obj.id }}</td>
        <td>{{ user_obj.username }}</td>
        <td>{{ user_obj.password }}</td>
        <td>{{ user_obj.sex }}</td>
        <td>
            <a href="{% url 'edit' user_obj.id %}" class="btn btn-primary btn-xs">编辑</a>
        </td>
    </tr>
{% endfor %}
"""
# 有名分组反向解析
    url(r'^delete/(?P<id>\d+)/', views.delete, name='delete'),
# 前段
	{% url 'delete' user_obj.id %}  # 和无名分组反向解析一样
# 后端
	# print(reverse('delete',kwargs={'id':123}))
    # 简便的写法
    # print(reverse('delete',args=(123)))  # 使用args的方式来进行传值也行

# 具体使用看myweb62中我的使用

路由分发

'''
django的每一个应用都有可以有自己的templates文件夹 urls.py static文件夹
正式基于上述的特点 django能够非常好的做到分组开发(每个人只写自己的app)
作为组长 只需要将手下书写的app全部拷贝到一个新的django项目中,然后在配置文件里面注册所有的app再利用路由分发的特点将所有的app整合起来

当一个django项目的url特别多,总路由url.py代码非常冗余不好维护
这个时候也可以用路由分发来减轻总路由的压力

利用路由分发之后 总路由不再干路由与视图函数的直接对应关系,而是做一个分发处理
	先识别当前url是属于那个应用下的 直接分发给对应的应用去处理


'''
# 总路由
# 第一种写法
from django.conf.urls import url, include
from django.contrib import admin

from app01 import urls as app01_urls
from app02 import urls as app02_urls

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    # 路由分发
    url(r'^app01/', include(app01_urls)),  # 只要url前缀是app01开头 全部交给app01处理
    url(r'^app02/', include(app02_urls)),  # 只要url前缀是app02开头 全部交给app02处理
]
# 第二种写法(推荐使用)
from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    # 路由分发
    url(r'^app01/', include('app01.urls')),
    url(r'^app02/', include('app02.urls')),
	# 注意事项,总路由里面的url千万不能加$结尾
]
# 子路由
# 第一个子路由
from django.conf.urls import url
from app02 import views

urlpatterns = [
    url(r'^reg/',views.reg)
]
# 第二个子路由
from django.conf.urls import url
from app01 import views

urlpatterns = [
    url(r'^reg/',views.reg)
]

名称空间(了解)

# 当多个应用出现了相同的别名 我们研究反向解析会不会自动识别应用前缀
"""
正常情况下的反向解析是没有办法自动识别前缀的

"""

# 名称空间
	# 总路由
    	url(r'^app01/', include('app01.urls',namespace='app01')),
    	url(r'^app02/', include('app02.urls',namespace='app02')),
    # 解析的时候
    # app01路由
    	urlpatterns = [
    		url(r'^reg/',views.reg,name='reg')
]
	# app01路由
        urlpatterns = [
    		url(r'^reg/',views.reg,name='reg')
]
    # app01视图函数解析
        print(reverse('app01:reg'))
    # app01视图函数解析
        print(reverse('app02:reg'))
	# 前段页面对两个路由的解析
    	{% url 'app01:reg' %}
        {% url 'app02:reg' %}
# 其实只要保证名字不冲突 就没有必要使用名称空间
'''
一般情况 有多个app的时候我们在起别名的时候会加上app的前缀
这样的话就能确保多个app之间名字不冲突的问题
'''
urlpatterns = [
    url(r'^reg/',views.reg,name='app01_reg')
]
urlpatterns = [
    url(r'^reg/',views.reg,name='app02_reg')
]
print(reverse('app01_reg'))
print(reverse('app02_reg'))

伪静态(了解)

'''
静态网页
	数据是写死的 万年不变

伪静态
	将一个动态网页伪装成静态网页

	为什么要伪装呢?
		伪装的目的在于增大本网站的seo查询力度
		并且增加搜索引擎收藏本网站的概率
	
	搜索引擎本质上就是一个巨大的爬取程序
	
	总结:
		无论你怎么优化 怎么处理
		始终还是干不过RMB玩家(垃圾百度,有钱就能上首页)
'''
# 在后面改为.html
urlpatterns = [
    url(r'^reg.html',views.reg,name='app02_reg')
]

虚拟环境(了解)

"""
在正常开发中 我们会给每一个项目配备一个该项目独有的解释器环境
该环境内只有该项目用到的模块 用不到一概不装

Linux:缺什么装什么

虚拟环境
	你每创建一个虚拟环境就类似于重新下载了一个纯净的python解释器
	
	但是虚拟环境不要创建太多,是需要消耗硬盘空间的

扩展:
	每一个项目都需要用到很多模块 并且每个模块版本可能还不一样
	开发当中我们会给每一个项目配备一个requirements.txt文件
	里面书写了该项目所有的模块即版本
	你只需要直接输入一条命令即可一键安装所有模块即版本
"""
# 创建虚拟环境方法
'''
1.点击pycharm左上的file
2.点击New Project
3.选择一个项目
4.带你New environment using
注意:
Inherit global site-packages:这个表示的是继承本机的已经存在的环境的所有的模块
Make available to all projects:这个表示的是让你创建的虚拟环境能够被其他的项目使用
点击Create
创建完毕之后有个叫做venv的文件夹表示使用的是虚拟环境
'''

django版本区别

"""
1.django1.x路由层使用的是url方法
而在django2.x和3.x版本中路由层使用的是path方法
	url()第一个参数支持正则
	path()第一个参数是不支持正则的 写什么就匹配什么
	
	如果不习惯使用path那么也提供了另外一种方法re_path
	from django.urls import re_path
	from django.conf.urls import url
	re_path的使用方法和1.x的使用方法是一样的,只不过换了名字,也可以直接导入url但是不推荐使用url
2.虽然path不支持正则 但是它的内部支持五种装换器
    path('index/<int:id>/', views.index)
    
    def index(request,id):
        print(id)
        return HttpResponse('index')
    # 将第二个路由里面的内容先装换为整型,然后以关键字的形式传递给后面的视图函数
    
    str,匹配除了路径分隔符(/)之外的非空字符串,这是默认的形式
    int,匹配正整数,包含0
    slug,匹配字母、数字以及横杠、下划线组成的字符串
    uuid,匹配格式化的uuid,如 075194d3-6885-417e-a8a8-6c931e272f00
    path,匹配任何非空字符串,包含了路劲分隔符(/)(不能用?)

3.除了有默认的五个装换器之外 还支持自定义转换器(了解)
	# 现在app01下创建path_converters.py文件,文件名可以随意命名,然后书写自定义类
	class MonthConverter:
        regex = '\d{2}'  # 属性名必须为regex

        def to_python(self, value):
            return int(value)

        def to_url(self, value):
            return value  # 匹配的regex是两个数字,返回的结果也必须是两个数字
	# 在urls.py中,使用register_converter将其注册到URL配置中
	from django.urls import path, register_converter
	from app01.path_converts import *
	from app01 import views
	
	urlpatterns = [
        path('admin/', admin.site.urls),
        # 将第二个路由里面的内容先装换为整型然后以关键字的形式传递给后面的视图函数
        path('index/<mon:id>/', views.index)
]
4.模型层里面1.x外键默认都是级联更新删除的
但是到了2.x和3.x中需要你自己动手配置参数
    models.ForeignKey(to="Publish")
    models.ForeignKey(to="Publish",on_delete=models.CASCADE)
"""

视图层(views.py)

三板斧

回顾一下上面的三板斧

'''
HttpResponse
	返回字符串类型的数据
	例:
		HttpResponse('你大爷的')
render
	返回HTML文件
	例:
		render(request,'firsthtml.html')
redirect
	重定向(跳转到指定的网页),也可以跳转自己的页面只写后缀
'''
# 视图函数必须要返回一个HttpResponse对象
"The view app01.views.index didn't return an HttpResponse object. It returned None instead."
# 表示必须返回一个HttpResponse对象
# render函数点进去查看源代码发现还是返回的是一个HttpResponse对象
# redirect函数点进去查看源码发现最后继承了HttpResponse对象,本质还是返回一个HttpResponse对象

# render简单内部原理
def index(request):
    from django.template import Template,Context
    res = Template('<h1>{{ user }}>/h1>')
    con = Context({'user':{'username':'jason','password':123}})
    ret = res.render(con)
    return HttpResponse(ret)

JsonResponse对象

"""
json格式的数据有什么用
	前后端数据交互需要使用到json作为过渡 实现夸语言传输数据
前段序列化与反序列化
	JSON.stringify()
	JSON.parse()
"""
import json
from django.http import JsonResponse
def ab_json(request):
    user_dict = {'username': 'json格式不设置参数中文会乱码', 'password': '123', 'hobby': 'girl'}
    l = [11,22,33,44,55,66,77]
    # json_data = json.dumps(l, ensure_ascii=False)
    # # 先装换为json格式的字符串
    # json_data = json.dumps(user_dict,ensure_ascii=False)  # ensure_ascii为False表示只改格式,不用进行转码
    # # 将该字符串返回
    # return HttpResponse(json_data)
    # return JsonResponse(user_dict, json_dumps_params={'ensure_ascii':False})
    return JsonResponse(l,safe=False)  # 如果safe不设置为False那么传除了字典以外的数据会报错,默认序列化字段

form表单上传文件及后端如何操作

"""
form表单上传文件类型的数据
	1.method必须指定成post
	2.enctype必须换成formdata

"""
def ab_file(request):
    if request.method == "POST":
        # print(request.POST)  # 只能获取普通的键值对数据 文件数据获取不到
        print(request.FILES)  # 获取文件数据
        "<MultiValueDict: {'file': [<InMemoryUploadedFile: Snipaste_2021-11-28_15-40-22.png (image/png)>]}>"
        file_obj = request.FILES.get('file')  # 文件对象
        print(file_obj.name)  # 获取文件的名字
        with open(file_obj.name,mode='wb') as f1:
            for line in file_obj:  # 推荐加上chunks方法(for line in file_obj.chunks()) 其实跟不加是一样的都是一行行的读取
                f1.write(line)
    return render(request, 'form.html')
# 前段
"""
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="" method="post" enctype="multipart/form-data">
    <p>username<input type="text" name="username"></p>
    <p>file:<input type="file" name="file"></p>
    <input type="submit" value="提交">
</form>
</body>
</html>
"""

request对象方法总结与补充

"""
request.method  # 获取请求方式
request.POST  # 用来获取post请求的键值对数据,不包含文件
request.GET  # 用来获取get请求
request.FILES  # 用来获取文件

# 补充
request.path
request.path_info
request.path_full_path()  # 能够获取完整的url及?号后面的参数
request.body  # 原生的浏览器发过来的二进制数据
"""
    print(request.path)  # /ab_file/
    print(request.path_info)  # /ab_file/
    print(request.get_full_path())  # /ab_file/?username=mubai

FBV与CBV

# 视图函数既可以是函数也可以是类
# FBV
def index(request):
    return HttpResponse('index')

# CBV
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    # CBV路由
    url(r'^login/',views.MyLogin.as_view())
]

from django.views import View

class MyLogin(View):
    def get(self, request):
        return render(request,'form.html')

    def post(self, request):
        return HttpResponse('post方法')
"""
FBV和CBV各有千秋
CBV特点:
	能够直接根据请求方式的不同直接匹配到对应的方法执行

"""

CBV源码剖析

# 突破口在urls.py
url(r'^login/', views.MyLogin.as_view())
# 上述代码在启动django的时候就会立刻执行as_view()方法
# 变形成:url(r'^login/',views.view)  # FBV一模一样
# CBV与FBV在路由匹配上本质都是一样的 都是路由 对应 函数内存地址
"""
函数名/方法名 加括号执行优先级最高
    as_view()
        要么是被@staicmethd修饰的静态方法
        要么是被@lassmethod修饰的类方法
    @classonlymethod
    def as_view(cls, **initkwargs):
        pass
        def view(request, *args, **kwargs):
            self = cls(**initkwargs)  # cls是我们自己写的类
            # self = Mylogin(**initkwargs)  产生一个我们自己写的类的对象
            return self.dispatch(request, *args, **kwargs)
            # 在看python源码的时候 一定要时刻提醒自己面向对象属性和方法查找顺序
                # 先从对象自己找
                # 在去产生对象的类里面找
                # 之后再去父类找
            总结:看源码只要看到了self点一个东西,一定要知道这个self到底是谁
    #CBV的精髓
    def dispatch(self, request, *args, **kwargs):
        # get请求为例
        if request.method.lower() in self.http_method_names:  # 获取当前请求的小写格式 然后比对当前的请求方式是否合法
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
            # 反射:通过字符串来操作对象的属性或者方法
            # handler = getattr(自己写的类产生的对象,'get',当找不到get属性或者方法的时候就会用第三个参数)
            # handler = 我们自己写的类的get方法
        else:
            handler = self.http_method_not_allowed  # 不在里面直接报错
        return handler(request, *args, **kwargs)
        # 自动调用get方法
"""
# 要求掌握到不看源码描述出CBV的内部执行流程

模版层(templates)

模板语法传值

前段之后两种书写方式

  • {{}}

    变量相关

  • {%%}

    逻辑相关

def index(request):
    # 模板语法可以传递的后端python数据类型
    booldata = True
    intdata = 123
    floatdata = 1.11
    strdata = '字符串类型'
    listdata = ['小红', '小花‘,’翠花']
    tupledata = (111, 233, 444, '等待')
    setdata = {'小明', '小芳', '小夏'}
    dictdata = {'username': 'jason', 'age': 18, 'info': 'jason老师牛逼'}

    # 基本数据类型都可以传递
    def func():
        print('func函数被执行了')
        return '前段拿到的是func函数的返回值'

    # 函数传递给前段会自动执行
    # 模版语法传递函数名会自动加括号调用函数 但是模版语法不支持给函数传额外的参数,如果有参数则不执行

    class MyClass(object):
        def get_selt(self):
            return 'self'

        @staticmethod
        def get_func():
            return 'func'

        @classmethod
        def get_class(cls):
            return 'cles'

        # 对象被展示到html页面上 就类似于执行了打印操作也会触发__str__方法
        def __str__(self):
            return '会展示出来'

    # 传类的时候也会加括号,将类进行实例化,有参数同上
    # 小结:内部能够自动判断出当前的变量名是否可以加括号调用 如果可以就会自动执行 针对的是函数名和类名
    obj = MyClass()
    # 在前段里面能调用类里面的方法
    return render(request, 'index.html', locals())

"""
前段
<p>布尔类型:{{ booldata }}</p>
<p>整型:{{ intdata }}</p>
<p>浮点型:{{ floatdata }}</p>
<p>字符串类型:{{ strdata }}</p>
<p>列表:{{ listdata }}</p>
<p>元组:{{ tupledata }}</p>
<p>集合:{{ setdata }}</p>
<p>字典:{{ dictdata }}</p>
<p>函数:{{ func }}</p>
<p>类:{{ MyClass }}</p>
<p>对象:{{ obj }}</p>
<p>对象的get_selt方法:{{ obj.get_selt }}</p>
<p>对象的get_func方法:{{ obj.get_func }}</p>
<p>对象的get_class方法:{{ obj.get_class }}</p>
{# 获取特殊值 #}
<p>{{ dictdata.hobby.3.info }}</p>
"""
# django语法的取值 是固定的格式 只能采用"句点符"
# <p>{{ dictdata.hobby.3.info }}</p>
# 既可以点键也可以点索引 还可以两者混用

过滤器

# 过滤器就类似于是模版语法内置的 内置方法
# django内置有60多个过滤器 先学习10个作用


# 基本语法
{{数据|过滤器:参数}}
# 转义
# 前段
   |safe
# 后端
   from django.utils.safestring import mark_safe
   res = mark_safe('<h1>敏敏</h1>')
"""
以后你在写全栈项目的时候 前段代码不一定非要在前段页面书写
也可以先在后端里面写好 然后传递给前段页面

"""
# 过滤器后端
def index(request):
   # 模板语法可以传递的后端python数据类型
   booldata = True
   intdata = 123
   floatdata = 1.11
   strdata = '字符串类型'
   listdata = ['小红', '小花‘,’翠花','小明','小涛']
   tupledata = (111, 233, 444, '等待')
   setdata = {'小明', '小芳', '小夏'}
   dictdata = {'username': 'jason', 'age': 18, 'info': 'jason老师牛逼', 'hobby': [111, 222, 333, {'info': 'dsb'}]}

   # 基本数据类型都可以传递
   def func():
       print('func函数被执行了')
       return '前段拿到的是func函数的返回值'

   # 函数传递给前段会自动执行
   # 模版语法传递函数名会自动加括号调用函数 但是模版语法不支持给函数传额外的参数,如果有参数则不执行

   class MyClass(object):
       def get_selt(self):
           return 'self'

       @staticmethod
       def get_func():
           return 'func'

       @classmethod
       def get_class(cls):
           return 'cles'

       # 对象被展示到html页面上 就类似于执行了打印操作也会触发__str__方法
       def __str__(self):
           return '会展示出来'

   # 传类的时候也会加括号,将类进行实例化,有参数同上
   # 小结:内部能够自动判断出当前的变量名是否可以加括号调用 如果可以就会自动执行 针对的是函数名和类名
   obj = MyClass()
   # 在前段里面能调用类里面的方法
   # 文件大小转换
   file_size = 1231321312
   # 日期转换
   current_time = datetime.datetime.now()
   # 切取操作,对文章进行一个摘要处理
   info = ''' 天色有点阴暗,看着挺舒服的。有一点微风,树梢微微摇起。天上应该布满了云,不然怎么会一点蓝也看不见。
  学校早说要翻新,一两年过去,翻新个屁,墙皮改掉的还是掉,操场已经被人巴拉的差不多了。'''
   egl = 'No matter how bad your heart has been broken, the world doesn’t stop for your grief. The sun comes right back up the next day'
   msg = "i don't know"
   htmldata = '<h1>敏敏</h1>'
   # 后端进行转移
   from django.utils.safestring import mark_safe
   res = mark_safe(htmldata)
   print(res)
   return render(request, 'index.html', locals())
"""
前段
<p>布尔类型:{{ booldata }}</p>
<p>整型:{{ intdata }}</p>
<p>浮点型:{{ floatdata }}</p>
<p>字符串类型:{{ strdata }}</p>
<p>列表:{{ listdata }}</p>
<p>元组:{{ tupledata }}</p>
<p>集合:{{ setdata }}</p>
<p>字典:{{ dictdata }}</p>
<p>函数:{{ func }}</p>
<p>类:{{ MyClass }}</p>
<p>对象:{{ obj }}</p>
<p>对象的get_selt方法:{{ obj.get_selt }}</p>
<p>对象的get_func方法:{{ obj.get_func }}</p>
<p>对象的get_class方法:{{ obj.get_class }}</p>
{# 获取特殊值 #}
<p>{{ dictdata.hobby.3.info }}</p>
{#过滤器#}
<p>统计长度:{{ strdata|length }}</p>
<p>默认值:{{ booldata|default:'啥也不是' }}</p>
{#default表示第一个参数布尔值是True就展示第一个参数的值,否展示冒号后面的值 #}
<P>文件大小:{{ file_size|filesizeformat }}</P>
<p>日期格式化:{{ current_time }},{{ current_time|date:'Y-m-d H:i:s' }}</p>
<p>切片操作(支持步长):{{ listdata|slice:'0:4:2' }}</p>
<p>切取字符(包含三个点):{{ info|truncatechars:20 }}</p>
{#后面跟的数字为截取字符数#}
<p>窃取单词(不包含三个点 按照空格切,中文也是一样):{{ egl|truncatewords:5 }}</p>
{#后面跟的数字为截取的单词数#}
<p>移除特定的字符:{{ msg|cut:' ' }}</p>
<p>拼接操作:{{ listdata|join:'$' }}</p>
<p>拼接操作(加法):{{ intdata|add:10 }},{{ strdata|add:msg }}</p>
<p>取消转义:{{ htmldata }}</p>
<p>前段转义:{{ htmldata|safe }}</p>
{#如果不取消转移如果在里面书写了js代码那么将会直接运行#}
<p>后端转义:{{ res }}</p>
"""

标签

# for循环
"""
<h1>标签</h1>
{% for foo in listdata %}  <!-- 等价于 for .. in .. 循环  ,变量名可以随便起可以不用foo-->
    <p>{{ forloop }}</p>
    <p>{{ foo }}</p>
{% endfor %}
<!--
forloop的循环打印结果
{'parentloop': {}, 'counter0': 0, 'counter': 1, 'revcounter': 4, 'revcounter0': 3, 'first': True, 'last': False}
{'parentloop': {}, 'counter0': 1, 'counter': 2, 'revcounter': 3, 'revcounter0': 2, 'first': False, 'last': False}
{'parentloop': {}, 'counter0': 2, 'counter': 3, 'revcounter': 2, 'revcounter0': 1, 'first': False, 'last': False}
{'parentloop': {}, 'counter0': 3, 'counter': 4, 'revcounter': 1, 'revcounter0': 0, 'first': False, 'last': True}
counter0:表示从0开始的下标,counter:表示从1开始的下标,first表示是否为for循环的第一个元素,last表示是否最后一个,revcounter0和revcounter对应着counter0和counter
 -->
"""
# if判断
"""
{% if booldata %}
    <p>if里面的语句</p>
    {% elif not intdata %}
        <p>elif里面的内容</p>
    {% else %}
        <p>else里面的内容</p>
{% endif %}
"""

# for和if混合使用
"""

{% if booldata %}
    <p>if里面的语句</p>
{% elif not intdata %}
    <p>elif里面的内容</p>
{% else %}
    <p>else里面的内容</p>
{% endif %}

{% for dictdatum in dictdata %}
    {% if forloop.first %}
        <p>打印开头的数据:{{ dictdatum }}</p>
    {% elif forloop.counter0 == 2 %}
        <p>这个是第三个数据:{{ dictdatum }}</p>
    {% elif forloop.last %}
        <p>这个是最后一个数据:{{ dictdatum }}</p>

    {% endif %}
{% empty %}
    <p>当for循环的对象为空,无法循环的时候打印这个</p>
{% endfor %}
"""
# 处理字典其他方法
"""
{% for foo in dictdata.keys %}
    <p>{{ foo }}</p>
{% endfor %}

{% for foo in dictdata.values %}
    <p>{{ foo }}</p>
{% endfor %}

{% for foo in dictdata.items %}
    <p>{{ foo }}</p>
{% endfor %}
"""
# with起别名
'''
{% with dictdata.hobby.3.info as dsb %}
    <p>在这里面才能使用别名{{ dsb }}</p>
{% endwith %}
'''

自定义过滤器、标签、inclusion_tag

"""
要想自定义过滤器、标签或者inclusion_tag
	1.在应用下创建一个名字"必须"叫templatetags文件夹
	2.在该文件夹创建"任意"名称的py文件  eg:mytag.py
	3.在该py文件"必须"书写两句代码(写死)
		from django import template
		register = template.Library()
	
"""
# 自定义过滤器(过滤器最多只能有两个参数,左边一个参数,右边一个参数(右边的揩油没有),一般写多个参数可以写一些特殊的字符串然后进行处理)
# mytag文件夹
@register.filter(name='my_sum')
def my_sum(x, y):
    return x + y
# 前段
'''
<!-- 加载过滤器对应的文件 -->
{% load mytag %}
<p>{{ 4|my_sum:123 }}</p>
'''

# 自定义标签(参数可以有多个)
@register.simple_tag(name='plus')
def index(a,b,c,d):
    return f'{a}-{b}-{c}-{d}'

# 前段
'''
<!-- 加载标签对应的文件,如果已经加载则不需要加载 -->
{% load mytag %}
<!-- 标签多个参数彼此之间空格隔开 -->
<p>{% plus 2021 12 8 8 %}</p>
'''

# 自定义inclusion_tag
"""
内部原理
	先定义一个方法
	在页面上调用该方法 并且可以传值
	该方法会生成一些数据然后传递给一个html页面
	之后将渲染好的结果放到调用的位置
"""
# mytag文件
# 自定义inclusion_tag, 一般inclusion_tag作用的页面不是一个完整的页面,一般都是作用一个局部的页面
@register.inclusion_tag(filename='left_menu.html')
def left(n):
    data = ['第{}项'.format(i) for i in range(n)]
    # 第一种传值的方式
    return locals()  # 将data传递给left_menu.html
    # 第二种传值方式
    return {'data': data}
    # 上面两种传值方式都和view.py文件里面的render传值方式一样
# 前段
'''
<!-- 加载对应inclusion_tag的文件,如果已经加载则不需要加载 -->
{% load mytag %}
{% left 10 %}
'''
# 总结,当html页面某一个地方需要传参数才能动态的渲染出来,并且在多个页面上都需要使用到该局部 那么就可有考虑将该局部页面做成inclusion_tag形式

模板的继承

'''
可能使用到模板继承的网站特点
	页面整体大差不差 只是某一些局部在
'''
# 模板的继承 自己先选好一个需要继承的模板页面
<!-- 模板的继承 -->
{% extends 'home.html' %}
# 继承之后子页面更模板页面一模一样,你需要在模板页面上提前划定可以被修改的页面
 <!-- 这个block只是做了一个标记,不会改变页面的内容,但是其他页面继承这个页面的时候这个部分是可以修改的 -->
{% block content %}
    <div class="jumbotron">
        <h1>Hello, world!</h1>
        <p>...</p>
        <p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a></p>
    </div>
{% endblock %}
# 子页面就可以声明想要修改那块划定了的区域
{% block content %}
    <h1 class="text-center">注册页面</h1>
    <form action="">
    <p>username: <input type="text" class="form-control"></p>
    <p>password: <input type="text" class="form-control"></p>
        <input type="submit" value="注册" class="btn btn-danger">
    </form>
{% endblock %}


# 一般情况下模板页面上应该至少有三块可以被修改的区域
	1.css区域
    {% block css %}
    {% endblock %}
    2.html区域
    {% block css %}
    3.js区域
    {% block css %}
# 每一个子页面就都可以有自己独有的css代码 html代码 js代码,网站的兼容性比较好

"""
一般情况下 模板的页面上划定的区域越多 那么该模板的扩展性就越高 但是如果太多 那么就不如自己重新写一份
"""

模板的导入

"""
将页面的某个局部当成模块的形式
那个地方需要就可以直接导入使用即可
"""
{% include 'wasai.html' %}

模型层(models.py)

单标操作

# django自带的sqlite3数据对日期格式不是很敏感 处理的时候容易出错

测试脚本

"""
当你只是想测试django中的某一个py文件内容 那么你可以不用书写前后端交互的形式 而是直接写一个测试脚本即可
脚本代码无论是写在应用下的tests.py还是自己单独开设py文件都可以
"""
# 测试环境的准备
# 进入manag.py文件里面输入以下代码
"""
import os


if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myweb_day64.settings")
    import django
    django.setup()
    # 在这个代码块的下面就可以测试django里面的单个py文件了
    # 所有的代码都必须等待环境准备完毕之后才能书写
"""
# 这样就可以单独测试py文件了

查看内部sql语句的方式

# 方式1
res = models.User.objects.values_list('name','age')
print(res)  # <QuerySet [('mubai', 21), ('tianyou', 18), ('luo', 18), ('jiami', 18)]>
print(res.query)  # 查看内部封装的sql语句
# queryset对象才能够点击query查看内部的sql语句

# 方式2:所有的sql语句都能查看
# 去配置文件中配置一下即可
# 在setting里面输入以下内容即可
"""
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console':{
            'level':'DEBUG',
            'class':'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'propagate': True,
            'level':'DEBUG',
        },
    }
}
"""

必知必会13条

from app01 import models

# # 增
# 第一种增加数据的方法
# res = models.User.objects.create(name='mubai',age='21',register_time='2000-12-08')  # 这个返回的对象就是被创建的数据本身
# print(res)
# 第二种增加数据的方法
# 第二种输入日期的方法
# import datetime
# ctime  = datetime.datetime.now()
# user_obj = models.User(name='tianyou',age=18,register_time=ctime)
# user_obj.save()
# 删
# 第一种删除数据的方法
# res = models.User.objects.filter(pk=2).delete()  # 这个表示你当前所影响的行数,简单来说就是你删除了几行
# print(res)
"""
pk会自动查找到当前表的主键字段 指代的就是当前表的主键字段
用了pk之后 你就不需要指代当前表的主键字段到底叫什么了
"""
# 第二种删数据的方式
# user_obj = models.User.objects.filter(pk=1).first()  # 获取主键为1的数据对象
# user_obj.delete()

# 修改
# 第一种修改方式
# models.User.objects.filter(pk=2).update(name='马天佑')

# 第二种修改方式
# user_obj = models.User.objects.get(pk=2)
# print(user_obj)
"""
get方法返回的直接就是当前数据对象
但是该方法不推荐使用
    一旦数据不存在该方法会直接报错
    而filter则不会
        所以我们还是用filter
"""
# user_obj.name = 'tianyou'
# user_obj.save()

# 必须知道的13条方法
# 1.all()  # 查询所以数据
# 2.filter()  # 带有过滤条件的查询
# 3.get()  # 直接拿数据对象 但是条件不存在直接报错
# 4.first()  # 拿queryset里面第一个元素
# 5.last()  # 拿queryset里面最后一个元素

# 6.values()  # 可以指定获取的数据字段
# 只拿用户表里面的名字
# res = models.User.objects.values('name')  # 类似于 select name from app01_user  返回的结果可以看为列表套字典
# print(res)  # 获取的是表里面所有name字段的值,数据可以看成列表加字典
# res = models.User.objects.values('name','age')
# print(res)  # 获取所有表里面的两个值name值和age值
# 7.values_list()
# res = models.User.objects.values_list('name','age')
# print(res)  # <QuerySet [('mubai', 21), ('tianyou', 18), ('luo', 18), ('jiami', 18)]>
# 获取的数据可以看成列表套 ,只不过外面的列表是自定义的列表对象,它的列表对象其实是自己定义的元素对象,只不过比较像列表元素,虽然想列表但是多了很多的功能
# Queryset对象基础介绍
# print(res.query)  # 查看内部封装的sql语句
"""
# 查看内部封装的sql语句
上述查看sql语句的方式 只能用于queryset对象
只有queryset对象才能够点击query查看内部的sql语句
"""
# 8.distinct()  # 去重
# res = models.User.objects.values('name').distinct()  # 去重默认去重的为主键数据,所以要其他条件去重的话那么需要使用values来指定去重的键
# print(res)
"""
去重一定要是一模一样的数据
如果带有主键那么肯定不一样 你在往后的查询中一定不要忽略主键
"""
# 9.order_by()  # 排序
# res = models.User.objects.order_by('age')  # 默认为升序
# res = models.User.objects.order_by('-age')  # 降序
# print(res)
# for i in res:
#     print(i.age)
# 10.reverse()  # 反转 , 但是反转的前提就是 数据已经进行排序了(了解即可)
# res = models.User.objects.order_by('age').reverse()
# print(res)
# for i in res:
#     print(i.age)
# 11.count()  # 统计当前数据的个数
# res = models.User.objects.count()
# print(res)
# 12.exclude()  # 排除在外
# res = models.User.objects.exclude(name='mubai')
# print(res)
# 13.exists()  # 判断某东西是否存在
res = models.User.objects.filter(pk=3).exists()
print(res)  # 存在返回True,不存在返回False,返回的是布尔值

双下划线查询

# 下划线查询
# 1. 获取年龄大于20岁的数据
print('获取年龄大于20岁的数据')
res = models.User.objects.filter(age__gt=20)
for i in res:
    print(i.name, ':', i.age)
# 2. 获取年龄小于20的数据
print('获取年龄小于20的数据')
res = models.User.objects.filter(age__lt=20)
for i in res:
    print(i.name, ':', i.age)
# 3. 获取年龄大于等于20岁的数据
print('获取年龄大于等于20岁的数据')
res = models.User.objects.filter(age__gte=20)
for i in res:
    print(i.name, ':', i.age)
# 4. 获取年龄小于等于20的数据
print('获取年龄小于20的数据')
res = models.User.objects.filter(age__lte=20)
for i in res:
    print(i.name, ':', i.age)

# 5. 获取年龄是18 或者30 或者40的数据
print('获取年龄是18 或者30 或者40的数据')
res = models.User.objects.filter(age__in=[18, 30, 40])
for i in res:
    print(f'{i.name},{i.age}')
# 6. 获取年龄在18到40岁之间的数据
print('获取年龄在18到40岁之间的数据')
res = models.User.objects.filter(age__range=[18, 40])  # range的是首尾都需要的数据
for i in res:
    print(f'{i.name},{i.age}')
# 7. 查询出名字里面含有mubai的数据 模糊查询
print('查询出名字里面含有mubai的数据')
res = models.User.objects.filter(name__contains='mubai')
for i in res:
    print(f'{i.name},{i.age}')
# 8. 查询出名字里面含有mubai的数据(不区分大小写) 模糊查询
print('查询出名字里面含有mubai的数据(不区分大小写)')
res = models.User.objects.filter(name__icontains='mubai')
for i in res:
    print(f'{i.name},{i.age}')
# 9. 查询出名字以j开头的数据
print('查询出名字以j开头的数据')
res = models.User.objects.filter(name__startswith='j')
for i in res:
    print(f'{i.name},{i.age}')
# 10. 查询出名字以j开头的数据(不区分大小写)
print('查询出名字以j开头的数据(不区分大小写)')
res = models.User.objects.filter(name__istartswith='j')
for i in res:
    print(f'{i.name},{i.age}')
# 11. 查询出名字以u结尾的数据
print('查询出名字以u结尾的数据')
res = models.User.objects.filter(name__endswith='u')
for i in res:
    print(f'{i.name},{i.age}')
# 12. 查询出名字以u结尾的数据(不区分大小写)
print('查询出名字以u结尾的数据(不区分大小写)')
res = models.User.objects.filter(name__iendswith='u')
for i in res:
    print(f'{i.name},{i.age}')
# 13. 查询出注册时间是2000-12 月份的数据
print("查询出注册时间是2000-12 月份的数据")
res = models.User.objects.filter(register_time__year="2000", register_time__month='12')  #
for i in res:
    print(f'{i.name},{i.age},{i.register_time}')
"""
register_time__week_day :表示的是工作日
register_time__day :表示日
"""

一对多的外键增删改查(一对一和一对多的增删改查是一样的)

# 一对多外键增删改查
# 增
# 1. 直接写实际字段 id
res = models.Book.objects.create(title='三国演义', price=99.99, publish_id=1)
# 2. 虚拟字段 对象
publish_obj = models.Publish.objects.filter(pk=2).first()
models.Book.objects.create(title='红楼梦', price=99.99, publish=publish_obj)

# 删
models.Publish.objects.filter(pk=1).delete()  # 级联删除(出版社删除对应的数据也要进行删除)

# 修改
# 1. 直接进行修改 id
models.Book.objects.filter(pk=1).update(publish_id=2)
# 2. 使用对象的方式进行修改
publish_obj = models.Publish.objects.filter(pk=1).first()
models.Book.objects.filter(pk=1).update(publish=publish_obj)

多对多的外键增删改查

# 多对多的增删改查  就是在操作第三张表
book_obj = models.Book.objects.filter(pk=1).first()
# 如何给数据绑定作者
# book_obj.authors就相当于直接到了第三张表里面,可以直接对第三张表进行操作
# book_obj.authors.add(1)  # 给书籍id为1的数据绑定一个书籍主键为1的作者
# 给数据id添加多个数据
# book_obj.authors.add(2,3)
# add函数除了可以输入数字以外还支持放入对象
# authers = models.Author.objects.filter(age=18).all()
# book_obj.authors.add(*(i for i in authers))
"""
add给第三张关系表添加数据
    括号内既可以传数字也可以传对象 并且都支持多个数字和对象
"""
# 删
# book_obj.authors.remove(2)  # 同样和上面一样支持多值,也同样支持对象和多个对象
# authers = models.Author.objects.filter(age=18).all()
# book_obj.authors.remove(*(i for i in authers))
# 修改 set
# authers = models.Author.objects.filter(age=18).all()
# book_obj.authors.set(i for i in authers)
"""
set
    括号内必须传一个可迭代对象,该对象内既可以数字也可以对象 并且都支持多个
    
    先删除后新增
"""
# 清空
# 在第三张表中清空某个数据和作者的绑定关系
book_obj.authors.clear()
"""
clear
    括号内不要加任何参数
"""

正反向概念

"""
eg:书籍和出版社
	在书籍的位置创建一个出版社
查书或者查出版社
# 正向
	由书来查对应的外键
# 反向
	由书的外键出版社来查书
小结:
	外键字段在我手上,那么我查你就是正向
	外键字段不在我手上,那么我查你就是反向

	book >>> 外键字段在书哪儿(正向)>>> publish
	publish >>> 外键字段在书哪儿(反向)>>> book
一对一和多对多查询也和这个一样
"""
"""
正向查询按字段
反向查询按表名小写
		_set
		...

"""

多表查询

子查询(基于对象的跨表查询)

# 基于对象的跨表查询

# 1.查询书籍主键为1的出版社名称
# book_obj = models.Book.objects.filter(pk=1).first()
# # 书查出版社 正向
# res = book_obj.publish
# print(res)  # 出版社对象
# print(res.name)
"""
# 自己的答案
publish_name = models.Book.objects.filter(pk=1).first().publish.name
print(publish_name)
"""
# 2.查询书籍主键为1的作者
# book_obj = models.Book.objects.filter(pk=1).first()
# # 书查作者 正向
# res = book_obj.authors.all()
# print(res)
"""
# 自己的答案
author_name = models.Book.objects.filter(pk=1).first().authors.all()
print(author_name)
for i in author_name:
    print(i.name) 
"""

# 3.查询作者jason的电话号码
# author_obj = models.Author.objects.filter(name= 'jason').first()
# res = author_obj.author_detail
# print(res)
# print(res.phone)
"""
# 自己的答案
author_json_phone = models.Author.objects.filter(name='jason').first().author_detail.phone
print(author_json_phone)
"""
"""
在书写orm语句的时候跟写sql语句一样的
不要企图一次性将orm语句写完 如果比较复杂 就写一点看一点
正向什么时候需要加.all()
    当你的结果可能有多个的时候就需要加.all()
    如果是一个则直接获取到数据对象
"""

# 4.查询出版社是新东方出版社出版的书
# publish_obj = models.Publish.objects.filter(name="新东方出版社").first()
# # 出版社查书 反向
# res = publish_obj.book_set.all()
# print(res)
"""
# 自己的答案
book_all = models.Publish.objects.filter(name='新东方出版社').first().book_set.all()
print(book_all)
"""

# 5.查询作者是jason写过的书
# author_obj = models.Author.objects.filter(name='jason').first()
# # 作者查书 反向
# res = author_obj.book_set.all()
# print(res)
"""
# 自己的答案
book_all = models.Book.objects.filter(authors__name='jason').all()
print(book_all)
book_all = models.Author.objects.filter(name = 'jason').first().book_set.all()
print(book_all)
"""

# 6.查询手机号是110的作者姓名
# author_detail_obj = models.AuthorDetail.objects.filter(phone=110).first()
# res = author_detail_obj.author
# print(res.name)
"""
# 自己的答案
username = models.Author.objects.filter(author_detail__phone=110).first().name
print(username)
username = models.AuthorDetail.objects.filter(phone=110).first().author.name
print(username)
"""
"""
基于对象
    反向查询
        当你的查询结果可以有多个的时候 就必须加_set.all()
        当你的结果只有一个的时候 不需要_set.all()
"""

联表查询(基于双下划线的跨表查询)

# 基于双下划线的跨表查询
# 1. 查询jason的手机号和作者的姓名
# res = models.Author.objects.filter(name='jason').values('author_detail__phone','name')
# print(res)
# 反向
# res = models.AuthorDetail.objects.filter(author__name='jason').values('phone','author__name')  # 拿作者姓名是jason的作者详情
# print(res)
"""
# 自己的答案
author_obj = models.Author.objects.filter(name='jason').first()
print(author_obj.name)
print(author_obj.author_detail.phone)
"""
# 2.查询书籍主键为1的出版社名称和书籍的名称(一行代码搞定)
# res = models.Book.objects.filter(pk=1).values('publish__name','title')
# print(res)
# 反向
# res = models.Publish.objects.filter(book__pk=1).values('name','book__title')
# print(res)
"""
# 自己的答案
book_all = models.Book.objects.filter(pk=1).values('publish__name','title')
print(book_all)
"""
# 3.查询书籍主键为1的作者姓名
# res = models.Book.objects.filter(pk=1).values('authors__name')
# print(res)
# 反向
# res = models.Author.objects.filter(book__pk=1).values('name')
# print(res)
"""
# 自己的答案
result = models.Book.objects.filter(pk=1).values('authors__name')
print(result)
"""
# 查询书籍主键1的作者的手机号
res = models.Book.objects.filter(pk=1).values('authors__author_detail__phone')
print(res)
"""
# 自己的答案
result = models.AuthorDetail.objects.filter(author__book__pk=1).values('phone')
print(result)
result = models.Book.objects.filter(pk=1).values('authors__author_detail__phone')
print(result)
result = models.Author.objects.filter(book__pk=1).values('author_detail__phone')
print(result)
"""
"""
你只要掌握了正反向的概念
以及双下划线
那么你就可以无限制的跨表
"""

聚合查询

# 聚合查询
"""
如果不想使用分组直接使用聚合函数需要使用到aggregate
聚合查询通常情况下都是配合分组一起使用的
只要是和数据库相关的模块
    基本上都在django.db.models里面
    如果上述都没有那么应该在django.db里面
"""
from django.db.models import Max, Min, Sum, Count, Avg

# 1. 所有书的平均价格
res = models.Book.objects.aggregate(Avg('price'))  # 求出书籍得平均价格
print(res)
# 2. 上述方法一次性使用
res = models.Book.objects.aggregate(Max('price'), Min('price'), Sum('price'), Count('price'))  # 最大值 最小值 和 计数
print(res)

分组查询

from django.db.models import Max, Min, Sum, Count, Avg

# 1. 统一每一本书的作者个数
# res = models.Book.objects.values.annotate()  # models后面.什么就是按什么分组
# res = models.Book.objects.annotate(author_num=Count('authors')).values('title','author_num')
# print(res)
"""
authors和authors__id的效果是一样的
author_num 是我给数据获取到的数据取的一个别名,在最后的计算结果当中会有这个数据,然后通过values来获取对应的值
author_num 使我们自己定义的字段 用来存储统计出来的每本书的对应的作者个数
"""
# 2. 统计每个出版社卖的最便宜的书的价格
# res = models.Publish.objects.annotate(price_min=Min('book__price')).values('name','price_min')
# print(res)

# 3. 统计不止一个作者的图书
#   (1). 先按图书分组
#   (2). 过滤出不止一个作者的图书
# res = models.Book.objects.annotate(authors_num=Count('authors__id')).filter(authors_num__gt=1).values('title',
#                                                                                                       'authors_num')
# print(res)
"""
只要你的orm语句得出的结果还是一个queryset对象
那么它就可以无限制的点queryset对象封装的方法
"""
# 4.查询每个作者出的书的总价格
# res = models.Author.objects.annotate(book_price_sum = Sum('book__price')).values('name','book_price_sum')
# print(res)

"""
如果我想按指定的字段分组该如何处理
    models.Book.objects.values('price').annotate()
    annotate函数如果前面出现了values函数那么会按照values函数里面的指定的字段进行分组,如果前面没有出现values那么就会按照models后面指定的表进行分组
如果出现了分组报错的情况,你需要修改数据库严格模式
"""

F查询和Q查询

# F与Q查询
# 1. 查询卖出数大于库存数的数据
# F查询
"""
能够帮助你直接获取到表中的某个字段对应的数据
"""
from django.db.models import F

# res = models.Book.objects.filter(sell_out__gt=F('stock'))
# print(res)

# 2. 将所有书籍的价格提升50元
# res = models.Book.objects.update(price=F('price')+50)
# 3. 将所有的书的名称后面加上爆款两个字
"""
在操作字符串数据的时候 F不能够直接做到字符串的拼接
如:
    res = models.Book.objects.update(title=f"{F('title')}爆款")
相当于:
    res = models.Book.objects.update(title="{F('title')}爆款")
要使用Concat方法和Value方法才能进行修改字符类型的数据
"""
from django.db.models.functions import Concat
from django.db.models import Value

# res = models.Book.objects.update(title = Concat(F('title'),Value('爆款')))

# Q查询
# 1.查询卖出数大于100或者价格大于100的数据
from django.db.models import Q

# 与
# res = models.Book.objects.filter(Q(sell_out__gt=100), Q(price__gt=100))  # Q包裹逗号分割还是and关系
# 或
# res = models.Book.objects.filter(Q(sell_out__gt=100) | Q(price__gt=100))  # 使用|才表示的是或的关系,如果使用python的or的话最后的返回结果还是为and
# print(res)
# 非
# res = models.Book.objects.filter(~Q(sell_out__gt=100) | Q(price__gt=100))  # ~符号表示的是非
"""
# 不用Q查询
res = models.Book.objects.filter(sell_out__gt=100)
res_two  = models.Book.objects.filter(price__gt = 100)
print(res)
"""
# Q的高阶用法 能够将查询条件的左边也变成字符串的形式
q = Q()
q.connector = 'OR'  # 默认是and关系
q.children.append(('sell_out__gt'), 100)
q.children.append(('price__gt'), 100)
res = models.Book.objects.filter(q)  # filter括号内也支持直接放Q对象
print(res)

django中如何开启事务

"""
事务
	ACID
		原子性
			不可分割的最小单位
		一致性
			跟原子性相辅相成
		隔离性
			事务之间互相不干扰
		持久性
			事务一旦确认永久生效
	事务的回滚
		rollback
	事务的确认
		commit
"""
# 目前你需要掌握Django中如何简单的开启事务
# 事务
from django.db import transaction

# 开启事务
with transaction.atomic():
    # 在with代码块内书写的所有的orm操作都是属于同一个事务
    print('执行事务')
print('执行其他操作')

# 也可以使用try来捕获事务可能会发生的异常
try:
    with transaction.atomic():
        # 在with代码块内书写的所有的orm操作都是属于同一个事务
        print('执行事务')
except Exception as e:
    print('事务出现报错之后执行的代码')

orm常用字段及参数+自定义字段

AutoField			主键(默认为id字段)
    promary_key = True

CharField			varchar
    verobose_name	字段的注释
    max_length		字符的长度

IntegerField		int
BigIntegerField		bigint

DecimalField
	max_digits		整数的个数
    decimal_places	小数部分的个数

EmailField			varchar(254)

DateField			date
DateTimeField		datatime
	auto_now		每次修改数据的时候都会自动更新当前时间
    auto_now_add	只在创建数据的时候记录创建时间后续不会自动修改了

BooleanField		布尔值
	该字段传布尔值(False/True) 数据库里面存0/1

TextField			文本类型
	该字段可以用来存大段内容(文章、博客..)	没有字数限制

FileField			文件字段
	看查下面的内容

# django除了给你提供了很多字段类型之外 还支持你自定义字段
# 自定义字段
class MyCharField(models.Field):
    # 必写的方法
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length
        # 调用父类的init方法
        super().__init__(max_length=max_length,*args,**kwargs)  # 一定要是关键字的形式传入
    # 必写的方法
    def db_type(self, connection):
        """
        返回真正的数据类型及各种约束条件
        :param connection:
        :return:
        """
        return 'char(%s)'%self.max_length
def Myfield(models.Model):
    # 自定义字段的使用
    myfield = MyCharField(max_length=20,null=True)
常用字段参数
null
用于表示某个字段可以为空。

unique
如果设置为unique=True 则该字段在此表中必须是唯一的 。

db_index
如果db_index=True 则代表着为此字段设置索引。

default
为该字段设置默认值。
外键字段和外键字段参数
# 一对多字段
ForeignKey
外键类型在ORM中用来表示外键关联关系,一般把ForeignKey字段设置在 '一对多''多'的一方。
ForeignKey可以和其他表做关联关系同时也可以和自身做关联关系。

字段参数
to
设置要关联的表

to_field
设置要关联的表的字段,默认不写关联的就是另外一张表的主键字段

on_delete
当删除关联表中的数据时,当前表与其关联的行的行为。
"""
django2.x版本及以上需要你自己指定外键字段的级联更新,级联删除
"""

models.CASCADE

删除关联数据,与之关联也删除

db_constraint
是否在数据库中创建外键约束,默认为True。
参数:
    models.DO_NOTHING
    删除关联数据,引发错误IntegrityError


    models.PROTECT
    删除关联数据,引发错误ProtectedError


    models.SET_NULL
    删除关联数据,与之关联的值设置为null(前提FK字段需要设置为可空)


    models.SET_DEFAULT
    删除关联数据,与之关联的值设置为默认值(前提FK字段需要设置默认值)


    models.SET

    删除关联数据,
    a. 与之关联的值设置为指定值,设置:models.SET()
    b. 与之关联的值设置为可执行对象的返回值,设置:models.SET(可执行对象)

# 一对一字段
ForeignKey(unique=True)  ===  OneToOneField()
OneToOneField
一对一字段。

通常一对一字段用来扩展已有字段。(通俗的说就是一个人的所有信息不是放在一张表里面的,简单的信息一张表,隐私的信息另一张表,之间通过一对一外键关联)

字段参数
to
设置要关联的表。

to_field
设置要关联的字段。

on_delete
当删除关联表中的数据时,当前表与其关联的行的行为。(参考上面的例子)

拓展字段内容
每一个字段都可以使用verobose_name来给字段起一个别名,方便自己认识
AutoField(Field)
    - int自增列,必须填入参数 primary_key=True

BigAutoField(AutoField)
    - bigint自增列,必须填入参数 primary_key=True

    注:当model中如果没有自增列,则自动会创建一个列名为id的列
    from django.db import models

    class UserInfo(models.Model):
        # 自动创建一个列名为id的且为自增的整数列
        username = models.CharField(max_length=32)

    class Group(models.Model):
        # 自定义自增列
        nid = models.AutoField(primary_key=True)
        name = models.CharField(max_length=32)

SmallIntegerField(IntegerField):
    - 小整数 -3276832767

PositiveSmallIntegerField(PositiveIntegerRelDbTypeMixin, IntegerField)
    - 正小整数 032767
IntegerField(Field)
    - 整数列(有符号的) -21474836482147483647

PositiveIntegerField(PositiveIntegerRelDbTypeMixin, IntegerField)
    - 正整数 02147483647

BigIntegerField(IntegerField):
    - 长整型(有符号的) -92233720368547758089223372036854775807

BooleanField(Field)
    - 布尔值类型

NullBooleanField(Field):
    - 可以为空的布尔值

CharField(Field)
    - 字符类型
    - 必须提供max_length参数, max_length表示字符长度

TextField(Field)
    - 文本类型

EmailField(CharField)- 字符串类型,Django Admin以及ModelForm中提供验证机制

IPAddressField(Field)
    - 字符串类型,Django Admin以及ModelForm中提供验证 IPV4 机制

GenericIPAddressField(Field)
    - 字符串类型,Django Admin以及ModelForm中提供验证 Ipv4和Ipv6
    - 参数:
        protocol,用于指定Ipv4或Ipv6, 'both',"ipv4","ipv6"
        unpack_ipv4, 如果指定为True,则输入::ffff:192.0.2.1时候,可解析为192.0.2.1,开启此功能,需要protocol="both"

URLField(CharField)
    - 字符串类型,Django Admin以及ModelForm中提供验证 URL

SlugField(CharField)
    - 字符串类型,Django Admin以及ModelForm中提供验证支持 字母、数字、下划线、连接符(减号)

CommaSeparatedIntegerField(CharField)
    - 字符串类型,格式必须为逗号分割的数字

UUIDField(Field)
    - 字符串类型,Django Admin以及ModelForm中提供对UUID格式的验证

FilePathField(Field)
    - 字符串,Django Admin以及ModelForm中提供读取文件夹下文件的功能
    - 参数:
            path,                      文件夹路径
            match=None,                正则匹配
            recursive=False,           递归下面的文件夹
            allow_files=True,          允许文件
            allow_folders=False,       允许文件夹

FileField(Field)
    - 字符串,路径保存在数据库,文件上传到指定目录
    - 参数:
        upload_to = ""      上传文件的保存路径
        storage = None      存储组件,默认django.core.files.storage.FileSystemStorage

ImageField(FileField)
    - 字符串,路径保存在数据库,文件上传到指定目录
    - 参数:
        upload_to = ""      上传文件的保存路径
        """给该字段传一个文件对象,会自动将文件保存到upload_to指定的目录下,然后将文件路径保存到数据库中"""
        storage = None      存储组件,默认django.core.files.storage.FileSystemStorage
        width_field=None,   上传图片的高度保存的数据库字段名(字符串)
        height_field=None   上传图片的宽度保存的数据库字段名(字符串)

DateTimeField(DateField)
    - 日期+时间格式 YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]

DateField(DateTimeCheckMixin, Field)
    - 日期格式      YYYY-MM-DD

TimeField(DateTimeCheckMixin, Field)
    - 时间格式      HH:MM[:ss[.uuuuuu]]

DurationField(Field)
    - 长整数,时间间隔,数据库中按照bigint存储,ORM中获取的值为datetime.timedelta类型

FloatField(Field)
    - 浮点型

DecimalField(Field)
    - 10进制小数
    - 参数:
        max_digits,小数总长度
        decimal_places,小数位长度

BinaryField(Field)
    - 二进制类型
对应关系:
    'AutoField': 'integer AUTO_INCREMENT',
    'BigAutoField': 'bigint AUTO_INCREMENT',
    'BinaryField': 'longblob',
    'BooleanField': 'bool',
    'CharField': 'varchar(%(max_length)s)',
    'CommaSeparatedIntegerField': 'varchar(%(max_length)s)',
    'DateField': 'date',
    'DateTimeField': 'datetime',
    'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
    'DurationField': 'bigint',
    'FileField': 'varchar(%(max_length)s)',
    'FilePathField': 'varchar(%(max_length)s)',
    'FloatField': 'double precision',
    'IntegerField': 'integer',
    'BigIntegerField': 'bigint',
    'IPAddressField': 'char(15)',
    'GenericIPAddressField': 'char(39)',
    'NullBooleanField': 'bool',
    'OneToOneField': 'integer',
    'PositiveIntegerField': 'integer UNSIGNED',
    'PositiveSmallIntegerField': 'smallint UNSIGNED',
    'SlugField': 'varchar(%(max_length)s)',
    'SmallIntegerField': 'smallint',
    'TextField': 'longtext',
    'TimeField': 'time',
    'UUIDField': 'char(32)',

数据库查询优化

# only与defer
"""
orm语句的特点:
	惰性查询
		如果你仅仅只是书写了orm语句 在后面根本没用到该语句查询出来的参数
		那么orm会自动识别 直接不执行
		eg:
		第一个语句:
		res = models.Book.objects.all()
		第二个语句:
		res = models.Book.objects.all()
		print(res)
		在上述语句中,因为第一个语句返回的结果没有被执行,所以django就不会进行查询,第二个语句返回的结果被执行了所以django会进行查询
		小结:只有使用到了数据才会走数据库
"""
# 想要获取到书籍中所有书的名字
# res = models.Book.objects.values('title')
# for i in res:
#     print(i.get('title'), type(i))
# 需求:获取到一个书的数据对象 然后.title就能拿到书名 且没有其他的字段
# res = models.Book.objects.only('title')
# for i in res:
#     print(i.title, type(i))
#     print(i.price)  # 点击only括号内没有的字段 会重新走数据库查询而all不需要走

res = models.Book.objects.defer('title')  # 生成的对象除了没有title属性之外其他的都有
for i in res:
    # print(i.title)
    print(i.price)
"""
defer和only刚好相反
    defer括号内放的字段不在查询的对象里面 查询该字段需要重新走数据库
    如果查询的是非括号内的字段 则不需要走数据库了
"""

# select_related与prefetch_related  跟跨表操作有关
# 获取的数据的出版社名称
# res = models.Book.objects.all()
# for i in res:
#     print(i.publish.name)  # 每循环一次就要走一次数据库刷新

# res = models.Book.objects.select_related('publish')
# for i in res:
#     print(i.publish.name)
"""
select_related内部直接先将book与publish连接起来 然后一次性将大表里面的所有数据
全部封装给查询出来的对象
    这个时候对象无论是点击book表的数据还是publish的数据都无需再走数据库查询
select_related括号内只能放外键字段    一对多,一对一
多对多字段也不行
"""
res = models.Book.objects.prefetch_related('publish')
for i in res:
    print(i.publish.name)
"""
prefetch_related该方法内部其实就是子查询
    将子查询查询出来的所有结果也给你封装到对象中
    给你的感觉好像也是一次性搞定的
"""
# 跨表查询当表的数据过多的时候加载表比较慢,当数据量需要查询的数据个数比较少的话子查询比较慢

图书管理系统案例:(自己看后端学习有关的项目文件夹)

choices参数(数据库字段设计常见)

"""
用户表
	性别
	学历
	工作经验
	是否结婚
	是否生子
	'''
针对某个可以列举完全额可能性字段,我们应该如何储存

只要某个字段的可能性是可以列举完全的,那么一般情况下都会采用choices参数
"""

class User(models.Model):
    uesrname = models.CharField(max_length=32)
    age = models.IntegerField()
    # 性别
    gender_choices = (
        (1, '男'),
        (0, '女')
    )
    gender = models.IntegerField(choices=gender_choices)
    """
    该gender字段存的还是数字 但是如果存的数字在上面元组列举的范围之内
    那么可以非常轻松的获取到数字对应的真正的内容
    
    1. gender字段存的数字不在上述元组列举的范围之内
    存的时候 没有列举出来的数字也能存,范围还是按照字段类型决定
    2. 如果在 如何获取对应的中文信息
    
    """
user_obj = models.User.objects.filter(pk=3).first()
# 只要是choices参数字段 如果你想要获取对应信息 固定写法 get_字段名_display()
print(user_obj.gender)
# 有对应关系直接获取到对应关系的数据,如果没有对应关系,那么是什么数据就得到什么数据
print(user_obj.get_gender_display())
# choices的应用场景是非常广泛的

MTV与MVC模型(了解)

# MTV:Django号称MTV模型
M:models
T:templates
V:views
# MVC:其实Django本质也是MVC
M:models
V:views
C:controller
# vue框架:MVVM模型

多对多三种创建方式

# 纯自动:利用orm自动帮我们创建第三张关系表
class Book(models.Model):
    name = models.CharField(max_length=32)
    authors = models.ManyToManyField(to='Author')


class Author(models.Model):
    name = models.CharField(max_length=32)
"""
优点:代码不需要你写 非常方便 还支持orm提供操作第三张关系表的方法...
不足之处:第三张表的扩展性极差(没有办法额外添加字段...)
"""
# 纯手动
class Book(models.Model):
    name = models.CharField(max_length=32)

class Author(models.Model):
    name = models.CharField(max_length=32)

class Book_Author(models.Model):
    book_id = models.ForeignKey(to='Book')
    author_id = models.ForeignKey(to='Author')
"""
优点:第三张表完全取决于你自己进行额外的扩展
不足之处:需要写的代码较多,不能够再使用orm提供的简单的方法
不建议你用该方法
"""
# 半自动
class Book(models.Model):
    name = models.CharField(max_length=32)
    authors = models.ManyToManyField(to='Author',
                                     through='Book_Author',  # 指定多对多使用自己创建的表
                                     through_fields=('books', 'authors'))  # 指定多对多使用那些字段表示书和作者的多对多关系
    """
    through_fields字段先后顺序
        判断本质:
            通过第三张表查询对应的表 需要用到哪个字段就把哪个字段放前面
        简化判断:
            当前表是谁 就把对应的关联字段放前面
    """


class Author(models.Model):
    name = models.CharField(max_length=32)


class Book_Author(models.Model):
    books = models.ForeignKey(to='Book')
    authors = models.ForeignKey(to='Author')
"""
半自动:可以使用orm正方向查询 但是没法使用add,set,remove,clear这四个方法
"""
# 总结:你需要掌握的是全自动和半自动 为了扩展性更高 一般我们都会采用半自动(写代码要给自己留条后路)

Ajax

"""
异步提交
局部更新
例子:github注册
	动态获取用户名实名时的跟后端确认并实时展示的前段(局部更新)

朝发送请求的方式
 	1. 浏览器地址栏直接输入url回车			GET请求
    2. a标签href属性					 GET请求
    3. from表单						  GET请求/POST请求
    4. Ajax							   GET请求/POST请求

Ajax 不是新的编程语言,而是一种使用现有标准的新方法:意思是是用小的知识点完成的类似于装饰器(用函数的知识点拼接出来的)

Ajax 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。(这一特点给用户的感受是在不知不觉中完成请求和响应过程)
Ajax 不需要任何浏览器插件,但需要用户允许JavaScript在浏览器执行

Ajax我们只学习jQuery封装之后的版本(不学原生的 原生的复杂并且在实际项目中也一般不用)
所以我们在前段使用Ajax的时候需要确保导入了JQuery
ps:并不只有jQuery能够实习Ajax 其他框架也可以 但是换汤不换药 原理是一样的
"""

黑马程序员原生JavaScript版本

Ajax的应用场景
  1. 页面上啦加载更多数据
  2. 列表数据无刷新分页
  3. 表单项离开焦点数据验证
  4. 搜索框提示文本字下拉列表
Ajax的使用
// 1. 创建Ajax对象
var xhr = new XMLHttpRequest();
// 2. 告诉Ajax请求地址以及请求方式
xhr.open('get', 'http://www.4399.com'); // 第一个参数为请求方式,第二个参数指的是请求地址,为服务器端的路由请求地址
// 3. 发送请求
xhr.send(); // 调用send方式进行请求
// 4. 获取服务端给与客户端的响应数据
xhr.onload = function() { // 因为响应的时间无法确定所以需要给xhr一个onload事件等待服务端发送数据
    console.log(xhr.responseText); // 获取服务端响应返回给客户端的数据
}
ajax请求参数的传递
get请求

传统网站请求参数都是通过表单的方式进行传递的

eg:

<form method="get" action = "http://www.example.com">
    <input type="text" name="username" value="mubai" />
    <input type="password" name="password" value="123" />
</form>
<!-- 提交的form表单数据为:http://www.example.com?username=mubai&password=123 -->

ajax的get请求方式

xhr.open('get','http://www.example.com?username=mubai&password=123');
// ajax的get请求参数需要自己进行拼接
post请求

ajax的post请求方式

xhr.open('post','http://www.example.com');
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); // post请求提交要在请求头中设置请求参数内容的类型,也就是Content-Type属性,来设置send发送的数据类型
xhr.send("name=mubai&password=123");  // 这种发送数据的固定写法就是对应着上面设置的参数
请求数据格式

在send发送之前使用setRequestHeader函数里面的Content-Type来进行规定数据格式

  1. application/x-www-form-urlencoded
name=mubai&password=123
  1. application/json
{name : '张三',age : '20', sex : '男'}

将json对象转换为json字符串

JSON.stringify()  // 将json对象转换为json字符串
获取服务器端的响应

ajax状态码:

在创建ajax对象,配置ajax对象,发送请求,以及接收完服务器响应数据,这个过程中的每一个步骤都会对应一个数值,这个数值就是ajax状态码

  • 0:请求未初始化(还没有调佣open())
  • 1:请求已经建立,但是还没有发送(还没有调用send())
  • 2:请求已经发送
  • 3:请求正在处理中,通常响应中已经有部分数据可以用了
  • 4:响应已经完成,可以获取并使用服务器的响应了
xhr.readyState // 获取Ajax状态码

监听ajax状态码事件

onreadystatechange 事件

当Ajax状态码发生变化时将自动触发该事件

// 调用方式
xhr.onreadystatechange = function (){
    console.log(xhr.readyState)
};

两种获取服务器端响应方式的区别

区别描述onload事件onreadystatechange事件
是否兼容IE低版本不兼容兼容
是否需要判断Ajax状态码不需要需要
被调用次数一次多次
Ajax错误处理
  1. 网络畅通,服务器端能接收到请求,服务端返回的结果不是预期结果

    可以判断服务单返回的状态码,分别进行处理。xhr.status获取http状态码

// 获取服务端发送过来的状态码
xhr.status
  1. 网络畅通,服务端没有接收到请求,返回404状态码

    检查请求地址是否错误

  2. 网络畅通,服务器端接收到请求,服务器返回500状态码

    服务器端错误,找后端程序员进行沟通

  3. 网络中断,请求无法发送到服务器端

    会触发xhr对象下面的onerror事件,在onerror事件处理函数中对错误进行处理

xhr.onerror = function () {
    alert('网络中断,无法发送Ajax请求');
}
低版本IE浏览器的缓存问题

问题:在低版本的IE浏览器中,Ajax请求有严重的缓存问题,即在请求地址不发生变化的情况下,只有第一次请求会真正发送到服务器端,后续的请求都会从浏览器的缓存中获取结果。及时服务器端的数据更新了,客户端依然拿到多的是缓存中的旧数据

解决方案:在请求地址的后面加请求参数,保证每一次请求中的请求参数的值不相同

xhr.open('get','http://www.example.com?t=' + Math.random());
同步异步概述

同步

  • 进程同步

异步

  • 进程异步

ajax代码就是异步代码,具体参考python异步进程异步线程

ajax封装

问题:发送一次请求代码过多,发送多次请求代码冗余且重复

解决方案:将请求代码封装到函数中,发请求时调用函数即可

ajax({
	type: 'get',
	url = 'http://www.example.com',
	susccess: function (data) {
		console.log(data)
	}
})
function ajax(options) {
    let defaults = {
        type: 'get',
        url: '',
        data: {},
        header: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        success: function (data, xhr) {
        },
        error: function (data, xhr) {}
    };
    // 使用options对象中的属性覆盖defaults对象中的属性
    Object.assign(defaults,options);
    // 创建ajax对象
    var xhr = new XHLHttpRequest();

    // 配置ajax对象
    xhr.open(defaults.type, defaults.url);
    if (defaults.type == 'post') {
        // 用户希望的向服务器端传递的请求参数的类型
        var contentType = defaults.header['Content-Type']
        // 设置请求参数格式的类型
        xhr.setRequestHeader('Content-Type', contentType)
        if (contentType == 'application/json') {
            xhr.send(JSON.stringify(defaults.data))
        } else {
            // 拼接请求参数的变量
            var params = '';

            // 循环用户传递进行的对象格式参数
            for (let attr in defaults.data) {
                // 将参数装换为字符串格式
                params += attr + '=' + defaults.data[attr] + '&';
            }
            params = params.substr(0, params.length - 1)

            // 判断请求方式
            if (defaults.type == 'get') {
                defaults.url = defaults.url + '?' + params;
            }
            xhr.send(params);
        }

    } else {
        // 发送请求
        xhr.send();
    }


    // 监听xhr对象下面的onload事件
    // 当xhr对象接收完响应数据后触发
    xhr.onload = function () {
        // 获取响应头的数据
        var contentType = xhr.getResponseHeader('Content-Type');
        var responseText = xhr.responseText;
        // 如果响应类型中包括application/json
        if (contentType.includes('application/json')) {
            // 将json字符串转换为json对象
            JSON.parse(responseText)
        }


        // 当http状态码为200的时候请求成功调用成功时候的函数,请求失败使用失败时候的函数
        if (xhr.status == 200) {
            defaults.success(responseText, xhr);
        } else {
            defaults.error(responseText, xhr);
        }
    }
}

ajax({
    type: 'get',
    url: 'http://127.0.0.1/first',
    data: {
        name: 'mubai',
        age: 20
    },
    header: {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    ,
    success: function (data, xhr) {
        console.log('接收到数据')
    },
    error: function (data, xhr) {
        console.log('这里是error函数' + data);
        console.log(xhr)
    }
})
模板引擎概述

作用:使用模板引擎提供的模板语法,可以将数据和HTML拼接起来

官方地址: https://aui.github.io/art-template/zh-cn/index.html

  1. 下载art-template模板引擎文件并在HTML页面中引入库文件
<script src="./js/template-web.js"></script>
  1. 准备art-template模板
<script id="tpl" type="text/html" >
	<div class ="box"></div>
</script>
  1. 告诉模板引擎将那个模板和那个数据进行拼接
var html = template('tpl',{username:'zhangsan',age:'20'})  // 第一个参数为模板对应的id,第二个参数就是对象,对象中存储的数据就是模板中就是展示的数据
  1. 将拼装好的html字符串添加到页面中
document.getElementById('container').innerHTML = html;
  1. 通过模板语法告诉引擎,数据和html字符串要如何拼接
<script id="tpl" type="text/html" >
	<div class ="box">{{ username }} {{ age }}</div>
</script>
FormData对象的使用
  1. 准备HTML表单
<form action="" id="form">
    <input type="text" name="username" id="" />
    <input type="password" name="passwrod" id="" />
    <input type="button" value="提交" />
</form>
  1. 将HTML表单转化为formDate对象
var form = document.getElementById('form');
var formDate = new FormData(form);
  1. 提交表单对象
xhr.send(formData);

老男孩AjaxjQuery版本

小案例
'''
页面上有三个input框
	在前两个框中输入数字 点击按钮 朝后端发送ajax请求
	后端计算出结果 再返回给前段动态展示的到第三个input框中
	(整个过程中页面不准有刷新,也不能在前段计算)
'''
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<input type="text" name="" id="d1">+
<input type="text" name="" id="d2">=
<input type="text" name="" id="d3">
<p>
    <button id="btn">点我</button>
</p>
<script type="text/javascript">
    // 先给按钮绑定一个点击事件
    $('#btn').click(function () {
        // 朝后端发送ajax请求
        $.ajax({
            // 1. 第一个参数为指定朝那个后端发送ajax请求
            url: '', // 不写就是朝当前地址提交r
            // 2. 第二个参数为请求方式
            type: 'post',  // 不指定默认为get(都是小写)
            // 3. 数据
            data: {'number_one': $("#d1").val(), 'number_two': $('#d2').val()},
            // 4. 回调函数:当后端给你返回结果的时候会自动触发
            // dataType: "JSON",  // 会自动将请求到的数据进行反序列化
            success: function (args) {
                // args = JSON.parse(args) // 如果后端是按照JsonResopnse格式来进行发送则不需要对数据进行序列化
                $('#d3').val(args.msg)  // 通过dom操作来对d3进行修改
            }
        })
    })
</script>
</body>
</html>
# view.py
from django.shortcuts import render, HttpResponse
from django.http import JsonResponse

# Create your views here.

def ab_ajax(request):
    if request.method == "POST":
        number_one = request.POST.get('number_one')
        number_two = request.POST.get('number_one')
        sum = int(number_one) + int(number_two)
        # 返回的是一个数字
        # return HttpResponse(sum)
        # 返回的是一个字典,数据就需要进行序列化
        # d = {'code': 100, 'msg': sum}
        # return HttpResponse(json.dumps(d))  # 如果前段jQuery将dataType设置为true进行序列化,那么需要导入django的JsonResopnse来进行序列化
        d = {'code': 100, 'msg': sum}
        return JsonResponse(d)
    return render(request, 'index.html')
"""

针对后端如果是用HttpResponse返回的数据 回调函数不会自动帮你反序列化
如果后端直接用的是JsonResopnse返回的数据 回调函数会自动帮你反序列化

HttpResponse解决方式
	1. 自己在前段利用JSON.parse()
	2. 在ajax里面配置一个参数
	dataType:'JSON'
"""
前后端传输数据编码格式(contentType)
# 我们主要研究post请求数据的编码格式
"""
不研究get请求的原因:
	get请求数据就是放在url后面
	url?username=mubai&password=123
"""
# 可以朝后端发送post请求的方式
	"""
	1. form表单
	2. ajax表单
	"""
"""
前后端传输数据的编码格式
	urlencoded
	
	formdata
	
	json
"""
# 研究form表单
"""
	默认的数据编码格式是urlencoded
	数据格式:username=mubai&password=123
	和get请求一样
	django后端针对符合urlencoded编码格式的数据都会自动帮你解析封装到request.POST中
		只要你的数据是username=mubai&password=123,都会解析到request.POST里面
	
	如果将编码格式改成formdata,那么针对普通的键值对还是解析到request.POST中而将文件解析到request.FILES中
	
	form表单是没有办法发送json格式数据的
"""
# 研究Ajax
"""
	默认的数据编码格式也是urlencoded
	数据格式:username=mubai&password=123
	django后端针对符合urlencoded编码格式的数据都会自动帮你解析封装到request.POST中
		只要你的数据是username=mubai&password=123,都会解析到request.POST里面
	
"""
ajax发送json格式数据
"""
前后端传输数据的时候一定要确保编码格式跟数据真正的格式是一致的
{"username":"jason","age":25} 在request.POST里面找不到 

	django针对json格式的数据 不会做任何的处理

request对象方法补充
	request.is_ajax()
		判断当前请求是否是ajax请求 返回布尔值
"""
# views.py
def ab_json(request):
    if request.is_ajax():
        # 针对json格式数据需要你自己手动处理
        json_bytes = request.body  # request.body的数据为一个二进制数据
        # json_str = json_bytes.decode('utf-8')
        # json_dict = json.loads(json_str)
        # print(json_dict)

        # json.loads括号内如果传入了一个二进制格式的数据那么内部自动解码再反序列化
        json_dict = json.loads(json_bytes)
        print(json_dict,type(json_dict))
    return render(request, 'ab_json.html')
# html
"""
<body>
<button class="btn btn-danger" id="d1">点我</button>

<script>
    $('#d1').click(function(){
        $.ajax({
            url:'',
            type:'post',
            data:JSON.stringify({'username':'jason','age':25}),  // 下面指定的编码格式是json这里发送的也应该为json格式
            contentType:'application/json',  // 指定编码格式
            success:function () {

            }
        })
    })
</script>
</body>
"""

"""
ajax发送json格式数据需要注意点
	1.contentType参数指定成:application/json
	2.数据是真正的json格式数据
	3.django后端不会帮你处理json格式数据需要你自己去request.body获取并处理
"""
ajax发送文件
"""
ajax发送文件需要借助于js内置对象FormData
"""
"""
html
<body>
<p>username:<input type="text" name="" id="d1"></p>

<p>password:<input type="text" name="" id="d2"></p>
<p><input type="file" name="" id="d3"></p>
<button class="btn btn-info" id="d4">点我</button>

<script>
    // 点击按钮朝后端发送普通键值对和文件数据
    $('#d4').on('click', function () {
        // 1. 需要先利用FormData内置对象
        let formDataObj = new FormData();
        // 2. 添加普通的键值对
        formDataObj.append('username', $('#d1').val())
        formDataObj.append('password', $('#d2').val())
        // 3. 添加文件对象
        formDataObj.append('myfile', $('#d3')[0].files[0])  // 获取文件对象.$()[0]是将jQuery对象装换成JavaScript对象,files[0]则是获取第一个文件对象
        // 4. 将对象基于ajax发送给后端
        $.ajax({
            url: '',
            type: 'post',
            data: formDataObj,  // 直接将对象放在data后面即可

            // ajax发送文件必须要指定的两个参数
            contentType: false,  // 不需要使用任何编码 django后端能够自动识别FormData对象
            processData: false,  // 告诉你的浏览器不要对你的数据进行任何处理
            success:function (args) {

            }
        })
    })
</script>
</body>
"""
# view.py
def ab_file(request):
    if request.is_ajax():
        if request.method == 'POST':
            print(request.POST)
            print(request.FILES)
    return render(request,'ab_file.html')
"""
总结:
	1.需要利用内置对象FormData
        // 2. 添加普通的键值对
        formDataObj.append('username', $('#d1').val())
        formDataObj.append('password', $('#d2').val())
        // 3. 添加文件对象
        formDataObj.append('myfile', $('#d3')[0].files[0])
    2.需要指定两个关键性的参数
    	    contentType: false,  // 不需要使用任何编码 django后端能够自动识别FormData对象
            processData: false,  // 告诉你的浏览器不要对你的数据进行任何处理
    3.django后端能够直接识别到formdata对象并且能够将内部的普通键值自动解析并封装到request.POST中 文件数据自动解析并分装到request.FILES中
"""

django自带的序列化组件(drf做铺垫)

from django.http import JsonResponse
from  django.core import serializers
def ab_ser(request):
    # 获取列表套字典数据第一种方法:
    # user_queryset = models.User.objects.all().values()
    #return render(request, 'ab_ser.html', locals())
    # 获取列表套字典数据第二种方法:
    # user_queryset = models.User
    # user_list = []
    # tmp = {}
    # user_models_key = []
    # for field in user_queryset._meta.fields:
    #     user_models_key.append(field.name)
    # for user_obj in user_queryset.objects.all():
    #     for key in user_models_key:
    #         tmp[key] = getattr(user_obj, key)
    #     user_list.append(tmp)
    """    
    [
    {'id': 4, 'username': 'tank', 'age': 19, 'gender': 2},
    {'id': 4, 'username': 'tank', 'age': 19, 'gender': 2},
    {'id': 4, 'username': 'tank', 'age': 19, 'gender': 2},
    {'id': 4, 'username': 'tank', 'age': 19, 'gender': 2}
    ]
    前后端分离的项目
        作为后端开发的你只需要写代码将数据处理好
        能够序列化返回给前段即可
            再写一个接口文档 告诉前段每个字段代表的意思即可
    """
    # return JsonResponse(user_list, safe=0)
    # 获取列表套字典数据第三种方法:
    # 序列化
    user_queryset = models.User.objects.all()
    res = serializers.serialize('json',user_queryset)  # 第一个参数表示的是需要装换的格式,第二种则是需要转换的对象
    # 会自动帮你将数据变成json格式的字符串 并且内部非常的全面
    '''
    [
    {"model": "app01.user", "pk": 1, "fields": {"username": "mubai", "age": 18, "gender": 1}},
    {"model": "app01.user", "pk": 2, "fields": {"username": "egon", "age": 31, "gender": 1}},
    {"model": "app01.user", "pk": 3, "fields": {"username": "json", "age": 13, "gender": 2}},
    {"model": "app01.user", "pk": 4, "fields": {"username": "tank", "age": 19, "gender": 2}}]
    后端开发写接口利用序列化组件渲染数据然后写一个接口文档 该交代的交代一下就ok了
    '''

    return HttpResponse(res)

批量插入数据

def ab_pl(request):
    # # 先给book插入一万条数据
    # for i in range(10000):
    #     models.Book.objects.create(title=f'第{i}本书')
    # # 再将所有的数据查询并展示到前段页面
    # book_queryset = models.Book.objects.all()

    # 批量插入
    book_list = []
    for i in range(100000):
        book_obj = models.Book(title=f'第{i}本书')
        book_list.append(book_obj)
    models.Book.objects.bulk_create(book_list)
    """
    当你想要批量插入数据的时候 使用rom给你提供的bulk_create能够大大的减少操作时间
    """
	# 能加快网页的加载速度
    return render(request,'ab_pl.html',locals())

分页器

"""
在制作页码的时候 一般情况下都是奇数个 符合中国对称美的标准
"""
# view.py
def ab_pl(request):
    # 分页
    book_list = models.Book.objects.all()
    # 想访问那一页
    current_page = request.GET.get('page', 1)  # 如果获取不到当前页码 就展示第一页
    # 数据类型转换
    try:
        current_page = int(current_page)
    except:
        # 如果用户瞎写数据那么将数据等于1
        current_page = 1
    # 每页展示多少条
    per_page_num = 10
    # 起始位置
    start_page = (current_page - 1) * per_page_num
    # 结束位置
    end_page = start_page + per_page_num
    # 计算出到底需要多少也
    all_count = book_list.count()
    print(all_count)
    per_page_num_all = (all_count + per_page_num - 1) // per_page_num
    if per_page_num_all > 5:
        if 1 <= current_page <= 3:
            page_count_list = [*[i for i in range(1,5)], '...', per_page_num_all]
        elif per_page_num_all - 2 <= current_page <= per_page_num_all:
            page_count_list = [1, '...',*[i for i in range(per_page_num_all-3,per_page_num_all+1)]]
        else:
            page_count_list = [1, '...', *[i for i in range(current_page-1,current_page+2)], '...', per_page_num_all]
    else:
        page_count_list = [i for i in range(1, per_page_num_all + 1)]
    # 获取那一页的数据
    book_queryset = book_list[start_page:end_page]
    return render(request, 'ab_pl.html', locals())
"""
前段
<body>
{% for book_obj in book_queryset %}
    <p>{{ book_obj.title }}</p>
{% endfor %}
<nav aria-label="Page navigation">
    <ul class="pagination">
        <li>
            <a href="#" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        {% for page in page_count_list %}
            {% if page == '...' %}
                <li><a href="#">{{ page }}</a></li>
            {% elif page == current_page %}
                <li class="active"><a href="?page={{ page }}">{{ page }}</a></li>
            {% else %}
                <li><a href="?page={{ page }}">{{ page }}</a></li>
            {% endif %}
        {% endfor %}
        <li>
            <a href="#" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
    </ul>
</nav>
</body>
"""
'''
django中自带的分页器模块 但是书写起来很麻烦并且功能太简单
所以我们自己想房设法的写自定义分页器

上述推导代码无需掌握 只需要知道内部逻辑即可
之后需要使用直接拷贝即可

'''

自定义分页器的拷贝和使用

博客页面
https://www.cnblogs.com/Dominic-Ji/articles/12035722.html

# mypage.py
class Pagination(object):
    def __init__(self, current_page, all_count, per_page_num=2, pager_count=11):
        """
        封装分页相关数据
        :param current_page: 当前页
        :param all_count:    数据库中的数据总条数
        :param per_page_num: 每页显示的数据条数
        :param pager_count:  最多显示的页码个数
        """
        try:
            current_page = int(current_page)
        except Exception as e:
            current_page = 1

        if current_page < 1:
            current_page = 1

        self.current_page = current_page

        self.all_count = all_count
        self.per_page_num = per_page_num

        # 总页码
        all_pager, tmp = divmod(all_count, per_page_num)
        if tmp:
            all_pager += 1
        self.all_pager = all_pager

        self.pager_count = pager_count
        self.pager_count_half = int((pager_count - 1) / 2)

    @property
    def start(self):
        return (self.current_page - 1) * self.per_page_num

    @property
    def end(self):
        return self.current_page * self.per_page_num

    def page_html(self):
        # 如果总页码 < 11个:
        if self.all_pager <= self.pager_count:
            pager_start = 1
            pager_end = self.all_pager + 1
        # 总页码  > 11
        else:
            # 当前页如果<=页面上最多显示11/2个页码
            if self.current_page <= self.pager_count_half:
                pager_start = 1
                pager_end = self.pager_count + 1

            # 当前页大于5
            else:
                # 页码翻到最后
                if (self.current_page + self.pager_count_half) > self.all_pager:
                    pager_end = self.all_pager + 1
                    pager_start = self.all_pager - self.pager_count + 1
                else:
                    pager_start = self.current_page - self.pager_count_half
                    pager_end = self.current_page + self.pager_count_half + 1

        page_html_list = []
        # 添加前面的nav和ul标签
        page_html_list.append('''
                    <nav aria-label='Page navigation>'
                    <ul class='pagination'>
                ''')
        first_page = '<li><a href="?page=%s">首页</a></li>' % (1)
        page_html_list.append(first_page)

        if self.current_page <= 1:
            prev_page = '<li class="disabled"><a href="#">上一页</a></li>'
        else:
            prev_page = '<li><a href="?page=%s">上一页</a></li>' % (self.current_page - 1,)

        page_html_list.append(prev_page)

        for i in range(pager_start, pager_end):
            if i == self.current_page:
                temp = '<li class="active"><a href="?page=%s">%s</a></li>' % (i, i,)
            else:
                temp = '<li><a href="?page=%s">%s</a></li>' % (i, i,)
            page_html_list.append(temp)

        if self.current_page >= self.all_pager:
            next_page = '<li class="disabled"><a href="#">下一页</a></li>'
        else:
            next_page = '<li><a href="?page=%s">下一页</a></li>' % (self.current_page + 1,)
        page_html_list.append(next_page)

        last_page = '<li><a href="?page=%s">尾页</a></li>' % (self.all_pager,)
        page_html_list.append(last_page)
        # 尾部添加标签
        page_html_list.append('''
                                           </nav>
                                           </ul>
                                       ''')
        return ''.join(page_html_list)
# view.py
def get_book(request):
   book_list = models.Book.objects.all()
   current_page = request.GET.get("page",1)
   all_count = book_list.count()
   page_obj = Pagination(current_page=current_page,all_count=all_count,per_page_num=10)
   page_queryset = book_list[page_obj.start:page_obj.end]
   return render(request,'booklist.html',locals())
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            {% for book in page_queryset %}
            <p>{{ book.title }}</p>
            {% endfor %}
            {{ page_obj.page_html|safe }}
        </div>
    </div>
</div>
"""
当我们需要使用到非django内置的第三方功能或者组件代码的时候
我们一般情况下会创建一个名为utils文件夹 在该文件夹内对模块进行功能性划分
	utils可以在每个应用下创建 具体结合实际情况

我们到了后期分装代码的时候 不在局限于函数
还是尽量朝面向对象去封装

我们自定义的分页器是基于Bootstrap样式来的 所以你需要提前导入Bootstrap
	Bootstrap 版本 v3
	jQuery	  版本 v3
"""

forms组件

前戏

"""
写一个注册功能
	获取用户名和密码 利用form表单提交数据
	在后端判断用户名和密码是否符合一定的条件
		用户名中不能含有特殊字符
		密码不能少于三位
		
	如果不符合条件需要你将提示信息展示到前段页面
"""
# view.py
def ab_form(request):
    back_dic = {'username':'','password':''}
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        import re
        if len(username) <3 or re.search('\W',username):
            back_dic['username'] = '账号不能小于三位,且不能出现特殊字符'
        if len(password) <6:
            back_dic['password'] = '密码不能小于6位'
    """
    无论是post请求还是get请求
    页面都能够获取到字典 只不过get请求来的时候 字典值都是空的
    而post请求来之后 字典可能有值
    """
    return render(request,'ab_form.html',locals())
'''
#html
<body>
<form action="" method="post">
    <p>username:<input type="text" name="username" id="">
        <span style="color: red">{{ back_dic.username }}</span>
    </p>
    <p>password:<input type="text" name="password" id="">
        <span style="color:red">{{ back_dic.password }}</span>
    </p>
    <input type="submit" value="提交" class="btn btn-info">
</form>
</body>
'''
"""
1. 手动书写前段获取用户数据的html代码		渲染html代码
2. 后端对用户数据进行校验				  校验数据
3. 对不符合要求的数据进行前段提示			展示提示信息


form组件
	能够完成的事情
		1.渲染html代码
		2.校验数据
		3.展示提示信息

为什么数据校验非要去后端 不能在前段利用js直接完成呢?
	数据校验前段可有可无
	但是后端必须要有!!!
	
	因为前段的校验是弱不禁风的 你可以直接修改
	或者利用爬虫程序绕过前段页面直接朝后端提交数据
	
	eg:购物网站
		选取货物之后 会计算一个价格发送给后端 如果后端不做价格的校验
	实际上存的操作:
		实际是获取到用户选择的所有商品的主键值
		然后在后端查询出所有商品的价格 再次计算一遍
		如果和前段一致 那么完成支付如果不一致直接拒绝
"""

基本使用

使用前提:

导入对应的模块并定义一个类(类名自定义)

from django import forms

class MyForm(forms.Form):
    # uername字符串类型最小3位最大8位
    username = forms.CharField(min_length=3,max_length=8)
    # password字符串类型最小3位最大8位
    password = forms.CharField(min_length=3,max_length=8)
    # email字段必须符合邮箱格式:xxx@xx.com
    email = forms.EmailField()

校验数据

"""
1. 测试环境的准备 可以自己拷贝代码准备(django提供的)
2. 其实在pycharm里面已经帮你准备一个测试环境(pycharm提供的)
python console
"""
# 1.将待校验的数据组织成字典的形式传入即可
from app01 import views
form_obj = views.MyForm({'username':'mubai','password':'12','email':'123'})
# 2.判断数据是否合法	注意该方法只有在所有的数据全部合法的情况下才会返回True
form_obj.is_valid()
False
# 3.查看所有校验通过的数据
form_obj.cleaned_data
{'username': 'mubai'}
# 4.查看所有不符合校验规则以及不符合的原因,不满足条件的情况有好几种,所以使用列表将报错信息全部装起来
form_obj.errors
{'password': ['Ensure this value has at least 3 characters (it has 2).'], 'email': ['Enter a valid email address.']}


# 5.校验数据只校验类中出现的字段 多传不影响 多传的字段直接忽略
form_obj = views.MyForm({'username':'mubai','password':'123','email':'123@qq.com','hobby':'study'})
form_obj.is_valid()
True
form_obj.cleaned_data
{'username': 'mubai', 'password': '123', 'email': '123@qq.com'}
form_obj.errors
{}

# 6.校验数据 默认情况下 类里面所有的字段都必须传值
{'username': 'mubai', 'password': '123', 'email': '123@qq.com'}
form_obj.errors
{}
form_obj = views.MyForm({'username':'mubai','password':'123'})
form_obj.is_valid()
False
form_obj.cleaned_data
{'username': 'mubai', 'password': '123'}
form_obj.errors
{'email': ['This field is required.']}
"""
也就意味着校验数据的时候 默认情况下数据可以多传但是绝不可以少传
"""

forms组件渲染标签

"""
froms组件只会自动帮你渲染获取用户输入的标签(input select radio checkbox)
不会帮你渲染提交按钮
"""
from django import forms

class MyForm(forms.Form):
    # uername字符串类型最小3位最大8位
    username = forms.CharField(min_length=3,max_length=8,label='用户名')  # 给传入label参数那么forms组件渲染前段的这个label标签将会修改,默认为变量名大写
    # password字符串类型最小3位最大8位
    password = forms.CharField(min_length=3,max_length=8)
    # email字段必须符合邮箱格式:xxx@xx.com
    email = forms.EmailField()

def index(request):
    # 1 先产生一个空对象
    form_obj = MyForm()
    # 2 直接将该空对象传递给html页面
    return render(request,'index.html',locals())
# 前段利用空对象做操作
"""
    <p>第一种渲染方式:代码书写极少,封装程度太高 不便于后续的发展 一般情况下只在本地测试使用</p>
    {{ form_obj.as_p }}
    {{ form_obj.as_ul }}
    {{ form_obj.as_table }}
    <p>第二种渲染方式:可扩展性很强 但是需要书写的代码太多 一般情况下不用</p>
    <p>><label for="form_obj.auto_id" >{{ form_obj.username.label }}</label>:{{ form_obj.username }}</p>
    <p>{{ form_obj.password.label }}:{{ form_obj.password }}</p>
    <p>{{ form_obj.email.label }}:{{ form_obj.email }}</p>
    <p>第三种渲染方式(推荐使用):代码书写简单 并且扩展性也高</p>
    {% for form in form_obj %}
        <p>{{ form.label }}:{{ form }}</p>
    {% endfor %}
"""
"""
label属性默认展示的是类中定义的字段的首字母大写的形式
也可以自己修改 直接给字段对象加label属性即可
	username = forms.CharField(min_length=3,max_length=8,label='用户名')
"""
# 可以通过auto_id属性来获取到对应的input的框的id值

展示提示信息

"""
浏览器会自动帮你校验数据 但是前段的校验弱不禁风
如何让浏览器不做校验
	在form表单添加参数novalidate
	<form action="" method="post" novalidate>
"""
# views.py
def index(request):	
    # 1 先产生一个空对象
    form_obj = MyForm()
    if request.method == "POST":
        # 获取用户数据并校验
        """
        1. 数据获取繁琐
        2. 校验数据需要构造字典的格式传入才行
        ps:但是request.POST可以看成就是一个字典
        """
        # 3.校验数据
        form_obj =  MyForm(request.POST)
        # 4.判断数据是否合法
        if form_obj.is_valid():
            # 5.如果合法 操作数据库存储数据
            return HttpResponse('保存成功')
        # 5.不合法 有错误

    # 2 直接将该空对象传递给html页面
    return render(request,'index.html',locals())

"""
# html
<body>
<form action="" method="post" novalidate>
    {% for form in form_obj %}
        <p>
            {{ form.label }}:{{ form }}
            <span style="color: red">{{ form.errors.0 }}</span> <!-- 这个返回的是一个列表所以会使用ul标签所以用.0取第一个报错信息 -->
        </p>
    {% endfor %}
    <input type="submit" value="提交" class="btn btn-info">
</form>
</body>
"""
"""
1. 必备条件 get请求和post请求传给html页面对象变量名必须一样
2. froms组件当你的数据不合法的情况下 会保存你上次的数据 让你基于之前的结果进行修改更加的人性化(form_obj一样之后才可以保留上次输入的结果)
"""
# 针对错误的提示信息还可以自定制
class MyForm(forms.Form):
    # uername字符串类型最小3位最大8位
    username = forms.CharField(
        min_length=3, max_length=8, label='用户名',
        error_messages={# 自定义报错
            'min_length': '用户名最小3位',
            'max_length': '用户名最大8位',
            'required': '用户名不能为空'
        }
    )
    # password字符串类型最小3位最大8位
    password = forms.CharField(
        min_length=3, max_length=8,
        error_messages={
            'min_length': '密码最小3位',
            'max_length': '密码最大8位',
            'required': '密码不能为空'
        }
    )
    # email字段必须符合邮箱格式:xxx@xx.com
    email = forms.EmailField(
        error_messages={
            'invalid': '邮箱格式不正确',
            'required': '邮箱不能为空'
        }
    )

forms组件钩子函数(HOOK)

"""
在特定的节点自动触发完成响应操作

钩子函数在forms组件中类似于第二道关卡,能够让我们自定义校验规则

在forms组件中有两类钩子
	1. 局部钩子
		当你需要给单个字段增加校验规则的时候可以使用
	2. 全局钩子
		当你需要给多个字段增加校验规则的时候可以使用
"""
# 实际案例
# 1.校验用户名中不能含有666			只是校验username字段	局部钩子
# 2.校验密码和确认密码是否一致		  password confirm两个字段	全局钩子
from django import forms
class MyForm(forms.Form):
    # uername字符串类型最小3位最大8位
    username = forms.CharField(
        min_length=3, max_length=8, label='用户名',
        error_messages={
            'min_length': '用户名最小3位',
            'max_length': '用户名最大8位',
            'required': '用户名不能为空'
        }
    )
    # password字符串类型最小3位最大8位
    password = forms.CharField(
        min_length=3, max_length=8, label='密码',
        error_messages={
            'min_length': '密码最小3位',
            'max_length': '密码最大8位',
            'required': '密码不能为空'
        }
    )
    cofirm_password = forms.CharField(
        min_length=3, max_length=8, label='确认密码',
        error_messages={
            'min_length': '确认密码最小3位',
            'max_length': '确认密码最大8位',
            'required': '确认不能为空'
        }
    )
    # email字段必须符合邮箱格式:xxx@xx.com
    email = forms.EmailField(
        error_messages={
            'invalid': '邮箱格式不正确',
            'required': '邮箱不能为空'
        }
    )
    # 钩子函数:只有上面的代码执行完毕之后才会执行下面的代码,在类里面书写方法即可
    # 局部钩子
    def clean_username(self):  # 这个函数名是规定的clean_加上字段名
        # 获取到用户名
        username = self.cleaned_data.get('username')  # cleaned_dataq就是上面校验成功的数据
        # 这里面就可以写其他的校验方式如正则
        if '666' in username:
            # 提示前段展示错误信息
            self.add_error(field='username',error='光喊666是不行滴~')
        # 将钩子函数钩出来数据再放回去
        return username

    # 全局钩子
    def clean(self):
        password = self.cleaned_data.get('password')
        confirm_password = self.cleaned_data.get('confirm_password')
        if not confirm_password == password:
            self.add_error('cofirm_password','两次密码不一致')
        # 将钩子函数钩出来的数据再放回去
        return self.cleaned_data

froms组件其他参数及补充知识点

'''
label  字段名
error_messages  自定义报错信息
initial  默认值
required  控制字段是否必填 True为必填 False为非必填 默认值为True
'''


"""
1. 字段没有样式
2. 针对不同类型的input如何修改
	text
	password
	date
	radio
	checkbox
	...
"""
widget=forms.widgets.TextInput(attrs={'class':'form-control c1'}
# TextInput表示最后生成的是text类型的input标签  多个属性值的话 直接空格隔开即可
# RegexValidator验证器 支持在里面写正则并且支持写多个正则,并依次运行
from django.core.validators import RegexValidator
validators=[RegexValidator(r'^[0-9]+$', '请输入数字'), RegexValidator(r'^159[0-9]+$', '数字必须以159开头')]
其他类型渲染
# password
class LoginForm(forms.Form):
    pwd = forms.CharField(
        min_length=6,
        label="密码",
        widget=forms.widgets.PasswordInput(attrs={'class': 'c1'}, render_value=True)
    )
# radioSelect
class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三",
        error_messages={
            "required": "不能为空",
            "invalid": "格式错误",
            "min_length": "用户名最短8位"
        }
    )
    pwd = forms.CharField(min_length=6, label="密码")
    gender = forms.fields.ChoiceField(
        choices=((1, "男"), (2, "女"), (3, "保密")),
        label="性别",
        initial=3,
        widget=forms.widgets.RadioSelect()
    )
# 单选Select
class LoginForm(forms.Form):
    hobby = forms.ChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"), ),
        label="爱好",
        initial=3,
        widget=forms.widgets.Select()
    )
# 多选Select
class LoginForm(forms.Form):
    hobby = forms.MultipleChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"), ),
        label="爱好",
        initial=[1, 3],
        widget=forms.widgets.SelectMultiple()
    )
# 单选checkbox
class LoginForm(forms.Form):
    keep = forms.ChoiceField(
        label="是否记住密码",
        initial="checked",
        widget=forms.widgets.CheckboxInput()
    )
# 多选checkbox
class LoginForm(forms.Form):
    hobby = forms.MultipleChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
        label="爱好",
        initial=[1, 3],
        widget=forms.widgets.CheckboxSelectMultiple()
    )
# choice字段注意事项
"""
在使用选择标签时,需要注意choices的选项可以配置从数据库中获取,但是由于是静态字段 获取的值无法实时更新,需要重写构造方法从而实现choice实时更新。
"""
# 方式一:
from django.forms import Form
from django.forms import widgets
from django.forms import fields

 
class MyForm(Form):
 
    user = fields.ChoiceField(
        # choices=((1, '上海'), (2, '北京'),),
        initial=2,
        widget=widgets.Select
    )
 
    def __init__(self, *args, **kwargs):
        super(MyForm,self).__init__(*args, **kwargs)
        # self.fields['user'].choices = ((1, '上海'), (2, '北京'),)
        # 或
        self.fields['user'].choices = models.Classes.objects.all().values_list('id','caption')
# 方式二:
from django import forms
from django.forms import fields
from django.forms import models as form_model

 
class FInfo(forms.Form):
    authors = form_model.ModelMultipleChoiceField(queryset=models.NNewType.objects.all())  # 多选
    # authors = form_model.ModelChoiceField(queryset=models.NNewType.objects.all())  # 单选
# Django Form所有内置字段
"""
Field
    required=True,               是否允许为空
    widget=None,                 HTML插件
    label=None,                  用于生成Label标签或显示内容
    initial=None,                初始值
    help_text='',                帮助信息(在标签旁边显示)
    error_messages=None,         错误信息 {'required': '不能为空', 'invalid': '格式错误'}
    validators=[],               自定义验证规则
    localize=False,              是否支持本地化
    disabled=False,              是否可以编辑
    label_suffix=None            Label内容后缀
 
 
CharField(Field)
    max_length=None,             最大长度
    min_length=None,             最小长度
    strip=True                   是否移除用户输入空白
 
IntegerField(Field)
    max_value=None,              最大值
    min_value=None,              最小值
 
FloatField(IntegerField)
    ...
 
DecimalField(IntegerField)
    max_value=None,              最大值
    min_value=None,              最小值
    max_digits=None,             总长度
    decimal_places=None,         小数位长度
 
BaseTemporalField(Field)
    input_formats=None          时间格式化   
 
DateField(BaseTemporalField)    格式:2015-09-01
TimeField(BaseTemporalField)    格式:11:12
DateTimeField(BaseTemporalField)格式:2015-09-01 11:12
 
DurationField(Field)            时间间隔:%d %H:%M:%S.%f
    ...
 
RegexField(CharField)
    regex,                      自定制正则表达式
    max_length=None,            最大长度
    min_length=None,            最小长度
    error_message=None,         忽略,错误信息使用 error_messages={'invalid': '...'}
 
EmailField(CharField)      
    ...
 
FileField(Field)
    allow_empty_file=False     是否允许空文件
 
ImageField(FileField)      
    ...
    注:需要PIL模块,pip3 install Pillow
    以上两个字典使用时,需要注意两点:
        - form表单中 enctype="multipart/form-data"
        - view函数中 obj = MyForm(request.POST, request.FILES)
 
URLField(Field)
    ...
 
 
BooleanField(Field)  
    ...
 
NullBooleanField(BooleanField)
    ...
 
ChoiceField(Field)
    ...
    choices=(),                选项,如:choices = ((0,'上海'),(1,'北京'),)
    required=True,             是否必填
    widget=None,               插件,默认select插件
    label=None,                Label内容
    initial=None,              初始值
    help_text='',              帮助提示
 
 
ModelChoiceField(ChoiceField)
    ...                        django.forms.models.ModelChoiceField
    queryset,                  # 查询数据库中的数据
    empty_label="---------",   # 默认空显示内容
    to_field_name=None,        # HTML中value的值对应的字段
    limit_choices_to=None      # ModelForm中对queryset二次筛选
     
ModelMultipleChoiceField(ModelChoiceField)
    ...                        django.forms.models.ModelMultipleChoiceField
 
 
     
TypedChoiceField(ChoiceField)
    coerce = lambda val: val   对选中的值进行一次转换
    empty_value= ''            空值的默认值
 
MultipleChoiceField(ChoiceField)
    ...
 
TypedMultipleChoiceField(MultipleChoiceField)
    coerce = lambda val: val   对选中的每一个值进行一次转换
    empty_value= ''            空值的默认值
 
ComboField(Field)
    fields=()                  使用多个验证,如下:即验证最大长度20,又验证邮箱格式
                               fields.ComboField(fields=[fields.CharField(max_length=20), fields.EmailField(),])
 
MultiValueField(Field)
    PS: 抽象类,子类中可以实现聚合多个字典去匹配一个值,要配合MultiWidget使用
 
SplitDateTimeField(MultiValueField)
    input_date_formats=None,   格式列表:['%Y--%m--%d', '%m%d/%Y', '%m/%d/%y']
    input_time_formats=None    格式列表:['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
 
FilePathField(ChoiceField)     文件选项,目录下文件显示在页面中
    path,                      文件夹路径
    match=None,                正则匹配
    recursive=False,           递归下面的文件夹
    allow_files=True,          允许文件
    allow_folders=False,       允许文件夹
    required=True,
    widget=None,
    label=None,
    initial=None,
    help_text=''
 
GenericIPAddressField
    protocol='both',           both,ipv4,ipv6支持的IP格式
    unpack_ipv4=False          解析ipv4地址,如果是::ffff:192.0.2.1时候,可解析为192.0.2.1, PS:protocol必须为both才能启用
 
SlugField(CharField)           数字,字母,下划线,减号(连字符)
    ...
 
UUIDField(CharField)           uuid类型
"""
小结

只要是选择框单选用的的字段都是ChoiceField,多选框用的字段都是MultipleChoiceField

froms组件源码

"""
切入点:
	from_obj.is_valid()
"""
def is_valid(self):
    """
    Returns True if the form has no errors. Otherwise, False. If errors are
    being ignored, returns False.
    """
    return self.is_bound and not self.errors
	# 如果is_valid要返回True的话,那么self.is_bound要为True self.errors要为False

self.is_bound = data is not None or files is not None
# 只要你传值了那么is_bound返回的就是True

@property
def errors(self):
    "Returns an ErrorDict for the data provided for the form"
    if self._errors is None:
        self.full_clean()
    return self._errors
# 因为_errors必为None所以full_clean()必执行

# forms组件所有的功能基本上都出自于该方法
def full_clean(self):
    """
    Cleans all of self.data and populates self._errors and
    self.cleaned_data.
    """
    self._errors = ErrorDict()
    if not self.is_bound:  # 这个是判断有没有数据的,基本上不会执行里面的东西	
        return
    self.cleaned_data = {}
    # If the form is permitted to be empty, and none of the form data has
    # changed from the initial data, short circuit any validation.
    if self.empty_permitted and not self.has_changed():
        return

    self._clean_fields()  # 校验字段 + 局部钩子
    self._clean_form()  # 全局钩子
    self._post_clean()  # 里面什么东西都没有


def _clean_fields(self):
    for name, field in self.fields.items():  # 循环获取字段名和字段对象
        # value_from_datadict() gets the data from the data dictionaries.
        # Each widget type knows how to retrieve its own data, because some
        # widgets split data over several HTML fields.
        if field.disabled:
            value = self.get_initial_for_field(field, name)
        else:
            # 获取字段对应的用户数据
            value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
        try:
            if isinstance(field, FileField):
                initial = self.get_initial_for_field(field, name)
                value = field.clean(value, initial)
            else:
                value = field.clean(value)
            self.cleaned_data[name] = value  # 将合法的字段添加到clean.pycke
            if hasattr(self, 'clean_%s' % name):  # 利用反射获取局部钩子函数
                value = getattr(self, 'clean_%s' % name)()  # 局部钩子需要返回值
                self.cleaned_data[name] = value
        except ValidationError as e:
            self.add_error(name, e)  # 添加提示报错信息
        # ValidationError就是返回给前段的报错数据,可以使用from django.core.exceptions import  ValidationError导入
        
def _clean_form(self):
    try:
        cleaned_data = self.clean()  # 全局钩子需要一个返回值就是cleaned_data,clean()函数里面什么东西都没写,全局钩子就是自己定义的clean方法
    except ValidationError as e:
        self.add_error(None, e)
    else:
        if cleaned_data is not None:
            self.cleaned_data = cleaned_data

cookie和session

"""
发展史
	1.网站都没有保存用户功能的需求 所有用户访问返回的结果都是一样的
		eg:新闻、博客、文章...
	
	2.出现了一些了需要保存用户信息的网站
		eg:淘宝、支付宝、京东...
		
		以登录功能为例:如果不保存用户登录状态 也就意味着用户每次访问网站都需要重复的输入用户名和密码
		当用户第一次登录成功之后 将用户的用户名和密码返回给用户浏览器 让用户浏览器保存在本地,之后访问网站的时候浏览器自动将保存在浏览器上的用户名和密码发送给服务端,服务端获取之后自动验证
		早期这种方式具有非常大的安全隐患
		
		优化:
			当用户登陆成功之后,服务端产生一个随机字符串(在服务端保存数据,用kv键值对的形式),交由客户端浏览器保存
			
			随机字符串1:用户1相关信息
			随机字符串2:用户2相关信息
			随机字符串3:用户3相关信息
			之后访问客户端的时候,都带着该随机字符串,服务端去数据库中比对是否有对应随机字符串从而获取到对应的用户信息

但是如果你拿到了截获到该随机字符串,那么你就可以冒充当前用户 其实还是有安全隐患的


在web里面没有绝对的安全也没有绝对的不安全
"""
cookie
	保存在浏览器上的信息都可以称之为cookie
    它的表现形式一般都是k:v键值对(可以都多个)
session
	数据是保存在服务端的并且它的表现形式一般也是k:v键值对(可以有多个)
token
	seession虽然数据是保存在服务端的 但是禁不住数据量大
    服务端不在保存数据
    	登录成功之后 将一段信息进行加密处理(加密算法之后你公司知道)
        将加密之后的结果拼接在信息后面,整体返回给浏览器保存
        浏览器下次访问的时候带着该信息 服务端自动切去前面一段信息再次使用自己的加密算法更浏览器尾部的密文进行比对
jwt认证
	三段信息
    (后期会讲)

总结
	1.cookie是保存在客户端浏览器上的信息
    2.session就是保存在服务端上的信息
    3.session是基于cookie工作的(因为session产生的随机字符串是存储到客户端浏览器的,其实大部分的保存用户状态的操作都需要使用到cookie)

cookie

# 虽然cookie是服务端告诉客户端浏览器需要保存内容
# 但是客户端浏览器可以选择拒绝保存 如果禁止了 那么 只要是需要记录用户状态的网站登陆功能都无法使用了

# 视图函数的返回值
return HttpResponse()
return render()
return redirect()


obj1 = HttpResponse()
# 操作cookie
return obj1

obj2 = render()
# 操作cookie
return obj2

obj3 = redirect()
# 操作cookie
return obj3

# 如果你想要操作cookie,你就不得不利用obj对象
"""
设置cookie
	obj.set_cookie(key,value)
获取cookie
	request.COOKIES.get(key)
在设置cookie的时候可以添加一个超时时间
    obj.set_cookie('username', 'mubai',max_age=4,expires=3)
    max_age
    expires
    两者都是设置超时时间的 并且都是以秒为单位
    需要注意的是 针对IE浏览器需要使用expires
主动删除cookie(注销功能)
    obj = redirect('/login/')
    obj.delete_cookie('username')
"""
# 我们完成一个真正的登陆功能
# 校验用户是否登陆的装饰器
"""
用户如果在没有登陆的情况下想想问一个需要登陆的页面
那么先跳转到登陆页面 当用户输入正确的用户名和密码之后
应该跳转到用户之前想要访问的页面去 而不是直接写死
    我的思想在url使用一个变量
"""

# 我们完成一个真正的登陆功能
def login_auth(func):
    def inner(request, *args, **kwargs):
        # print(request.path_info)
        # print(request.get_full_path())  # 能够获取用户上一次想要访问的url
        target_url = request.get_full_path()
        if request.COOKIES.get('username') != 'mubai':
            return redirect(f'/login/?next={target_url}')
        res = func(request, *args, **kwargs)
        return res

    return inner

def login(request):
    if request.method == "POST":
        username = request.POST.get('username')
        password = request.POST.get('password')
        if username == 'mubai' and password == '123':
            # 获取用户上一次想要访问的url
            target_url = request.GET.get('next')  # 这个结果可能是none
            if target_url:
                # 保存用户登陆状态
                obj = redirect(target_url)
            else:
                obj = redirect('/home/')
            # 让浏览器记录cookie数据
            obj.set_cookie('username', 'mubai', max_age=40, expires=40)
            """
            浏览器不单单会帮你存
            而且后面每次访问你的时候还会带着它过来
            """
            # 跳转到一个需要用户登陆之后才能看的页面
            return obj
    return render(request, 'login.html', locals())

@login_auth
def home(request):
    print(request.POST)
    return HttpResponse('home页面')

@login_auth
def logout(request):
    obj = redirect('/login/')
    obj.delete_cookie('username')
    return obj

session操作

"""
session数据是保存在服务端的,给客户端返回的是一个随机字符串
	sessionid : 随机字符串

1.在默认情况下操作session的时候需要django默认的一张django_session表
	数据库迁移命令
		django会自己创建很多表 django_session就是其中的一张

django默认session的过期时间是14天
	但是你也可以人为的修改它

设置session
request.session[key] = value

获取session
reqeust.session.get('key')


设置过期时间
request.session.set_expiry()
	括号内可以放四种类型的参数
		1.整数			多少秒
		2.日期对象			到指定日期失效 
		3.0					一旦当前浏览器窗口关闭立刻失效
		4.不写				失效时间取决于django内部全局session默认的失效时间(14天)

清除session
	request.session.delete()	# 只删除服务端的session 客户端的session不删除
	request.session.flush()		# 浏览器和服务端都清空(推荐使用)

session是保存在服务端的 但是session的保存位置可以有多种选择
	1.MySQL
	2.文件
	3.redis
	4.memcache
django_session表中的数据条数,是取决于浏览器的
	同一个计算机上(IP地址)同一个浏览器只会有一条数据生效,当session过期的时候和不同的计算机或者浏览器才会出现多条(哪怕设置了多个值还是只有一条数据)
	但是在多条数据出现的时候过期的数据不会出现很久,内部会自动识别过期的数据清除 你也可以通过代码清除
	
	主要是为了节省服务端数据库资源
"""
def set_session(request):
    request.session['hobby'] = 'sleep'
    request.session.set_expiry(10)
    """
    设置session值发生的事情
    内部进行了什么操作
        1. django内部会自动帮你生成一个随机字符串
        2. django内部自动将随机字符串和对应的数据存储到django_session表中(这一步不是直接生效的)
            2.1在内存中产生操作数据的缓存
            2.2在响应结果django中间件的时候才真正的操作数据库
            ('django.contrib.sessions.middleware.SessionMiddleware',)中间件做的事情
        3. 将产生的随机字符串返回给客户端浏览器保存
    """
    return HttpResponse('嘿嘿嘿')

def get_session(request):
    res = request.session.get('hobby')
    """
    获取session值发生的事情
        1. 自动从浏览器请求中获取sessionid对应的随机字符串
        2. 拿着该随机字符串去django_session表中查找对应的数据
        3. 
            如果比对上了 则将对应的数据取出并以字典的形式封装到request.session中
            如果比对不上 则request.session.get()返回的是None


    """
    if res:
        return HttpResponse(f'获取的值为{res}')
    else:
        HttpResponse('大爷关门了')

def del_session(request):
    # request.session.delete()
    request.session.flush()
    return HttpResponse('删除了')
# session扩展
# 获取、设置、删除Session中数据
request.session['k1']
request.session.get('k1',None)
request.session['k1'] = 123
request.session.setdefault('k1',123) # 存在则不设置
del request.session['k1']


# 所有 键、值、键值对
request.session.keys()
request.session.values()
request.session.items()
request.session.iterkeys()
request.session.itervalues()
request.session.iteritems()

# 会话session的key
request.session.session_key

# 将所有Session失效日期小于当前日期的数据删除
request.session.clear_expired()

# 检查会话session的key在数据库中是否存在
request.session.exists("session_key")

# 删除当前会话的所有Session数据
request.session.delete()
  
# 删除当前的会话数据并删除会话的Cookie。
request.session.flush() 
    这用于确保前面的会话数据不可以再次被用户的浏览器访问
    例如,django.contrib.auth.logout() 函数中就会调用它。

# 设置会话Session和Cookie的超时时间
request.session.set_expiry(value)
    * 如果value是个整数,session会在些秒数后失效。
    * 如果value是个datatime或timedelta,session就会在这个时间后失效。
    * 如果value是0,用户关闭浏览器session就会失效。
    * 如果value是None,session会依赖全局session失效策略。

1. 数据库Session
SESSION_ENGINE = 'django.contrib.sessions.backends.db'   # 引擎(默认)

2. 缓存Session
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'  # 引擎
SESSION_CACHE_ALIAS = 'default'                            # 使用的缓存别名(默认内存缓存,也可以是memcache),此处别名依赖缓存的设置

3. 文件Session
SESSION_ENGINE = 'django.contrib.sessions.backends.file'    # 引擎
SESSION_FILE_PATH = None                                    # 缓存文件路径,如果为None,则使用tempfile模块获取一个临时地址tempfile.gettempdir() 

4. 缓存+数据库
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'        # 引擎

5. 加密Cookie Session
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'   # 引擎

其他公用设置项:
SESSION_COOKIE_NAME = "sessionid"                       # Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串(默认)
SESSION_COOKIE_PATH = "/"                               # Session的cookie保存的路径(默认)
SESSION_COOKIE_DOMAIN = None                             # Session的cookie保存的域名(默认)
SESSION_COOKIE_SECURE = False                            # 是否Https传输cookie(默认)
SESSION_COOKIE_HTTPONLY = True                           # 是否Session的cookie只支持http传输(默认)
SESSION_COOKIE_AGE = 1209600                             # Session的cookie失效日期(2周)(默认)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False                  # 是否关闭浏览器使得Session过期(默认)
SESSION_SAVE_EVERY_REQUEST = False                       # 是否每次请求都保存Session,默认修改之后才保存(默认)

Django中Session相关设置

小案例:

from functools import wraps


def check_login(func):
    @wraps(func)
    def inner(request, *args, **kwargs):
        next_url = request.get_full_path()
        if request.session.get("user"):
            return func(request, *args, **kwargs)
        else:
            return redirect("/login/?next={}".format(next_url))
    return inner


def login(request):
    if request.method == "POST":
        user = request.POST.get("user")
        pwd = request.POST.get("pwd")

        if user == "alex" and pwd == "alex1234":
            # 设置session
            request.session["user"] = user
            # 获取跳到登陆页面之前的URL
            next_url = request.GET.get("next")
            # 如果有,就跳转回登陆之前的URL
            if next_url:
                return redirect(next_url)
            # 否则默认跳转到index页面
            else:
                return redirect("/index/")
    return render(request, "login.html")


@check_login
def logout(request):
    # 删除所有当前请求相关的session
    request.session.delete()
    return redirect("/login/")


@check_login
def index(request):
    current_user = request.session.get("user", None)
    return render(request, "index.html", {"user": current_user})

Session版登录验证

CBV如何添加装饰器

from django.views import View
from django.utils.decorators import method_decorator

"""
CBV中django不建议你直接给类的方法加装饰器
无论该装饰器能否正常给你 都不建议直接加 
"""


# @method_decorator(login_auth,name='post')  # 方式2:(可以添加多个针对不同的方法加不同的装饰器)
# @method_decorator(login_auth,name='get')
class MyLogin(View):
    @method_decorator(login_auth)  # 方式3:它会直接作用于当前类里面的所有的方法
    def dispatch(self, request, *args, **kwargs):
        pass
    # @method_decorator(login_auth)  # 方式1:指名道姓
    def get(self, request):
        return HttpResponse('get请求')

    def post(self, request):
        return HttpResponse('post请求')

django中间件

"""
django中间件是django的门户
1.请求来的时候先经过中间件才能到达真正的django后端
2.响应走的时候最后也需要经过中间件才能发送出去

django自带七个中间件
"""
# django请求生命周期流程图(上面有)

# 研究django中间件代码规律
MIDDLEWARE = [  # 下面这些字符串都是模块的路径eg:from django.middleware.security import SecurityMiddleware
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',  # 操作session的中间件
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]


class SessionMiddleware(MiddlewareMixin):
    def process_request(self, request):
        session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
        request.session = self.SessionStore(session_key)

    def process_response(self, request, response):

        return response

class CsrfViewMiddleware(MiddlewareMixin):
    def process_request(self, request):
        csrf_token = self._get_token(request)
        if csrf_token is not None:
            # Use same token next time.
            request.META['CSRF_COOKIE'] = csrf_token

    def process_view(self, request, callback, callback_args, callback_kwargs):
        return self._accept(request)

    def process_response(self, request, response):
        return response

class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        request.user = SimpleLazyObject(lambda: get_user(request))

"""
django支持程序员自定义中间件并且暴露给程序员五个可以自定义的方法
	1.必须要掌握
		process_request
		
		process_response
	2.了解即可
		process_view
		
		process_template_response
		
		process_exception
"""

如何自定义中间件

"""
1.在项目名或者应用名下创建一个任意名称的文件夹
2.在该文件夹内创建一个任意名称的py文件
3.在该py文件内需要书写类(这个类必须继承MiddlewareMixin)
	然后在这个类里面就可以自定义五个方法了
	(这五个方法并不是全部都需要书写,用几个写几个)
4.需要将类的路径以字符串的形式注册到配置文件中才能生效
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',
    '你自己写的中间件路径'
]
"""

"""
django支持程序员自定义中间件并且暴露给程序员五个可以自定义的方法
	1.必须要掌握
		process_request
			1.请求来的时候需要经过每一个中间件里面的process_request方法
			结果的顺序是按照配置文件中注册的中间件从上往下的顺序依次执行
			2.如果中间件里面没有定义process_request方法,那么直接跳过执行下一个中间件
			3.如果该方法返回了HttpResponse对象,那么请求将不在继续往后执行,而是直接原路返回(像:校验失败不允许访问...)
			
			process_response方法就是用来做全局相关的所有限制功能
		
		process_response
			1.响应走的时候需要经过每一个中间件里面的process_response方法
			该方法有两个额外额参数request,response
			2.该方法必须返回一个response对象
				1.默认返回的就是形参response
				2.你也可以自己返回自己的
			3.顺序是按照配置文件中注册了的中间件从下往上依次经过
				如果你没有定义的话 直接跳过执行下一个,中间如果将返回的response对象使用HttpResponse返回那么经过后面的
				response对象都是HttpResponse处理之后的对象
		
		研究如果在第一个process_request方法就已经返回了HttpResponse对象,那么响应走的时候是经过所有的中间件里面的process_response还是有其他情况?
		是其他情况,
			会直接走同级别的process_reponse然后按照原路返回
		
		flask框架也有一个中间件但是它的规律
			只要返回数据了就必须经过所有中间件里面的类似于process_response方法
			
	2.了解即可
		process_view
			路由匹配成功之后视图函数之前,会自动执行中间件的process_view方法
			顺序是按照配置文件中注册的中间件从上往下的顺序依次执行
			view_name参数带有url匹配函数 
			
		process_template_response
			返回的HttpResponse对象有render属性的时候才会触发
			顺序是照配置文件中注册了的中间件从下往上依次经过
			def index(request):
                print('我是视图函数index')
                obj = HttpResponse('index')

                def render():
                    print('内部的render')
                    return HttpResponse("98K")

                obj.render = render
                return obj
			
		process_exception
			当视图函数中出现异常的情况下触发
			顺序是照配置文件中注册了的中间件从下往上依次经过
			exception参数带有报错信息
			
# 案例查看day70
"""

csrf跨站请求伪造

"""
钓鱼网站
	我搭建一个跟正规网站一模一样的界面(eg:中国银行)
	用户不小心进入了我们的网站,用户给某个人打钱
	打钱的操作确确实实是提交给了中国银行的系统,用户的钱也确确实实减少了
	但是唯一不同的是打钱的账户不是用户想要打的账户变成了一个莫名其妙的账户

eg:
大学英语四六级
	考之前需要学生自己网站登录缴费

内部本质
	我们在钓鱼网站的页面 针对对方账户 只给用户提供一个没有name属性的普通input框
	然后我们在内部隐藏一个已经写好name和value的input框

如何规避上述问题
	csrf跨站请求伪造校验
		网站在给用户返回一个具有提交数据功能页面的时候会给这个页面加一个唯一标识
		当这个页面朝后端发送post请求的时候 我的后端会先校验唯一标识,如果唯一标识不对直接拒绝(403 forbbiden)如果成功则正常执行
"""
如何符合校验
# form表单如何符合校验
<form action="" method="post">
    {% csrf_token %}
    <p>username:<input type="text" name="username" id=""></p>
    <p>target_user:<input type="text" name="target_user" id=""></p>
    <p>money:<input type="text" name="money" id=""></p>
    <input type="submit" value="提交">
</form>
# 转化为
<form action="" method="post">
    <input type="hidden" name="csrfmiddlewaretoken" value="A4uY6SGqWy0q4G6jyiMoo3AlBGSjmEccxSnwpzP4wEE6xhLDpi4LLrIK5NQOYmTK">
    <p>username:<input type="text" name="username" id=""></p>
    <p>target_user:<input type="text" name="target_user" id=""></p>
    <p>money:<input type="text" name="money" id=""></p>
    <input type="submit" value="提交">
</form>
# csrf_token将自动装换成一个input表单数据,并且每次请求这个数据都不一样

# ajax如何符合校验

{% load static %}
<script src="{% static 'js/mysetup.js' %}"></script>
<script>
    $("#d1").click(function () {
        $.ajax({
            url: '',
            type:'post',
            // 第一种 利用标签查找获取页面的随机字符串
            // data: {"username": "cxk","csrfmiddlewaretoken":$('[name=csrfmiddlewaretoken]').val()},  // 键的必须为csrfmiddlewaretoken
            // 第二种方式 利用模板语法提供的快捷方式
            // data: {"username": "cxk","csrfmiddlewaretoken":{{ csrf_token }}},
            // 第三种 通用方式(拷贝js代码并导入js文件)
            data: {"username": "cxk"},
            success: function () {

            }
        })
    })
</script>
// js 代码

function getCookie(name) {
    var cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        var cookies = document.cookie.split(';');
        for (var i = 0; i < cookies.length; i++) {
            var cookie = jQuery.trim(cookies[i]);
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}
var csrftoken = getCookie('csrftoken');

function csrfSafeMethod(method) {
  // these HTTP methods do not require CSRF protection
  return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

$.ajaxSetup({
  beforeSend: function (xhr, settings) {
    if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
      xhr.setRequestHeader("X-CSRFToken", csrftoken);
    }
  }
});
csrf相关装饰器
"""
1.网站整体都不校验csrf,就单单几个视图函数需要校验
	将csrf中间件注释,然后在想要添加的函数前面使用csrf_protect装饰器,打开csrf验证
2.网站整体都校验csrf,就单单几个视图函数不需要校验
	将csrf中间件打开,然后在想要添加的函数前面使用csrf_exempt装饰器,关闭csrf验证
"""

from django.views.decorators.csrf import csrf_protect, csrf_exempt

"""
csrf_protect    # 需要校验
    针对csrf_protect符合我们之前所学的装饰器的三种玩法
csrf_exempt     # 忽视校验
    针对csrf_exempt只能给dispatch方法加才有效
"""
from django.views import View
from django.utils.decorators import method_decorator

"""
# 没有打开scrf中间件
# @method_decorator(csrf_protect,name='post')  # 针对csrf_protect 第二种方式可以
class MyCsrfToken(View):
    @method_decorator(csrf_protect)  # 针对csrf_protect 第三种方式可以
    def dispatch(self, request, *args, **kwargs):
        return super(MyCsrfToken, self).dispatch(request, *args, **kwargs)

    def get(self, request):
        return HttpResponse('get')

    # @method_decorator(csrf_protect)  # 针对csrf_protect 第一种方式可以
    def post(self, request):
        return HttpResponse('post')
"""

# 打开csrf中间件
# @method_decorator(csrf_protect,name='post')  # 针对csrf_exempt 第二种方式也不可以可以
class MyCsrfToken(View):
    @method_decorator(csrf_protect)  # 针对csrf_exempt 第三种方式可以
    def dispatch(self, request, *args, **kwargs):
        return super(MyCsrfToken, self).dispatch(request, *args, **kwargs)

    def get(self, request):
        return HttpResponse('get')

    # @method_decorator(csrf_exempt)  # 针对csrf_exempt 第一种方式不可以
    def post(self, request):
        return HttpResponse('post')

Auth模块

"""
其实我们在创建好一个django项目之后直接执行数据库迁移命令会自动生成很多表
	django_session
	
	auth_user
django在启动之后就可以直接访问admin路由,需要输入用户名和密码,数据参考的就是auth_user表,并且还必须是管理员用户才能进入

创建超级用户(管理员用户)
	python3 manage.py createsuperuser
	windows
	python manage.py createsuperuser

依赖于auth_user表完成用户相关的所有功能	
"""

案例:具体案例看day71

# view.py
from django.shortcuts import render, redirect, HttpResponse
from django.contrib import auth

# Create your views here.
"""
使用auth模块要用就用全套,不然会出现一些莫名其妙的报错
"""


def login(request):
    if request.method == "POST":
        username = request.POST.get('username')
        password = request.POST.get('password')
        # 去用户表中校验数据
        # 1.表如何获取
        # 2.密码如何比对
        user_obj = auth.authenticate(request, username=username, password=password)
        """
        1.自动查找auth_user标签
        2.自动给密码加密再对比
        该方法注意事项
            括号内必须同时传入用户名和密码
            不能只传用户名(一步就帮你筛选出用户对象)
        """
        print(user_obj)  # 用户对象 mubai  数据不符合则返回None
        # print(user_obj.username)
        # print(user_obj.password)  # pbkdf2_sha256$36000$VsvZZ8cFuVJy$9vCPxUzJwcavsIAoU/ujiteu/HPFC7T9tFcNQq4278I=
        # 判断用户是否存在
        if user_obj:
            # 保存用户状态
            auth.login(request, user_obj)  # 类似于request.session[key] = user_obj
            # 主要执行了该方法 你就可以在任何地方通过request.user获取当前登陆的用户对象
            return redirect('/home/')

    return render(request, 'login.html')


from django.contrib.auth.decorators import login_required


# @login_required(login_url='/login/')  # 局部配置:当用户没有登陆跳转到login_url后面指定的网址 ,全局配置在配置文件里面设置
@login_required  # 设置全局配置之后就不需要写login_url了
def home(request):
    """
    用户登录之后才能看到home
    """
    print(request.user)  # 用户对象  返回为AnonymousUser表示为匿名用户
    # 判断用户是否登陆
    print(request.user.is_authenticated)  # True表示管理员 False表示匿名用户
    # 自动去django_session表里面查找对应的用户对象给你封装到request.user中
    return HttpResponse('home页面')


"""
1. 如果局部和全局都有该听谁的?
    局部的优先级大于全局的优先级
2. 局部和全局的优缺点
    全局的好处在于无需重复写代码 但是跳转的页面很单一
    全局的好处在于不同的视图函数在用户设置没有登陆的情况下可以跳转到不同的页面
"""


@login_required
def set_password(request):
    if request.method == "POST":
        print(request.user.username)
        old_password = request.POST.get('old_passsword')
        new_password = request.POST.get('new_password')
        confirm_password = request.POST.get('confirm_password')
        # 先校验两次密码是否一致
        if new_password == confirm_password:
            # 校验老密码对不对
            is_right = request.user.check_password(old_password)  # 自动加密比对密码,正确返回True错误返回False
            if is_right:
                # 修改密码
                request.user.set_password(new_password)  # 仅仅是在修改对象的属性
                request.user.save()  # 这一步才是真正的操作修改数据库
            else:
                return HttpResponse('检验密码不对')
        else:
            return HttpResponse('两次输入的密码不对')
    return render(request, 'set_pasword.html', locals())


@login_required
def logout(request):
    auth.logout(request)  # 会进入自己的request对应的 session表里面找到当前登录的对象 类似于 dequest.session.flush()
    return redirect('/logi/')


from django.contrib.auth.models import User  # 这个就是那个user表


def register(request):
    if request.method == "POST":
        username = request.POST.get('username')
        password = request.POST.get('password')
        # 操作auth_user表写入数据
        # User.objects.create(username=username,password=password)
        # 这样创建用户,密码是明文的,这样后期用户验证为密文,这里是明文会导致密码用不上,且不能创建管理员账户
        # 创建普通用户使用create_user
        # User.objects.create_user(username=username,password=password)
        # 创建管理员用户create_superuser(了解):使用代码创建超级用户邮箱是必填的,而用命令创建则可以不填
        User.objects.create_superuser(username=username,email='123@qq.com',password=password)

    return render(request, 'register.html')

方法总结

# 1.比对用户名和密码是否正确
user_obj = auth.authenticate(request, username=username, password=password)
# 括号内必须同时传入用户名和密码
print(user_obj)  # 用户对象 mubai  数据不符合则返回None
print(user_obj.username)  # mubai
print(user_obj.password )  # 密文

# 2.保存用户状态
auth.login(request, user_obj)  # 类似于request.session[key] = user_obj
# 主要执行了该方法 你就可以在任何地方通过request.user获取当前登陆的用户对象

# 3.判断当前用户是否登陆
request.user.is_authenticated

# 4.获取当前登录用户
request.user
# 5.校验用户是否登装饰器
from django.contrib.auth.decorators import login_required
# 局部配置
@login_required(login_url='/login/')
# 全局配置
# 在setting.py中
# 全局配置:没有登录跳转到指定的页面
LOGIN_URL = '/login/'
# 然后在对应的函数中直接使用,不用指定url
@login_required
"""
1. 如果局部和全局都有该听谁的?
    局部的优先级大于全局的优先级
2. 局部和全局的优缺点
    全局的好处在于无需重复写代码 但是跳转的页面很单一
    全局的好处在于不同的视图函数在用户设置没有登陆的情况下可以跳转到不同的页面
"""
# 6.比对原密码
request.user.check_password(old_password)  # 自动加密比对密码,正确返回True错误返回False
# 7.修改密码
request.user.set_password(new_password)  # 仅仅是在修改对象的属性
request.user.save()  # 这一步才是真正的操作修改数据库
# 8.注销
auth.logout(request)  # 会进入自己的request对应的 session表里面找到当前登录的对象 类似于 dequest.session.flush()
# 9.注册
# User.objects.create(username=username,password=password)
# 这样创建用户,密码是明文的,这样后期用户验证为密文,这里是明文会导致密码用不上,且不能创建管理员账户
# 创建普通用户使用create_user
User.objects.create_user(username=username,password=password)
# 创建管理员用户create_superuser(了解):使用代码创建超级用户邮箱是必填的,而用命令创建则可以不填
# User.objects.create_superuser(username=username, email='123@qq.com', password=password)

auth模块表扩展

from django.db import models

# Create your models here.
from django.contrib.auth.models import User,AbstractUser
# 第一种:一对一关系  不推荐使用
# class UserDetail(models.Model):
#     phone = models.BigIntegerField()
#     user = models.OneToOneField(to='User')

# 第二种:面向对象的继承
class UserInfo(AbstractUser):
    """
    如果继承了AbstractUser
    那么在执行数据库迁移命令的时候auth_user表就不会创建出来了
    而UserInfo表中会出现auth_user所有的字段外加自己扩展的字段

    这么做的好处在于你能直接点击你自己的表更加快速的完成操作和扩展

    前提:
        1.在继承之前没有执行过数据库迁移命令(auth_user没有被创建)
            如果当前库已经创建了那么你就重新换一个库
        2.继承的类里面不要覆盖AbstractUser里面的字段名
            表里面有的字段都不要都动
        3.需要在配置文件中告诉django你要用UserInfo代替auth_user(在setting.py文件进行配置)
            AUTH_USER_MODEL = 'app01.UserInfo'
                                应用名.表名
    """
    phone = models.BigIntegerField()
"""
你如果自己写表替代了auth_user那么
auth模块的功能还是照常使用,参考表页由原来的auth_user变成了UserInfo
"""

BBS

项目开发流程

# 1.需求分析
	架构师+产品经理+开发者组长
        在跟客户谈需求之前,会大致先了解客户端的需求,然后自己先设计一套比较好写方案
        在跟客户沟通交流中引导客户往我们之前想好的方案上面靠
        形成一个初步的方案
# 2.项目设计
	架构师
    	编程语言选择
        框架选择
        数据库选择
        	主库:MySQL,postgreSQL...
            缓存数据库:redis、mongodb、memcacha...
        功能划分
        	将整个项目划分成几个功能模块
        找组长开会
        	给每个组分发任务
        项目报价
        	技术这块需要多少人,多少天(一个程序员一天1500~2000计算(大致))
            产品经理公司层面 再加点钱
            	公司财务签字确认
                公司老板签字确认
            产品经理去跟客户沟通
            
            后续需要加功能 继续加钱
# 3.分组开发
	组长找组员开会,安排各自功能模块
    我们其实就是在架构师设计好的框架里面填写代码而已(码畜)
    
    我们在写代码的时候 写完需要自己先测试是否有bug
    如果是一些显而易见的bug,你没有避免而是直接交给了测试部门测出来了
    那你可能就需要被扣绩效了(一定要跟测试搞好关系)
    	薪资组成 15k(这样组成工资的目的是为了:合理合规合法的避税)
        	底薪 10k
            绩效 3k
            岗位津贴 1k
            生活补贴 1k
# 4.测试
	测试部门测试你的代码
    	压力测试
        黑盒测试
        白盒测试
        ...
# 5.交付上线
	1.交给对方的运维人员
    2.直接上线到我们的服务器上 收取维护费用
    3.其他

表设计

"""
一个项目中最重要的不是业务逻辑的书写
而是前期的表设计,只有将表设计好了,后续的功能书写才会一帆风顺

bbs表设计
	1.用户表(UserInfo)
		继承AbstractUser
		扩展
			phone			# 电话号码
			avatar			# 存储用户头衔
			create_time		# 注册时间
		
		外键字段
			一对一个人站点表

	2.个人站点表(Blog)
		site_name		# 站点名称
		site_title		# 站点标题
		site_theme		# 站点样式
		
	
	3.文章标签表(Category)
		name		# 标签名
		
		外键字段
			一对多个人站点
		
	4.文章分类表(Tag)
		name		# 分类名
		
		外键字段
			一对多个人站点
			 
	5.文章表(Article)
		title		# 文章标题
		desc		# 文章简介
		content		# 文章内容
		create_time	# 发布时间
		
		数据库字段设计优化(虽然下述的三个字段可以从其他表里面跨表查询计算得出,但是频繁跨表效率低)
		up_num		# 点赞数
		down_num	# 点踩数
		comment_num	# 评论数
		
		
	6.点赞点踩表(UpAndDown)
		记录哪个用户给哪篇文章点了站还是点了踩
		user		# 用户名		ForeignKey(to="User")
		article		# 文章名		ForeignKey(to="article")  
		is_up		# 是否点赞		BooleanField()
		
		user和article就是外键字段
		
		
	7.文章评论表(Comment)
		用来记录哪个用户给那篇文章写了那些评论内容
		user			# 用户名	ForeignKey(to="User")
		article			# 文章名	ForeignKey(to="article")  
		content			# 评论内容	CharField()
		comment_time	# 评论时间	DatetimeField()
		# 自关联
		# parent			ForeignKey(to="Comment",null=True) 
		# ORM专门提供的自关联写法
		parent				ForeignKey(to="self",null=True) 

根评论子评论的概念
	根评论就是直接评论当前发布的内容的
	
	子评论是评论别人的评论
		1.PHP是世界上最牛逼的语言
			1.1 python才是最牛逼的
				1.2Java才是
	
	根评论和子评论是一对多的关系(问题是跟评论和子评论用的是同一张表)
"""

bbs是一个前后端不分离的全栈项目,前段和后端都需要我们自己一步步的完成

数据库表创建及同步

"""
由于django自带的sqlite数据库对日期不敏感,所以我们换成MySQL
"""
# model.py
from django.db import models

from django.contrib.auth.models import AbstractUser

# Create your models here.
"""
先写普通字段
之后再写外键字段
"""


class UserInfo(AbstractUser):
    phone = models.BigIntegerField(verbose_name='手机号', null=True)
    # 头像
    avatar = models.FileField(verbose_name='用户头像', upload_to='avatar/', default='avatar/default.ico')
    """
    给avatar字段传文件对象 该文件会自动存储到avatar文件下 然后avatar字段值只保存文件路径avatar/default.ico
    """
    create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)

    blog = models.OneToOneField(to='Blog', null=True)  # bull=true测试需要方便测试数据


class Blog(models.Model):
    site_name = models.CharField(verbose_name='站点名称', max_length=32)
    site_title = models.CharField(verbose_name='站点标题', max_length=32)
    # 简单模拟 带你认识样式内部原理的操作
    site_theme = models.CharField(verbose_name='站点样式', max_length=64)  # 存css/js的文件路径


class Category(models.Model):
    name = models.CharField(verbose_name='文章分类', max_length=32)
    blog = models.ForeignKey(to='Blog', null=True)  # bull=true测试需要方便测试数据


class Tag(models.Model):
    name = models.CharField(verbose_name='文章标签', max_length=32)
    blog = models.ForeignKey(to='Blog', null=True)  # bull=true测试需要方便测试数据


class Article2Tag(models.Model):
    article = models.ForeignKey(to='Article')
    tag = models.ForeignKey(to='Tag')


class Article(models.Model):
    title = models.CharField(verbose_name='文章标题', max_length=64)
    desc = models.CharField(verbose_name='文章标题', max_length=255)
    # 文章内容有很多 一般情况下都是使用TextField
    content = models.TextField(verbose_name='文章内容')
    create_time = models.DateField(auto_now_add=True)
    # 数据库字段设计优化
    up_num = models.BigIntegerField(verbose_name='点赞数', default=0)
    down_num = models.BigIntegerField(verbose_name='点踩数', default=0)
    comment_num = models.BigIntegerField(verbose_name='评论数', default=0)

    blog = models.ForeignKey(to='Blog', null=True)  # bull=true测试需要方便测试数据
    category = models.ForeignKey(to='Category', null=True)  # bull=true测试需要方便测试数据
    tags = models.ManyToManyField(to='Tag',
                                  through='Article2Tag',
                                  through_fields=('article', 'tag'))


class UpAndDown(models.Model):
    user = models.ForeignKey(to='UserInfo')
    article = models.ForeignKey(to='Article')
    is_up = models.BooleanField(verbose_name='是否点赞')  # 传布尔值 存0/1


class Comment(models.Model):
    user = models.ForeignKey(to='UserInfo')
    article = models.ForeignKey(to='Article')
    content = models.CharField(verbose_name='评论内容', max_length=255)
    comment_time = models.DateTimeField(auto_now_add=True)
    # 自关联
    parent = models.ForeignKey(to='self', null=True)  # 有些评论就是根评论,没有这个所有的评论都是子评论

注册功能

"""
我们之前是直接在views.py中书写的forms组件代码
但是为了接耦合 应该将所有的forms组件代码单独写一个地方

如果你的项目自始至终只用到了一个forms组件那么你可以直接建一个py文件即可
	myforms.py
但是你的项目需要使用多个froms组件,那么你可以创建一个文件夹在文件夹内根据
forms组件功能的不同创建不同的py文件
	myforms文件夹
		regform.py
		loginform.py
		userform.py
		orderform.py
"""
forms组件
# myforms.py
from django import forms
from app01 import models


class MyRegForm(forms.Form):
    username = forms.CharField(label='用户名', min_length=3, max_length=8,
                               error_messages={
                                   'required': '用户名不能为空',
                                   'min_length': '用户名最小3位',
                                   'max_length': '用户名最大8位',
                               },
                               widget=forms.widgets.TextInput(attrs={'class': 'form-control'}),
                               )
    password = forms.CharField(label='密码', min_length=6, max_length=20,
                               error_messages={
                                   'required': '密码不能为空',
                                   'min_length': '密码最小6位',
                                   'max_length': '密码最大20位',
                               },
                               widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'}),
                               )
    confirm_password = forms.CharField(label='确认密码', min_length=6, max_length=20,
                              error_messages={
                                  'required': '密码不能为空',
                                  'min_length': '密码最小6位',
                                  'max_length': '密码最大20位',
                              },
                              widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'}),
                              )
    email = forms.EmailField(label='邮箱',
                             error_messages={
                                 'required': '邮箱不能为空',
                                 'invalid': '邮箱格式不正确',
                             },
                             widget=forms.widgets.EmailInput(attrs={'class': 'form-control'})

                             )

    # 钩子函数
    # 局部钩子;校验用户名是否存在
    def have_username(self):
        username = self.cleaned_data.get('username')
        # 去数据库中校验数据是否存在
        is_exist = models.UserInfo.objects.filter(username=username)
        if is_exist:
            self.add_error('username', '用户名已存在')
        return username

    # 全局钩子:校验两次是否一致
    def clean(self):
        password = self.cleaned_data.get('password')
        confirm_password = self.cleaned_data.get('confirm_password')
        if not password == confirm_password:
            self.add_error('confirm_password', '两次密码不一致')
        return self.cleaned_data
# view.py
from django.shortcuts import render
from app01.myfroms import MyRegForm
from app01 import models
from django.http import JsonResponse, HttpResponse
def register(request):
    form_obj = MyRegForm()
    if request.method == "POST":
        # 设置请求返回的信息,code用来表示正确还是错误的1000为正确,2000为错误msg用来存储返回信息
        back_dic = {'code': 1000, 'msg': ''}
        # 校验数据是否合法
        form_obj = MyRegForm(request.POST)
        # 判断数据是否合法
        if form_obj.is_valid():
            # print(form_obj.cleaned_data)  # {'username': 'mubai', 'password': '123456', 'confirm_password': '123456', 'email': 'beijing@qq.com'}
            clean_data = form_obj.cleaned_data  # 将校验通过的数据赋值给变量
            # 将字典里面的confirm_password键值对删除
            clean_data.pop('confirm_password')  # {'username': 'mubai', 'password': '123456', 'email': 'beijing@qq.com'}
            # 用户头像
            file_obj = request.FILES.get('avatar')
            """针对用户头像一定要判断是否传值 不能直接添加到字典里面去"""
            if file_obj:
                clean_data['avatar'] = file_obj  # 这个键的key只能是在models写的键的key值,不能随便写一个值,因为后面传值是用字典打散的方式一键创建数据对象
            # 直接操作数据库保存数据
            models.UserInfo.objects.create_user(**clean_data)
            # 设置url的跳珠路由给前段跳转网页
            back_dic['url'] = '/login/'
        else:
            back_dic['code'] = 2000
            # 将报错信息放到msg里面
            back_dic['msg'] = form_obj.errors
        return JsonResponse(back_dic)
    return render(request, 'register.html', locals())
用户头像前段实时展示和ajax
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
    {% load static %}
</head>
<body>
<div class="container-fluid">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h1 class="text-center">注册</h1>
            <form action="" id="myform"> <!-- 这里不用form表单提交数据,只是单纯的用一下form标签而已 -->
                {% csrf_token %}
                {% for form in form_obj %}
                    <div class="form-group">
                        <label for="{{ form.auto_id }}">{{ form.label }}</label>
                        {{ form }}
                        <!-- <span style="color: red">{{ form.errors.0 }}</span>  ajax这个报错信息显示不出来 -->
                        <span style="color: red" class="pull-right"></span>
                    </div>
                {% endfor %}
                <div class="form-group">
                    <label for="myfile">
                        <p>头像</p>
                        <img src="{% static 'img/default.ico' %}" alt="" style="height: 200px" id="show_img">
                    </label>
                    <input type="file" id="myfile" name="avatar" style="display:none ">
                </div>

                <input type="button" class="btn-primary btn pull-right" value="注册" id="id_commit">
            </form>
        </div>
    </div>
</div>
</body>
</html>
<script>
    // 因为现在的浏览器不可以使用绝对路径来访问本地文件,不能通过修改src来达到跟换图片的功能
    //$('#myfile').change(function () {
    //    $('#show_img')[0].src = $('#myfile').val()
    //    console.log($('#myfile').val())
    //})


    $('#myfile').change(function () {
        // 文件阅读器对象
        // 1 先成功一个文件阅读器对象
        let myFileReaderObj = new FileReader();
        // 2 获取用户上传的头像文件
        let fileObj = $(this)[0].files[0];
        // 3 将文件对象交给阅读对象读取
        myFileReaderObj.readAsDataURL(fileObj);  // 异步操作  IO操作
        // 4 利用文件阅读器将文件展示到前段页面  修改src属性
        // 等待文件阅读器加载完毕之后再执行
        myFileReaderObj.onload = function () {
            $('#show_img').attr('src', myFileReaderObj.result);
        }

    })

    $('#id_commit').click(function () {
        // 发送Ajax请求 我们发送的数据中既包含普通的键值对也包含文件
        let formDataObj = new FormData();
        // 1. 添加普通的键值对
        // console.log($('#myform').serializeArray())  // 获取form标签里面所有的普通的键值对[{},{},{}]
        $.each($('#myform').serializeArray(), function (index, obj) {
            // obj获取的到的数据就是一个个自定义对象
            formDataObj.append(obj.name, obj.value);
        });
        // 2. 添加文件数据
        formDataObj.append('avatar', $('#myfile')[0].files[0]);
        // 3. 发送ajax请求
        $.ajax({
            url: "",
            type: 'post',
            data: formDataObj,
            // 需要指定两个关键性的参数
            contentType: false,
            processData: false,
            success: function (args) {
                if (args.code == 100) {
                    // 跳转到登陆页面
                    window.location.href = args.url
                } else {
                    // 如何将对应的错误的提示展示到对应的input框下面
                    //for (let key in args.msg) {
                    //    $('[name="'+key+'"]').next().text(args.msg[key])
                    //    console.log($('[name="'+key+'"]').next())
                    //}
                    // forms组件渲染的标签id值都是 id_字段名
                    $.each(args.msg, function (index, obj) { // index为字段名 obj为数组
                        let target_id = '#id_' + index
                        $(target_id).next().text(obj[0]).parent().addClass('has-error')
                    })
                }

            }
        })
    })
    // 给所有的input框绑定获取焦点事件
    $('input').focus(function () {
        // 将input下面的span标签和input外面的div标签修改内容及属性
        $(this).next().text('').parent().removeClass('has-error')
    })
</script>

登录功能

"""
img标签的src属性
	1.图片路径
	2.url
	3.图片的二进制数据

我们的计算机上面之所以能够输出各种各样的字体样式
内部其实对应的是一个个.ttf结尾的文件
http://www.zhaozi.cn/ai/2019/fontlist.php?page=1&classid=32&line=50&tempid=38&ph=1&andor=and&softsq=%E5%85%8D%E8%B4%B9%E5%95%86%E7%94%A8&orderby=&myorder=0
"""
自己实现图片验证码和ajax
# view.py
def login(request):
    if request.method == "POST":
        back_dic = {'code': 1000, 'msg': ''}
        username = request.POST.get('username')
        password = request.POST.get('password')
        code = request.POST.get('code')
        # 1.先校验验证码是否正确  自己决定是否忽略大小写
        if request.session.get('code').upper() == code.upper():
            # 2 校验用户名和密码是否正确
            user_obj = auth.authenticate(request, username=username, password=password)
            if user_obj:
                # 保存用户状态
                auth.login(request, user_obj)
                back_dic['url'] = '/home/'
                back_dic['msg'] = '登录成功'

            else:
                back_dic['code'] = 2000
                back_dic['msg'] = '用户名或密码错误'
        else:
            back_dic['code'] = 3000
            back_dic['msg'] = '验证码错误'
        return JsonResponse(back_dic)
    return render(request, 'login.html')


import random


def get_random():
    return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)


"""
图片相关的模块
    pip3 install pillow就是PIL模块
"""
from PIL import Image, ImageDraw, ImageFont

"""
Image:生成图片
ImageDraw:能够在图片上乱涂乱画
ImageFont:控制字体样式
"""

from io import BytesIO, StringIO

"""
内存管理器模块
BytesIO:临时帮你存储数据 返回的时候数据是二进制格式
StringIO:临时帮你存储数据 返回的时候数据是字符串格式
"""


def get_code(request):
    # 推导步骤1.直接获取后端已有的图片(不需要)
    # with open('static/img/default.ico','rb') as f:
    #     data = f.read()
    # return HttpResponse(data)

    # 推导步骤2.利用pillow动态的产生图片
    # 产生图片
    # img_obj = Image.new('RGB', (430, 35), get_random())  # 第一个写图片的通道 第二个参数写生成图片的尺寸 第三个参数写颜色第三个参数可以放颜色参数(111,111,111)
    # # 先将图片对象保存起来,然后再讲图片对象读取出来
    # with open('static/img/codeimg.png', 'wb') as f:
    #     img_obj.save(f, 'png')
    # # 再将图片对象读取出来
    # with open('static/img/codeimg.png', 'rb') as f:
    #     data = f.read()
    # return HttpResponse(data)

    # 推导步骤3:文件存储繁琐IO操作效率低 借助于内存管理器模块
    # img_obj = Image.new('RGB', (430, 35), get_random())
    # io_obj = BytesIO()  # 生成一个内存管理器对象  你可以看成是文件句柄
    # img_obj.save(io_obj,'png')
    # return HttpResponse(io_obj.getvalue())  # 从内存管理器中读取二进制图片数据返回给前段

    # 最终步骤4:写图片验证码
    img_obj = Image.new('RGB', (430, 35), get_random())
    img_draw = ImageDraw.Draw(img_obj)  # 产生一个画笔对象
    img_font = ImageFont.truetype('static/font/myonfont.ttf', 30)  # 第一个参数字体样式  第二个参数字体大小

    # 随机验证码  五位数的随机数验证码 数字 小写字母 大写字母
    code = ''
    for i in range(5):
        random_upper = chr(random.randint(65, 90))  # 随机大写字母
        random_lower = chr(random.randint(97, 122))  # 随机小写字母
        random_int = str(random.randint(0, 9))  # 随机数字
        # 从上面三个里面随机选择一个
        tmp = random.choice([random_lower, random_upper, random_int])
        # 将产生的随机字符串写入到图片上
        """
        为什么一个个写而不是生成好了之后再写
        因为一个个写能够控制每个字体的间隙 而生成好之后再写间隙没发控制
        """
        img_draw.text((i * 45 + 100, 0), tmp, get_random(), img_font)  # 第一个参数表示写的位置,第二个参数表示字体 第三个放背景颜色 第四个参数放字体样式
        # 凭借随机字符串
        code += tmp
    # 随机验证码在登录的视图函数里面需要用到 要比对 所以要找地方存起来 并且其他视图函数也能拿到
    request.session['code'] = code
    io_obj = BytesIO()
    img_obj.save(io_obj, 'png')
    return HttpResponse(io_obj.getvalue())
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
    {% load static %}
</head>
<body>
<div class="container-fluid">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h1 class="text-center">登录</h1>
            <div class="form-group">
                <label for="username">用户名</label>
                <input type="text" name="username" id="username" class="form-control">
            </div>
            <div class="form-group">
                <label for="password">密码</label>
                <input type="text" name="password" id="password" class="form-control">
            </div>
            <div class="form-group">
                <label for="id_code">验证码</label>
                <div class="row">
                    <div class="col-md-6">
                        <input type="text" name="code" id="id_code" class="form-control">
                    </div>
                    <div class="col-md-6">
                        <img src="/get_code/" alt="验证码正在加载" width="430px" height="35px" id="id_img">
                    </div>
                </div>

            </div>
            <input type="button" value="登录" class="btn btn-success" id="id_commit">
            <span style="color:red" id="error"></span>
        </div>
    </div>
</div>
</body>
</html>
<script>
    var number_bool = true;
    // 1 先获取标签之前的src
    var oldVal = $('#id_img').attr('src');

    $('#id_img').click(function () {
        console.log(oldVal)
        if (number_bool) {
            $(this).attr('src', oldVal + '0');
            number_bool = false;
        } else {
            $(this).attr('src', oldVal + '1');
            number_bool = true;
        }

    })

    // 点击按钮发送ajax请求
    $("#id_commit").click(function () {
        $.ajax({
            url: '',
            type: 'post',
            data: {
                'username': $('#username').val(),
                'password': $('#password').val(),
                'code': $('#id_code').val(),
                // 也可以导入模块来进行提交
                'csrfmiddlewaretoken': '{{ csrf_token }}'
            },
            success: function (args) {
                if (args.code == 1000) {
                    // 跳转到首页
                    window.location.href = args.url;
                } else {
                    // 渲染错误信息
                    $('#error').text(args.msg)
                }

            }
        })
    })
</script>

搭建bbs首页

导航条根据用户是否登录展示不同的内容

admin后台管理

"""
django给你提供了一个可视化界面用来让你方便的对你的模型表
进行数据的增删改查操作

如果你想要使用admin后台管理操作模型表
你需要先注册你的模型表告诉admin你需要操作那些表

去你的应用下的admin.py中注册你的模型表
"""
# admin.py
from django.contrib import admin
from app01 import models

# Register your models here.
admin.site.register(models.UserInfo)
admin.site.register(models.Blog)
admin.site.register(models.Category)
admin.site.register(models.Tag)
admin.site.register(models.Article2Tag)
admin.site.register(models.Article)
admin.site.register(models.UpAndDown)
admin.site.register(models.Comment)


# model.py
from django.db import models
from django.contrib.auth.models import AbstractUser


class UserInfo(AbstractUser):
    phone = models.BigIntegerField(verbose_name='手机号', null=True)
    # 头像
    avatar = models.FileField(verbose_name='用户头像', upload_to='avatar/', default='avatar/default.ico')
    """
    给avatar字段传文件对象 该文件会自动存储到avatar文件下 然后avatar字段值只保存文件路径avatar/default.ico
    """
    create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)

    blog = models.OneToOneField(to='Blog', null=True)  # bull=true测试需要方便测试数据

    class Meta:
        verbose_name_plural = '用户表'  # 这个是修改admin后台管理默认的表名
        # verbose_name = '用户表'  # 用这个也是修改admin后台管理默认的表名,不过设置这个会在后面添加一个s

class Blog(models.Model):
    site_name = models.CharField(verbose_name='站点名称', max_length=32)
    site_title = models.CharField(verbose_name='站点标题', max_length=32)
    # 简单模拟 带你认识样式内部原理的操作
    site_theme = models.CharField(verbose_name='站点样式', max_length=64)  # 存css/js的文件路径


class Category(models.Model):
    name = models.CharField(verbose_name='文章分类', max_length=32)
    blog = models.ForeignKey(to='Blog', null=True)  # bull=true测试需要方便测试数据


class Tag(models.Model):
    name = models.CharField(verbose_name='文章标签', max_length=32)
    blog = models.ForeignKey(to='Blog', null=True)  # bull=true测试需要方便测试数据


class Article2Tag(models.Model):
    article = models.ForeignKey(to='Article')
    tag = models.ForeignKey(to='Tag')


class Article(models.Model):
    title = models.CharField(verbose_name='文章标题', max_length=64)
    desc = models.CharField(verbose_name='文章标题', max_length=255)
    # 文章内容有很多 一般情况下都是使用TextField
    content = models.TextField(verbose_name='文章内容')
    create_time = models.DateField(auto_now_add=True)
    # 数据库字段设计优化
    up_num = models.BigIntegerField(verbose_name='点赞数', default=0)
    down_num = models.BigIntegerField(verbose_name='点踩数', default=0)
    comment_num = models.BigIntegerField(verbose_name='评论数', default=0)

    blog = models.ForeignKey(to='Blog', null=True)  # bull=true测试需要方便测试数据
    category = models.ForeignKey(to='Category', null=True)  # bull=true测试需要方便测试数据
    tags = models.ManyToManyField(to='Tag',
                                  through='Article2Tag',
                                  through_fields=('article', 'tag'))


class UpAndDown(models.Model):
    user = models.ForeignKey(to='UserInfo')
    article = models.ForeignKey(to='Article')
    is_up = models.BooleanField(verbose_name='是否点赞')  # 传布尔值 存0/1


class Comment(models.Model):
    user = models.ForeignKey(to='UserInfo')
    article = models.ForeignKey(to='Article')
    content = models.CharField(verbose_name='评论内容', max_length=255)
    comment_time = models.DateTimeField(auto_now_add=True)
    # 自关联
    parent = models.ForeignKey(to='self', null=True)  # 有些评论就是根评论,没有这个所有的评论都是子评论


# admin会给每一个注册了的模型表自动生成增删改查四条url
http://127.0.0.1:8000/admin/app01/userinfo/  查
http://127.0.0.1:8000/admin/app01/userinfo/add/  增
http://127.0.0.1:8000/admin/app01/userinfo/1/change/  改
http://127.0.0.1:8000/admin/app01/userinfo/1/delete/  删

http://127.0.0.1:8000/admin/app01/blog/  查
http://127.0.0.1:8000/admin/app01/blog/add/  增
http://127.0.0.1:8000/admin/app01/blog/1/change/  改
http://127.0.0.1:8000/admin/app01/blog/1/delete/"""
关键点就在url.py中的第一条自带的url
    url(r'^admin/', admin.site.urls)
奥秘在get_urls函数中
前期我们需要自己手动苦逼的录入数据

在model.py定义了str方法那么在admin后台管理时候选择数据可以看到你想看到的数据名,而不是数据类的名字
"""
# 1.数据绑定尤其需要注意的是用户和个人站点不要忘记绑定了

# 2.标签

# 3.标签和文章
	千万不要把别人的文章绑错了

用户头像展示

"""
1 网站所使用的静态文件默认是放在static文件夹下
2 用户上传的静态文件也应该单独放在某个文件夹下

media配置
	该配置可以让用户上传的所有文件都固定存放在某一个指定的文件夹下
	# 配置用户上传的文件存储位置(setting.py)
    MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
    # 文件名随你自己,没有这个文件夹会自己创建,如果指定了用户头像必须存储在一个目录那么他会递归创建对应目录,
    # 如这次会自动创建avatar文件夹(因为在model指定了头像必须存储在avatar文件夹下)
    
如何开设后端指定文件夹资源
	首先你需要自己去url.py书写一个固定代码
	from django.views.static import serve   # 导入对应模块
	from BBS14 import settings              # 导入对应配置文件
	
	# 暴露后端指定文件夹资源
    url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}),
"""

图片防盗链

# 如何避免别的网站通过本网站的url访问本网站资源
# 简单的防盗链
	我可以做到请求来的时候先看看当前请求是从那个网站过来的
    如果是本网站那么正常访问
    如果是其他网站直接拒绝
    	请求头里面有一个专门记录请求来自于那个网址的参数
        	referer: http://127.0.0.1:8000/

# 如何避免
	1.要么修改请求头referer
    2.直接写爬虫把对方网站的所有资源直接下载到我们自己的服务器上

个人站点

# 全是每个用户都可以有自己的站点样式
# 原理特别简单就是将别人的css样式上传到博客园的后端中并使用Link进行导入并覆盖原来的css
    <link rel="stylesheet" href="/media/css/{{ blog.site_theme }}">


id			content			create-time
1			111				2021-11-11
2			222				2022-12-1
3			33				2010-11-13

"""
django官网提供的一个orm语法

from django.db.models import Count
from django.db.models.functions import TruncMonth

result = models.Article.objects \  
    .annotate(month=TruncMonth('create_time')) \  # 将日期按照月份提取出来并添加到查询列表里面去
    .values('month') \		# 按照月份分组
    .annotate(c=Count('id')) \		# 统计month的个数
    .values('month', 'c')  # 获取里面的值
"""

侧边栏筛选功能

# https://www.cnblogs.com/wupeiqi/tag/Python/				标签
# https://www.cnblogs.com/wupeiqi/category/850028.html		分类
# https://www.cnblogs.com/wupeiqi/archive/2019/10.html		日期

# url设计,我们的方案
# https://www.cnblogs.com/wupeiqi/tag/1/				标签,后面参数为主键值
# https://www.cnblogs.com/wupeiqi/category/1			分类,后面参数为主键值
# https://www.cnblogs.com/wupeiqi/archive/2019-10		日期,后面参数为日期

文章详情页

# https://www.cnblogs.com/wupeiqi/p/11647089.html
# url设计
# /username/article/1  # 用户名+article+主键值
# 先验证url是否会被其他url顶替

# 文章详情页和个人站点基本一致 所以用模板继承


# 侧边栏的渲染需要传入数据才能渲染,并且该侧边栏在很多页面都需要使用
	1.哪个地方用就拷贝需要用的代码(不推荐 有点繁琐)
    
    2.将侧边栏制作成inclustion_tag
    """
    步骤:
    	1.在应用下创建一个名字必须叫templatetags文件夹
    	2.在该文件夹内创建一个任意名称的py文件
		3.在该py文件类固定写两行代码
			from django import template
			register = template.Library()
			# 自定义过滤器
			
			# 自定义标签
			
			# 自定义inclusion_tag
    """

文章点赞点踩

"""
浏览器上你看到的花里胡哨的页面,内部都是HTML(前段)代码

那现在我们的文章内容应该写什么 --> html代码


如何拷贝文章
 copy outerhtml
 
1.拷贝文章
2.拷贝点赞点踩
	1.拷贝前段点赞点踩图标  只拷了html
	2.css也要拷贝
		由于有图片防盗链的问题 所以将图片直接下载到本地

课下思考
	前段如何区分用户是点了站还是点了踩
	1.给标签各自绑定一个事件
		不足之处:两个标签对应的代码其实基本一样,仅仅是是否点赞点踩这一个参数不一样而已
	2.二合一
		给两个标签绑定一个事件
		        // 给所哟的action类绑定事件
        $('.action').click(function () {
            alert($(this).hasClass('diggit'))
        })
由于点赞点踩内部有一定的业务逻辑,所以后端单独开设视图函数处理
"""

文章评论

 """
我们先写根评论
再写子评论

点击评论按钮需要将评论框里面的内容清空
根评论有两部渲染方式
	1.DOM临时渲染
	2.页面刷新render渲染


子评论
	点击回复按钮发送了几件事
		1.评论框自动聚焦
		2.将回复按钮所在的那一行评论人的姓名
			拼接成: @username 的格式
		3.评论框内部自动换行

根评论和子评论都是点击一个按钮朝后端提交数据的
根评论和子评论的区别:
	parent_id
"""

后台管理

# 所有的文件夹内部都可以再根据个功能的细化 再细化成多个文件夹
	templates文件夹
    	backend文件夹
        应用1文件夹
        应用2文件夹

添加文章

有两个需要注意的问题
	1.文章的简介
    	不能直接切取
        	应该先想办法获取到当前页面的文本内容之后截取150个文本字符
    
    2.XSS攻击
    	针对支持用户直接编写html代码的网站
        针对用户直接书写的script标签 我们需要处理
        	1.注释标签内部的内容
            2.直接将script删除

如何解决?
	我们自己解决
    	针对1 后端通过正则表达式筛选
        针对2 首先需要确定及获取script标签
    这两部有一个模块解决这个问题
    	beautifulsoup模块		简称:		bs4模块
            专门用来帮你处理html页面内容的
            该模块主要用于爬虫程序


        content = request.POST.get('content')
        # bs4模块使用
        soup = BeautifulSoup(content,'html.parser')
        tags = soup.find_all()
        # 获取所有的标签
        for tag in tags:
            # print(tag.name)  # 获取到所有的标签名
            # 针对script标签 直接删除
            if tag.name == 'script':
                # 删除标签
                tag.decompose()

        # 我们还缺一个文章简介
        # 1 先加单暴力的直接切去content 150个字符
        # desc = str(soup)[:150]
        # 2 截取文本150个
        desc = soup.text[:150]
"""
当你发现一个数据处理起来不是很方便的时候
可以考虑百度搜索有没有现成的模块帮你完成响应的功能
"""

kindeditor富文本编辑器

# 编辑器的种类有很多,你可以自己去网上去搜索

编辑器如何上传图片处理

# 别人写好了接口 但是接口不是你自己的 你需要去手动去修改
# 在使用别人的框架或者模块的时候 出现了问题不要慌 看看文档可能会有对应的处理方法

修改用户头像

@login_required
def set_avatar(request):
    if request.method == 'POST':
        file_obj = request.FILES.get('avatar')
        # models.UserInfo.objects.filter(pk=request.user.pk).update(avatar=file_obj)  # 直接这样不会自动加avatar前缀
        # 1. 自己手动加前缀
        # 2. 换一种更新方式
        user_obj = request.user
        user_obj.avatar = file_obj
        user_obj.save()
        return redirect('/home/')
    return render(request, 'set_avatar.html', locals())

BBS项目总结

"""
在开发任意web项目的时候 其实到了后期需要写的代码会越来越少
都是用已经写好的url填写到a标签href属性完成跳转即可
"""
主要功能总结
	表设计 开发流程(粗糙流程,还可以细化)
    注册功能
    	forms组件的使用
        头像动态展示
        错误信息提示
    登录功能
    	图片验证码
        滑动验证码
        	极验科技
    首页展示
    	media配置
        主动暴露任意资源接口
    个人站点展示
    	侧边栏展示
        侧边栏筛选
        侧边栏inclusion_tag
    文章详情页
    	点赞点踩
        评论
    后台管理
"""
针对bbs需要你掌握每一个功能的书写思路 内部逻辑
之后再去敲代码熟悉 找感觉
"""

以下django版本都是使用的4版本书写

web开发模式

# 前后端混合开发(前后端不分离)

api接口

通过网络,规定了前后端信息交互规则的url链接,也就是前后台信息交互的媒介

postman的使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xJvrRJZV-1661314539653)(mdimage/image-20220306142015857.png)]

Restful规范

REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征性状态转移)。它首次出现在2000年RoyFielding的博士论文中

RESTful是一种定义Web API接口的设计风格,尤其适用于前后端分离的应用模式中

这种风格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接口,所以在定义接口时,客户端访问的URL路径就表示这种要操作的数据资源

事实上,我们可以使用任何一个框架都可以实现符合restful规范的API接口

一、协议

API与用户的通信协议,总是使用HTTPs协议。(保证数据的安全性)

二、域名

应该尽量将API部署在专用域名之下。

https://api.example.com

如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。

https://example.org/api/
// 看到api字眼,就代表该请求url链接是完成前后台数据交互的
// 第二中写法
https://api.example.org

三、版本(Versioning)

应该将API的版本号放入URL。

https://api.example.com/v1/
https://api.example.com/v2/
// 注:url链接中的v1、v2就是不同数据版本的体现(只有在一种数据资源有多版本情况下)
// 这样做的原因是因为有些还在使用老版本,所以老版本的接口不能直接删除,这样对接口进行多版本管理

另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。

四、路径(Endpoint)

路径又称"终点"(endpoint),表示API的具体网址。

在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。

举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。

  • https://api.example.com/v1/zoos
  • https://api.example.com/v1/animals
  • https://api.example.com/v1/employees

注:一般提倡用资源的复数形式,在url链接中尽量不要出现操作资源的动词:错误示范https://api.baidu.com/delete-user

特殊的接口可以出现动词,因为这些接口一般没有一个明确的资源,或者动词就是接口的核心含义

  • https://api.baidu.com/place/search

操作资源一般都会涉及到增删改查,我们提供请求方式来表示增删改查动作

  • https://api.baidu.com/books - get请求:获取所有书
  • https://api.baidu.com/books/1 - get请求:获取主键为1的书
  • https://api.baidu.com/books - post请求:新增一本书
  • https://api.baidu.com/books/1 - put请求:整体修改主键为1的书
  • https://api.baidu.com/books/1 - patch请求:局部修改主键为1的书
  • https://api.baidu.com/books/1 - delete请求:删除主键为1的书

五、HTTP动词

对于资源的具体操作类型,由HTTP动词表示。

常用的HTTP动词有下面五个(括号里是对应的SQL命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

还有两个不常用的HTTP动词。

  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

下面是一些例子。

  • GET /zoos:列出所有动物园
  • POST /zoos:新建一个动物园
  • GET /zoos/ID:获取某个指定动物园的信息
  • PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
  • PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
  • DELETE /zoos/ID:删除某个动物园
  • GET /zoos/ID/animals:列出某个指定动物园的所有动物
  • DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物

六、过滤信息(Filtering)

如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。

下面是一些常见的参数。

  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。

七、状态码(Status Codes)

服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。

  • 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
  • 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
  • 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
  • 204 NO CONTENT - [DELETE]:用户删除数据成功。
  • 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
  • 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
  • 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
  • 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
  • 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
  • 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
  • 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
  • 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

状态码的完全列表参见这里

注意

  • 2开头的状态码表示的是正常响应
  • 3开头的状态码表示的是重定向响应
  • 4开头的状态码表示的是客户端异常
  • 5开头的状态码表示的是服务端异常

八、错误处理(Error handling)

如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。

{
    error: "Invalid API key"
}

九、返回结果

针对不同操作,服务器向用户返回的结果应该符合以下规范。

  • GET /collection:返回资源对象的列表(数组)
  • GET /collection/resource:返回单个资源对象
  • POST /collection:返回新生成的资源对象
  • PUT /collection/resource:返回完整的资源对象
  • PATCH /collection/resource:返回完整的资源对象
  • DELETE /collection/resource:返回一个空文档

十、Hypermedia API

RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。

{"link": {
  "rel":   "collection https://www.example.com/zoos",
  "href":  "https://api.example.com/zoos",
  "title": "List of zoos",
  "type":  "application/vnd.yourformat+json"
}}

上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。

Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。

{
  "current_user_url": "https://api.github.com/user",
  "authorizations_url": "https://api.github.com/authorizations",
  // ...
}

从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。

{
  "message": "Requires authentication",
  "documentation_url": "https://developer.github.com/v3"
}

上面代码表示,服务器给出了提示信息,以及文档的网址。

十一、其他

(1)API的身份认证应该使用OAuth 2.0框架。

(2)服务器返回的数据格式,应该尽量使用JSON,避免使用XML。

drf的安装和简单使用

安装:pip install djangorestframework

# 简单使用
# 1. 在setting.py中注册
INSTALLED_APPS = [
    'rest_framework'
]
# 2. 在model.py中书写表模型
class Book(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    author = models.CharField(max_length=32)
# 3. 新建一个序列化类
# 新建一个py文件放在app中名字为任意名字这里使用的是ser
# 在文件中写对应的类
from rest_framework.serializers import ModelSerializer
from app01.models import Book
class BookModelSerialize(ModelSerializer):
    class Meta:
        model = Book
        fields = "__all__"
# 4.在视图中写视图类
from rest_framework.viewsets import ModelViewSet
from app01.models import Book
from .ser import BookModelSerialize

class BooksViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookModelSerialize
# 5.写路由关系
from django.contrib import admin
from django.urls import path
from app01 import views
from rest_framework.routers import DefaultRouter

router = DefaultRouter()  # 可以处理视图的路由器
router.register('book', views.ModelViewSet)  # 向路由器职工注册视图集
# 将路由器中的所有路由信息追加到django的路由列表中
urlpatterns = [
    path('admin/', admin.site.urls),
]
# print(router.urls)
# 两个列表相加就相当于合并
urlpatterns += router.urls

cbv源码

# ModelViewSet继承View(django原生View)
# APIView继承了View
# 先读View的源码
from django.views import view

# urls.py
path('books1/',view.Book.as_view())  # fbv原来在这个地方应该写个函数内存地址,所以views.Books.as_view()执行完返回的是个函数内存地址
"""
@classonlymethod
def as_view(cls, **initkwargs):
	return view
# 查看classonlymethod源码
class classonlymethod(classmethod):
    def __get__(self, instance, cls=None):
        if instance is not None:
            raise AttributeError("This method is available only on the class, not on instances.")
        return super().__get__(instance, cls)
# 查看源码得知as_view被classonlymethod装饰,点击进去发现classonlymethod继承classmethod类,所以得知as_view为一个静态方法,类直接来调用,会自动把类传入
放一个view的内存地址(View--》as_view--》内层函数)
请求来了,如果路径匹配,会执行,函数内存地址
"""
def view(request, *args, **kwargs):
	# request是当次请求的request
    self = cls(**initkwargs)  # 实例化一个对象,book对象
    if hasattr(self, 'get') and not hasattr(self, 'head'):
        self.head = self.get
        self.setup(request, *args, **kwargs)
        if not hasattr(self, 'request'):
            raise AttributeError(
                "%s instance has no 'request' attribute. Did you override "
                "setup() and forget to call super()?" % cls.__name__)
	return self.dispatch(request, *args, **kwargs)

def setup(self, request, *args, **kwargs):
    """Initialize attributes shared by all view methods."""
    self.request = request
    self.args = args
    self.kwargs = kwargs

def dispatch(self, request, *args, **kwargs):
    # request是当次请求的request self是book对象
    if request.method.lower() in self.http_method_names: # 判断你的请求时候在不在http_method_names里面
        # handler现在是:反射出来的函数就是你写的book类的对应的方法的内存地址,相当于handler = gettar(self,'get')
        handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
    else:
        handler = self.http_method_not_allowed
    return handler(request, *args, **kwargs)  # 执行你自己书写的方法

# 怎么让自己写的cbv只有get请求(重新定义http_method_names参数)
class Books(View):
    http_method_names = ['get',]  # 重写参数
    def get(self, request):
        print(self.request)
        print(self.args)
        print(self.kwargs)
        return HttpResponse('ok')

APIView源码分析

from rest_framework.views import APIView
# urls.py
path('booksapiview/', views.BooksAPIView.as_view()),   # 在这个地方应该写个函数内存地址

# APIView的as_view方法(类的绑定方法)
@classmethod
def as_view(cls, **initkwargs):
    if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
        def force_evaluation():
            raise RuntimeError(
                    'Do not evaluate the `.queryset` attribute directly, '
                    'as the result will be cached and reused between requests. '
                    'Use `.all()` or call `.get_queryset()` instead.'
                )
    	cls.queryset._fetch_all = force_evaluation
        
    
    view = super().as_view(**initkwargs)  # 调用父类的as_view的方法(APIView的父类就是View)也就是上面cbv的View方法
    view.cls = cls
    view.initkwargs = initkwargs
    # 以后所有的请求,都没有csrf认证了,只要继承了APIView,就没有csrf的认证
    return csrf_exempt(view)  # 这个写法和在视图函数前面加@csrf_exempt是一样的
# 去掉csrf另外一种写法(在视图上加csrf_exempt)
path('test/', csrf_exempt(views.test))  # 和在views.py视图函数中的test函数中加入@csrf_exempt
# 请求来了--》路由匹配上--》 view(request)--》调用了self.dispath(),会执行APIView的dispatch
# APIView的dispactch方法
    def dispatch(self, request, *args, **kwargs):
		
        self.args = args
        self.kwargs = kwargs
        # 参数request是当次请求的request,self为BooksAPIView
        # 返回的request是一个新的request对象
        # 重新包装成一个request对象,以后再用就是新的request对象了
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?
        try:
            # 三大认证模块(request是新的request)
            self.initial(request, *args, **kwargs)
			# 在将返回给后端的数据进行包装
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed
			# 响应模块
            response = handler(request, *args, **kwargs)

        except Exception as exc:
            # 异常模块
            response = self.handle_exception(exc)
		# 渲染模块
        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
    
     
    def initialize_request(self, request, *args, **kwargs):
        """
        Returns the initial request object.
        """
        parser_context = self.get_parser_context(request)

        return Request(
            request,
            parsers=self.get_parsers(),
            authenticators=self.get_authenticators(),
            negotiator=self.get_content_negotiator(),
            parser_context=parser_context
        )

# APIView的initial方法
    def initial(self, request, *args, **kwargs):
        """
        Runs anything that needs to occur prior to calling the method handler.
        """
        self.format_kwarg = self.get_format_suffix(**kwargs)

        # Perform content negotiation and store the accepted info on the request
        neg = self.perform_content_negotiation(request)
        request.accepted_renderer, request.accepted_media_type = neg

        # Determine the API version, if versioning is in use.
        version, scheme = self.determine_version(request, *args, **kwargs)  # 做版本控制
        request.version, request.versioning_scheme = version, scheme
        
        
		# 认证组件:校验用户-游客、合法用户、非法用户
        # 游客:代表校验通过直接进入下一步校验(权限校验)
        # 合法用户:代表校验通过、将用户存储在request.user中、再进入下一步校验(权限校验)
        # 非法用户:代表校验失败、抛出异常,返回403权限异常结果
        self.perform_authentication(request)
        # 权限组件:校验用户权限-必须登录、所有用户、登录读写游客只读、自定义用户角色
        # 认证通过:可以进入下一步校验(评率认证)
        # 认证失败:抛出异常,返回403权限异常结果
        self.check_permissions(request)
        # 频率组件:限制视图接口被访问的频率次数-限制的条件(IP、id、唯一键)、频率周期时间(s、m、h)、频率的次数(3/s)
        # 没有达到限次:正常访问接口
        # 达到限次:限制时间内不能访问,限制时间达到后、可以重新访问
        self.check_throttles(request)
from rest_framework import Request
# 只要继承了APIView,视图类中的request对象,都是新的,也就是上面那个request的对象了
# 老的request在新的request._request
class Empty:
    """
    Placeholder for unset attributes.
    Cannot use `None`, as that may be a valid value.
    """
    pass

class Request:
    def __init__(self, request, parsers=None, authenticators=None,
                 negotiator=None, parser_context=None):
        assert isinstance(request, HttpRequest), (
            'The `request` argument must be an instance of '
            '`django.http.HttpRequest`, not `{}.{}`.'
            .format(request.__class__.__module__, request.__class__.__name__)
        )

        self._request = request
        self.parsers = parsers or ()
        self.authenticators = authenticators or ()
        self.negotiator = negotiator or self._default_negotiator()
        self.parser_context = parser_context
        self._data = Empty
        self._files = Empty
        self._full_data = Empty
        self._content_type = Empty
        self._stream = Empty

        if self.parser_context is None:
            self.parser_context = {}
        self.parser_context['request'] = self
        self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET

        force_user = getattr(request, '_force_auth_user', None)
        force_token = getattr(request, '_force_auth_token', None)
        if force_user is not None or force_token is not None:
            forced_auth = ForcedAuthentication(force_user, force_token)
            self.authenticators = (forced_auth,)
    def __getattr__(self, attr):
        """
        If an attribute does not exist on this instance, then we also attempt
        to proxy it to the underlying HttpRequest object.
        """
        try:
            return getattr(self._request, attr)  # 反射_request里面的东西,没有取到在自己的里面获取对应的值
        except AttributeError:
            return self.__getattribute__(attr)
    # 获取post请求的数据request.data
    @property
    def data(self):  # request.data感觉是个属性,其实是一个方法@property修饰
        if not _hasattr(self, '_full_data'):
            self._load_data_and_files()
        return self._full_data  # 这个其实就是一个Empty对象,不过将这个对象当做一个字典使用,post请求不管使用什么编码传来的数据都在request.data里面
	
    # 获取get请求传过来的数据request.query_params
    @property
    def query_params(self):  # 虽然是使用了django的get方法但是为了比较好认识就将这个方法进行了重命名
        """
        More semantically correct name for request.GET.
        """
        return self._request.GET
    
    @property
    def FILES(self):
        # Leave this one alone for backwards compat with Django's request.FILES
        # Different from the other two cases, which are not valid property
        # names on the WSGIRequest class.
        if not _hasattr(self, '_files'):
            self._load_data_and_files()
        return self._files

流程

用户请求–》匹配路由–》执行as_view里面的view函数执行里面的self.dispatch方法,应为是apiview调用所以是APIView执行自己的这个方法–》dispatch重新包装了request方法–》执行了self.initial里面的三个参数认证、权限、频率–》然后执行django的View的dispatch方法调用自己写的函数,然后再将返回的response进行渲染发送给前段,并且这个是有一个异常处理,捕获异常处理

pycharm查看源码类函数设置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U4RWf4z9-1661314539654)(mdimage/image-20220309204045212.png)]

序列化器Serializer

序列化组件介绍

作用:

  1. 序列化,序列化器会把模型对象装换成字典,经过response以后变成json字符串
  2. 反序列化,把客户端发送过来的数据,经过request以后变成字典,序列化器可以把字典转化成模型
  3. 反序列化,完成数据校验功能

简单使用

# 1 写一个序列化的类,继承Serializer
# 2 在列中写要序列化的字段,想序列化哪个字段,就在类中写哪个字段
# 3 在视图类中使用,导入--》实例化得到序列化类的对象,把要序列化的对象传入
# 4 序列化类的对象.data 是一个字典
# 5 把字典返回,如果不使用rest_framework提供的Response,就得使用JsonResopnse,使用的为JsonResponse没有好看的页面,如果使用的为JsonResponse返回那么在setting的INSTALLED_APPS就不需要注册rest_framework
序列化类的字段类型
重点:CharFileld、IntegerField,DateField
序列化组件修改数据
# 1 写一个序列化的类,继承Serializer
# 2 在列中写要序列化的字段,想序列化哪个字段,就在类中写哪个字段,字段的属性(max_lenth......)
# 3 在视图类中使用,导入--》实例化得到序列化类的对象,把要序列化的对象传入,修改的数据传入
	boo_ser = BookSerializer(book, request.data)
    boo_ser = BookSerializer(instance = book, data = request.data)
# 4 数据校验 if book_ser.is_valid()
# 5 如果校验通过,就保存
	book_ser.save()  # 注意不是book.save()
# 6 如果不通过,逻辑自己写
# 7 如果字段的校验不够,可以写钩子函数(局部和全局)
# 8 可以使用字段的author = serializers.CharField(validators=[check_author]),来校验
	#-写一个函数
    #-将函数放到validators里面的列表里面
序列化字段选项

**注意:**serializer不是只能为数据库模型类定义,也可以为非数据库模型类的数据定义。serializer是独立于数据库之外的存在

字段字段构造方式
BooleanFieldBooleanField()
NullBooleanFieldNullBooleanField()
CharFieldCharField(max_length=None, min_length=None, allow_blank=False, trim_whitespace=True)
EmailFieldEmailField(max_length=None, min_length=None, allow_blank=False)
RegexFieldRegexField(regx, max_length=None, min_length=None, allow_blank=False)
SlugFieldSlugField(maxlength=50, min_length=None, allow_blank=False) 正则字段,验证正则模式[a-zA-Z0-9-]+
URLFieldURLField(maxlength=200, min_length=None, allow_blank=False)
UUIDFieldUUUIDField(format=‘hex_verbose’)
IPAddressFieldIPAddressField(protocol=‘both’, unpack_ipv4=False, **options)
IntegerFieldIntegerField(max_value=None, min_value=None)
FloatFieldFloatField(max_value=None, min_value=None)
DecimalFieldDecimalField(max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None)
max_digits:最多位数,decimal_places:小数点位置
DateTimeFieldDateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None, default_timezone=None)
DateFieldDateField(format=api_settings.DATETIME_FORMAT, input_formats=None)
TimeFieldTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None)
DurationFieldDurationField()
ChoiceFieldChoiceField(choices) choices与Django的用法相同
MultipleChoiceFieldMultipleChoiceField(choices)
FileFieldFileField(max_length=None, allow_empty_file=False, use_url=URLOADED_FILES_USE_URL)
ImageFieldImageField(max_length=None, allow_empty_file=False, use_url=URLOADED_FILES_USE_URL)
ListFieldListField(child=, allow_empty=True, max_length=None, min_length=None)
DictFieldDictField(child=)

选项参数

参数名称作用
max_length最大长度
min_length最小长度
allow_blank是否允许为空
trim_whitespace是否截断空白字符
max_value最大值
min_value最小值

通用参数

参数名称说明
read_only表明该字段仅作用于序列化输出,默认False
write_only表明该字段仅用于反序列化输入,默认False
required表明该字段在反序列化时必须输入,默认True
default反序列化时使用的默认值
allow_null表明该字段是否允许传入None,默认False
validators该字段使用的验证器
error_messages包含错误编号与错误信息的字典
label用于HTML展示API页面时,显示的字段名称
help_text用于HTML展示API页面时,显示的字段帮助提示信息
案例
# drf_serializer.urls.py
"""drf_serializer URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.2/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,re_path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path('books/(?P<pk>\d+)', views.BookView.as_view()),  # 原来的url就是用re_path
]

# app01.ser
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from app01 import models


def check_author(data):
    if data.startswith('sb'):
        raise ValidationError('作者名字不能以sb开头')
    return data


class BookSerializer(serializers.Serializer):
    id = serializers.CharField(read_only=True)  # 表示输入的时候可以没有该字段,输出的时候带有该字段的数据
    # id = serializers.CharField()
    name = serializers.CharField(max_length=32, min_length=1)
    # price = serializers.CharField()
    price = serializers.DecimalField(max_digits=5, decimal_places=2, write_only=True)  # 表示输入的时候需要该字段,但是不会输出该字段的结果
    author = serializers.CharField(validators=[check_author])  # 列表中写函数内存地址
    publish = serializers.CharField()

    # 局部钩子
    def validate_price(self, data):  # validate_price_字段名 接收一个参数
        print(data, type(data))
        # 如果价格小于10,就校验不通过
        if data < 10:
            raise ValidationError('价格太低')
        return data

    # 全局钩子
    def validate(self, validate_data):
        author = validate_data.get('author')
        publish = validate_data.get('publish')
        if author == publish:
            raise ValidationError('作者名字和出版社一样')
        return validate_data

    # 修改数据
    def update(self, instance, validated_data):
        """
        :param instance:  是book这个对象
        :param validated_data:  校验后的数据
        :return:
        """
        instance.name = validated_data.get('name')
        instance.price = validated_data.get('price')
        instance.author = validated_data.get('author')
        instance.publish = validated_data.get('publish')
        instance.save()  # book.save() 是django的orm提供的
        return instance

    # 新增数据
    def create(self, validated_data):
        instance = models.Book.objects.create(**validated_data)
        return instance


class BookModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Book  # 对应上models.py中的模型
        # 1 序列化所有字段,用fields来明确字段,__all__表名包含所有字段,也可以写具体那些字段
        # fields  = '__all__'  # 这个必须为fields名,不然会报错
        # 2 只序列化指定的字段
        # fields = ('name','price')  # 这个数据可以是列表,也可以是元组
        # fields = ['name','price']
        # 3 序列化除name之外的字段 注意:exclude和fields不能同时出现,使用exclude可以明确排除掉那些字段
        # exclude = ('name',)
        # 4 指定只读字段,可以通过read_only_fields指明只读字段,即仅用于序列化输出的字段
        fields = '__all__'
        # read_only_fields = ('id',)  # 表示可以查看这个值但是修改不需要传输这个值
        # write_only_fields = ('author',)  # 表示需要传输数据进行修改,但是不会显示这个值  # 但是在3.2版本中弃用了
        # 5 添加额外参数选项 我们可以使用extra_kwargs,并且这个参数可以设置write_only参数,但是id字段不可设置这个参数
        extra_kwargs = {
            'price': {
                'write_only': True
            }
        }
    #

# app01.models
from django.db import models


class Book(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    author = models.CharField(max_length=32)
    publish = models.CharField(max_length=32)
# app01.view
import abc

from django.shortcuts import render, HttpResponse
from rest_framework.views import APIView
from app01 import models
from app01.ser import BookSerializer
from app01.ser import BookModelSerializer
from rest_framework.response import Response
from django.http import JsonResponse
from rest_framework.request import Request
from app01.utils import MyResponse


# Create your views here.

class BookView(APIView):
    # 获取指定数据
    def get(self, request, pk):
        book = models.Book.objects.filter(id=pk).first()
        # 要序列化谁,就把谁传过来
        book_ser = BookSerializer(book)
        # book_ser.data 序列化对象.dada 就是序列化后的字典
        # return Response(book_ser.data)
        return Response(book_ser.data)

    # 修改指定数据
    def put(self, request, pk):
        response_msg = {'status': 100, 'msg': "成功"}
        # 找到这个对象
        book = models.Book.objects.filter(id=pk).first()
        # 得到一个序列化类的对象
        book_ser = BookSerializer(book, request.data)
        # 要数据验证(form表单的验证)
        # if book_ser.is_valid():  # 返回True表示验证通过
        #     book_ser.save()
        #     return Response(book_ser.data)
        # else:
        #     return Response({'status':1001,'msg':'数据校验没通过','dada':book_ser.errors})
        if book_ser.is_valid():  # 返回True表示验证通过
            book_ser.save()
            response_msg['data'] = book_ser.data
            return Response(book_ser.data)
        else:
            response_msg['status'] = 101
            response_msg['msg'] = '数据校验失败'
            response_msg['data'] = book_ser.errors
            return Response({'status': 1001, 'msg': '数据校验没通过', 'dada': book_ser.errors})

    # 删除指定数据
    def delete(self, request, pk):
        response_msg = {'status': 100, 'msg': "删除成功"}
        models.Book.objects.filter(id=pk).delete()
        return Response(response_msg)


class BooksView(APIView):
    # 获取所有书本数据
    def get(self, request):
        response_msg = {'status': 100, 'msg': "成功"}
        book = models.Book.objects.all()
        book_ser = BookSerializer(book, many=True)  # 序列化多条数据,序列化一条就不需要写
        response_msg['data'] = book_ser.data
        return Response(response_msg)

    # 新增数据
    def post(self, request):
        response_msg = {'status': 100, 'msg': "成功"}

        # 修改才有instance,新增没有只有data
        # __init__(self, instance=None, data=empty, **kwargs)
        # book_ser  = BookSerializer(request.data)  # 这样写的话会将request.data传给instance,所以会报错
        book_ser = BookSerializer(data=request.data)
        if book_ser.is_valid():
            book_ser.save()
            response_msg['data'] = book_ser.data
        else:
            response_msg['status'] = 102
            response_msg['msg'] = "数据校验失败"
            response_msg['data'] = book_ser.errors
        return Response(response_msg)


class BooksView2(APIView):
    def get(self, request):
        response = MyResponse()
        book = models.Book.objects.all()
        book_ser = BookModelSerializer(book, many=True)  # 序列化多条数据,序列化一条就不需要写
        response.data = book_ser.data
        return Response(response.get_dict)

    def post(self, request):
        response = MyResponse()
        book_ser = BookModelSerializer(data=request.data)
        if book_ser.is_valid():
            book_ser.save()
            response.data = book_ser.data
        else:
            response.status = 102
            response.msg = "数据校验失败"
            response.data = book_ser.errors
        return Response(response.get_dict)

源码分析
# 序列化多条,需要传many=True
book_ser = BookModelSerializer(book, many=True)  # 序列化多条数据,序列化一条就不需要写
book_one_ser = BookModelSerializer(book)
print(type(book_ser))
<class 'rest_framework.serializers.ListSerializer'>
print(type(book_one_ser))
<class 'app01.ser.BookModelSerializer'>

# 对象的生成 --》先调用类的__new__方法,生成空对象
# 对象=类名(name=lqz),触发类的__init__()
# 类的__new__方法控制对象的生成

def __new__(cls, *args, **kwargs):
    if kwargs.pop('many', False):
        return cls.many_init(*args, **kwargs)
    # 没有传many=True,走下面。正常的对象实例化
    return super().__new__(cls, *args, **kwargs)
# 如果many等于True则将创建一个新的类并将所有的BookModelSerializer类放在ListSerializer类里面

Serializer高级用法

source使用

  1. 可以让字段名不受model限制

    pub_date = serializers.CharField(source='publish_date')
    # model.py
    publish_date = models.DateTimeField(auto_now_add=True)
    
  2. 可以私用.进行跨表查询

    publish = serializers.CharField(source='publish.email')
    # model.py
    class Publish(models.Model):
        name = models.CharField(max_length=32)
        email = models.EmailField()
    
  3. 可以执行方法,获取方法的返回结果

    title = serializers.CharField(source='title_get',max_length=32, min_length=1)
    # 跨表执行方法
    authors = serializers.CharField(source='publish.get_name')
    
    # model.py
    class Book(models.Model):
        title = models.CharField(max_length=32)
        def title_get(self):
            return f'《{self.title}》'
    
    class Publish(models.Model):
        name = models.CharField(max_length=32)
        def get_name(self):
            return self.name
    

SerializerMethodField()的使用

class BookSerializer(serializers.Serializer):
	authors = serializers.SerializerMethodField()  # 它需要有一个配套方法,方法名叫get_字段名

    def get_authors(self, instance):
        # instance 其实就是book对象
        authors = instance.authors.all()  # 这个就是跨表查询,取出所有作者

        return_list = []
        for author in authors:
            return_list.append({'name': author.name, 'age': author.age})
        return return_list

Request源码详细查看

# from rest_framework.request import Request
class Request:
    def __init__(self, request, parsers=None, authenticators=None,
                 negotiator=None, parser_context=None):
        assert isinstance(request, HttpRequest), (
            'The `request` argument must be an instance of '
            '`django.http.HttpRequest`, not `{}.{}`.'
            .format(request.__class__.__module__, request.__class__.__name__)
        )  # 这个断言判断这里获取到的request是django的原生request,如果不是直接报错
		# 二次封装request,将原生request作为drf request对象的_request属性
        self._request = request
        self.parsers = parsers or ()
        self.authenticators = authenticators or ()
        self.negotiator = negotiator or self._default_negotiator()
        self.parser_context = parser_context
        self._data = Empty
        self._files = Empty
        self._full_data = Empty
        self._content_type = Empty
        self._stream = Empty

    @property
    def query_params(self):
        """
        More semantically correct name for request.GET.
        """
        return self._request.GET
    
    
    def __getattr__(self, attr):
        """
        If an attribute does not exist on this instance, then we also attempt
        to proxy it to the underlying HttpRequest object.
        """
        try:
            return getattr(self._request, attr)  # 先在原生里面获取数据
        except AttributeError:
            return self.__getattribute__(attr)

Response源码详细查看

# from rest_framework.response import Response
class Response(SimpleTemplateResponse):
    def __init__(self, data=None,  # 数据,字典
                 status=None,  # 状态码这个继承from django.http import HttpResponseBase,默认状态码为200,status_code参数为最后返回的状态码
                 template_name=None,  # 渲染的模板的名字(自定制模板),不需要了解
                 headers=None,  # 响应头,可以往响应头放东西,就是一个字典
                 exception=False,  
                 content_type=None  # 响应的编码格式,application/json格式,html格式,txt格式
                ):

        super().__init__(None, status=status)

        if isinstance(data, Serializer):
            msg = (
                'You passed a Serializer instance as data, but '
                'probably meant to pass serialized `.data` or '
                '`.error`. representation.'
            )
            raise AssertionError(msg)

        self.data = data
        self.template_name = template_name
        self.exception = exception
        self.content_type = content_type

        if headers:
            for name, value in headers.items():
                self[name] = value
# 常用属性
"""
1. data
传给request对象的序列化后,但尚未render处理的数据
2. status_code
状态码的数字
3. content
经过render处理后的响应数据

eg
return Response(data={'name':'张三'},status=201,headers={'token':'xxx'},cotent_type='json ')
一般status的写法
from rest_framework import status
HTTP_100_CONTINUE = 100
...
尽量使用rest_framework提供的状态码
"""
# 原生django添加请求头值
class BookView(View):
    def get(self,request):
        data = '13212313'
        response = HttpResponse(data)
        response.setdefault('nidayede','nieryede')
        return response

响应格式

浏览器响应成浏览器的格式,postman响应成json格式,通过配置实现的(默认配置)
不管是postman还是浏览器,都返回json格式数据

  • 局部使用:对单个视图类有效,对其他的无效
  • 全局使用:全局的视图类,所有请求,都有效

REST framework提供了一个响应类Response,使用该类构造响应对象时,响应的具体数据内容会被装换(render渲染)成符合前段需求的类型

REST framework提供了Renderer渲染器,用来根据请求头的Accept(接收数据类型声明)来自动转换响应数据对应格式。如果前段请求中未进行Accept声明,则会采用默认方式处理响应数据,我们可以通过配置来修改默认响应格式

可以在rest_framework.settings查找所有的drf默认配置项,直接将这个变量放在django的setting里面就可以了

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [  # 默认响应渲染类
        'rest_framework.renderers.JSONRenderer',  # json渲染器 
        'rest_framework.renderers.BrowsableAPIRenderer',  # 自带的浏览API渲染器,如果是TemplateHTMLRenderer可以设置自定义的模板返回数据
    ]
}
# drf默认的配置文件--》先从项目的setting中找,找不到,采用默认的
from rest_framework.renderers import JSONRenderer
# 局部配置
class BookView(APIView):
    # 局部配置
    renderer_classes = [JSONRenderer]
    # 获取指定数据
    def get(self, request: Request, pk):
        book = models.Book.objects.filter(id=pk).first()
        book_ser = BookSerializer(book)
        response = Response(book_ser.data)

        return response

视图

# 两个视图基类
APIView
GenericAPIView
基于APIview书写
from app01 import models
from rest_framework.response import Response

from app01.ser import BookSerializer
from rest_framework import status
# 基于APIView书写的
from rest_framework.views import APIView
import rest_framework.settings


class BookView(APIView):
    def get(self, request):
        book_list = models.Book.objects.all()
        book_ser = BookSerializer(book_list, many=True)
        return Response(book_ser.data)

    def post(self, request):
        book_ser = BookSerializer(data=request.data)
        if book_ser.is_valid():
            book_ser.save()
            return Response(book_ser.data, status=status.HTTP_201_CREATED)
        else:
            return Response({'status': 101, 'msg': "校验失败"})


class BookDetailView(APIView):
    def get(self, request, pk):
        book = models.Book.objects.filter(pk=pk).first()
        book_ser = BookSerializer(book)
        return Response(book_ser.data)

    def put(self, request, pk):
        book = models.Book.objects.filter(pk=pk).first()
        book_ser = BookSerializer(instance=book, data=request.data)
        if book_ser.is_valid():
            book_ser.save()
            return Response(book_ser.data)
        else:
            return Response({'status': 101, 'msg': "校验失败"})

    def delete(self, request, pk):
        ret = models.Book.objects.filter(pk=pk).delete()
        return Response({'status': 100, 'msg': '删除成功'})
基于GenericAPIView书写
from rest_framework.generics import GenericAPIView


class Book2View(GenericAPIView):
    queryset = models.Book.objects.all()  # 这个要传queryset对象,查询了所有的图书
    # 第二种写法
    # queryset = models.Book.objects  # 可以这样写的原因是因为源码中有判断函数可以判断,如果没有all()他会自己给你带上,具体看get_queryset源码部分
    serializer_class = BookSerializer  # 使用了那个序列化类来序列化这堆数据

    def get(self, request):
        book_list = self.get_queryset()
        book_ser = self.get_serializer(book_list, many=True)
        return Response(book_ser.data)

    def post(self, request):
        book_ser = self.get_serializer(data=request.data)
        if book_ser.is_valid():
            book_ser.save()
            return Response(book_ser.data, status=status.HTTP_201_CREATED)
        else:
            return Response({'status': 101, 'msg': "校验失败"})


class BookDetail2View(GenericAPIView):
    queryset = models.Book.objects
    serializer_class = BookSerializer

    def get(self, request, pk):
        book = self.get_object()
        book_ser = self.get_serializer(book)
        return Response(book_ser.data)

    def put(self, request, pk):
        book = self.get_object()
        book_ser = self.get_serializer(instance=book, data=request.data)
        if book_ser.is_valid():
            book_ser.save()
            return Response(book_ser.data)
        else:
            return Response({'status': 101, 'msg': "校验失败"})

    def delete(self, request, pk):
        ret = self.get_object().delete()
        return Response({'status': 100, 'msg': '删除成功'})
使用rest_framework.mixins自带的类书写这些方法
class Book3View(GenericAPIView, ListModelMixin, CreateModelMixin):
    queryset = models.Book.objects
    serializer_class = BookSerializer

    def get(self, request):
        return self.list(request)

    def post(self, request):
        return self.create(request)


class BookDetail3View(GenericAPIView, RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin):
    queryset = models.Book.objects
    serializer_class = BookSerializer
    def get(self, request, pk):
        return self.retrieve(request, pk)

    def put(self, request, pk):
        return self.update(request, pk)

    def delete(self, request, pk):
        return self.destroy(request, pk)
使用rest_framework.GenericAPIView的视图子类书写这些方法
from rest_framework.generics import CreateAPIView, ListAPIView, UpdateAPIView, RetrieveAPIView, DestroyAPIView, \
    ListCreateAPIView, RetrieveUpdateDestroyAPIView, RetrieveDestroyAPIView, RetrieveUpdateAPIView


class Book4View(ListCreateAPIView):  # 获取所有,新增一个
    queryset = models.Book.objects.all()
    serializer_class = BookSerializer


class BookDetail4View(RetrieveUpdateDestroyAPIView):
    queryset = models.Book.objects
    serializer_class = BookSerializer
使用ModelViewSet编写5个接口
# 使用ModelViewSet编写5个接口
from rest_framework.viewsets import ModelViewSet
class Book5View(ModelViewSet):  # 5个接口都有,但是路由有点问题
    queryset = models.Book.objects
    serializer_class = BookSerializer
快速书写publish类
# 快速书写publish类
from app01.ser import PublishSerializer
from rest_framework.renderers import JSONRenderer


# 使用自带的页面但是需要自己复制代码,
class PublishView(GenericAPIView):
    queryset = models.Publish.objects.all()
    serializer_class = PublishSerializer

    def get(self, request):
        book_list = self.get_queryset()
        book_ser = self.get_serializer(book_list, many=True)
        return Response(book_ser.data)

    def post(self, request):
        book_ser = self.get_serializer(data=request.data)
        if book_ser.is_valid():
            book_ser.save()
            return Response(book_ser.data, status=status.HTTP_201_CREATED)
        else:
            return Response({'status': 101, 'msg': "校验失败"})


class PublishDetailView(GenericAPIView):
    queryset = models.Publish.objects.all()
    serializer_class = PublishSerializer

    def get(self, request, pk):
        book = self.get_object()
        book_ser = self.get_serializer(book)
        return Response(book_ser.data)

    def put(self, request, pk):
        book = self.get_object()
        book_ser = self.get_serializer(instance=book, data=request.data)
        if book_ser.is_valid():
            book_ser.save()
            return Response(book_ser.data)
        else:
            return Response({'status': 101, 'msg': "校验失败"})

    def delete(self, request, pk):
        ret = self.get_object().delete()
        return Response({'status': 100, 'msg': '删除成功'})


# 可以使用继承的方式来书写,但是使用继承的方式去书写就不能用rest_framework的html请求模板
# class PublishView(Book2View):
#     renderer_classes = [JSONRenderer]
#     queryset = models.Publish.objects.all()
#     serializer_class = PublishSerializer
#
# class PublishDetailView(BookDetail2View):
#     renderer_classes = [JSONRenderer]
#     queryset = models.Publish.objects.all()
#     serializer_class = PublishSerializer
其他代码
app01.model.py
from django.db import models


# Create your models here.
class Book(models.Model):
    name = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=5,decimal_places=2)
    publish = models.CharField(max_length=32)



class Publish(models.Model):
    name = models.CharField(max_length=32)
    email = models.CharField(max_length=32)
drf_view.urls.py
urlpatterns = [
    path('admin/', admin.site.urls),

    path('books/', views.BookView.as_view()),
    re_path('books/(?P<pk>\d+)', views.BookDetailView.as_view()),

    path('books2/', views.Book2View.as_view()),
    re_path('books2/(?P<pk>\d+)', views.BookDetail2View.as_view()),

    path('publishs/', views.PublishView.as_view()),
    re_path('publishs/(?P<pk>\d+)', views.PublishDetailView.as_view()),

    path('books3/', views.Book3View.as_view()),
    re_path('books3/(?P<pk>\d+)', views.BookDetail4View.as_view()),

    path('books4/', views.Book4View.as_view()),
    re_path('books4/(?P<pk>\d+)', views.BookDetail4View.as_view()),
    # 使用ModelViewSet编写5个接口
    path('books5/', views.Book5View.as_view(actions={'get':'list','post':'create'})),  # 当路径匹配,又是get请求,会执行Book5View的list方法
    re_path('books5/(?P<pk>\d+)', views.Book5View.as_view(actions={'get':'retrieve','put':'update',"delete":"destroy"})),
]

app01.ser.py
from rest_framework import serializers
from app01 import models


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Book
        fields = '__all__'

class PublishSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Publish
        fields = '__all__'

源码分析ViewSetMixin
# 重写了as_view()
from rest_framework.viewsets import ViewSetMixin

class ViewSetMixin:
    @classonlymethod
    def as_view(cls, actions=None, **initkwargs):
        cls.name = None
        cls.description = None
        cls.suffix = None
        cls.detail = None
        cls.basename = None
        if not actions:
            raise TypeError("The `actions` argument must be provided when "
                            "calling `.as_view()` on a ViewSet. For example "
                            "`.as_view({'get': 'list'})`")

        # sanitize keyword arguments
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r" % (
                    cls.__name__, key))

        if 'name' in initkwargs and 'suffix' in initkwargs:
            raise TypeError("%s() received both `name` and `suffix`, which are "
                            "mutually exclusive arguments." % (cls.__name__))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)

            if 'get' in actions and 'head' not in actions:
                actions['head'] = actions['get']
			
            
            # 核心代码
            self.action_map = actions  # {'get':'list','post':'create'}
			
            for method, action in actions.items():
                """
                method : get
                action :list
                """
                handler = getattr(self, action)
                # handler是获取list的内存地址,如果没有对应的方法则什么都获取不到
                setattr(self, method, handler)
				# 对象.get = list,之后调用get方法就相当于调用get方法
            self.request = request
            self.args = args
            self.kwargs = kwargs

            return self.dispatch(request, *args, **kwargs)

        update_wrapper(view, cls, updated=())

        update_wrapper(view, cls.dispatch, assigned=())


        view.cls = cls
        view.initkwargs = initkwargs
        view.actions = actions
        return csrf_exempt(view)
剖析ViewSetMixin新路由书写
# view.py
from rest_framework.viewsets import ViewSetMixin


class Book6View(ViewSetMixin, APIView):  # 一定要将ViewSetMixin放在APIView前面,因为as_view我们要使用的是ViewSetMixin的as_view所以先放在前面
    def get_all_book(self, request):
        book_list = models.Book.objects.all()
        book_ser = BookSerializer(book_list, many=True)
        return Response(book_ser.data)

    def post_two(self, request):
        book_ser = BookSerializer(data=request.data)
        if book_ser.is_valid():
            book_ser.save()
            return Response(book_ser.data, status=status.HTTP_201_CREATED)
        else:
            return Response({'status': 101, 'msg': "校验失败"})

# url.py
	path('books6/', views.Book6View.as_view(actions={'get':'get_all_book','post':'post_two'})),
ReadOnlyModelViewSet
from rest_framework.viewsets import ReadOnlyModelViewSet
class Book7View(ReadOnlyModelViewSet):  # 2个接口,获取一条和获取所有两个接口,其他和ModelViewSet一样
    queryset = models.Book.objects
    serializer_class = BookSerializer

路由

# 1.在urls.py中配置
    path('publishs/', views.PublishView.as_view()),
    re_path('publishs/(?P<pk>\d+)', views.PublishDetailView.as_view()),
# 2.一旦视图类,继承了ViewSetMixin,路由需要加上一个action参数
    path('books6/', views.Book6View.as_view(actions={'get':'get_all_book','post':'post_two'})),
# 3.继承自视图类,modelViewSet的路由写法(自动生成路由)
	- urls.py
        from django.contrib import admin
        from django.urls import path, re_path
        from app01 import views
        # 1.导入routers模块
        from rest_framework import routers

        # 2.有两个类,实例化得到对象
        # routers.DefaultRouter  和SimpleRouter比生成的路由更多
        # routers.SimpleRouter
        router = routers.DefaultRouter()
        # router = routers.SimpleRouter()
        # 3.注册
        router.register('books', views.BookViewSet)  # prefix:前缀(注意:不要加斜杠), viewset:继承自ModelViewSet视图类,basename:方向解析别名
        # 4.自动生成路由
        """
            SimpleRouter生成的url
            <URLPattern '^books/$' [name='book-list']>
            <URLPattern '^books/(?P<pk>[^/.]+)/$' [name='book-detail']>
            DefaultRouter生成的url
            <URLPattern '^books/$' [name='book-list']>  # 这一条和SimpleRouter生成的url的一样
            <URLPattern '^books\.(?P<format>[a-z0-9]+)/?$' [name='book-list']>  # 可以通过/books.json来访问book的所有json数据 
            <URLPattern '^books/(?P<pk>[^/.]+)/$' [name='book-detail']>  # 这一条也和SimpleRouter生成的url的一样
            <URLPattern '^books/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$' [name='book-detail']>  # 可以通过/books/1.json来访问id为1的json数据
            <URLPattern '^$' [name='api-root']>  # 根,根路径会显示所有可以访问的地址
            <URLPattern '^\.(?P<format>[a-z0-9]+)/?$' [name='api-root']>  # 可以通过/.json来访问可以访问的json对象路由,{"books":"http://127.0.0.1:8000/books.json"}
        """
        urlpatterns = [
            path('admin/', admin.site.urls),
        ]
        urlpatterns += router.urls
    - view.py
        from django.shortcuts import render
        from app01 import models
        from rest_framework.viewsets import ModelViewSet
        from app01 import ser


        # Create your views here.

        class BookViewSet(ModelViewSet):  # 5个接口都有,但是路由有点问题
            queryset = models.Book.objects
            serializer_class = ser.BookSerializer




action的使用

action的作用

  • 为了给继承自ModelViewSet的视图类中定义的函数也添加路由
# 使用
# 位置
from rest_framework.decorators import action  # 装饰器


class BookViewSet(ModelViewSet):  # 5个接口都有,但是路由有点问题
    queryset = models.Book.objects.all()
    serializer_class = ser.BookSerializer
    """
    action是一个装饰器
    methods:是一个列表,列表中放请求方式,为小写,虽然大写不影响,但是建议小写
    ^books/get_2/$ [name='book-get-2'] 当向这个地址发送get请求,会执行下面的函数
    detail:是一个bool类型
    当detail为True的时候
    ^books/(?P<pk>[^/.]+)/get_2/$ [name='book-get-2'],需要加pk参数
    """
    @action(methods=['get','post'], detail=True)  # 在源码中可以看到如果写成大写也会装换成小写
    def get_2(self, request,pk):
        book = self.get_queryset()[:2]  # 从0开始截取2条,这条数据要求queryset对象中写all()
        ser = self.get_serializer(book, many=True)
        return Response(ser.data)
# 总结:detail为false的时候不带pk,为true的时候带pk,可以通过这个参数来进行分页

认证

认证的写法
"""
认证的实现
	1. 写一个类,继承BaseAuthentication(位置:from rest_framework.authentication import BaseAuthentication)重写authenticate方法,认证的逻辑写在里面,认证通过,返回两个值,一个值最终给了Request对象的user,认证失败,抛异常:APIException或者AuthenticationFailed
	2. 全局使用(setting中配置),局部使用(在视图中配置)
"""
认证的源码分析
# APIView ---》 dispatch方法--》self.initial(request, *args, **kwargs)----》有认证,权限,频率
# 只读认证源码:self.perform_authentication(request)
    def perform_authentication(self, request):
		# 需要去drf的Request对象中找user的属性或者(方法)
        request.user

class APIView(View):
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES  # 就是在setting中配置的认证类的列表
    def dispatch(self, request, *args, **kwargs):
        request = self.initialize_request(request, *args, **kwargs) # 在这里传输了authenticator这个值


    def initialize_request(self, request, *args, **kwargs):

        parser_context = self.get_parser_context(request)

        return Request(
            request,
            parsers=self.get_parsers(),
            authenticators=self.get_authenticators(),  # 调用了这个方法传输给了request
            negotiator=self.get_content_negotiator(),
            parser_context=parser_context
        )
    def get_authenticators(self):
		# 这个列表中是一堆实例化之后的对象,是在视图类中配置的对象,所有Request的_authenticate的self._authenticator获取的就是一堆对象
        return [auth() for auth in self.authentication_classes]  # 将authentication_classes里面的值进行运行

class Request:
    @property
    def user(self):
        if not hasattr(self, '_user'):  # 判断是否有用户
            with wrap_attributeerrors():
                # 没用户,认证出用户
                self._authenticate()
        # 有用户,直接返回用户
        return self._user


    def _authenticate(self):
        def __init__(self, request, parsers=None, authenticators=None,
                 negotiator=None, parser_context=None):
            
            self.authenticators = authenticators or ()  # 这个就是你在视图函数中配置的一个个认证类(authentication_classes = [认证类1,认证类2]),对象的列表
            
        # 遍历拿到一个个认证器,进行认证,
        for authenticator in self.authenticators:
            try:
                # 认证器(对象)调用认证方法authenticate(认证类对象self, request请求对象)
                # 返回值:登录的用户名与认证的信息组成的tuple
                # 该方法被try包裹,代表该方法会抛异常,抛异常就代表认证失败
                user_auth_tuple = authenticator.authenticate(self)  # 这里执行了对象的authenticate方法也就是为什么写认证器要书写这个方法
            except exceptions.APIException:
                self._not_authenticated()
                raise

			# 返回值的处理
            if user_auth_tuple is not None:
                self._authenticator = authenticator  # 这个值是APIView传输给他的
                # 如果有返回值,就将登录用户与登录认证分别保存到request.user、request.auth
                self.user, self.auth = user_auth_tuple
                return
		# 如果返回值user_auth_tuple为空,代表认证通过,但是没有登录用户与登录认证信息,代表游客
        self._not_authenticated()
认证组件的使用
app01.app_auth.py

写一个认证类

from rest_framework.authentication import BaseAuthentication
from rest_framework.request import Request
from rest_framework.exceptions import AuthenticationFailed
from app01 import models


class MyAuthenctication(BaseAuthentication):
    def authenticate(self, request: Request):
        # 认证逻辑,如果认证通过返回两个值,如果认证失败,抛出AuthenticationFailed异常
        token = request.GET.get('token')
        if token:
            user_token = models.UserToken.objects.filter(token=token).first()
            # 认证通过
            if user_token:
                return user_token.user, token
            else:
                raise AuthenticationFailed('认证失败')
        raise AuthenticationFailed('请求地址中需要携带token')
drf_router_auth.setting.py
# 这个是定义视图函数的全局验证器
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "app01.app_auth.MyAuthenctication",
    ]
}
app01.views.py
from django.shortcuts import render
from app01 import models
from rest_framework.viewsets import ModelViewSet
from app01 import ser

from rest_framework.authentication import SessionAuthentication
from rest_framework.authentication import BasicAuthentication
# Create your views here.
from rest_framework.decorators import action  # 装饰器

from app01 import app_auth
from rest_framework.views import APIView
from rest_framework.response import Response
import uuid


class BookViewSet(ModelViewSet):  # 5个接口都有,但是路由有点问题
    # authentication_classes = [app_auth.MyAuthenctication]  # 局部验证器
    queryset = models.Book.objects.all()
    serializer_class = ser.BookSerializer
    """
    action是一个装饰器
    methods:是一个列表,列表中放请求方式,为小写,虽然大写不影响,但是建议小写
    ^books/get_2/$ [name='book-get-2'] 当向这个地址发送get请求,会执行下面的函数
    detail:是一个bool类型
    当detail为True的时候
    ^books/(?P<pk>[^/.]+)/get_2/$ [name='book-get-2'],需要加pk参数
    总结:detail为false的时候不带pk,为true的时候带pk,可以通过这个参数来进行分页
    # """

    @action(methods=['get', 'post'], detail=True)  # 在源码中可以看到如果写成大写也会装换成小写
    def get_2(self, request, pk):
        """
        user_auth_tuple = authenticator.authenticate(self)
        self.user, self.auth = user_auth_tuple  # 所以第二个值也是我们传输给,为token值
        """
        # 书写认证之后可以通过request.对象.属性来直接获取数据库查询的数据
        print(request.user.username)
        print(request.auth)
        book = self.get_queryset()[:2]  # 从0开始截取2条,这条数据要求queryset对象中写all()
        ser = self.get_serializer(book, many=True)
        return Response(ser.data)


class TestView(APIView):
    def get(self, request):
        print(request.user)
        return Response({'msg': '我是测试'})


class LoginView(APIView):
    authentication_classes = []  # 当定义全局验证器的时候,可以通过重写这个变量来需要全局验证器的选择

    def post(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        user = models.User.objects.filter(username=username, password=password).first()
        if user:
            # 登录成功,生成一个随机字符串
            token = uuid.uuid4()

            # models.UserToken.objects.create(token=token,user=user)  # 用户它每次登录都会记录一条,如果有记录的话就不好处理
            models.UserToken.objects.update_or_create(defaults={'token': token},
                                                      user=user)  # 使用user这个值查找,找到了就使用defaults里面的值更新,没有找到就用这两个值进行创建
            return Response({'status': 100, 'msg': '登录成功', 'token': token})
        return Response({'status': 101, 'msg': '用户名或密码错误'})

drf_router_auth.urls.py
from django.contrib import admin
from django.urls import path
from rest_framework import routers
from app01 import views
# 1.导入routers模块
from rest_framework import routers

# 2.有两个类,实例化得到对象
# routers.DefaultRouter  和SimpleRouter比生成的路由更多
# routers.SimpleRouter
router = routers.DefaultRouter()
# router = routers.SimpleRouter()
# 3.注册
router.register('books', views.BookViewSet)  # prefix:前缀(注意:不要加斜杠), viewset:继承自ModelViewSet视图类,basename:方向解析别名
# 4.自动生成路由
"""
    SimpleRouter生成的url
    <URLPattern '^books/$' [name='book-list']>
    <URLPattern '^books/(?P<pk>[^/.]+)/$' [name='book-detail']>
    DefaultRouter生成的url
    <URLPattern '^books/$' [name='book-list']>  # 这一条和SimpleRouter生成的url的一样
    <URLPattern '^books\.(?P<format>[a-z0-9]+)/?$' [name='book-list']>  # 可以通过/books.json来访问book的所有json数据 
    <URLPattern '^books/(?P<pk>[^/.]+)/$' [name='book-detail']>  # 这一条也和SimpleRouter生成的url的一样
    <URLPattern '^books/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$' [name='book-detail']>  # 可以通过/books/1.json来访问id为1的json数据
    <URLPattern '^$' [name='api-root']>  # 根,根路径会显示所有可以访问的地址
    <URLPattern '^\.(?P<format>[a-z0-9]+)/?$' [name='api-root']>  # 可以通过/.json来访问可以访问的json对象路由,{"books":"http://127.0.0.1:8000/books.json"}
"""
urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.LoginView.as_view()),
    path('test/', views.TestView.as_view()),
]
urlpatterns += router.urls

app01.ser.py
from rest_framework.serializers import ModelSerializer
from app01 import models


class BookSerializer(ModelSerializer):
    class Meta:
        model = models.Book
        fields = '__all__'


class UserSerializer(ModelSerializer):
    class Meta:
        model = models.User
        fields = '__all__'
app01.models.py
from django.db import models


# Create your models here.

class Book(models.Model):
    name = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=5, decimal_places=2, default=10)


class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)


class UserToken(models.Model):
    token = models.CharField(max_length=64)
    user = models.OneToOneField(to=User, on_delete=models.CASCADE)  # 一对一联到User表

权限

权限的源码分析
# APIView ---》 dispatch方法--》self.initial(request, *args, **kwargs)----》有认证,权限,频率
# 只读权限源码:self.check_permissions(request)
# 因为权限类是在认证类之后的,所以可以拿到request.user对象
class APIView(View):
    def check_permissions(self, request):
        """
        Check if the request should be permitted.
        Raises an appropriate exception if the request is not permitted.
        """
        # 便利权限对象列表得到一个个权限对象(权限器),进行权限认证
        for permission in self.get_permissions():  # 权限类的对象,放到列表中
            # 权限类一定有一个has_permission权限方法,用来做权限认证的
            # 参数:权限对象self、请求对象request、视图类对象
            # 返回值:有权限返回True、无权限返回False 
            if not permission.has_permission(request, self):  # 执行权限类的has_permission方法
                self.permission_denied(
                    request,
                    message=getattr(permission, 'message', None),
                    code=getattr(permission, 'code', None)
                )
权限组件的使用
app01.models.py
from django.db import models


# Create your models here.
class Book(models.Model):
    name = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    publish = models.CharField(max_length=32)


class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)
    user_type = models.IntegerField(choices=((1, '超级用户'), (2, '普通用户'), (3, '二笔用户')))


class UserToken(models.Model):
    token = models.CharField(max_length=64)
    user = models.OneToOneField(to=User,on_delete=models.CASCADE)  # 一对一联到User表
app01.ser.py
from rest_framework import serializers
from app01 import models


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Book
        fields = '__all__'

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.User
        fields = '__all__'
app01.app_auth.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.request import Request
from rest_framework.exceptions import AuthenticationFailed
from app01 import models


class MyAuthenctication(BaseAuthentication):
    def authenticate(self, request: Request):
        # 认证逻辑,如果认证通过返回两个值,如果认证失败,抛出AuthenticationFailed异常
        token = request.GET.get('token')
        if token:
            user_token = models.UserToken.objects.filter(token=token).first()
            # 认证通过
            if user_token:
                return user_token.user, token
            else:
                raise AuthenticationFailed('认证失败')
        raise AuthenticationFailed('请求地址中需要携带token')
app01.auth.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.request import Request
from rest_framework.exceptions import AuthenticationFailed
from app01 import models


class MyAuthenctication(BaseAuthentication):
    def authenticate(self, request: Request):
        # 认证逻辑,如果认证通过返回两个值,如果认证失败,抛出AuthenticationFailed异常
        token = request.GET.get('token')
        if token:
            user_token = models.UserToken.objects.filter(token=token).first()
            # 认证通过
            if user_token:
                return user_token.user, token
            else:
                raise AuthenticationFailed('认证失败')
        raise AuthenticationFailed('请求地址中需要携带token')
app01.views.py
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
import uuid
from app01 import models, app_auth, app_power


# Create your views here.


class LoginView(APIView):
    authentication_classes = []  # 当定义全局验证器的时候,可以通过重写这个变量来需要全局验证器的选择
    permission_classes = []
    def post(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        user = models.User.objects.filter(username=username, password=password).first()
        if user:
            # 登录成功,生成一个随机字符串
            token = uuid.uuid4()

            # models.UserToken.objects.create(token=token,user=user)  # 用户它每次登录都会记录一条,如果有记录的话就不好处理
            models.UserToken.objects.update_or_create(defaults={'token': token},
                                                      user=user)  # 使用user这个值查找,找到了就使用defaults里面的值更新,没有找到就用这两个值进行创建
            return Response({'status': 100, 'msg': '登录成功', 'token': token})
        return Response({'status': 101, 'msg': '用户名或密码错误'})


class TestView(APIView):
    authentication_classes = app_auth.MyAuthenctication,
    # permission_classes = app_power.SuperUserPermission,   # 全局配置那么就不用配置了
    def get(self, request):  # 一般写法
        # def get(self, request,*args,**kwargs):  # 标准写法
        return Response('这个是管理员权限测试数据')


class Test2View(APIView):
    authentication_classes = app_auth.MyAuthenctication,
    permission_classes = []  # 权限的局部禁用
    def get(self, request):  # 一般写法
        # def get(self, request,*args,**kwargs):  # 标准写法
        return Response('这是是登录用户权限测试数据')
drf_other.setting.py
REST_FRAMEWORK = {
    # 'DEFAULT_RENDERER_CLASSES': [  # 这个发送给前段支持的类型
    #     'rest_framework.renderers.JSONRenderer',
    #     'rest_framework.renderers.BrowsableAPIRenderer',
    # ],
    # 'DEFAULT_PARSER_CLASSES': [  # 这个是前段发送过来支持的解析类型
    #     'rest_framework.parsers.JSONParser',
    #     'rest_framework.parsers.FormParser',
    #     'rest_framework.parsers.MultiPartParser'  # 这个是文件格式
    # ],
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "app01.app_auth.MyAuthenctication",
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'app01.app_power.SuperUserPermission',
    ],

}
drf_other.urls.py
from django.contrib import admin
from django.urls import path
from app01 import views


urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.LoginView.as_view()),
    path('test/', views.TestView.as_view()),

]
内置权限
# 演示一下内置权限的使用:IsAdminUser
# 1 创建超级管理员
# 2 写一个测试视图类
# 演示内置权限,超级管理员可以查看
class TestView3(APIView):
    authentication_classes = authentication.SessionAuthentication,
    permission_classes = permissions.IsAdminUser,

    def get(self, request):  # 一般写法
        print(request.user.is_staff)
        # def get(self, request,*args,**kwargs):  # 标准写法
        return Response('这是是超级权限测试数据')
# 3 超级用户登录到admin,再访问test3就有权限
# 4 正常的话,普通管理员,没有权限看(判断的是is_staff字段)

频率/限流(Throttling)(本质上就是权限器)

可以对接口访问的频次进行限制,以减轻服务器压力

一般用于付费购买次数,投票等场景使用

内置的频率限制

BaseThrottle是所有类的基类:方法:**def get_ident(self, request)**获取标识,其实就是获取ip,自定义的需要继承它

AnonRateThrottle:未登录用户ip限制,需要配合auth模块用,限制所有匿名未认证用户,使用IP区分用户。使用DEFAULT_THROTTLE_RATES[‘anon’]来设置频次

SimpleRateThrottle:重写此方法,可以实现频率现在,不需要咱们手写上面自定义的逻辑

UserRateThrottle:登录用户频率限制,这个得配合auth模块来用,限制认证用户,使用User id来区分,使用DEFAULT_THROTTLE_RATES[‘user’]来设置频次

ScopedRateThrottle:应用在局部视图上的,限制用户对于每个视图的访问频次,使用ip或userid。

例如:

class ContactListView(APIView):
    throttle_classes = MyThrottle
    ...


class MyThrottle(throttling.SimpleRateThrottle):
    scope = 'contacts'
    def get_cache_key(self, request, view):  # 重写这个方法才能有限制
        return request.META.get('REMOTE_ADDR')  # 返回什么数据就表示对什么数据进行限制



class TwoThrottle(throttling.SimpleRateThrottle):
    scope = 'uploads'
    def get_cache_key(self, request, view):
        return request.META.get('REMOTE_ADDR')



REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [],
    'DEFAULT_THROTTLE_RATES': {
        'contacts': '1000/day',
        'uploads': '20/day',
    }
}

限制为登录用户

**位置:**from rest_framework.throttling import BaseThrottle

# 全局使用 限制未登录用户1分钟访问5次
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '5/m',  # 1分钟5次
    }
}
# view.py
# test4演示未登录用户的全局配置访问频次
class TestView4(APIView):
    authentication_classes = []
    permission_classes = []

    def get(self, request, *args, **kwargs):  # 一般写法
        return Response('我是未登录用户')

# 局部使用
# test5演示未登录用户的局部配置访问频次
class TestView5(APIView):
    authentication_classes = []
    permission_classes = []
    throttle_classes = throttling.AnonRateThrottle,  # 使用频次不要取消掉
    
    def get(self, request, *args, **kwargs):  # 一般写法
        return Response('我是未登录用户')
# setting.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_RATES': {
        'anon': '5/m',  # 1分钟5次
    }
}
限制登录用户的访问频次
# 全局使用 限制未登录用户1分钟访问5次
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        # 'app01.app_thorttles.MyThrottles',
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'

    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '5/m',  # 1分钟5次
        'user': '10/m',  # 1分钟5次
    }
}
# view.py
# test4演示未登录用户的全局配置访问频次
class TestView4(APIView):
    authentication_classes = []
    permission_classes = []

    def get(self, request, *args, **kwargs):  # 一般写法
        return Response('我是未登录用户')

# 局部使用
# test5演示未登录用户的局部配置访问频次
# test6演示登录用户每分钟访问10次,未登录用户每分钟访问5次
class TestView6(APIView):
    authentication_classes = authentication.SessionAuthentication  # 注意:登录就必须使用认证
    permission_classes = []
    throttle_classes = throttling.UserRateThrottle, throttling.AnonRateThrottle  # 使用频次不要取消掉

    def get(self, request, *args, **kwargs):  # 一般写法
        return Response('我是未登录用户,TestView6')

# setting.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_RATES': {
        'anon': '5/m',  # 1分钟5次
        'user': '10/m',  # 1分钟5次
    }
}

限制的是admin的那个登录操作系统

全局使用
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'app01.app_thorttles.MyThrottles',
    ]

}
局部使用
# 在视图类里使用
from app01 import app_thorttles
class LoginView(APIView):
	throttle_classes = app_thorttles.MyThrottles,
    # 第二种写法:throttle_classes = [app_thorttles.MyThrottles,]
自定义频率
# 自定义额逻辑
# 需要写两个方法
  #- 判断是否限次:没有限次可以请求True,限次了不可以请求False
# 一个是def allow_request(self,request,view):
  #- 限次后调用,显示还需等待多长时间才能再访问,返回等待的时间seconds
# 另外一个是 def wait(self)
# 1 取出访问者ip
# 2 判断当前ip不在访问字典里面,添加进去,并且直接返回True,表示第一次访问,在字典里面,继续往下走
# 3 循环判断当前ip的列表,有值,并且当前时间减去列表的最后一个时间大于60s,就把这种数据pop掉,这样列表中只有60秒以内的访问
# 4 判断,当列表小于3,说明一分钟以内访问不足三次,把当前时间插入到列表第一个位置,返回True,顺利通过
# 5 当大于等于3,说明一分钟内访问超过三次,返回False验证失败

class MyThrottles:
    # 定义成类属性
    VISIT_RECORD = {}
    def __init__(self):
        self.history = None
    def allow_request(self,request,view):
        # 1 取出访问者ip
        ip = request.META.get('REMOTE_ADDR')
        ctime = time.time()
        # 2 判断当前ip不在访问字典里面,添加进去,并且直接返回True,表示第一次访问
        if ip not in self.VISIT_RECORD:
            self.VISIT_RECORD[ip] = [ctime,]
            return True
        self.history = self.VISIT_RECORD.get(ip)
        # 3 循环判断当前ip的列表,有值,并且当前时间减去列表的最后一个时间大于60s,把这种数据pop掉,这样列表中只有60s以内访问的数据
        while self.history and ctime - self.history[-1] > 60:
            self.history.pop()
        # 4 判断,当列表小于3,说明一分钟以为访问不足三次,把当前时间插入到列表第一个位置,返回True,顺利通过
        if len(self.history) < 3:
            self.history.insert(0,ctime)
            return True
        else:
            return False
    def wait(self):
        ctime = time.time()
        return 60-(ctime-self.history[-1])
# 第二种写法就是根据继承的方式进行书写

源码
def check_throttles(self, request):
        for throttle in self.get_throttles():
            if not throttle.allow_request(request, self):
                self.throttled(request, throttle.wait())
    def throttled(self, request, wait):
        #抛异常,可以自定义异常,实现错误信息的中文显示
        raise exceptions.Throttled(wait)
class SimpleRateThrottle(BaseThrottle):
    # 咱自己写的放在了全局变量,他的在django的缓存中
    cache = default_cache
    # 获取当前时间,跟咱写的一样
    timer = time.time
    # 做了一个字符串格式化,
    cache_format = 'throttle_%(scope)s_%(ident)s'
    scope = None
    # 从配置文件中取DEFAULT_THROTTLE_RATES,所以咱配置文件中应该配置,否则报错
    THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES

    def __init__(self):
        if not getattr(self, 'rate', None):  # 在反射中找rate,如果没有执行下面的参数
            # 从配置文件中找出scope配置的名字对应的值,比如咱写的‘3/m’,他取出来
            self.rate = self.get_rate()
        #     解析'3/m',解析成 3      m
        # 同一时间访问的最大次数  访问的间隔,单位是秒
        self.num_requests, self.duration = self.parse_rate(self.rate)
    # 这个方法需要重写
    def get_cache_key(self, request, view):
        """
        Should return a unique cache-key which can be used for throttling.
        Must be overridden.

        May return `None` if the request should not be throttled.
        """
        raise NotImplementedError('.get_cache_key() must be overridden')
    
    def get_rate(self):
        if not getattr(self, 'scope', None):
            msg = ("You must set either `.scope` or `.rate` for '%s' throttle" %
                   self.__class__.__name__)
            raise ImproperlyConfigured(msg)

        try:
            # 获取在setting里配置的字典中的之,self.scope是 咱写的luffy
            return self.THROTTLE_RATES[self.scope]
        except KeyError:
            msg = "No default throttle rate set for '%s' scope" % self.scope
            raise ImproperlyConfigured(msg)
    # 解析 3/m这种传参
    def parse_rate(self, rate):
        """
        Given the request rate string, return a two tuple of:
        <allowed number of requests>, <period of time in seconds>
        """
        if rate is None:
            return (None, None)
        num, period = rate.split('/')
        num_requests = int(num)
        # 只取了第一位,也就是 3/mimmmmmmm也是代表一分钟
        duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
        return (num_requests, duration)
    # 逻辑跟咱自定义的相同
    def allow_request(self, request, view):
        """
        Implement the check to see if the request should be throttled.

        On success calls `throttle_success`.
        On failure calls `throttle_failure`.
        """
        if self.rate is None:  # 判断有没有这个参数没有这个参数直接返回为True
            return True

        self.key = self.get_cache_key(request, view)  # 上面学的返回了当前ip地址
        if self.key is None:
            return True

        self.history = self.cache.get(self.key, [])
        self.now = self.timer()

        # Drop any requests from the history which have now passed the
        # throttle duration
        while self.history and self.history[-1] <= self.now - self.duration:
            self.history.pop()
        if len(self.history) >= self.num_requests:
            return self.throttle_failure()  # 返回False
        return self.throttle_success()
    # 成功返回true,并且插入到缓存中
    def throttle_success(self):
        """
        Inserts the current request's timestamp along with the key
        into the cache.
        """
        self.history.insert(0, self.now)
        self.cache.set(self.key, self.history, self.duration)
        return True
    # 失败返回false
    def throttle_failure(self):
        """
        Called when a request to the API has failed due to throttling.
        """
        return False

    def wait(self):
        """
        Returns the recommended next request time in seconds.
        """
        if self.history:
            remaining_duration = self.duration - (self.now - self.history[-1])
        else:
            remaining_duration = self.duration

        available_requests = self.num_requests - len(self.history) + 1
        if available_requests <= 0:
            return None

        return remaining_duration / float(available_requests)

SimpleRateThrottle源码分析

过滤Filtering

对于列表数据可能需要根据字段进行过滤,我们可以通过添加django-fitlter扩展来增强支持

pip install django-filter

在配置文件中添加过滤后端的设置:

INSTALLED_APPS = {
    'django_filters',  # 需要注册应用
}
REST_FRAMEWORK = {

    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend'
    ],
}

在视图中添加filter_fields属性,指定可以过滤的字段

class BookView(rest_framework.generics.ListAPIView):
    queryset = models.Book.objects.all()
    serializer_class = ser.BookSerializer
    filter_fields = ['name',]  # 配置可以按照那个字段来过滤
    filter_backends = [django_filters.filters.OrderingFilter,]  # 局部配置过滤器 
# http://127.0.0.1:8000/books/?name=你的

注意:

  • 如果需要使用至少需要继承GenericAPIView

    使用GenericAPIView这个原因是在里面调用了filter_queryset这个方法,而在DjangoFilterBackend中重写了这个方法

排序(新版本中有问题)

对于列表数据,RESTframework提供了OrderingFilter过滤器来帮助我们快速指明数据按照指定字段进行排序

使用方法:

在类视图中设置filter_backends,使用rest_framework.filters.OrderingFilter过滤器,RESTframework会在请求的查询字符串参数中检查是否包含了ordering参数,如果包含了ordering参数,则按照ordering参数指明的排序字段对数据集进行排序

前段可以传递的ordering参数的可选字段值需要在ordering_fields中指明。

示例:

class BookView(rest_framework.generics.ListAPIView):
    queryset = models.Book.objects.all()
    serializer_class = ser.BookSerializer
    filter_backends = [django_filters.filters.OrderingFilter,]
    ordering_fields = ['name','price','id']

# http://127.0.0.1:8000/books/?ordering=-price
# -price 表示针对price字段进行倒叙排序
# price 表示针对price字段进行升序排序

如果需要在过滤以后再次进行排序,则需要两者结合

class BookView(rest_framework.generics.ListAPIView):
    queryset = models.Book.objects.all()
    serializer_class = ser.BookSerializer
    filter_fields = ['name','price']
    filter_backends = [django_filters.filters.OrderingFilter,]
    ordering_fields = ['name','price','id']

异常处理

注意:

  • 异常处理没有局部配置
app01.app_exception.py
# 统一接口的返回

# 自定义异常方法,替换掉全局
# 写一个方法
# 自定义异常处理的方法
from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework import status


# 自定义异常处理的方法

def my_exception_handler(exc, context):
    response = exception_handler(exc, context)
    # 两种情况,一个是None,drf没有处理
    # 第二种是response对象,django处理了,但是处理的不符合我们的要求
    if response is None:
        if isinstance(exc, ZeroDivisionError):  # 可以通过判断exc的类型来知道出现了什么类型的报错从而进行细分
            return Response(data={'status': 777, 'msg': "函数出现错误" + str(exc)}, status=status.HTTP_400_BAD_REQUEST)
        # status为999定义的是服务器内部错误
        return Response(data={'status': 999, 'msg': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
    # return response
    return Response(data={'status': 888, 'msg': response.data.get('detail')}, status=status.HTTP_200_OK)
drf_others.setting.py
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'app01.app_exception.my_exception_handler',
}

封装response对象

# 以后都用自己封装的
from rest_framework.response import Response


class APIResponse(Response):
    def __init__(self, code=100, msg='成功', data=None, status=None, headers=None,**kwargs):
        dic = {
            'code': code,
            'msg': msg
        }
        if data:
            dic['data'] = data
        dic.update(kwargs)
        super(APIResponse, self).__init__(data=dic, status=status, headers=headers)

        """
        def __init__(self, data=None, status=None,
                 template_name=None, headers=None,
                 exception=False, content_type=None):
        """

自动生成接口文档

安装模块:pip install coreapi

# 1 在路由中配置
from rest_framework.documentation import include_docs_urls


urlpatterns = [
    ...
    path('docx/',include_docs_urls(title='站点页面标题')),
]
# 2 视图类:自动接口文档能生成的是继承子APIView及子类的视图
# 1) 单一方法的视图,可以直接使用类视图的文档字符串(需要给函数书写注释),如
class BookListView(generics.ListAPIView):
    """
    返回所有图书信息
    """
# 2) 包含多个方法的视图,在类视图的文档字符串中,分开方法定义,如:
class BookListCreateView(gennerics.ListCreateAPIView):
    """
    get:
    返回所有图书信息.
    
    post:
    新建图书.
    """
# 3) 对于视图集ViewSet,仍在类视图的文档字符串中封开定义,但是应使用action名称区分,如
from rest_framework import mixins
from rest_framework import viewsets
class BookInfoViewSet(mixins.ListModelMixin,mixins.RetrieveModelMixin,viewsets.GenericViewSet):
    """
    list:
    返回图书列表数据
    retrieve:
    返回图书详情数据
    latest:
    返回最新的图书数据
    read:
    修改图书的阅读量
    """

JWT认证

在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证,我们不需要使用Session认证机制,而使用Json Web Token(本质就是token)认证机制

Json web token(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开发标准((RFC 7519).该token被设置为紧凑且安全的,特别适合于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其他业务逻辑所必须的声明信息,该token也可以直接被用于认证,也可以被加密

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tsUSpIu7-1661314539656)(mdimage/image-20220521200030424.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9JIh9Gkj-1661314539656)(mdimage/image-20220521200050073.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vwPyQAZU-1661314539656)(mdimage/image-20220521200246169.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CiUuHWOD-1661314539656)(mdimage/image-20220523111123491.png)]

JWT的构成

JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload,类似于飞机上承载的物品),第三部分是签证(signature)

header

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法通常直接使用HMACSHA265

完整的头部就像下面这样的JSON:

{
    "typ": "JWT",
    "alg": "HS256"
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分

eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9
payload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标注中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明(建议但不强制使用)

  • iss:jwt签发者
  • sub:jwt所面向的用户
  • aud:接收jwt的一方
  • exp:jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf:定义在上面时间之前,该jwt都是不可以用的
  • iat:jwt的签发时间
  • jti:jwt的唯一身份标识 ,主要用来作为一次性token,从而回避时序攻击

**公共的声明:**公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密

**私有的声明:**私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息

定义一个payload:

{
    "sub": "1234567890",
    "name": "mubai",
    "admin": true
}

然后将其进行base64加密。得到JWT的第二部分

eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIm11YmFpIiwgImFkbWluIjogdHJ1ZX0=
signature

JWT的第三部分是一个签证信息,这个签证信息由三部分组成

  • header(base64后的)
  • payload(base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用,连接组成的字符串,然后通过header的声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret');  // MG0ktClE71Ukgy350RmxsNoFyIxt282ROhJ0KCWyJwo=

将这三部分用 . 连接成一个完整的字符串,构成了最终的jwt

eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIm11YmFpIiwgImFkbWluIjogdHJ1ZX0=.MG0ktClE71Ukgy350RmxsNoFyIxt282ROhJ0KCWyJwo

注意:secret是保存在服务端的,jwt的签发生成也是在服务端的,secret就是用来进行jwt签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去,一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt了

关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成

文档网站:http://getblimp.github.io/django-rest-framework-jwt/

JWT本质原理
jwt本质
  1. jwt分三段式:头,体,签名(head.payload.sign)

  2. 头和体是可逆加密,让服务器可以反解出user对象:签名是不可逆加密,保证整个token的安全性的

  3. 头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法

  4. 头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息

    {
        "company": "公司信息",
        ...
    }
    
  5. 体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间

    {
        "user_id": 1,
        ...
    }
    
  6. 签名中的内容时安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密

    {
        "head": "头的加密字符串",
        "payload": "体的加密字符串",
        "secret_key": "安全码"
    }
    
签发:根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token
  1. 用基本信息存储json字典,采用base64算法加密得到 头字符串
  2. 用关键信息存储json字典,采用base64算法加密得到 体字符串
  3. 用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串

账号密码就能根据User表得到user对象,形成三段字符串用.拼接成token返回给前台

校验:根据客户端带token的请求 返回出user对象
  1. 将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理
  2. 第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且是同一设备来的
  3. 再用 第一段 + 第二段 + 服务安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户
drf项目的jwt认证开发流程(重点)
  1. 用账号密码访问登录接口,登录接口逻辑中调用 签发token算法,得到token,返回给客户端,客户端自己存到cookies中
  2. 校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user镀锡,在视图类中国用request.user就能访问登录的用户

注:

登录接口需要做 认证+权限 两个局部禁用

drf-jwt安装和简单使用

**官网:**https://github.com/jpadilla/django-rest-framework-jwt

安装pip install djangorestframework-jwt

使用:

drf_token.urls.py
# 1 创建超级用户
# python manage.py createsuperuser
# 2 简单使用:配置路由urls.py
from django.contrib import admin
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    path('admin/', admin.site.urls),
    path('books/', views.BookView.as_view()),
    path('login/', obtain_jwt_token),
]
# 3 postman测试
# 向后端接口发送post请求,携带用户名密码,即可看到生成的token

# 4 setting.py中配置认证使用jwt提供jsonwebtoken
# 5 psotman发送访问请求(必须带jwt空格)

drf_token.setting.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'api.apps.ApiConfig',
    'rest_framework',
    'rest_framework_jwt'
]

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = False

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# 配置头像相关路径
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
AUTH_USER_MODEL = 'api.user'  # 在使用django自带的auth用户表的时候一定需要配置这个参数
JWT_AUTH = {
    'JWT_AUTH_HEADER_PREFIX':""
}
api.models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.

class User(AbstractUser):  # 使用django自带的user表作为用户表
    phone =  models.CharField(max_length=11)
    icon = models.ImageField(upload_to='icon')  # ImageField依赖于pillow模块,写了upload_to就要配置media文件夹
api.views.py
from rest_framework.views import APIView
from rest_framework.response import Response

from api.auth import MyToken
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated

# 可以通过认证类:JSONWebTokenAuthentication和权限类IsAuthenticated,来控制用户登录以后才能访问某些接口
# 如果用户不登录也可以访问,只需要把权限类去掉就行
class BookView(APIView):
    # 第一种写法
    # authentication_classes = MyToken,
    # 第二种写法
    # 如果使用了JSONWebTokenSerializer则必须添加permission_classes = IsAuthenticated,如果只有JSONWebTokenSerializer那么用户不登录也能访问
    authentication_classes = JSONWebTokenAuthentication,
    permission_classes = IsAuthenticated,

    def get(self, request):
        return Response('ok')

api.auth.py
from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.settings import api_settings
from django.contrib.auth import get_user_model

import jwt
from django.utils.translation import ugettext as _
from rest_framework import exceptions

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER


class MyToken(BaseAuthentication):
    def authenticate(self, request):
        jwt_value = str(request.META.get('HTTP_AUTHORIZATION'))
        if jwt_value == 'None':
            msg = _("签名不能为空")
            raise exceptions.AuthenticationFailed(msg)
        # 认证
        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            msg = _('签名已过期。')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = _('解码签名错误。')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()
        print(payload)
        user = self.authenticate_credentials(payload)
        print(jwt_value)
        return (user, jwt_value)

    def authenticate_credentials(self, payload):
        """
        Returns an active user that matches the payload's user id and email.
        """
        User = get_user_model()
        username = jwt_get_username_from_payload(payload)

        if not username:
            msg = _('Invalid payload.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            user = User.objects.get_by_natural_key(username)
        except User.DoesNotExist:
            msg = _('Invalid signature.')
            raise exceptions.AuthenticationFailed(msg)

        if not user.is_active:
            msg = _('User account is disabled.')
            raise exceptions.AuthenticationFailed(msg)

        return user

控制jwt登录接口返回的数据格式
  1. 支持多方式登录
# 第一种方案,自己写登录接口
# 第二种写法,用内置,控制登录返回的数据格式
	# -jwt的配置信息中有这个属性
    'JWT_RESPONSE_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_response_payload_handler',
# jwt_response_payload_handler源码
def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'token': token
    }
# 我们可以重写jwt_response_payload_handler使用我们自己的返回格式

api.utils.py
def my_jwt_response_payload_handler(token, user=None, request=None):
    print(user)
    return {
        'token': token,
        'msg': '成功',
        'status': 100,
        'username': user.username
    }

基于角色的权限控制(Django内置auth体系)

# RBAC:是基于角色的访问控制(Role-Based Access Control),公司内部系统
# Django的auth就是内置一套基于RBAC的权限系统
"""
Django
	# 后台的权限控制(公司内部系统,crm,erp,协同平台)
	user 表
	permssion表
	group表
	user_groups表是user和group的中间表
	group_permssions表是group和permssion的中间表
	user_user_permssions表是user和permssion的中间表
	# 前站(主站),需要用三大认证
	可以在创建的时候在给用户设置指定的权限
"""

Django缓存

开发调试(此模式为开发调试使用,实际上不执行任何操作)

settings.py文件配置

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.dummy.DummyCache',  # 缓存后台使用的引擎
        'TIMEOUT': 300,  # 缓存超时时间(默认300秒,None表示永不过期,0表示立即过期)
        'OPTIONS': {
            'MAX_ENTRIES': 300,  # 最大缓存记录的数量(默认300)
            'CULL_FREQUENCY': 3,  # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3)
        }
    }

}

内存缓存(将缓存内容保存至内存区域中)

settings.py文件配置

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',  # 指定缓存使用的引擎
        'LOCATION':'unique-snowflake',  # 写在内存中的变量的唯一值
        'TIMEOUT': 300,  # 缓存超时时间(默认300秒,None表示永不过期,0表示立即过期)
        'OPTIONS': {
            'MAX_ENTRIES': 300,  # 最大缓存记录的数量(默认300)
            'CULL_FREQUENCY': 3,  # 缓存到达最大个数之后,剔除缓存个数的比例,即
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值