No.017-Python-学习之路-Django

一. WEB框架简介

web框架的本质即一个socket服务端,所有的web框架本质就是下面一个socketServer;

import socket

def hand_request(client):
    buf = client.recv(1024)
    print(buf)
    client.send(bytes("HTTP/1.1 200 OK\r\n\r\n", encoding="utf-8"))
    client.send(bytes("<h1 style='background-color:red;'>Hello World", encoding="utf-8"))

def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('localhost', 8000))
    sock.listen(5)

    while True:
        connection, address = sock.accept()
        hand_request(connection)
        connection.close()

if __name__ == '__main__':
    main()

1.1 一个简单的web框架

如下代码中,取自environ中的一写用户反过来信息,然后根据信息进行处理;

from wsgiref.simple_server import make_server

def handle_index():
    return ['<h1>Hello, index!</h1>'.encode("utf-8"), ]

def handle_date():
    return ['<h1>Hello, date!</h1>'.encode("utf-8"), ]

def RunServer(environ, start_response):
    # environ封装所有的客户端发来的所有数据
    # start_response 封装要返回给用户的数据,响应头状态
    start_response('200 OK', [('Content-Type', 'text/html')])

    # 返回给用户的数据
    ##return ['<h1>Hello, web!</h1>'.encode("utf-8"), ]
    current_url = environ['PATH_INFO']
    if current_url == '/index':
        return handle_index()
    elif current_url == "/date":
        return handle_date()
    else:
        return ['<h1>404</h1>'.encode("utf-8"), ]

if __name__ == "__main__":
    # 根据ip,端口创建一个socketServer,并设置回调函数RunServer
    httpd = make_server('', 8000, RunServer)
    print("Serving HTTP on port 8000...")
    # 开始监听端口,当有链接进来时,触发回调函数
    httpd.serve_forever()

wsgi-Web Session Gateway Interface一种协议,

1.1.2 使key与func代替上面的判断

这样比上面好处在于,如果有新的url,只需修改dict即可;

在真正的WEB框架中,确实是这么来处理url的,唯一改进的是使用re将一类url进行分类处理;

from wsgiref.simple_server import make_server

# 若要返回相关的html页面,理论上就使用这一种方式来处理的
def handle_index():
    with open("s2.html", "rb") as f:
        data = f.read()
    return [data, ]
def handle_date():
    with open("s3.html", "rb") as f:
        data = f.read()
    return [data, ]

# 如果有新的请求在这里面添加参数即可,无需修改下面的东西
ULR_DICT = {
    "/index": handle_index,
    "/date": handle_date
}

# 此函数内定义框架内主要的处理逻辑
def RunServer(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    current_url = environ['PATH_INFO']
    func = None
    if current_url in ULR_DICT:
        func = ULR_DICT[current_url]
    if func:
        return func()
    else:
        return ['<h1>404</h1>'.encode("utf-8"), ]

if __name__ == "__main__":
    # 根据ip,端口创建一个socketServer,并设置回调函数RunServer
    httpd = make_server('', 8000, RunServer)
    print("Serving HTTP on port 8000...")
    # 开始监听端口,当有链接进来时,触发回调函数
    httpd.serve_forever()
1.1.3 MVC与MTV

MVC,全名是Model View Controller,是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller),具有耦合性低、重用性高、生命周期成本低等优点;

所有的文件包括py文件,html文件,数据文件等都放置在同一目录下会显得非常的乱,所以一般我们会将他们进行分类存放,即新建文件夹进行存放,方式大约有2种:

  1. MVC类型:Model里放置数据库文件,View里放置模板文件,Controller放置业务处理;
  2. MTV类型:Model里放置数据库文件,Template存放模板文件,View放置业务处理;

Django框架的不同之处在于它拆分的三部分为:Model(模型)、Template(模板)和View(视图),也就是MTV框架。

img

1.2 HTTP协议

超文本传输协议(Hyper Text Transfer Protocol, HTTP)

  1. 工作在应用层,最著名的是用于web浏览器与web服务器之间的双向通信;

版本迭代:

  1. 最广泛使用版本为HTTP1.1 由IETF 1999年6月发布,文档RFC 2616。
  2. HTTP2.0在2015年5月已RFC 7540的方式发布;

如果需要详细了解推荐:<<HTTP权威指南>>

1.2.1 HTTP请求的方法

HTTP1.0中定义了三种请求方法:GETPOSTHEAD方法,在HTTP1.1中新增了6种。

No. 方法 描述
01 GET 请求指定的页面信息,并返回实体主体。
02 HEAD 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头。
03 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。
04 PUT 从客户端向服务器传送的数据取代指定的文档的内容。
05 DELETE 请求服务器删除指定的页面。
06 CONNECT HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。
07 OPTIONS 允许客户端查看服务器的性能。
08 TRACE 回显服务器收到的请求,主要用于测试或诊断。
09 PATCH 是对 PUT 方法的补充,用来对已知资源进行局部更新 。
1.2.2 HTTP状态吗

​ 所有HTTP响应第一行都是状态行,依次为当前HTTP版本号,3为数字的状态吗,描述状态码的短语(可以自定义),彼此由空格分隔;

  • 1xx消息:请求已被服务器接收,还需要等等,在处理;
  • 2xx成功:请求已被服务器接收,理解,并接受;
  • 3xx重定向:需要后续操作才能完成这一请求;
  • 4xx请求错误:请求含有此法错误或者无法被执行;
  • 5xx服务器错误:服务器在处理某个正确请求时发生错误;
1.2.3 URL

HTTP统一资源定位符,使用5个基本元素唯一标识Internet中的某一资源;

5个基本元素分别是:

  1. 传输协议;
  2. [可选]访问资源需要的凭证信息;
  3. 服务器,通常为域名及IP;
  4. 端口,不加使用默认端口;
  5. 路径

URL后的查询方式有两种:

  1. GET传参方式在以上中添加?n=1&n=2
  2. #...,在HTML中的锚点跳转;
1.2.4 请求格式

请求具体格式如下:

image-20210601184530951


GET /user/home/ HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache

请求数据...
1.2.5 响应格式

响应具体格式如下:

image-20210601184258350


HTTP/1.1 200 OK
Date: Sat, 19 Sep 2020 13:13:38 GMT
Server: WSGIServer/0.2 CPython/3.8.0
Content-Type: text/html; charset=utf-8
X-Frame-Options: DENY
Content-Length: 434
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin

响应正文

二. Django

2.1 简单使用

2.1.1 安装django

# 安装django
pip install django

2.1.2 建立project[工程]

安装完成后,会在Python/scripts目录生成两个程序

image-20200915104333932

用来生成相关的project文件

# 生成project文件mysite
django-admin.exe startproject mysite
# 提交相关的表结构到自带的sqlite数据库->默认会生成一些
python manage.py migrate
# 测试运行下project,默认为8000端口
python manage.py runserver
python manage.py runserver 127.0.0.1:8001
# 访问

image-20210601194729113

生成project的目录结构

firstDjango
	- firstDjango ##整个程序的配置
    	- init
    	- asgi.py # Django3.0新出的异步功能
    	- settings.py # Django需要的全局配置文件
    	- urls.py # 设置url入口<url对应关系>
    	- wsgi.py # django不负责socket,这里配置遵循wsgi规范socketserver,推荐uwsgi+nginx
    - db.sqlite3 # 自带的sqlite数据库
    - manage.py # 但凡需要管理Django程序,都是通过manage.py来做的
    	- python manage.py
    	- python manage.py startapp xx
    	- python manage.py makemigrations
    	- python manage.py migrate

通过pycharm 来创建django project

 只需要在file/new project/django即可

image-20210601195334624

如果cmd下建的project,可手动配置相关项目,注意点在与需要这两个环境变量设置

PYTHONUNBUFFERED=1;DJANGO_SETTINGS_MODULE=firstDjango.settings

第一个自定义响应

根目录下新建一个文件夹myFirstApp,新建s1.py

from django.shortcuts import HttpResponse

def say_hello(request):
    return HttpResponse("hello web, Hello Django")

在firstDjango/firstDjango下的urls.py添加映射

from django.contrib import admin
from django.urls import path
from myFirstApp import s1
urlpatterns = [
    path('admin/', admin.site.urls),
    path("index/", s1.say_hello)
]

image-20200915110606293

2.2.3 工程下面的app[ 应用]

一个正常的网站往往包含很多完全不同的功能,比如任意一个正常的网址最起码包含这几项:

myProject
	- myProject
		- 配置
	- 主站app
	- 后台管理app

使用manage.py来建立app/在pycharm中

python manage.py startapp cmdb
python manage.py startapp openstack

这样会在工程的目录下生成响应的APP目录,目录结构如下

cmdb
	- migrations # 数据库表结构操作记录
	- admin.py # Django提供的后台管理
	- apps.py  # 配置当前app
	- models.py # ORM,写指定的雷,通过命令创建数据库结构
	- tests.py # 单元测试
	- views.py	# 业务代码

需要在django中注册app

#方式共两种:
INSTALLED_APPS = [
 	# 方式一,直接写名字即可,这种是能让django知道有这个app
    'app01',
    # 方式二,调用apps中的类,推荐这种,某些时候可以复写这个类
    'app01.apps.App01Config'
]

如下,新增cmdb应用

# firstDjango/firstDjango/urls.py
from django.contrib import admin
from django.urls import path
from cmdb import views as cmdbv
from openstack import views as openstackv
urlpatterns = [
    path('admin/', admin.site.urls),
    path("cmdb/", cmdbv.say_hello),
    path("openstack/", openstackv.say_hello)
cmdb/view.py
from django.shortcuts import HttpResponse

def say_hello(request):
    return HttpResponse("CMDB")

image-20210601204113741

2.2.4 整体django工程流程
  1. 通过pycharm或者django-admin新建工程;

  2. 在工程根目录下新建templates目录用于存放html相关文件;

    # 新建templates后,需要在setting中进行设置:
    TEMPLATES = [
        {
            'BACKEND': 'django.template.backends.django.DjangoTemplates',
            # 将相关目录添加到该位置
            'DIRS': [BASE_DIR / 'templates']
            ,
            'APP_DIRS': True,
            'OPTIONS': {
                'context_processors': [
                    'django.template.context_processors.debug',
                    'django.template.context_processors.request',
                    'django.contrib.auth.context_processors.auth',
                    'django.contrib.messages.context_processors.messages',
                ],
            },
        },
    ]
    
  3. 在工程根目录下新建 static文件夹用于存放js,css等html中的静态文件;

    # 工程根目录下新建static目录
    STATIC_URL = '/static/'
    # 配置静态文件存放目录[css,js文件所在目录]
    STATICFILES_DIRS = (
        [BASE_DIR / 'static']
    )
    
  4. 新建app,并配置业务代码;

    python manage.py startapp BruceApp
    # 配置views
    from django.shortcuts import render
    
    # Create your views here.
    
    def login(request):
        return render(request, 'login.html')
    
  5. 将相关业务程序导入urls并配置映射

    from django.contrib import admin
    from django.urls import path
    from BruceApp import views
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('login/', views.login),
    ]
    
2.2.5 报错处理
  1. CSRF 异常告警,中间件的一个东西,暂时放放...

    image-20200915180647565


    处理方法:

    ​ 将settings.py中的django.middleware.csrf.CsrfViewMiddleware注释掉<暂时这么处理>

    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middlewared.common.CommonMiddleware',
        # 'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]
    
  2. RuntimeError at /login

    image-20210601205639178


    按照提示里面的操作进行处理即可,主要原因是在form中action中是/login/,则在urls必须为login/,form中 比urls中多的前面那个/,其代指本地域名<完整的意思就是跳转到本地的login/这个url地址中>

    还有注意排错的时候,需要将程序手动关闭并重启;

2.2.6 静态文件引入

所有的图片,css,js,plugins等html中会用到的文件均建议放置在此文件夹。

django中setting.py中配置static目录

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/

STATIC_URL = '/static/' # 调用static文件夹们时使用的url,并不是static的文件夹的名字
STATICFILES_DIRS = [ # 指定static文件夹的路径
    BASE_DIR / 'static', # 因为时列表当然可以指定多个
    BASE_DIR / 'static1', 
    BASE_DIR / 'static2',
] 
# 注意在调用的时候只能使用/static/而非使用/static1-2/等,使用/static/时,程序会依次在列表中找资源

建议目录分类

static
	- css
	- js
	- img
	- plugins
2.2.7 简单使用django总结
# 新建django工程
django-admin startproject mydjango

# 新建app
cd mydjango
python manage.py startapp cmdb

# 添加static静态文件目录
## 新增mydjango.static目录
## mydjango.setting中添加如下配置
STATICFILES_DIRS = (
    [BASE_DIR / 'static']
)

# 添加templates目录
## 新增mydjango.templates目录
## mydjango.setting中添加如下配置
TEMPLATES = [
    {
        # 将相关目录添加到该位置
        'DIRS': [BASE_DIR / 'templates']
    },
]

# 定义路由规则
## 在mydjango.urls.py在中新建规则
urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.login),
]

# 定义视图函数
## 在app.views.py下定义,并导入mydjango.urls.py中
request.method # 获取用户请求的类型
## 简单获取数据
request.POST.get('keyname', None) # POST过来的数据,类似与一个字典,使用get获取;
request.GET.get('keyname', None) # GET过来的数据,类似与一个字典,使用get获取;
## 简单回应
return HttpResponse("str")
return render("request", "HTML template lujing ")
return redicrt('url') #这个跳转是301告诉客户端需要跳转,所以天template是不合理也不行的

# 模板渲染
## 在django中存在特殊的模板语言
### 在视图函数中给模板传数据
def func(request):
	return render(request, "index.html", { "error":  "error_infunc", 'error_list': [1, 2, 3]})
###变量替换:
{{ error }}
###对list取值:
{{ list.0 }}
###对dict取值:
{{ dict.key1 }}
###循环语句1####
{% for row in error_list%}
	<li>{{row}}</li>
{% endfor %}
###判断语句1####
{%if age %}
	{%if ange > 16%}
		...
	{% endif%}
{%else%}
		...
{%endif%}	

2.2 Django请求生命周期

在一个请求发送到django页面时,会经过一个基本的处理过程然后返回结果,这个过程即是生命周期;

image-20200924185251972(1)

具体的请求的流程:

  1. 请求发送到wsgi,wsgi封装请求的相关数据(request);
  2. django去匹配路径,根据路径判断要执行哪个函数;
  3. 执行函数,函数处理具体的业务逻辑;
  4. 函数返回响应,django按照HTTP协议的相依的格式进行返回;

发送请求的途径:

  1. 在浏览器的地址栏种输入地址,回车发出get请求;
  2. a标签,发出的是get请求;
  3. form表单提交,默认get,一般使用post方式;

2.3 获取数据

2.3.1 从HTML标签获取

在html的form表单中有多种方式提交数据,form表单中提交的数据由在http协议发向提交的url:

  1. input text标签
  2. input radio标签
  3. input checkbox标签
  4. input file标签
  5. select options标签

有些标签传递是单个值,有些则传递是多个值:

  1. get("key", "default") 方式获取单个值,若获取单个值,则只获取最后一个值;
  2. getlist("key", "default") 方式获取多个值,返回一个列表;
  3. 上传文件通过FILES来获取对象,通过name获取的是名字,接收需要使用file.chunks,具体看例中;
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>上传信息至后端</title>
</head>
<body>
<div>
    <!-- action指定url为本域名的 upload/ 使用http.post方法,支持传文件-->
    <!-- 文件的上传不能使用默认的编码方式,需要使用mutipart/form-data编码方式才行-->
    <form action="/upload/" method="post" enctype="multipart/form-data">
        <p>
            <!-- 在视图函数中用name来取值 -->
            <input type="text" name="name" placeholder="name">
        </p>
        <p>
            <label for="#s1">男:</label><input id="#s1" type="radio" name="sex" value="男">
            <label for="#s2">女:</label><input id="#s2" type="radio" name="sex" value="女">
        </p>
        <p>
            ball:<input type="checkbox" name="favor" value="ball">
            run:<input type="checkbox" name="favor" value="run">
            cpu:<input type="checkbox" name="favor" value="cpu">
        </p>
        <p>
            <select name="city" size="3" multiple="multiple">
                <option value="北京">北京</option>
                <option value="上海">上海</option>
                <option value="广州">广州</option>
            </select>
        </p>
        <p>
            照片上传:<input type="file" name="photo">
        </p>
        <p>
            <input type="submit" value="提交">
            <input type="reset" value="重置">
        </p>
    </form>
</div>
</body>
</html>

2.3.2 从http请求中获取

在视图函数中,所有从client处获取的数据均由request这个形参来接收:

  1. http.post 使用request.POST来接收;
  2. http.get 使用request.GET来接收;
  3. 文件通过request.FILES来接收;
  4. 可以使用request.path_info获取当前在用的url,在template和视图中均可使用;
def upload(request):

    if request.method == "GET":
        return render(request, "upload.html")
    elif request.method == "POST":
        print("name", request.POST.get("name", "无名"))
        print("sex", request.POST.get("sex", "无性别"))
        print("get_favor", request.POST.get("favor", "无爱好"))
        print("getlist_favor", request.POST.getlist("favor", "无爱好们"))
        print("get_city", request.POST.get("city", "无城市"))
        print("getlist_city", request.POST.getlist("city", "无城市们"))
        # 文件提交
        file = request.FILES.get('photo')
        # file是文件对象,存在内存中;
        print("file", file.name)
        path = os.path.join("BruceApp", "receive")
        # 建立文件句柄,然后将文件一点点写入句柄;
        with open(os.path.join(path, file.name), "wb") as f:
            # 获取二进制方式一
            for chunk in file.chunks():
                f.write(chunk)
            # 直接通过文件对象获取
            for i in file():
                f.write(i)
        # 注,可以看到上传的文件类型判断其实是通过文件名来判断的,二进制流无法判断的;
        return render(request, "upload.html")

[17/Sep/2020 13:01:50] "POST /upload/ HTTP/1.1" 200 1240
name alvin
sex 女
get_favor run
getlist_favor ['ball', 'run']
get_city 广州
getlist_city ['上海', '广州']
file 仓库库存.xlsx

2.4 视图

2.4.1 之FBV与CBV

Django同时支持FBV(function base view)与CBV(class base view)这两种对应关系,两种没有好坏之分,建议两种都用;

2.4.1.1 FBV

在此种方式中view中的url对应的是function

视图函数:

def home(request):
    if request.method == "GET":
        pass
    elif request.method == "POST":
        USER_LIST.append({'user': request.POST.get('user1'),
                          'pwd': request.POST.get('pwd'),
                          'mail': request.POST.get('mail')})

    # 通过dict将USER_LIST映射给模板中的user_list
    # 模板中的user_list接到数据后,会执行循环语句遍历并替换
    return render(request, 'home.html', {"user_list": USER_LIST})

urls:

urlpatterns = [
    path('home/', views.home),
]
2.4.1.2 CBV

在此种方式中view中的url对应的是一个类<此类事views类的子类>

支持的HTTP请求方法类型:

['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

用户发来请求前:

  1. 调用as-view(**kwargs)方法,返回view方法;
  2. 在view方法的处理方式:
    • 使用获取的相关参数实例化cls;
    • 使用的setup方法初始化一些属性;
      • self.request = request
    • 调用dispatch方法;

用户发来请求:

  1. 首选django会根据url来找到指定的function或者class;
  2. 然后在根据methon来选择具体的处理方式;
  3. views会使用dispatch来判断请求的类型,从而调用类中不同的函数:

view中的dispatch

def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        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
        return handler(request, *args, **kwargs)

视图类

from django.views import View
class Home(View):

    def dispatch(self, request, *args, **kwargs):
        print("在进请求前,可以做些事情")
        result = super(Home, self).dispatch(request, *args, **kwargs)
        print("当请求处理完毕,可以做些事情")
        # 这里为什么要return呢?
        # 原因一个请求过来,终端要拿到返回,还是需要通过dispatch拿到结果;
        # 如get方法,返回值是返回给dispatch,dispatch再返回给用户;
        return result
    def get(self, request):
        return HttpResponse("get..")

    def post(self, request):
        return HttpResponse("post...")

可以额外做的操作

# 在dispatch中有这个检测:
if request.method.lower() in self.http_method_names:...
# 所以可以修改http_method_names来限制可使用的http method
http_method_names = ['get'']
                     
# 在dispatch中对调用前及调用后的数据进行处理...
2.4.1.3 装饰器

 装饰器常用来在不改变函数代码的情况下,为其添加一定的功能;

装饰器

# 这是一个普通的装饰器
def time_cost(func):
    def inner(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        print("spend time", time.time() - start)
        return res
    return inner

# 但是这个装饰器有个问题,就是其函数相关的所有信息会从func变为inner的信息,比如fun.__name__,fun.__info__等;
# 使用wraps可以将func的相关信息保留并在被装饰完成后重新交给func
from functools import wraps
def time_cost(func):
    @wraps(func)
    def inner(*args, **kwargs):
        '''from inner'''
        start = time.time()
        res = func(*args, **kwargs)
        print("spend time", time.time() - start)
        return res
    return inner

在装饰视图时,我们一般使用这种装饰器结构,让request独立出来,方便调用;

from functools import wraps
def time_cost(func):
    @wraps(func)
    def inner(request, *args, **kwargs):
        start = time.time()
        res = func(reuqest, *args, **kwargs)
        print("spend time", time.time() - start)
        return res
    return inner

@time_cost # FBV方式直接装饰即可
def get(request):
    return HttpResponse("hello decorator...")

装饰fbv如上,装饰cbv如下:

from django.views import View

# 需要导入method_decorator
# 作用是让传入的第一个参数为request,与FBV方式相同,而非self即当前对象;
from django.utils.decorators import method_decorator 

@method_decorator(time_cost, name='post') # 可以写在类上面,并指定被装饰方法
@method_decorator(time_cost, name='dispatch') # 可以写在类上面,并指定被装饰方法
class Home(View):
	
    @method_decorator(time_cost) # 为所有的方法添加装饰器
    def dispatch(self, request, *args, **kwargs):
        result = super(Home, self).dispatch(request, *args, **kwargs)dispatch
        return result
    
    @method_decorator(time_cost) # 为单独的某个方法添加装饰器
    def get(self, request):
        return HttpResponse("get..")
	
    @method_decorator(time_cost) # 为单独的某个方法添加装饰器
    def post(self, request):
        return HttpResponse("post...")

2.4.2 request对象

HTTP协议发送过来的内容由wsgi封装成一个request对象;

属性:

request.method # HTTP的请求方法,get,post,put,delete等
request.GET #URL携带的的参数,不仅仅是get请求,只要是url中?k1=v1&k2=v2都会放到GET中
request.POST #post请求提交的数据
request.path_info #路径信息,不包含ip,port,?参数
request.body # 获取发过来请求体,原始数据经过urlcode编码,发送给后台,后台可通过这个解析出post的相关数据;
request.META # 获取请求头里面的所有信息,返回的是一个字典,变更(小写->大写,添加HTTP_开头)
request.COOKIES # 返回字典,cookies信息
request.session # 可读可写的类字典对象,表示当前的会话,只有当django启用会话支持时才使用;
request.FILES # 长传的文件
# 其他一些不常用的
request.scheme # 获取协议类型,如http or https等
request.user # 返回当前的用户,这个是jango中的一套认证系统,大部分情况下不适用;

方法:

request.get_host() # 根据META里的信息获取请求主机名
request.get_full_path() # 相对于path_info的不同点在于会带上?参数
request.get_signed_cookie() # 获取一个加密的cookie
request.is_secure() # 是否是安全的,即请求是不是通过HTTPS发起的
request.is_ajax() # 判断请求是否是ajax请求;

2.4.3 response对象

from django.shortcuts import HttpResponse, render, redirect, JsonResponse

HttpResponse('str') # 返回字符串,最根本的返回对象;
render(request, 'template_namae', {参数字典}) # 渲染HTML,生成相关字符串;
redirect('url') # 重定向, 注意重定向环路
JsonResponse({'foo': 'bar'}) # 用于前后端分离,发送的是json响应数据;

关于render

def render(request, template_name, context=None, content_type=None, status=None, using=None):
    content = loader.render_to_string(template_name, context, request, using=using)
    return HttpResponse(content, content_type, status)
# 可以看到是使用loader.render_to_string的方法来对渲染html文件的,这里面可以替换使用jinja2的方式来渲染;
# 可以看到传递的除了我自定义的context,同时也传递了request对象,所以默认情况下template就可以理解request对象,即支持{{ request }}这个方法;

关于redirect

# 返回的是3xx状态 + 在head中添加Location:url 即完成跳转;
# 如下用HttpResponse来实现
ret = HttpResponse('', status=301)
ret['Location'] = '/publisher_list/' # 使用setattr来赋值属性;

关于jsonResponse

# 返回时json序列后的str,同时返回content_type设置为json类型
# 如下用HttpResponse来实现
ret = HttpResponse(json.dump({'k1':'v1'})) # 返回字符串,最根本的返回对象;
ret['Content_type'] = "application/json" # 返回类型为json类型;
# 以上等同于以下方式:
JsonResponse({'k1':'v1'}) # 用于前后端分离,发送的是json响应数据;

2.5 模板语言

​ 在django种有两种特殊符号{{}}和{% %},前者表示变量,后者表示逻辑相关的操作;

2.5.1 数据

2.5.1.1 变量

使用{{ var }}来定义,var为变量名,var会在渲染的时候被替换成对应的值;

  1. 值的处理相当于python种print对数据的处理;
  2. 可以使用“.”来取列表种值,取属性等,如{{ list.0 }},{{ obj.name }};
    • list不支持-n取值,不如{{ list.-1 }};
  3. 因为方法后不加括号,所以一般不可以加参数;
  4. 取值优先级:.key > .属性 .方法 > .索引;
2.5.1.2 字典的传递与遍历

与在python中的遍历相似,默认循环取key,可以指定只去value,items返回key:value元组;

字典具有方法,可以使用诸如:

  1. dict.keys
  2. dict.values
  3. dict.items
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>dict</title>
</head>
<body>
    <div>
        <ul>
            {% for key in my_dict %}
                <li>{{ key }}</li>
            {% endfor %}
        </ul>
        <ul>
            {% for value in my_dict.values %}
                <li>{{ value }}</li>
            {% endfor %}
        </ul>
        <ul>
            {% for key, value in my_dict.items %}
                <li>{{ key }}-{{ value }}</li>
            {% endfor %}
        </ul> 
    </div>
</body>
</html>
2.5.1.3 过滤器

有时候传入的值,我们还想做些修改,就会用到过滤器,即filter,过滤器最多只能有1个参数;

#通用语法
{{}}
{{ baby|default: "Alvin"}} # 如果不传或者传空值则使用这个默认值,":"左右不可有空行注意
{{ filesize|filesizeformat }} # 在数字后边添单位,byte
{{ 4|add:2 }} # 一个加法作用,支持数字加法,字符串即列表拼接

{{ Bruce|lower }} # 将字符变为小写;
{{ Bruce|upper }} # 将字符变为大写;
{{ Bruce|title }} # 将首字母大写;

{{ list|length }} # 返回变量长度
{{ list|slice:'-1:-3:-1' }} # 列表切片,注意如果是反向取值的话,步长也要指定,否则取不到;
{{ list|first }} # 取第一个元素
{{ list|last }} # 取第二个元素
{{ list|join:'--' }} # 即list的拼接str方法

{{ longStr|truncatechars:'n'}} # 取n-3个字符+...构成返回用来处理长字符串
{{ longStr|truncatewords:'n'}} # 取n个单词+...构成返回;

{{ now|date:'Y-m-d H:i:s' }} # 格式化datetime对象,不一样地方在于i表示分,s表示s;

{{ a|safe }} # 默认情况下模板传递的html标签会被转义为字符串,如果想使用需要safe关闭转义

格式化时间可以在工程中的setting种做调整

USE_L10N = False
# datetime类型
DATETIME_FORMAT = 'Y-m-d H:i:s'
# date类型
DATE_FORMAT = 'Y-m-d'
# time类型
TIME_FORMAT = 'H:i:s'

2.5.2 语句

2.5.2.1 for循环
{% for item in list %}
	...
{% endfor}

for中的一些特殊值

Variable Description
forloop.counter 当前循环的索引值(从1开始)
forloop.counter0 当前循环的索引值(从0开始)
forloop.revcounter 当前循环的倒序索引值(到1结束)
forloop.revcounter0 当前循环的倒序索引值(到0结束)
forloop.first 当前循环是不是第一次循环(布尔值)
forloop.last 当前循环是不是最后一次循环(布尔值)
forloop.parentloop 本层循环的外层循环

如果循环的内容是空,加一些显示

{% for item in kong %}
	如果不空这个生效
{% empty %}
	如果时空循环显示这个
{% endfor %}
2.5.2.2 条件语句

if语句支持 and 、or、==、>、<、!=、<=、>=、in、not in、is、is not判断;

{% if age > 73 %}
  老年人
{% elif age > 40 %}
  中年人
{% else %}
  年轻人
{% endif %}

注意:本身判断里面时不支持加减乘除的,但是可以使用filter来实现,比如:

{% if age|add:3 > 73 %}
	3年后就成为老年人了
{% endif %}

注意:不支持连续判断,比如 7 > 5 > 1

  1. 在python中表示一个and运算,即7 > 5 and 5 > 1 所以返回true;
  2. 而在js和模板语言中时按照顺序来判断的,7>5为true(1),而true(1) > 1为false,返回false;
2.5.2.3 with定义中间变量
#写法1:为某个值取个别名,在with的范围内都可以使用
{% with business.employees.count as total %}
    {{ total }} employee{{ total|pluralize }}
{% endwith %}

#写法2:为某个值取个别名,在with的范围内都可以使用
{% with total=business.employees.count %}
    {{ total }} employee{{ total|pluralize }}
{% endwith %}
2.5.2.4 csrf_token

这个标签用于跨站请求伪造保护,添加在form标签内;

<form method="post">
     {% csrf_token %}
    <input type="text" name="name">
    <input type="submit" value="submit">
</form>

这个是将form进行了相关的渲染,生成认证用的字符串;

2.5.3 页面组建

对于一个Project中的不同页面,有一部分页面是相同的,可以使用母版与继承来处理;

2.5.3.1 母版

一个包含多个页面的公共部分,定义多个block块,让子页面进行覆盖;

# 在母版中指定
{% block main %} #可以添加名称,用来定义多个block块 
	....替换的内容
{% endblock %}

{% block css %}
	各子类在里面定义自己独有的样式
{% endblock %}

{% block js %} 
	各子类在里面定义自己独有的 js
{% endblock %}
2.5.3.2 继承

其他业务html可以继承母版,并重新复写block块;

注意点:

  1. 在引用时,加不加引号时两种不同的结果;
  2. 要显示的内容需要放置block块中,如果不在block中则无法替换;
  3. 子html中 有时需要自己的样式,这些可以在css处及js处定义css block块及js block块;
# 继承母版内容的方式
# 直接写母版的名称,要加引号;
{% extends ‘mother.html’ %} 

# 不加引号实际为变量,需要在render时传参;
{% extends mother %} 
render(request, "template.name", {'name':'mother.html'})

{% block main %}
	子页面html
{% endblock %}
2.5.3.3 组件

有时候有些内容在很多的页面中都有调用,此时可以使用组件的方式进行操作;

# 把一小段公用的HTML文本写入一个HTML文件,如nav.html中;
# 在需要该组件的模块中:
{% include 'nav.html' %}
2.5.3.4 静态文件

静态文件的引入方式有两种一种是url直接引入,另外一种使用load来引入:

<!--引入方式一:存在问题在变更url名称时,更改的地方过多-->
<link href="/static/css/dashboard.css" rel="stylesheet">

<!--引入方式二:通过语法去setting中获取url并跟相对路径进行拼接-->
{% load static %}
<link href="{% static 'css/dashboard.css' %}" rel="stylesheet">

<!--补充:获取静态文件的url,即setting中的配置的url-->
<!--补充:也可以使用这种方式拼接静态文件的引用-->
<link href="{% get_static_prefixe %}css/dashboard.css" rel="stylesheet">
2.5.3.5 django内默认寻找顺序

django内部有一个逻辑的顺序来寻找static及templates文件夹;

在django内部,如果使用html模板/或者static文件<{%static 'dir'}导入>时, 先查找最外层目录有没有static或templates文件夹,如果没有则会按照app注册顺序,顺序的查找每个app直到找到为止;

所以, 如果默认配置名称为static及templates时,可以不再setting中配置TEMPLATES的DIRS及STATICFILES_DIRS.

注: 基于这样的规则,有时候我们找到的并不是我们想要的, 比如我找的是rbac/templates/login.html, 但是找到的却是它前面一个app-web下的login.html; 可以在templates下面新建文件夹用于区分, 比如在rbac/templates/中新建rbac目录存放login.html, 在调用的时候使用, 如下:

@register.inclusion_tag('rbac/menu.html') # 加rbac的原因, 模板寻找的顺序
def create_menu(request):
    menu = request.session.get(settings.SESSION_MENU, None)
    current_url = request.path_info
    print(menu)
    for row in menu:
        if re.match("^%s$" % row['menu__url'], current_url):
            row['class'] = "active"
    return {'menu_info': menu}

2.5.4 tag自定义

有时候模板中处理的东西并不满足我们的需求,所以需要自定义,总共有三种自定义

2.5.4.1 filter自定义

filter是唯一可以放置在if中的标签,其他两个会报错;

  1. 在app下创建一个名为templatetags的python包,固定写法;

  2. 创建一个python文件,文件名自定义;

  3. 文件内固定写法:

    from django import template
    
    register = template.Library() # register名字不能错
    
  4. 写函数 + 装饰器

    @register.filter # 固定装饰器
    def has_permission(request, palias): # request是装饰的变量, palias是参数
        permissions = request.session.get(settings.SESSION_PER, {})
        if palias in permissions:
            return True
        else:
            return False
    
  5. 调用

    # 在template中调用tag
    # 注意如果报错,记得重启django进程
    {% load rbac %}
    
    # filter_tag可以在if调用
    # 同系统filter_tag一样的调用方式
    {% if request|has_permission:'customer_import' %}
       <a class="btn btn-default" href="{% url 'customer_import' %}">
         <i class="fa fa-file-excel-o" aria-hidden="true"></i> 批量导入
       </a>
    {% endif %}
    
2.5.4.2 simple tag自定义

filter的自定义可以接受的参数太少了,所以也可以使用simple tag,更像一个函数;

前面步骤同filter,装饰器及变量灵活

@register.simple_tag
def plus(*args, **kwargs):
    return "{}_{}".format('_'.join(args), "*".join(kwargs.values()))

调用方式

# 导入
{% load app01_demo %}
# 使用与传参
{% plus "1" "2" "3" k1="d" k2="e" k3="f" %}

但是simple tag时无法在判断等语句种使用的,而filter时可以的;

2.5.4.3 inclusion_tag自定义

simple tag相当于函数,但是在生成html代码段的时候不太方便,这时候就用到inclusion_tag来处理;

前面步骤同filter,

# templatetags/my_inclusion.py种内容
from django import template
register = template.Library()
@register.inclusion_tag('result.html')
def show_results(n):
    n = 1 if n < 1 else int(n)
    data = ["第{}项".format(i) for i in range(1, n+1)]
    return {"data": data}

inclusion_tag中需要传入模板templates/result.html

<ul>
  {% for choice in data %}
    <li>{{ choice }}</li>
  {% endfor %}
</ul>

然后再模板中调用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>inclusion_tag test</title>
</head>
<body>

{% load my_inclusion %}

{% show_results 10 %}
</body>
</html>
2.5.4.4 三种自定义总结
  1. filter只能有一个变量,但是优点时可以写在判断内,而其他两个不行;
  2. simple tag支持不限量变量数,返回的是一个具体的值,但是再渲染html时不太灵便;
  3. inclusion_tag用来生成灵活调整的组件时非常有用,比如分页的页标;

2.6 路由系统

URL配置(URLconf)就像Django所支撑网站的目录,它的吧恩智是URL与要为URL调用的视图函数之间的映射表;我们已这种方式告诉DJangon遇到那个URL时,要执行哪个函数;

  1. 匹配顺序是从上到下的;

  2. 默认情况下url最后加不加“/”是由django来自动添加的,如果不想自动添加,可以settinging中关闭:

    APPEND_SLASH=True
    

2.6.1 对应关系类型

如果确定项目的urls.py位置呢,其实是在setting中设置的:

ROOT_URLCONF = 'FcNetWeb.urls'
2.6.1.1 一对一对应

一个url对应一个function/class

在处理详细显示这个需求时,可以使用get方法进行传参

  1. 在urls中添加两个url,dict和details
urlpatterns = [
    path('dict/', views.Dict.as_view()),
    path('details/', views.Details.as_view()),
]
  1. 在views新建两个视图类
MY_DICT = {
    "1": {"name": "bruce", "age": 18, "favor": "ball"},
    "2": {"name": "alvin", "age": 18, "favor": "run"},
    "3": {"name": "hebburn", "age": 18, "favor": "sing"},
    "4": {"name": "diana", "age": 18, "favor": "sleep"},
}

class Dict(View):
	
    def get(self, request):
        return render(request, "mydict.html", {"my_dict": MY_DICT})

class Details(View):
	
    def get(self, request):
        person_id = request.GET.get("id", None)
        # 获取get传过来的id,根据这个在dict中取值并用details.html渲染传给用户
        if person_id in MY_DICT:
            return render(request, "details.html", {"person": MY_DICT[person_id]})
        else:
            return HttpResponse("<div style='color:red'>无此人数据<div>")
  1. 在templates中新建两个模板

mydict.html

使用每条数据的key+url为其构建一条请求;

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>dict</title>
</head>
<body>
    <div>
        <ul>
            {% for key, value in my_dict.items %}
                <li><a href="/details/?id={{ key }}">{{ value.name }}</a></li>
            {% endfor %}
        </ul>
    </div>
</body>
</html>

details.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ person.name }}的详细信息</title>
</head>
<body>
{{ person.name }}
    <div>
        <ul>
            {% for key, value in person.items %}
                <li>{{ key }}:{{ value }}</li>
            {% endfor %}
        </ul>
    </div>
</body>
</html>
2.6.1.2 正则一对多-分组位置传参

使用正则可以实现一个function/class对应多个url;

在处理详细显示这个需求时,使用正则的实现方式如下:

  1. 在urls中添加两个url,dict和details
from django.contrib import admin
from django.urls import path
from BruceApp import views
from django.urls import re_path

urlpatterns = [
    path('dict/', views.Dict.as_view()),
    # re_path在3.0中用于正则匹配
    re_path(r'^details-(\d+).html/', views.Details.as_view()),
]
  1. 在views中修改Details
class Details(View):

    def get(self, request, person_id):
        # person_id = request.GET.get("id", None)
        print(person_id)
        if person_id in MY_DICT:
            return render(request, "details.html", {"person": MY_DICT[person_id]})
        else:
            return HttpResponse("<div style='color:red'>无此人数据<div>")
  1. 在templates中修改mydict.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>dict</title>
</head>
<body>
    <div>
        <ul>
            {% for key, value in my_dict.items %}
                <li><a href="/details-{{ key }}.html">{{ value.name }}</a></li>
            {% endfor %}
        </ul>
    </div>
</body>
</html>
2.6.1.3 正则一对多-分组键值对传参

 键值对既可以使用**kwargs来接收成一个dict,也可以当做对形参的直接赋值;

以上一对多url中一对多关系根据正则的分组返回一个或多个正则的值,然后由视图函数中的参数获取,还有一种可以在正则中为分组添加key,从而获取键值对;

注:命名分组与顺序分组是不能混用的….

  1. urls中在正常正则中添加?P
urlpatterns = [
    re_path(r'^details-(?P<person_id>\d+).html/', views.Details.as_view()),
]
  1. 在视图类中的方法中添加获取
class Details(View):

    def get(self, request, **kwargs):
        print(kwargs)
        if kwargs.get('person_id') in MY_DICT:
            return render(request, "details.html", {"person": MY_DICT[kwargs.get('person_id')]})
        else:
            return HttpResponse("<div style='color:red'>无此人数据<div>")
2.6.1.4 默认参数

在一对多的正则匹配规则中,如果分组未传递参数,则视图函数可以通过默认值的方式给予形参一个值;

# 应用场景中,比如对分页进行操作;
# urls.py中
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^blog/$', views.page),
    url(r'^blog/page(?P<num>[0-9]+)/$', views.page),
]

# views.py中,可以为num指定默认值
def page(request, num="1"):
    pass
2.6.1.5 在url中额外的参数传递

除了在url中的分组捕获以外, 也可以通过使用字典的方式,额外向视图函数传些参数;

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^blog/(?P<year>[0-9]{4})/$', views.year_archive, {'foo': 'bar'}),
]
2.6.1.6 一些对应技巧
  1. 可以使用一对多正则将多个比较类似的功能合并到一个url中,并利用反射完成取类;

    # url中
    urlpatterns = [
        # 也可以写成(\w+)(\d+).html,这样需要在里加写判断
        url(r'(publisher|book|author)(\d+).html', views.del, name = "del"),
    ]
    # view中
    def del(request, single_url_name, nid):
        # 反射取类
        cls = getattr(models, single_url_name.capitalize())
        # 通过nid差值删除
        cls.object.fiter(id=nid).delete()
        # single_url_name对应我们显示的各个模块的url,可以使用反射
        return redirect(reverse(single_url_name))
    	# 这边我们手动反射,其实在redirect中有针对single_url_name的检测及反射
        # 所以下面这么写,也是OK的,也可以使用model来做
        return redirect(single_url_name)
    # template中
    <a hrep="{% url del 'publisher' nid %}">删除</a>
    

2.6.2 命名name与反向解析

在django中可以在定义url中为其命名(即创建url同时添加参数name="xxx");

作用:可以根据此名称生成自己想要的URL,通过字符串拼接也可以实现,这种更优雅,更灵活些;

<!--templates中:-->
    <ul>
        {% for key, value in my_dict.items %}
            <li><a href="/details-{{ key }}.html">{{ value.name }}</a></li>
            <li><a href="{% url "dt" person_id=key %}">{{ value.name }} + 1</a></li>
        {% endfor %}
    </ul>
<!-- 两种都可以实现跳转,但是第二种方式在url变化时,不需要修改template -->

本质: 本质上看是{name:{url:function}}这种关系,取得三个中的任意一个值即可找到另外两个的值;

2.6.2.1 urls,py中定义name
path('dict/', views.Dict.as_view(), name="n1")
re_path(r'^details-(\d+).html/', views.Details.as_view(), name="n2")
re_path(r'^details-(?P<person_id>\d+).html/', views.Details.as_view(), name="n2")
2.6.2.2 在视图函数反向解析
from django.urls import reverse
# 注在django.shortcuts.py中也导入了reverse所以可以从django.shortcuts导入
from django.shortcuts import reverse
url1 = reverse('n1')
url2 = reverse('n2', args=("1314",))
url3 = reverse('n3', kwargs={"person_id": "1316"})
2.6.2.3 在templates中反向解析
<a href="{% url "n1" %}">{{ value.name }}</a>
<a href="{% url "n1" "1314" %}">{{ value.name }}</a>
<a href="{% url "n1" person_id='1316' %}">{{ value.name }}</a>

2.6.3 路由分发

比如我一个程序中有很多的app,所有的app都导入项目中的urls.py中,会让整个urls.py的修改过于频繁,这种是完完全全不行的,所以我们使用路由分发;

路由分发,即根据一定的规则将一类url全部转到app中的某个urls.py中;

2.6.3.1 一级url分发举例

工程名:AlvinDjanjo

APP01:BruceApp

APP02:DianaAPp

Alvin.Djanjo.urls.py中需要引入include

from django.contrib import admin
from django.urls import path, include
urlpatterns = [
    path('admin/', admin.site.urls),
    path('bruce/', include("BruceApp.urls")),
    path('diana/', include("DianaApp.urls")),
]

BruceApp.urls.py中写具体的路由规则,也可以构建二级路由分发;

from django.urls import path
from BruceApp import views
from django.urls import re_path
urlpatterns = [
    path('login/', views.login),
    path('home/', views.Home.as_view()),
    path('upload/', views.upload),
    path('dict/', views.Dict.as_view()),
    re_path(r'^details-(?P<person_id>\d+).html/', views.Details.as_view(), name='dt'),
 ]

DianaApp.urls.py中写具体的路由规则,也可以构建二级路由分发;

from django.urls import path
from DianaApp import views
from django.urls import re_path
urlpatterns = [
    path('home/', views.Home.as_view()),
 ]

访问方式变成:

http://127.0.0.1:8004/bruce/home/
http://127.0.0.1:8004/diana/home/

2.6.4 命名空间

 用来区分不同APP中的相同URL的name,URL的命名空间模式也可以让你唯一反转命名的URL;

在project.urls中添加namespace

from django.contrib import admin
from django.urls import path
from django.urls import include
urlpatterns = [
    path('admin/', admin.site.urls),
    # namespace需要为不同的app添加名称两种方式,一种在app的url中加,如下
    path('app01/', include('app01.urls',  namespace="app1")),
    # 一种是在project中添加,如下
    path('app02/', include(('app02.urls', 'app02'), namespace="app2")),
]

在app01.urls中

from django.urls import path
from app01 import views
# 第一种方式,为app添加一个名称
app_name = "app01"
urlpatterns = [
    path('index/', views.index, name="index"),
]

在app02.urls中,不用添加

from django.urls import path
from app02 import views
urlpatterns = [
    path('', views.index, name="home"),
    path('index/', views.index, name="index"),
]

在app01.views及app02.views中reverse时,额外添加命名空间指定

from django.shortcuts import render, reverse

def index(request):
    u1 = reverse('app02:index')
    str2 = "from app02\n" + u1
    return render(request, 'app02.html', {"str2": str2})

在tempalate.app01.html中reverse时,额外添加命名空间指定

<body>
    <div>{{ str1 }}</div>
    <div>{% url 'app01:index' %}</div>
    <div>{% url 'app02:index' %}</div>
</body>

2.7 Django的ORM

ORM分为DBfirst及CODEfirst两种;DBfirst指的是需要现在mysql中创建相关的数据库及表,再在code中创建相关的代码;而CODEfirst则相反,大多数ORM都是这类,写好相关的类自动给你完成相关的创建;

ORM与数据库本质上分为两大方面:

  1. 根据类自动创建数据库表;
  2. 根据类对数据库表中的数据进行各种操作;

2.7.1 简单使用

2.7.1.1 setting中的设置
# 在这里注册的自己的APP,不添加makemigrations不会去找app下的modules文件来生成记录
INSTALLED_APPS = [
	...,
    'DianaApp'
]

# 这里用来修改使用的数据库类型
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

若使用mysql数据库,需要下面几步

  1. 因为django的orm默认使用Mysqldb连接数据库,而我们使用pymysql通过下面方式转换:

    # 在工程.工程名.\_\_init\_\_.py中添加
    import pymysql
    pymysql.version_info = (1, 4, 1, "final", 0) # django3.0后有版本要求
    pymysql.install_as_MySQLdb()
    
  2. 在setting中修改DATABASES

    DATABASES = {
        'default': {
            # 'ENGINE': 'django.db.backends.sqlite3',
            # 'NAME': BASE_DIR / 'db.sqlite3',
            'ENGINE': 'django.db.backends.mysql',
            'NAME': 'djangouse',
            "USER": "django",
            "PASSWORD": "xxx",
            "HOST": "127.0.0.1",
            "PORT": '3306',
            'OPTIONS': {
                'isolation_level': "repeatable read",
            },
        }
    }
    

    注:若不加OPTIONS会导致django.db.utils.OperationalError: 1665的报错,解决方法有两种,另一种是修改binlog format

    mysql> SET GLOBAL binlog_format = 'ROW';
    # 查询修改
    mysql> show variables like 'binlog_format';
    
  3. 生成表结构

    python manage.py makemigrations DianaApp
    python manage.py migrate
    
2.7.1.2 models中创建类
from django.db import models
class UserInfo(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)
2.7.1.3 在数据库内创建表
python ./manage.py makemigrations
python ./manage.py migrate
2.7.1.4 简单的增删改查
from DianaApp import models
class Orm(View):

    def get(self, request):
        # 新建数据方式一
        models.UserInfo.objects.create(username="Bruce", password="123")
        # 新建数据方式一->变种
        dic = {"username": "Alvin", "password": "123"}
        models.UserInfo.objects.create(**dic)
        # 新建数据方式二
        obj = models.UserInfo(username="Hebburn", password="123")
        obj.save()

        # 查询
        result01 = models.UserInfo.objects.all()
        result02 = models.UserInfo.objects.filter(username="Bruce")
        # 返回的是一个django对象,可迭代,内部放置的是UserInfo对象;
        print(result01, result02)

        # 更新
        # 这是批量的修改
        # 只改特定的字段
        models.UserInfo.objects.filter(username="Alvin").update(password="123.com")
        result03 = models.UserInfo.objects.filter(username="Alvin")
        
        # 值的修改,save后存入数据库
        # 这种方式,会对所有的字段更新
        result03.username = "hebburn" # 只会在内存中修改
        resule03.save() # 这一步才会修改数据库
        for row in result03: print(row.password)
        # 删除
        models.UserInfo.objects.filter(id=2).delete()

        return HttpResponse("orm操作测试")

2.7.2 字段

2.7.2.1 常用字段
字段名 字段描述
AutoField 自增整型字段,必填primary_key=True,则成为数据库主键,无django自动创建。
IntegerField 一个整数类型范围 -2147483648 ~ 2147483647。
CharField 字符类型,必须提供max_length参数,max_length表示字符的长度。
DateField/timeField 日期类型,日期格式为YYYY-MM-DD,详单与Python中的datetime.date的实例。
auto_now=True每此条目更新都会跟随更新为最新时间;
auto_now_add=True新增条目时会自动保存当前的时间
DatetimeField 日期时间字段,格式为]DD HH:MM[:ss[.uuuuuu]][TZ],对应Python的datetime.datetime实例。
BooleanField bool类型
TextField 文本类型
FloatField 浮点型
DecimalField 10进制小时,参数max_digits总长度,参数decimal_places小数总长度
2.7.2.2 自定义字段
class MyCharField(models.Field):
    """
    自定义的char类型的字段类
    """
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length
        super(MyCharField, self).__init__(max_length=max_length, *args, **kwargs)
 
    def db_type(self, connection):
        """
        限定生成数据库表的字段类型为char,长度为max_length指定的值
        """
        return 'char(%s)' % self.max_length

class Test(models.Model):
    phone = MyCharField(max_length=11)
2.7.2.3 字段参数
参数名 说明
null 数据库中字段是否可以为空
db_column 指定数据库中字段的列表
default 设置默认值
primary_key 数据库中字段是否为主键
db_index 数据库中字段是否可以建议索引
unique 数据库中字段是否可以建立唯一索引
unique_for_date 数据库中字段日期部分是否可以
verbose_name Admin中显示的字段名称
blank Admin中是否允许用户输入为空,和null连用,否则在admin中会歧义
editable Admin中是否可以编辑
choices Admin中显示选择框的内容,用不变动的数据放在内存中从而避免跨表操作
render =models.IntegerField(choices=[(0,'男'),(1,'女'),],default=1)

2.7.3 外键

使用外键建立表与表之间的关系,主要一对一,一对多及多对多关系;

publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, to_field="id")
# 在外键中创建时,数据库内存储字段会自动为其添加上Class_id;
2.7.3.1 主要参数说明
  1. to:指定关联的类,可以是类名,也可是直接是字符串,不过字符串时,Publisher必须先于当前类;
  2. on_delete:指定当foregin关联的类中数据删除时,本表数据的操作方式 2.0版本必填:
    • models.CASCADE:当关联删除时,当前表中条目跟随删除<默认>;
    • models.PROTECT:当关联删除时,禁止关联删除,报错;
    • models.SET系列:当关联删除时,相关条目从新设置关联<某个值,默认值,NULL>;
2.7.3.2 一对多映射
  1. 从ForeginKey字段拿到的是关联类的对象,如果要拿id,可以直接使用Publisher_id;

        def get(self, request, **kwargs):
            books = models.Book.objects.all()
            print(books)
            for book in books:
                print(book.publisher) # 拿到的是对象
                print(book.publisher_id) # 拿到是ID
            return HttpResponse("BookList", books)
    
  2. 对ForeginKey的赋值方式也有两种,一种是使用对象赋值,一种是使用ID赋值;

    # 直接使用ID进行赋值
    models.Book.objects.create(
                    name=name,
                    publisher_id=publisher_id
                )
    
    # 获取响应的对象进行赋值
    models.Book.objects.create(
                    name=name,
                    publisher=models.Publisher.objects.get(publisher_id=publisher_id)
                )
    
2.7.3.3 多对多映射

在python种使用ManyToManyField建议表与表之间建立多对多映射;

class Book(models.Model):
    name = models.CharField(max_length=64)
    publisher = models.ForeignKey(to=Publisher, on_delete=models.SET('1'), to_field="id")

class Author(models.Model):
    name = models.CharField(max_length=16)
    book = models.ManyToManyField("Book")

以上会生成三张表,第三张表不可直接使用python维护,除非手动生成第三张表;

image-20200920231015598

多对多映射有些特点如下:

  1. 从多对多的字段拿到的值不是对象,而是一个中间商related_descriptors
app01.Book.None 
# 如下的中间商->关系管理对象;
<class 'django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager'>

# 可以使用相关方法获取对象,比如获取所有对象的.all()方法;
<QuerySet [<Book: Book object (1)>, <Book: Book object (2)>]>

对多对对象进行增删改查

author_obj.books.set([1,2]) # 设置多对多关系
author_obj.delete() # 删除时会直接删除条目+第三张表中的多对多关系

2.7.4 Model Meta参数

class UserInfo(models.Model):
    nid = models.AutoField(primary_key=True)
    username = models.CharField(max_length=32)
 
    class Meta:
        # 数据库中生成的表名称 默认 app名称 + 下划线 + 类名
        db_table = "table_name"
 
        # admin中显示的表名称
        verbose_name = '个人信息'
 
        # verbose_name加s
        verbose_name_plural = '所有用户信息'
 
        # 联合索引 
        index_together = [
            ("pub_date", "deadline"),   # 应为两个存在的字段
        ]
 
        # 联合唯一索引
        unique_together = (("driver", "restaurant"),)   # 应为两个存在的字段

2.7.5 脚本执行ORM及13条

import os

# 初始化相关的环境变量,并取得setting的相关值
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FcNetWeb.settings')

# 导入django模块
import django
# 执行django模块
django.setup()
# 以上初始化步骤必须按照顺序来,否则报错
from app01 import models

# 1:all查询所有的数据,返回QuerySet-对象列表 [对象,对象]
ret01 = models.Publisher.objects.all()

# 2:获取一个有且唯一的数据,返回对象,没有或者多个就报错;
ret02 = models.Publisher.objects.get(id=1)

# 3:filter获取满足条件的数据
ret03 = models.Publisher.objects.filter(id__gt=1)

# 4: 获取不满足条件的所有条目数
ret04 = models.Publisher.objects.exclude(id=1)

# 5: order_by排序,默认升序,-field即为降序
# 支持排序完成后再对相同的值根据第二个字段再次排序
ret05 = models.Publisher.objects.order_by("name", "pid")

# 6: reverse反转,对排序完成QuerySet反转;
ret06 = ret04.reverse()

# 7: values 返回QuerySet内部非对象而是对象属性<指定属性>组成的字典
# all可以省略
ret07 = models.Publisher.objects.all().values("id", "name")

# 8: values_list 返回QuerySet内部非对象而是对象属性<指定属性>组成的元祖
# all可以省略
ret08 = models.Publisher.objects.all().value_list("id", "name")

# 9: distinct 去重,根据字段及字段去重
ret09 = models.Publisher.objects.values("name").distinct("name")

# 10: count获取条目的数量
# all可以省略
# 可以用len同样获取,len效果更高
ret10 = models.Publisher.objects.all().count() 
len(models.Publisher.objects.all())

# 11:返回第一个对象
ret11 = models.Publisher.objects.all().first()

# 12:返回最后一个对象
ret12 = models.Publisher.objects.all().last()

# 13:判断是否存在
ret13 = models.Publisher.objects.filter(id=1).exists()

2.7.6 ORM中的双下划线

2.7.6.1 单表
# 值比较
ret14 = models.Publisher.objects.filter(pk__lt=2) # less than
ret15 = models.Publisher.objects.filter(pk__lte=2) # less equal than
ret16 = models.Publisher.objects.filter(pk__gt=1)  # greate than
ret17 = models.Publisher.objects.filter(pk__gte=1) # grate equal tha
ret18 = models.Publisher.objects.filter(pk__range=[1, 2]) # 1< ok < 2
ret19 = models.Publisher.objects.filter(pk__in=[1, 2, 3]) # 包含1,2,3

# 模糊查找
ret20 = models.Publisher.objects.filter(name__contains='123') # 相当于like
ret21 = models.Publisher.objects.filter(name__icontains='123') # 忽略大小写
ret22 = models.Publisher.objects.filter(name__startswith='123') # 以什么什么开始
ret23 = models.Publisher.objects.filter(name__istartswith='123') # 忽略大小写
ret24 = models.Publisher.objects.filter(name__endswith='123') # 以什么什么结束
ret25 = models.Publisher.objects.filter(name__iendswith='123') # 忽略大小写

# 时间查询
ret26 = models.Publisher.objects.filter(age__year='2019') # 
ret27 = models.Publisher.objects.filter(age__contains='2019-01')

# null查询
ret28 = models.Publisher.objects.filter(age__isnull=True)
2.7.6.2 外键与反向
# Book中有外键->连接Publisher

# 正向查-我想查询书,根据出版社的名字的查
ret29 = models.Book.objects.filter(publisher__name='人民邮电出版社')
# 可以再加单表操作查询
ret30 = models.Book.objects.filter(publisher__name__contains='人民邮电出版社')


# 反向查-我想查出版社, 根据书籍obj或者书籍字段
# 查Publisher根据book_obj来查
ret31 = models.Publisher.objects.get(pk=1)
ret31.book_set # 约定格式,即类名小写并加_set, 
# 查Publisher根据book中的字段,约定也是类名小写加__field
ret32 = models.Publisher.objects.filter(book__name__contains="图解")

# 如下语句中在Book中设置了别名即查询别名,用来反向查找, 共两种,
#一种为related_name, 影响xxx_set及双下划线连表查询;
#一种为related_query_name, 影响耍下划线连表查询, 优于related_name;
publisher = models.ForeignKey(to=Publisher, on_delete=models.SET('1'), to_field="id", related_query_name='mybook')
# 外键中设置别名
# related_name='books' 反向查询别名,代替book_set
ret31.books
# 根据外键字段查询时,代替表表名
ret32 = models.Publisher.objects.filter(books__name__contains="图解")

# 外键中设置查询别名
# related_query_name='mybook'
# 代替表名,外键别名,作用于字段查询中
ret32 = models.Publisher.objects.filter(mybook__name__contains="图解")
2.7.6.3 多对多与增删改查
author = models.Author.objects.get(pk=1) 
book = models.Book.objects.get(pk=1)

# 多对多正向-对象
author.book # 关系对象
author.book.all() # 从关系对象中获取book对象

# 多对多反向-对象
book.author_set # 反向默认方式获取,返回关系对象
book.author_set.all() #  从关系对象中获取author对象

# 多对多正向-字段
alvin_book = models.Book.objects.filter(author__name="Alvin")

# 多对多反向-字段
tupu_author = models.Author.objects.filter(book__name="图解网络硬件")

# 关系设置-包括多对多双方的,及一对多中,从一与多的关系
##################################################
# 多对多-设置关系

# set设置多对多关系,需要引用对象:
# 作者的书设置为书-pk;
author.book.set([1, 2, 3])
# 作者的书设置为书-对象
author.book.set(models.Book.objects.filter(id__in=[2, 3]))

# add/remove/clear添加/删除/清空多对多关系
# 多对多中支持id及obj
author.book.add(1, 2)
author.book.remove(*models.Book.objects.filter(id__in=[2, 3]))
author.book.clear()

# 新增一个和当前对象有关系的对象,关系自动生成
author.book.create(name="兔姐自传")

#######################################################
# 一对多-设置关系
pub01 = models.Publisher.objects.get(pk=1)
# 支持上面所有方法,区别
# set/add/remove仅支持obj及*Queryset,不支持pk列表
pub01.book_set.add(*models.Book.objects.filter(pk__in=[1, 2, 3]))
# clear及/remove这类会造成book中的pubulisher_id字段为空,需要在数据库中设置为空才可使用
pub01.book_set.remove(*models.Book.objects.filter(pk__in=[1, 2]))

2.7.6 ORM高级操作

2.7.6.1 聚合
##############################
# 需要使用aagregate加聚合不同的方法
# aggregate终止字句,后面没法再跟其他放发
from django.db.models import Max, Min, Count, Sum, Avg
books = models.Book.objects.all()
print(books)
print(books.aggregate(Max('price'), Min('price'), Count('price'), sum=Sum('price'), avg=Avg('price')))

####返回结果是一个字典,默认生成的key是field__funName,如果想改变,可以指定形参生成如sum及avg;
# {'sum': Decimal('1163.00'),
#  'avg': Decimal('96.916667'),
#  'price__max': Decimal('100.00'),
#  'price__min': Decimal('77.00'),
#  'price__count': 12}
2.7.6.2 分组

分组(group)一般和聚合(aggregate)联合使用,单独的分组是没有意义的,当涉及多个表时,可以使用join进行连表;

这个是一个查询+连表+分组+聚合的sql

select app01_publisher.name, avg(app01_book.price) 
from app01_book 
inner join app01_publisher 
on (app01_book.pub_id = app01_publisher.id)
group by app01_publisher;

查询一本书有多少个作者

sql写法:

 select app01_book.name as book_name, count(app01_author.name) as count 
 from app01_book 
 inner join app01_author_book on (app01_book.id=app01_author_book.book_id) 
 inner join app01_author on (app01_author_book.author_id=app01_author .id) 
 group by app01_book.name;

orm写法:

# 通过book对象查询方法
models.Book.objects.annotate(count=Count('author__id')).values('id', 'name', 'count')
# 通过author对象查询方法
models.Author.objects.values('book__id', 'book__name').annotate(count=Count('id'))

查询每个出版社最便宜的一本书

sql写法:

select app01_publisher.id, app01_publisher.name, min(app01_book.price) 
from app01_publisher
left join app01_book on (app01_publisher.id = app01_book.pub_id) 
group by app01_publisher.id;

orm写法:

models.Book.objects.values('pub_id', 'pub__name').annotate(Min('price'))
models.Publisher.objects.annotate(m=Min("book__price")).values('id', 'name', 'm')
# 注意两种写法对空数据的处理方式不同,一个为left,一个为right

image-20200923190626932

查询不止一个作者的书

# 方法1
models.Book.objects.annotate(count=Count('author__id')).values('id', 'count').filter(count__gt=1)
# 方法2
models.Author.objects.values('book').annotate(count=Count('id')).filter(count__gt=1)

根据一本书的作者数量排序

models.Book.objects.annotate(count=Count('author__id')).values('id', 'count').order_by('-count')

一个作者他所有书的价格之和

models.Author.objects.annotate(Sum("book__price")).values()
models.Book.objects.values('author', 'author__name').annotate(Sum("price"))
2.7.6.3 F查询

在ORM中,我要想比较一个字段与另一个字段,或者赋值时根据另外一个字段赋值,需要F函数;

F字段支持与常数的加减乘除和取模操作

from django.db.models import F
# 查询所有库存大于销量的书籍
models.Book.objects.filter(score__gt=F('soild'))

# 对所有书本打8折销售
models.Book.objects.all().update(price=F('price')*0.8)
2.7.6.4 Q查询

在filter中多个条件是与的关系,当我们需要用到或,非等更复杂的查询条件时,需要Q函数;

支持条件:

  1. | 或
  2. & 与
  3. ~ 非
# 多条件合并查询,id<3或者id>10,切名字不是以‘图解;开头的书
models.Book.objects.filter(Q(Q(id__lt=3) | Q(id__gt=10)) & ~Q(name__startswith="图解"))
2.7.6.5 事物

对应mysql中的事物操作,在ORM中使用固定的格式;

格式如下:

from django.db import transaction
with transaction.atomic(): # 原子型操作
    ... #  要写得到操作内容

但是正常情况下,我不希望报错影响程序,正常写法应该如下:

try: # try必须在外边,如果在with里面,会使原子操作无效
    from django.db import transaction
    with transaction.atomic():
        ... #  要写得到操作内容
except Exception as e:
    print(e)

2.8 cookie与session

2.8.1 cookies

因HTTP是无状态的协议,即每次请求都是独立的。但是有时候我们需要保存一个状态<比如登录状态>,这个时候就需要用到cookies;

cookies是保存在浏览器中,用来保存一些状态的键值对,可以用来验证:

  1. 由服务器让浏览器进行设置的;
  2. cookies信息保存在浏览器本地的,浏览器有权不保存;
  3. 浏览器再次访问时自动携带应对的cookie;
2.8.1.1 django中操作cookie
# 设置cookies:
# 需要为Response类的对象
# 本质上是在http的响应头中添加set_cookie字段,添加键值对
response.set_cookie('is_login', 'value')
response.set_signed_cookie('is_login', "cookies_bruce", salt="s14", max_age=100, path="/", secure=False, httponly=True)
#参数说明
## max_age 超时时间
## path cookie生效的路径,默认为/,可以设置比如/home/
## secure=True 是否可以进行HTTPS传输
## httponly=True 只能传输,限制js获取,比如document.cookies时,就无法获取了;

# 获取cookies:
#  本质是通过请求头中的set_cookie字段获取
request.COOKIES.get('key') # 非加密获取方式,加密这种方式获取的是密文
request.get_signed_cookie('is_login', salt="s14", default="") # 加密方式获取

# 删除cookies:
#  本质上仍然是设置cookie,只是将内容变为“”,将超时时间变为0
reponse.delete_cookie(key)
2.8.1.2 简单举例
from django.shortcuts import render, redirect, reverse

# Create your views here.
from app01 import models
from functools import wraps

# 新建一个装饰器,用于验证登录cookie
def isLogin(fun):
    @wraps(fun)
    def wp(request, *args, **kwargs):
        b_url = request.get_full_path()
        ## 获取明文的cookie
        # login_status = request.COOKIES.get('is_login')
        
        # 获取密文的cookie
        login_status = request.get_signed_cookie('is_login', salt="s14", default="")
        if login_status != 'cookies_bruce':
            return redirect(reverse('login') + "?url={}".format(b_url))
        else:
            ret = fun(request, *args, **kwargs)
            return ret
    return wp

def login(request):
    md = request.method
    # 跳转会原来页面-login页面接受传递的参数
    url = request.GET.get('url')
    if url:
        to_url = url
    else:
        to_url = reverse('home')

    if md == "GET":
        return render(request, 'log.html')
    elif md == "POST":
        name = request.POST.get("user")
        pwd = request.POST.get("pwd")
        ret = redirect(to_url)
        if models.Muser.objects.filter(name=name, pwd=pwd).first():
            ##设置cookie的两种方法
            # ret.set_cookie('is_login', '1')
            ret.set_signed_cookie('is_login', "cookies_bruce", salt="s14", max_age=100, path="/", secure=False, httponly=True)
        return ret

@isLogin
def home(request):
    print(request.COOKIES.get("is_login"))
    return render(request, 'home.html')

@isLogin
def main(request):
    return render(request, 'main.html')

@isLogin
def delcookie(request):
    rt = redirect('home')
    rt.delete_cookie('is_login')
    return rt

2.8.2 session

cookie是保存在浏览器中的键值对,而session是保存在服务器上的一组组键值对并依赖cookie;

2.8.2.1 session存在的意义

cookie存在的问题

  1. cookie是信息存在浏览器本地,不太安全;
  2. 浏览器会对cookie的大小和个数有一定限制的,一般不大于4K;

而session存储Server上相对安全,同时也没有大小额的限制;

2.8.2.2 django操作session

前提:

# setting中
## app里要有session的注册
INSTALLED_APPS = [
    'django.contrib.sessions',

]
## 中间件里要有session的设置
MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
]

# 数据库中要有django_session相关的表

操作:

#  获取、设置、删除Session中的数据-操作方法类似于列表
## 设置
request.session['k1']
request.session.setdefault('k1',123) # 存在则不设置
## 获取
request.session.get('k1',None)
request.session['k1'] = 123
## 删除
del request.session['k1']
request.pop('k1')

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

# 会话session的key
request.session.session_key

# 将所有Session失效日期小于当前日期的数据删除
	## 默认情况下,即使session超时,数据也不会删除的
request.session.clear_expired()

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

# 删除当前会话的所有Session数据,但不会删除浏览器cookie
request.session.delete()

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

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



设置

# 在django的global_setting中可以看到session的默认配置
	##查看方法,在任意文件中
    from django.conf import global_settings
    ## 然后,ctrl + 点击global_settings即可看到
    
# Cache to store session data if using the cache session backend. #跟缓存相关
SESSION_CACHE_ALIAS = 'default'
# Cookie name. This can be whatever you want.# 在cookie中的名称
SESSION_COOKIE_NAME = 'sessionid'
# Age of cookie, in seconds (default: 2 weeks). # 默认超时时间
SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2
# A string like "example.com", or None for standard domain cookie.# cookie的几个参数 
SESSION_COOKIE_DOMAIN = None
# Whether the session cookie should be secure (https:// only).
SESSION_COOKIE_SECURE = False
# The path of the session cookie.
SESSION_COOKIE_PATH = '/'
# Whether to use the HttpOnly flag.
SESSION_COOKIE_HTTPONLY = True
# Whether to set the flag restricting cookie leaks on cross-site requests.
# This can be 'Lax', 'Strict', 'None', or False to disable the flag.
SESSION_COOKIE_SAMESITE = 'Lax'
# Whether to save the session data on every request. # 每次请求来后,都会更新超时时间
SESSION_SAVE_EVERY_REQUEST = False
# Whether a user's session cookie expires when the Web browser is closed. #浏览器关闭即超时
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
# The module to store session data # 设置存储 session的存储方式,有缓存,数据库等
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
# Directory to store session files if using the file session module. If None,
# the backend will use a sensible default.
SESSION_FILE_PATH = None
# class to serialize session data
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'

2.9 中间件

中间件是一个用来处理Django的请求和响应的框架级别的钩子。

它是一个轻量、低级别的插件系统,用于在全局范围内改变Django的输入和输出。

每个中间件组件都负责做一些特定的功能。

本质是一个自定义类,类中定义了几个方法,Django框架会在处理请求的特定的时间去执行这些方法。

2.9.1 配置步骤

  1. 新建文件夹,一般放置在app下,eq:middlewares

  2. 新建py文件,名字任意,eq:mid01.py

  3. 文件中新建类

    from django.utils.deprecation import MiddlewareMixin
    class MID01(MiddlewareMixin):
        ...
    
  4. 在project下的setting中注册middlewares;

    MIDDLEWARE = [
        'app.middlewares.mid01.MID01',
    ]
    

2.9.2 分类介绍

中间件共5个方法:

  • process_request(self,request) : 处理request请求
  • process_view(self, request, view_func, view_args, view_kwargs):修改视图行为
  • process_template_response(self,request,response):修改模板渲染
  • process_exception(self, request, exception):异常时处理
  • process_response(self, request, response):处理response
2.9.2.1 process_request

process_request(self, request)用来处理request请求

执行时间:路由之前

参数:

  • request:请求的对象,和视图函数时同一个对象;

执行顺序:按照注册的顺序,顺序执行

返回值:

  • None:正常流程
  • HttpResponse后跳转至同类下的response方法,向上执行;
2.9.2.2 process_response

process_response(self, request, response):处理response

执行时间:视图及所有的中间件之后

参数:

  • request:请求的对象,和视图函数时同一个对象;
  • response:返回的对象,和视图函数返回的对象是同一个;

执行顺序:倒序执行

返回值:

  • 必须返回HttpResponse
2.9.2.3 process_view

process_view(self, request, view_func, view_args, view_kwargs):修改视图行为

执行时间:路由之后,视图之前

参数:

  • request:请求的对象,和视图函数时同一个对象;
  • view_func:视图函数
  • view_args:视图函数的位置参数
  • view_kwargs:视图函数的关键字参数

执行顺序:顺序执行

返回值:

  • None:正常流程
  • HttpResponse后跳过之后的view及视图函数直接转至最后一级response函数;
2.9.2.4 process_exception

process_exception(self, request, exception):异常时处理

执行时间:视图函数中,当视图函数异常时执行

参数:

  • request:请求的对象,和视图函数时同一个对象;
  • exception:异常的对象;

执行顺序:倒序执行

返回值:

  • None:当前中间件没有处理异常,交给下一个中间件处理异常,直至最后django处理异常;
  • HttpResponse当前中间件处理了异常,跳过之后的exception直接转至最后一级response函数;
2.9.2.5 process_template_response

process_template_response(self,request,response):修改模板渲染

执行时间:视图中返回的对象时TemplateResponse对象时触发;

参数:

  • request:请求的对象,和视图函数时同一个对象;
  • response:返回的对象,和视图函数返回的对象是同一个,为TemplateResponse对象;

执行顺序:倒序执行

返回值:

  • 再次处理过的TemplateResponse对象
    • TemplateResponse相对于Render对象时,此时的TR还没有完成渲染;
    • 修改模板的名字:response.template.name
    • 修改内容数据:response.context.data

2.9.3 整体执行顺序

2.9.3.1 非触发式流程如下

img

2.9.3.2 如果包含触发式流程如下:

img

2.9.4 csrf中间件

2.9.4.1 csrf攻击简介

CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF

如图中所示,B利用了A在浏览器中的cookie信息,模拟合法用户在A上完成了一个用户完全不知得到操作,从而达成一定的目的;

img

2.9.4.2 django中csrf的防御流程

在django中使用csrf-token的方式来防御csrf攻击;

  1. 当用户访问有post等需求的页面时,django会在用户get请求时设置两个东西:

    • django会在response中设置浏览器cookies<csrftoken:64位字符串>;

    • django会在渲染的页面中放置一个input框;

      <input type="hidden" name="csrfmiddlewaretoken" value=64位字符串>
      
  2. 当用户post类操作提交相关数据时,会同时提交input框里的内容至django,当然cookie也会带;

  3. django收到后,会在csrf中间件中完成input提交的token与cookie携带的cookie的比较,详细:

    • 在process_request中会从session/cookie中获取token,并赋值给META.CSRF_COOKIE;

    • 在process_views中完成整体的验证,步骤详细:

      1. 首先判断是否豁免验证,一般有csrf_processing_done,csrf_exempt及非重点method;

      2. 然后会尝试获取submit的cookie,共有两种:

        • 从post中获取

          request.POST.get('csrfmiddlewaretoken', '')
          
        • 从META中的settings.CSRF_HEADER_NAME('HTTP_X_CSRFTOKEN')属性获取

          equest.META.get(settings.CSRF_HEADER_NAME, '')
          
      3. 对比两个token,判断其解密后的secret是否一致,详细:

        • 一个token长64,其中前32为为salt,后32为加密后的secret;
        • 分别使用salt对密文解密得到secret,并对比是否相同;
    • 在process_response中根据request中的cookie异常属性值来维护csrf_cookie;

2.9.4.3 csrf装饰器

在django中,除了使用中间件统一验证csrf,还可以使用装饰器为某些页面设置豁免,或者在关闭中间件的的情况下,为某些页面加上csrf验证;

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

@csrf_exempt   # 设置豁免,本质是为视图函数index添加 csrf_exempt=True的属性
def index(request):  
    pass

@csrf_protect  # 设置csrf验证-方式1
def index(request):  
    pass

urlpatterns = [  # 设置csrf验证-方式2
    url(r'^index/',csrf_protect(views.index)),  
]

2.9.5 配置举例

2.9.4.1 登录认证及所有中间方法配置

class MID01(MiddlewareMixin):
    
    # 执行在路由之前
    def process_request(self, request):
        ip = request.META.get('REMOTE_ADDR')
        history = TIME.get(ip, [])
        TIME[ip] = history
        now = time.time()
        times = 0
        for item in history[:]:
            if now - item < 5:
                times += 1
            else:
                TIME[ip].remove(item)
        TIME[ip].append(now)
        print("次数", times)
        if times > 2:
            return HttpResponse("访问过于频繁")
        status = request.session.get('is_log')
        login_url = reverse('login')
        if request.path_info != login_url:
            t_url = request.path_info
            if not status:
                return redirect(login_url + "?url={}".format(t_url))

    # 执行在路由之后,视图函数之前
    def process_view(self, request, view_fun, view_args, view_kwargs):
        print("process_view_MID01")

    # 执行在视图函数之后
    def process_response(self, request, response):
        print("process_response_MID01")
        return response

    # 视图函数之后且视图中有异常时执行
    def process_exception(self, request, exception):
        print("process_exception_MID01")

    # 执行条件时必须返回TemplateResponse
    def process_template_response(self, request, response):
        print("process_template_response_MID01")

2.10 AJAX

Asynchronous Javascript And Xml (异步的js与xml):使用js语言与服务器进行异步交互,传输XML数据,现在也可以传输json格式;

AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内;

  • AJAX使用JavaScript技术向服务器发送异步请求;
  • AJAX请求无须刷新整个页面;
  • 因为服务器响应内容不再是整个页面,而是页面中的部分内容,所以AJAX性能高;

2.10.1 json

在python中及js中普遍使用json进行,不同模块间的数据交互,如下是一个序列化与反序列化图;

img

2.10.2 ajax的两种使用方式

2.10.2.1 使用jquery实现 ajax方法

这里面使用get请求来传递参数,实际是因为未传递csrf-token,所以避免认证环节;

template中:


<body>
{% csrf_token %}
<input id="i1" type="text" name="i1">+
<input id="i2" type="text" name="i2">=
<input id="i3" type="text" name="i3">
<button>提交</button>

<script src="/static/jquery-3.5.1.js"></script>
<script>
    $("button").on('click', function (){
        $.ajax(
            {
                
                url:"/hide/", 提交向的URL
                type:"GET",   提交的方法
                data: {  提交的数据
                    i1:$("#i1").val(),
                    i2:$("#i2").val()},
                success: function (data){ 回调函数,参数data接收视图函数的返回
                    $("#i3").val(data)
                }
            }
        )
    })
</script>
</body>

view中:

def hide(request):
    i1 = request.GET.get('i1') # 从提交的数据中获取值
    i2 = request.GET.get('i2') # 从提交的数据中获取值
    i3 = int(i1) + int(i2) # 处理
    return HttpResponse(i3) # 返回值到回调函数的data形参中,一般返回json格式;
2.10.2.2 使用原生js实现
var b2 = document.getElementById("b2");
  b2.onclick = function () {
    // 原生JS
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open("POST", "/ajax_test/", true);
    xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xmlHttp.send("username=q1mi&password=123456");
    xmlHttp.onreadystatechange = function () {
      if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
        alert(xmlHttp.responseText);
      }
    };
  }; 

2.10.3 csrf-token上传

csrf-token在client中能找到的位置有两个:cookie中及隐藏的input;

csrf-token在request的携带位置总共有两个:POST.csrfmiddlewaretoken及头部的X_CSRFTOKEN;

2.10.3.1 从隐藏input到POST.csrfmiddlewaretoken
$.ajax({
  url: "/cookie_ajax/",
  type: "POST",
  data: {
    "username": "Q1mi",
    "password": 123456,
    "csrfmiddlewaretoken": $("[name = 'csrfmiddlewaretoken']").val()  // 使用jQuery取出csrfmiddlewaretoken的值,拼接到data中
  },
  success: function (data) {
    console.log(data);
  }
})
2.10.3.2 从cookie到头部的X_CSRFTOKEN
$.ajax({
  url: "/cookie_ajax/",
  type: "POST",
  headers: {"X-CSRFToken": $.cookie('csrftoken')},  // 从Cookie取csrftoken,并设置到请求头中
  data: {"username": "Q1mi", "password": 123456},
  success: function (data) {
    console.log(data);
  }
})

2.10.2.3 上传文件
$("#b3").click(function () {
  var formData = new FormData();
  formData.append("csrfmiddlewaretoken", $("[name='csrfmiddlewaretoken']").val());
  formData.append("f1", $("#f1")[0].files[0]);
  $.ajax({
    url: "/upload/",
    type: "POST",
    processData: false,  // 告诉jQuery不要去处理发送的数据
    contentType: false,  // 告诉jQuery不要去设置Content-Type请求头
    data: formData,
    success:function (data) {
      console.log(data)
    }
  })
})

三. django的组件

四. 其他操作

4.1 前端模板的COPY

大部分情况下,我们并不写HTML网页,我们只需要copy并修改即可;

4.1.1 常用的几个COPY的网址

  1. 最有名的bootstrap,首页

  2. 较炫酷的jq22,首页

4.1.2 Copy的一般步骤

前面页面的复制设计HTML及相关的静态文件,根据复杂度可以使用两种方式;

4.1.2.1 较简单的页面

一般较简单的页面,比如bootstrap中的简约登录框,复制步骤如下:

  1. 浏览器-检查-将页面HTML中的body复制下来替换自己的HTML中的body;
  2. 在head中查看需要哪些静态文件,然后下载或者新建引入;
4.1.2.2 复杂的页面

针对复杂的页面,使用上述方法就太慢并且有时候会有异常,那么我们可以使用下面的步骤:

  1. Ctrl+S另存为:因为涉及的静态文件太多一个个下载过慢,另存一次下载;

    • 复制前将相关的广告之类删除
  2. Ctrl+U复制源码贴到本地:为防止页面代码在显示中被JS等改变,复制源码是最准确的;

    • 复制前将相关的广告之类删除
  3. 将下载的相关静态文件,按类复制到static文件夹;

  4. Pycharm中使用Ctril+R替换,因为一个个改太慢了,Pycharm支持正则替换哦;

    #查找
    'css/(.*?.css)'
    #替换为
    "/static/css/loginin/$1"
    
  5. 查漏补缺,比如以上操作结束后,仍然未加载背景图片,去源网址取,然后找到要修改的位置:

    • 缺少的文件位置不一定只在HTML中,也可以在css及js中;

4.2 上线时的操作

4.2.1 关闭debug

在setting.py中设置

DEBUG=False #原来为True
ALLOWED_HOST = ['*'] # 设置允许所有的主机访问;

4.3 使用django.admin

4.3.1 步骤

  1. 创建一个超级用户

    python manage.py createsuperuser
    
  2. 注册model

    # 在app下面的admin.py中
    from django.contrib import admin
    from app01 import models
    admin.site.register(models.Person)
    
  3. url登录及obj显示

    # 使用http://127.0.0.1/admin/
    # 可以为类添加__str__(),这个可以用来在admin显示obj的细节
    

4.3.2 调整

整体通过定义admin.ModelAdmin的子类来对显示及修改作修改;

调整的内容:

  1. 修改显示的字段
  2. 修改显示中可修改的字段
  3. 隐藏非关键字段,显示部分字段;
  4. 将forgin-key放在关联目标的下面显示等;
  5. 显示多对多关系;

model.py

from django.db import models

# Create your models here.


# 用户表,存放所有的所有用户
## 关联role,分配角色信息
class User(models.Model):
    name = models.CharField(max_length=32, verbose_name="用户名")
    password = models.CharField(max_length=32, verbose_name="密码")
    role = models.ManyToManyField(to="Role", verbose_name="角色")

    class Meta:
        verbose_name = "用户"
        verbose_name_plural = "用户表"

# 角色表
## 关联permission,基于角色分配权限
class Role(models.Model):
    name = models.CharField(max_length=32, verbose_name="角色名")
    permission = models.ManyToManyField(to="Permission", verbose_name="权限")

    class Meta:
        verbose_name = "角色"
        verbose_name_plural = "角色表"

    def __str__(self):
         return self.name

# 权限表
## 存储所有可使用的权限信息
class Permission(models.Model):
    url = models.CharField(max_length=64, verbose_name="url")
    name = models.CharField(max_length=32, verbose_name="权限")
    is_menu = models.BooleanField(verbose_name="是否菜单", default=False)
    menu = models.OneToOneField(to="Menu", null=True, blank=True, on_delete=models.CASCADE)

    class Meta:
        verbose_name = "权限"
        verbose_name_plural = "权限信息表"

    def __str__(self):
         return self.name

class Menu(models.Model):
    title = models.CharField(max_length=32, verbose_name="标题", default="default")
    icon = models.CharField(max_length=64, verbose_name="图标")

    class Meta:
        verbose_name = "菜单"
        verbose_name_plural = "菜单信息表"

    def __str__(self):
        return self.title

rbac/admin.py

from django.contrib import admin
from rbac import models
# 根据permission到menu的外键关系,建立table放置在menu下面;
class MenuInPermision(admin.TabularInline):
    model = models.Permission

# 建立useradmin
class UserAdmin(admin.ModelAdmin):
    list_display = ['name', 'password', '角色字段'] # 显示展示的字段,默认为obj
    list_editable = ['password',] # 显示以上字段中可直接编辑的字段,默认无

    def 角色字段(self, obj): # 转换下多对多字段,让其可以在display中显示
         return [role for role in obj.role.all()]
 
class MenuAdmin(admin.ModelAdmin): 
    inlines = [MenuInPermision] # 关联上面建立的table,并放置在同一页面

class PermissionAdmin(admin.ModelAdmin):
    list_display = ['name', 'url', 'is_menu', "menu"]
    # fields = ['name', 'url', 'is_menu', "menu"] # 可编辑的字段,默认所有
    search_fields = ('name', 'url') # 建立搜索框,并制定可搜索的字段
    fieldsets = ( # 与fields冲突,将所有的字段进行分类,并可额外指定样式
        ['主要', {
            'fields': ('name', 'url'),
        }
        ],
        [
          '高级', {
            'classes': ('collapse',), #css样式 # 可隐藏及展示的样式
            'fields': ('is_menu', 'menu'),
        }
        ]
    )

admin.site.register(models.User, UserAdmin) # 注册同时使用UserAdmin自定义内容
admin.site.register(models.Role)
admin.site.register(models.Permission, PermissionAdmin)
admin.site.register(models.Menu, MenuAdmin)
posted @ 2020-10-03 22:20  FcBlogs  阅读(128)  评论(0编辑  收藏  举报