django

Django

一、Django介绍

1.1 简介

Django是python语言中的一个web框架,Python语言中主流的web框架有Django、Tornado、Flask 等多种。Django相较与其它WEB框架,其优势为:大而全,框架本身集成了ORM、模型绑定、模板引擎、缓存、Session等功能,是一个全能型框架,拥有自己的Admin数据管理后台,第三方工具齐全,性能折中。缺点:功能太多,数据分表复杂,高性能扩展复杂。

1.2 安装

  1. pip安装:在cmd命令窗口中,输入

    pip  install  django
    
  2. 下载安装包安装

1.3 文档

官方文档的链接在:https://docs.djangoproject.com

点击页面右下角的 language 按钮,可以选择 zh-hans ,显示中文,只有部分页面有中文翻译

点击页面右下角的Documentation version,可以选择版本

二、Django相关知识学习

2.1 相关术语和规范

  1. B/S和C/S

    Django是用于开发B/S架构的软件的,软件主要分为B/S架构和C/S架构:

    • B/S:全称Browser/Server(浏览器/服务器)

    • C/S:全称Client/Server(客户端/服务器)

  2. MVC

    MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

    • M: 管理应用程序的状态(通常存储到数据库中),并约束改变状态的行为(或者叫做“业务规则”)。

    • V: 负责把数据格式化后呈现给用户。

    • C: 接受外部用户的操作,根据操作访问模型获取数据,并调用“视图”显示这些数据。控制器是将“模型”和“视图”隔离,并成为二者之间的联系纽带。

  3. MTV

    Django也是一个MVC框架。但是在Django中,控制器接受用户输入的部分由框架自行处理,所以 Django 里更关注的是模型(Model)、模板(Template)和视图(Views),称为 MTV模式:

    • M: 代表模型(Model),即数据存取层。 该层处理与数据相关的所有事务: 如何存取、如何验证有效性、包含哪些行为以及数据之间的关系等。

    • T: 代表模板(Template),即表现层。 该层处理与表现相关的决定: 如何在页面或其他类型文档中进行显示。

    • V: 代表视图(View),即业务逻辑层。 该层包含存取模型及调取恰当模板的相关逻辑。 你可以把它看作模型与模板之间的桥梁。

  4. ORM

    ORM 就是通过实例对象的语法,完成关系型数据库的操作的技术,是"对象-关系映射"(Object/Relational Mapping) 的缩写。

    • ORM把数据库映射成对象

      数据库的表(table) ---------> 类 (class)

      记录(record,行数据) ---------> 对象(object)

      字段(field) ---------> 对象的属性(attitude)

    • 举例

      • sql语句

        SELECT id, first_name, last_name, phone, birth_date, sex
        FROM persons
        WHERE id = 10

      • 程序直接运行sql,操作数据库的写法,如下:

        res = db.execSql(sql);
        name = res[0]["FIRST_NAME"];

      • 改成ORM写法如下

        p = Person.get(10);
        name = p.first_name;

    一比较就可以发现,ORM使用对象,封装了数据库操作,因此可以不碰sql语言,开发者只使用面向对象编程,与数据对象交互,不用关系底层数据库。

    • ORM的优点
      • 数据模型定义在一个地方便于维护,也利于重用代码
      • ORM有现成的工具,很多功能都可以自动完成
      • 它迫使你使用mvc架构
      • 基于ORM的业务代码比较简单,代码量少,语义性好
    • ORM的缺点
      • ORM库不是轻量级工具,需要花很多精力学习和设置
      • 对应复杂的查询,ORM要么无法表达,要么性能不如原生的sql
      • ORM抽象掉了数据库层,开发者无法了解底层的数据库操作,也无法定制一些特定的sql

三、Django项目构建

3.1 cmd命令行构建项目

  1. 在cmd窗口中,切换到指定的项目文件夹,执行:

    django-admin startproject mysite

    其中:mysite是项目名

    这时会在指定的项目文件夹中,生成一个mysite文件夹,目录结构如下:

    mysite/
        manage.py
        mysite/
            __init__.py
            settings.py
            urls.py
            wsgi.py
    

    这些目录和文件的说明如下:

    • 最外层的mysite/ 根目录是项目的名称
      • manage.py 是管理 Django 项目的命令行工具,启动和结束等
    • 里面一层的 mysite/ 目录包含你的项目主应用,它是一个 Python 包
    • mysite/_init_.py:一个空文件,告诉 Python 这个目录是一个 Python 包
    • mysite/settings.py:项目的配置文件,有关于数据库、编码、时区等
    • mysite/urls.py:项目的 url路由配置,即url路由与其函数的对应配置
    • mysite/wsgi.py:用于你的项目的与WSGI兼容的Web服务器入口,用于项目部署
  2. cmd 窗口中,进入最外层的mysite

  3. 在这个目录下,我们可以输入如下命令,创建一个 新的子应用

    python manage.py startapp myapp01

  4. 在该cmd目录下执行如下命令,可以启动项目

    python manage.py runserver

    该命令后续可以增加参数,如:

    python manage.py runserver 8081 # 指定端口
    python manage.py runserver 127.0.0.1:8082 # 指定host和端口

    cmd窗口中会出现如下信息:

    F:\django_study\first_pro>python manage.py runserver
    Watching for file changes with StatReloader
    Performing system checks...
    
    System check identified no issues (0 silenced).
    
    You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
    Run 'python manage.py migrate' to apply them.
    July 03, 2019 - 16:11:57
    Django version 2.2.1, using settings 'first_pro.settings'
    Starting development server at http://127.0.0.1:8000/
    Quit the server with CTRL-BREAK.
    [03/Jul/2019 16:13:06] "GET / HTTP/1.1" 200 16348
    [03/Jul/2019 16:13:06] "GET /static/admin/css/fonts.css HTTP/1.1" 200 423
    [03/Jul/2019 16:13:06] "GET /static/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 200 85876
    [03/Jul/2019 16:13:06] "GET /static/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 200 85692
    [03/Jul/2019 16:13:06] "GET /static/admin/fonts/Roboto-Bold-webfont.woff HTTP/1.1" 200 86184
    Not Found: /favicon.ico
    [03/Jul/2019 16:13:06] "GET /favicon.ico HTTP/1.1" 404 1975
    
  5. 在浏览器中输入,启动服务时的ip:端口对网站访问

3.2 第一个Django请求

  1. 请求流程

  1. urls.py配置

    from first_app import views
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('test/',views.first_test),
    ]
    
  2. view.py配置

    from django.shortcuts import render,HttpResponse
    
    # Create your views here.
    def first_test(request):
        print('第一个Django项目views')
        return HttpResponse('Django项目第一次请求成功')
    
  3. 启动服务

    python manage.py runserver

  4. 浏览器发送请求

    127.0.0.1:8000/test

  5. 其中在views.py文件中,每一个请求方法需要有一个request参数,通过该参数可以获取请求相应信息。可以使用dir(request)查看详细。

    ['COOKIES', 'FILES', 'GET', 'META', 'POST', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', 
    '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_current_scheme_host', '_encoding', '_get_full_path', '_get_post', '_get_raw_host', 
    '_get_scheme', '_initialize_handlers', '_load_post_and_files', '_mark_post_parse_error', '_messages', '_read_started', '_set_content_type_params', '_set_post', '_stream', '_upload_handlers', 'accepted_types', 'accepts', 'body', 'build_absolute_uri', 'close', 'content_params', 'content_type', 'csrf_processing_done', 'encoding', 'environ', 'get_full_path', 'get_full_path_info', 'get_host', 'get_port', 'get_raw_uri', 'get_signed_cookie', 'headers', 'is_ajax', 'is_secure', 'method', 'parse_file_upload', 'path', 'path_info', 'read', 'readline', 'readlines', 'resolver_match', 'scheme', 'session', 'upload_handlers', 'user']
    

3.3 开发第一个登录表单

  1. urls.py配置

     path('login_form/',views.login_form),
    
  2. views.py添加业务方法

    def login_form(request):
        html = '''
        <html>
           <body>
              <form method="post">
               用户名:<input name = "username" type="text"></input></br>
               密码:<input name = "password" type = "password"></input></br>
               <input type="submit" value="登录"></input>
              </form>
           </body>
        </html>
        
        '''
        return HttpResponse(html)
    
  3. 浏览器发送请求

四、Django配置

4.1 整体介绍

django项目创建后,在主应用中,会有一个settings.py文件,这个就是该项目的配置文件

  • settings文件包含Django安装的所有配置
  • settings文件是一个包含模块级变量的python模块,所以该模块本身必须符合python规则,并且可以使用python的语法
  • settings中的所有配置项的key必须全部大写
  • settings中每一个配置项都有默认值,默认配置内容在django/conf/global_settings.py中可以查看到,项目中不需要导入该模块,django框架会自动获取
  • settings中可以添加自定义的配置项

4.2 settings文件

  1. manage.py 启动

    默认在manage.py 中配置

    if __name__ == "__main__":
        os.environ.setdefault("DJANGO_SETTINGS_MODULE", "firstdjango.settings")
    
  2. 手动指定配置文件位置

    cmd命令启动如下

    python manage.py runserver 0.0.0.0:8000 --settings=firstdjango.settings
    
  3. 服务部署启动

    在wsgi.py配置

    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "firstdjango.settings")
    

4.3 常用配置项

import os


"""
    当前文件所在文件夹的上一级目录的绝对路径
    切记2个 os.path.dirname
"""
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

"""
    用于加密session,一个随机的字符串
    这样生成:
    from django.core.management import utils
    utils.get_random_secret_key()
"""
SECRET_KEY = '=*f&bx760nyar7@8lb8!w$9h(3ea6p3apl$iua!td1q%-u5r4='

# 调试模式,可以看到错误的所有相信信息,部署时一定要修改为False
DEBUG = True

"""
    允许访问的域名设置
    开发环境不用理会
    运行环境,配置 DEBUG = False后,
    如果允许所有域名访问,则设置 ALLOW_HOSTS = ['*']
    如果指定某些域名可以访问,则设置 ALLOW_HOSTS = ['*.baidu.com']
"""
ALLOWED_HOSTS = []


"""
    应用的配置,
    如:'polls.apps.PollsConfig'
    如果没有 PollsConfig ,那么可以配置为  'polls'
"""
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles', # 只有 DEBUG = Ture 才有效
    
    'polls'  # 子应用必须配置,否则不起作用
]

"""
    中间层配置
    自己编写的 中间层 需要配置在最后
    譬如:
    mymidlle.md.TestMiddleware
"""
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',

]

# 配置基础的urls
ROOT_URLCONF = 'firstdjangopy.urls'

# 配置模板
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')]
        ,
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# 服务器部署的WSGI配置
WSGI_APPLICATION = 'firstdjango.wsgi.application'


"""
    数据库配置
    mysql在python3的使用,需要在 __init__.py 中加入以下代码:
    import pymysql

    pymysql.install_as_MySQLdb()
"""
# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
#     }
# }
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'django_test1',
        'USER': 'root',
        'PASSWORD': '123456',
        'HOST': '127.0.0.1',
        'PORT': '3306',
    }
}


"""
    用户密码验证
"""
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# 语言选择 , zh-Hans 为中文
LANGUAGE_CODE = 'en-us'

# 时区 Asia/Shanghai 是中国时区
TIME_ZONE = 'UTC'

# 国际化
USE_I18N = True

# 本地化
USE_L10N = True

# 使用时区,配套TIME_ZONE使用,必须设置为 False
USE_TZ = False

"""
    静态文件的路径,默认是 static
    如果在各自项目的static目录以外,还有目录存放静态文件,需要添加如下属性:
    STATICFILES_DIRS = (
        os.path.join(BASE_DIR, "common_static1"),
        '/var/www/static/',
    )
"""
STATIC_URL = '/static/'

4.4 程序中获取settings中的配置项

如果在项目代码中需要获取settings中的配置项,这样获取:

# 切记不要导入具体的settings模块的路径,会形成高耦合
# 这样的方式是不可取的:from mysite import settings
from django.conf import settings

d = settings.DEBUG

五、URL调度器

5.1 工作原理

django通过urlconf来映射视图函数,只区分路径,不区分http方法

  • Django确定要使用的根URLconf模块,一般是在settings中的ROOT_URLCONF设置的值,但是如果传入 HttpRequest 对象具有一个urlconf 属性(由中间件设置),则其值将用于代替 ROOT_URLCONF设置。
  • Django加载该URLconf模块并查找变量 urlpatterns,它是一个列表django.urls.path() 和 / 或django.urls.re_path()实例。
  • Django按顺序遍历每个URL模式,并停在与请求的URL匹配的第一个URL模式,需要特别注意编写的顺序
  • 一旦某个URL模式匹配,Django就会导入并调用给定的视图,该视图是一个简单的Python函数(或基于类的视图方法)。该视图通过以下参数传递:
    • 一个HttpRequest实例。
    • 如果匹配的URL模式没有返回任何命名组,则来自正则表达式的匹配作为位置参数提供。
    • 关键字参数由路径表达式匹配的任何命名部分组成,并由可选的kwargs参数传给 django.urls.path()或django.urls.re_path()。
  • 如果没有URL模式匹配,或者在此过程中的任何点发生异常,Django将调用适当的错误处理视图

5.2 简单示例

  1. 在子应用下创建urls.py内容如下

    urlpatterns = [
        
    ]
    
  2. 在项目路由文件中添加子应用urls.py配置

    from django.contrib import admin
    from django.urls import path
    from djangoStudy.first_project.book import views 
    from  django.urls.conf import  include   # 可以引入其他应用配置项
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('book/',views.book_detail_query_string),
        path('book/<int:book_id>',views.book_detail_path)
    ]
    
  3. 配置子应用urls.py

    from django.urls import path
    
    from . import views
    
    urlpatterns = [
        path('articles/2003/', views.special_case_2003),
        path('articles/<int:year>/', views.year_archive),
        path('articles/<int:year>/<int:month>/', views.month_archive),
        path('articles/<int:year>/<int:month>/<slug:slug>/', views.article_detail),
        path('articles/<str:string>/', views.str_archive),
        path('articles/<path:path>/', views.path_archive),
    ]
    

    笔记:

    • 从URL中捕获值,请使用尖括号

    • 捕获的值可以选择包含转换器类型。例如,用于 <int:name>捕获,前面的int指整数参数,name是参数的名称

    • 没有必要添加一个前导斜杠,因为每个URL都有,例如,使用articles而不是/articles。

    • 示例请求说明:

      • /articles/2005/03/ 匹配列表中的第三个条目。Django会调用这个函数,views.month_archive(request, year=2005, month=3)
      • /articles/2003/ 会匹配列表中的第一个模式,而不是第二个模式,因为模式是按顺序测试的,而第一个模式是第一个要传递的测试。看看利用匹配顺序插入像这样的特殊情况。在这里,Django会调用这个函数 views.special_case_2003(request)
      • /articles/2003 不匹配任何这些模式,因为每种模式都要求URL以斜线结尾,不过在浏览器访问时,会自动添加 / 。
      • **/articles/2003/03/building-a-django-site/ **将匹配最终模式。Django会调用这个函数 。views.article_detail(request, year=2003, month=3, slug="building-a-django-site")

5.3 路径转换器

  • str:匹配任何非空字符串,不包括路径分隔符'/'。如果转换器不包含在表达式中,这是默认值。
  • int:匹配零或任何正整数。返回一个int。
  • slug:匹配由ASCII字母或数字组成的字符串,以及横线和下划线字符。例如, building-your-1st-django_site可以匹配,django_@site是不可以匹配的。
  • uuid:匹配格式化的UUID。为防止多个URL映射到同一页面,必须包含破折号,并且字母必须是小写。例如,075194d3-6885-417e-a8a8-6c931e272f00。返回一个 UUID实例。
  • path:匹配任何非空字符串,包括路径分隔符 '/',可以匹配完整的URL路径,而不仅仅是URL路径的一部分str,使用时要谨慎,因为可能造成后续的所有url匹配都失效。
path('articles/<uuid:uuid>/',views.article_uuid),
#获取uuid
import uuid
print(uuid.uuid1())

5.4 自定义路径转换器

转换器是一个包含以下内容的类:

  • 一个regex类属性,作为一个re匹配字符串。
  • to_python(self, value)方法,它处理匹配的字符串转换成要传递到视图函数的类型。
  • to_url(self, value)方法,用于处理将Python类型转换为URL中使用的字符串。

定义方法如下:

  1. 新建一个converters.py文件,在文件中定义一个FourDigitYearConverter类:

    class FourDigitYearConverter(object):
        regex = '[0-9]{4}'
    
        def to_python(self, value):
            return int(value)
    
        def to_url(self, value):
            return '%04d' % value
    
  2. 使用register_converter()方法在URLconf中注册自定义转换器类:

    from django.urls import register_converter, path
    
    from . import converters, views
    
    register_converter(converters.FourDigitYearConverter, 'yyyy')
    
    urlpatterns = [
        path('articles/2003/', views.special_case_2003),
        path('articles/<yyyy:year>/', views.year_archive)
    ]
    

5.5 使用正则表达式

使用正则表达式匹配路径,请使用 re_path()而不是path()

​ 在Python正则表达式中,命名正则表达式组的语法是(?P<name>pattern),其中name是组的名称,并且 pattern是一些要匹配的模式

示例:

from django.urls import path, re_path

from . import views

# url() 是 re_path 的别名,不推荐使用
urlpatterns = [
    path('articles/2003/', views.special_case_2003),
path('articles/<int:year>/', views.year_archive),
re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
    re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
    re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<slug>[\w-]+)/$', views.article_detail),
]

注意事项:

  • 匹配的URL会受到一些限制。例如,年份10000将不再匹配,因为年份整数限制为四位数字
  • 无论正则表达式匹配什么类型,每个捕获的参数都以字符串的形式发送到视图
  • 除了命名的组语法,例如(?P<year>[0-9]{4}),也可以使用较短的未命名组,例如([0-9]{4}),但是不建议这样使用,会引起未知的匹配

嵌套参数:

from django.urls import re_path

urlpatterns = [
    # 不推荐, 匹配 blog/page-3/
    re_path(r'^blog/(page-(\d+)/)?$', blog_articles),
    # 推荐 ,匹配:comments/page-2/  路径到 comments(request, page_numer)
    re_path(r'^comments/(?:page-(?P<page_number>\d+)/)?$', comments),
]

5.6 使用默认值

URLConf中

from django.urls import path

from . import views

urlpatterns = [
    # http://127.0.0.1:8000/polls/blog/ 等同于 http://127.0.0.1:8000/polls/blog/1/
    path('blog/', views.page),
    # http://127.0.0.1:8000/polls/blog/1/
    # http://127.0.0.1:8000/polls/blog/10/
    # http://127.0.0.1:8000/polls/blog/99/
    path('blog/<int:num>/', views.page),
]

views中:

def page(request, num=1):
    # 编写对应的业务逻辑

5.7 错误处理

  • handler400- 状态码400
  • handler403- 状态码403
  • handler404- 状态码404
  • handler500- 状态码500
  1. 在settings中修改配置

    DEBUG = False
    ALLOWED_HOSTS = ['*', ]
    
  2. 在主应用的urls中配置

    # polls是子应用
    handler404 = "polls.views.page_not_found"
    
  3. 在polls应用的views中添加函数page_not_found:

    def page_not_found(request, exception):
        return HttpResponse('自定义的404错误页面')
    
  4. 浏览器测试访问,找不到匹配的路由

5.8 引用其他URL调度器

  1. 多个patterns

    from django.urls import include, path
    
    extra_patterns = [
        path('reports/', credit_views.report),
        path('reports/<int:id>/', credit_views.report),
        path('charge/', credit_views.charge),
    ]
    
    urlpatterns = [
        path('', main_views.homepage),
        path('help/', include('apps.help.urls')),
        path('credit/', include(extra_patterns)),
    ]
    
    
  2. 使用include消除重复前缀

    from django.urls import path
    from . import views
    
    urlpatterns = [
        path('<page_slug>-<page_id>/history/', views.history),
        path('<page_slug>-<page_id>/edit/', views.edit),
        path('<page_slug>-<page_id>/discuss/', views.discuss),
        path('<page_slug>-<page_id>/permissions/', views.permissions),
    ]
    
    # 修改为:
    from django.urls import include, path
    from . import views
    
    urlpatterns = [
        path('<page_slug>-<page_id>/', include([
            path('history/', views.history),
            path('edit/', views.edit),
            path('discuss/', views.discuss),
            path('permissions/', views.permissions),
        ])),
    ]
    
    
  3. 传递捕获的参数

    在urls中配置

    urlpatterns = [
        path('admin/', admin.site.urls),
        # 这里捕获username参数,类型为字符串
        path('<username>/polls/', include('polls.urls'))
    ]
    

    对应的polls应用下的urls中配置:

    urlpatterns = [
        path('arg_test/', views.arg_test),
    ]
    

    对应的polls应用下的views中编写函数

    def arg_test(request, username):
        # 编写对应的业务逻辑
        return HttpResponse(f'username {username}')
    

5.9 额外的参数

from django.urls import path
from . import views

urlpatterns = [
    # 会传递给 views.year_archive(request, year=2005, foo='bar')
    path('blog/<int:year>/', views.year_archive, {'foo': 'bar'}),
]

5.10 URL反向解析

url调度器除了从用户发起请求,到匹配对应的view,还能在python程序中调用进行匹配,通过 path或re_path 中 的name属性进行解析

  • 在模板中,使用url模板标签
  • 在Python代码中(主要是views),使用 reverse() 函数
  • 在模型实例中,使用 get_absolute_url() 方法

示例:

uls中配置:

from django.urls import path

from . import views

urlpatterns = [
    #...
    path('articles/<int:year>/', views.year_archive, name='news-year-archive'),
    #...
]
  1. 在模板中测试

    1. views.py跳转页面

      def do_html(request):
          return render(request,'redirect_test.html')
      
      def year_archive(request,year):
          return HttpResponse(f'重定向成功{year}')
      
    2. 模板中代码

      # 模板中:
      <a href="{% url 'news-year-archive' 2012 %}">2012 Archive</a>
      
  2. python代码

    from django.urls import reverse
    from django.http import HttpResponseRedirect
    
    def redirect_to_year(request):
        # ...
        year = 2006
        # ...
        return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))
    
  3. 在模型中:

    """
        在模型中实现方法:
        def get_absolute_url(self):
            from django.urls import reverse
            return reverse('news-year-archive', args=[str(self.id)])
    
        然后在 模板 中如下使用:
    """
    <a href="{{ object.get_absolute_url }}">{{ object.name }}</a>
    

5.11 命名空间

​ 主要用于配合第 10 点 url反向解析 使用,多个不同的urls文件中可能配置同名的 name,那么为了进行区分,给不同的urls进行不同的命名,切记同一个项目下命名空间不能重复!

​ 通过在 url调度器的模块中,定义 app_name = 'polls' 来命名

from django.urls import path

from . import views
# 定义,一般命名空间和子应用名相同,便于记忆
app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    ...
]

# 调用,一旦有了命名空间,调用时就必须使用 polls: 前缀
reverse('polls:index', current_app=self.request.resolver_match.namespace)

命名空间可以进行嵌套:

# 在 urls 中配置如下:
from django.urls import path

from . import views
# 定义命名空间,一般命名空间名和子应用名相同,便于记忆
app_name = 'polls'

extra_patterns = (
    [
        path('app_name/', views.app_name, name='app_name'),
    ],
    # 此处就是嵌套的命名空间
    'extra'
)

urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('extra/', include(extra_patterns)),
    ...
]

# 在模板中使用:
<a href="{% url 'polls:extra:app_name' %}">点击链接</a>

六、Django模型

模型,就是python中的类对应数据库中的表

ORM就是通过实例对象的语法,完成关系型数据库的操作技术,是对象-关系映射的缩写

6.1 简单示例

模型:

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField()

对应mysql数据库中的表

CREATE TABLE `polls_question` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `question_text` varchar(200) NOT NULL,
  `pub_date` datetime(6) NOT NULL,
  PRIMARY KEY (`id`)
)

笔记:

  1. 模型类必须继承models.Model
  2. 每个属性对应数据库中的一个字段
  3. 表名自动使用 mysite_类名 的小写(如:polls_question),可以覆盖
  4. 如果模型类中没有指定 primary_key ,那么会自动创建一个 id 字段,自增,主键

6.2 应用模型

当编写了模型之后,需要将模型应用到数据库中:

  1. 创建项目 model_study 及 子应用 model_app

    #创建项目
    django-admin startproject model_study
    #进入项目目录创建子应用
    python manage.py startapp model_app
    
  2. 配置应用

    1. 将模型对应的应用程序添加到项目的settings中:

      INSTALLED_APPS = [
          'model_app'
      ]
      
    2. 在settings中配置正确的数据库连接:

      # sqlite3
      DATABASES = {
          'default': {
              'ENGINE': 'django.db.backends.sqlite3',
              'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
          }
      }
      
      # mysql
      DATABASES = {
          'default': {
              'ENGINE': 'django.db.backends.mysql',
              'NAME': 'model_study',
              'USER': 'root',
              'PASSWORD': 'root',
              'HOST': '127.0.0.1',
              'PORT': '3306',
          }
      }
      
    3. 安装对应数据库的驱动

      ps:

      ​ 如果是mysql,Django2.2 请安装 mysqlclient 库

      ​ 如果是sqlite3,是不需要额外安装,python自带驱动库

      注: 需要在mysql数据库中创建数据库 model_study

  3. 预备迁移

    在项目根目录的cmd中运行

    python manage.py makemigrations model_app
    

    注:

    ​ model_app是子应用的名称,如果不指定,那么就会对所有INSTALLED_APPS 中的应用都进行预备迁移

    ​ 指定该命令后,对应的子应用下的migrations 中会生成一个对应的迁移文件

  4. 正式迁移

    在根目录cmd中运行:

    python manage.py migrate
    

    ps: 没有添加子应用名,那么就会把django项目中所有的应用都迁移到数据库中

  5. 模型修改后重新应用

    不管是新增模型,还是修改已有模型后,只需要重复执行第3步和第四步,即可自动实现数据库中的表结构,表关系等信息的修改

6.3 逆向从数据库表生成模型类

  1. settings中设置好 DATABASES 配置

  2. 在对一个数据库中建立好表、约束和表关系等

  3. 在根目录的cmd中运行:

    python manage.py inspectdb > model_app/models.py
    

    ps: model_app是子应用名

  4. 第三步执行后会在models中生成对应的模型类

    譬如:

    class DjangoSession(models.Model):
        session_key = models.CharField(primary_key=True, max_length=40)
        session_data = models.TextField()
        expire_date = models.DateTimeField()
    
        class Meta:
            managed = False   # 这个属性是通知django,不需要进行从模型到数据库的迁移管理
            db_table = 'django_session'  # 对应的数据库中的表名
    

6.4 字段Field

模型类的属性对应数据库中表的字段,都是对应的Field类的实例

6.4.1 字段命名限制

  • 字母,数字,下划线,首字母不能是数字

  • 字段名称不能是Python保留字

  • 由于Django查询查找语法的工作方式,字段名称不能在一行中包含多个下划线,譬如“abc__123”就是不允许的,一个下划线是可以的,如:'first_name'

6.4.2 AutoField、ID、PRIMARY_KEY

默认会自动创建一个自增,主键的id列

如果指定了 primary_key 为其它列,那么不会自动创建id列

可以在模型中指定:

id = models.AutoField(primary_key=True)

6.4.3 常见Field字段

所有的Field类型,见 https://docs.djangoproject.com/en/2.2/ref/models/fields/#model-field-types

常用Field

  • AutoField
  • BooleanField
  • CharField
  • DateField
  • DateTimeField
  • FloatField
  • SmallIntegerField
  • IntegerField
  • TextField

示例:UUIDField这样使用:

import uuid
from django.db import models

class MyUUIDModel(models.Model):
    # uuid.uuid4 千万别写成 uuid.uuid4() ,不要写 ()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

6.4.4 Field常见参数

  • max_length:字段最大长度,用于字符串等,字符串类型CharField必须设置该值
  • null:如果True,Django将在数据库中存储NULL空值。默认是False
  • blank:如果True,该字段被允许为空白("")。默认是False。请注意,这不同于null。null纯粹是与数据库相关的,而blank与验证相关。如果一个字段有blank=True,表单验证将允许输入一个空值。如果一个字段有blank=False,该字段将是必需的。
  • choices:示例:YEAR_IN_SCHOOL_CHOICES = (('FR', 'Freshman'),('SO', 'Sophomore'),('JR', 'Junior'),('SR', 'Senior'),('GR', 'Graduate')) ,中文示例:SEX_CHOICES=((1, '男'),(2, '女')),元组中的第一个元素是将存储在数据库中的值,第二个元素是将在页面中显示的值,最常见用于下拉选择框select
  • default:字段的默认值
  • help_text:用于显示额外的“帮助”文本
  • primary_key:如果True,这个字段是模型的主键,默认是False
  • unique:如果True,该字段在整个表格中必须是唯一的
  • verbose_name:详细字段名,不指定则是属性名的小写,并且用 空格 替换 '_'

6.4.5 模型之间的关系

  • 主外关系中,关联操作最常用的是: models.CASCADE 级联删除 和 models.SET_NULL 设置为null

  • 一对多关系中,ForeignKey 写在一对多关系中,多的那个模型中

6.4.5.1 一对多

​ 使用django.db.models.ForeignKey,例如,如果一个Car模型有一个Manufacturer, 也就是说,一个 Manufacturer可以对应多个汽车,但Car只有一个汽车生产商Manufacturer,那么使用以下定义:

from django.db import models

class Manufacturer(models.Model):
    name = models.CharField(max_length=20)

class Car(models.Model):
    # 外键名是 对应类名的小写 
    # on_delete 是必须属性
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
    
    name = models.CharField(max_length=20)
6.4.5.2 一对一

​ 使用django.db.models.OneToOneField,例如:地址Place和餐馆Restaurant是一对一的关系,而餐馆Restaurant和服务员Waiter是一对多的关系

from django.db import models

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

    def __str__(self):
        return "%s the place" % self.name

class Restaurant(models.Model):
    place = models.OneToOneField(
        Place,
        on_delete=models.CASCADE,
        primary_key=True,
    )
    # BooleanField 在数据库使用 tinyint 类型
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

    def __str__(self):
        return "%s the restaurant" % self.place.name

class Waiter(models.Model):
    restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
    name = models.CharField(max_length=50)

    def __str__(self):
        return "%s the waiter at %s" % (self.name, self.restaurant)
6.4.5.3 多对多
  1. 自关联

    from django.db import models
    
    class Student(models.Model):
        name = models.CharField(max_length=20)
        friends = models.ManyToManyField("self")
    

    笔记:

    • 会生成一个中间表,如下

      CREATE TABLE `test_app_student_friends` (
      	`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
      	`from_student_id` INT ( 11 ) NOT NULL,
      	`to_student_id` INT ( 11 ) NOT NULL,
      	PRIMARY KEY ( `id` ),
      	UNIQUE KEY `test_app_student_friends_from_student_id_to_stude_7ef9880e_uniq` ( `from_student_id`, `to_student_id` ),
      	KEY `test_app_student_fri_to_student_id_154a4deb_fk_test_app_` ( `to_student_id` ),
      	CONSTRAINT `test_app_student_fri_from_student_id_c400b5d4_fk_test_app_` FOREIGN KEY ( `from_student_id` ) REFERENCES `test_app_student` ( `id` ),
      CONSTRAINT `test_app_student_fri_to_student_id_154a4deb_fk_test_app_` FOREIGN KEY ( `to_student_id` ) REFERENCES `test_app_student` ( `id` ) 
      ) ENGINE = INNODB DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci
      
  2. 简易多对多

    class SchoolClass(models.Model):
        name = models.CharField(max_length=20)
    
    class Teacher(models.Model):
        name = models.CharField(max_length=10)
        school_class = models.ManyToManyField(SchoolClass)
    

    笔记:

    • 会自动生成一个中间表,DDL语句如下

      CREATE TABLE `test_app_teacher_school_class` (
        `id` int(11) NOT NULL AUTO_INCREMENT,
        `teacher_id` int(11) NOT NULL,
        `schoolclass_id` int(11) NOT NULL,
        PRIMARY KEY (`id`),
        UNIQUE KEY `test_app_teacher_school__teacher_id_schoolclass_i_f52c7361_uniq` (`teacher_id`,`schoolclass_id`),
        KEY `test_app_teacher_sch_schoolclass_id_7ac34d1e_fk_test_app_` (`schoolclass_id`),
        CONSTRAINT `test_app_teacher_sch_schoolclass_id_7ac34d1e_fk_test_app_` FOREIGN KEY (`schoolclass_id`) REFERENCES `test_app_schoolclass` (`id`),
        CONSTRAINT `test_app_teacher_sch_teacher_id_8c52afbd_fk_test_app_` FOREIGN KEY (`teacher_id`) REFERENCES `test_app_teacher` (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
      

      其中:

      test_app_teacher_school_class是表名: test_app是应用名, teacher是第一个模型名,school_class是第二个模型名

  3. 自定义中间表

    from django.db import models
    
    class Person(models.Model):
        first_name = models.CharField(max_length=50)
        last_name = models.CharField(max_length=50)
    
    class Group(models.Model):
        name = models.CharField(max_length=128)
        members = models.ManyToManyField(
            Person,
            through='Membership', # 必须是 类名的字符串 ,用 '' 包裹
            through_fields=('group', 'person'),
        )
    
    class Membership(models.Model):
        group = models.ForeignKey(Group, on_delete=models.CASCADE)
        person = models.ForeignKey(Person, on_delete=models.CASCADE)
        level = models.IntegerField(default=1)
    

    笔记:

    • 通过 through='Membership' 指定Membership作为中间表
    • 通过 through_fields=('group', 'person') 指定中间模型的属性
    • 一般需要自定义中间表时,都是有额外的字段,譬如 level = models.IntegerField(default=1)

6.5 方法

除了运行程序时,可以测试模型,还可以在根目录的cmd执行:

python manage.py shell

打开django脚本控制台,测试执行模型的方法,会比启动项目更方便

​ 模型对象中有一个objects属性,该属性是管理器Manager类型的对象,几乎所有的方法都是通过该对象执行的,具体见下面的代码:

6.5.1 新增 save 或 create
#运行之前导入模块
from moremore_app.models import *  #其中moremore_app是子应用名
p = Person.objects.create(first_name="Bruce", last_name="Springsteen")

或
p1 = Person(first_name="Bruce", last_name="Springsteen")
p1.save()

#一对多关系新增  方式一
#先新增一方
models.Account.objects.create(user_name='lili',email='[email protected]',password='123',signature='li')
#再创建多方对象
article = models.Article(title='对象关系映射',content='对象关系映射内容',pub_date='2021-7-6')
#设置外键的值
article.account_id=2  #其中account_id是数据库的字段   2是account表的主键值
#保存对象
article.save()
#一对多关系新增多方  方式二
account =  models.Account.objects.get(pk=3)  #查询account表主键为3的Account实体
models.Article.objects.create(title='暑假安排',content='好好学习',account=account,pub_date='2021-7-9')
#多对多关系新增
#先新增实体
models.Tag.objects.create(name='电影')
models.Tag.objects.create(name='科技') 
models.Tag.objects.create(name='教育')
#再创建关联
article.tags.set([1,2]) #执行后中间表中有数据
article.tags.set([3,])

#set是直接赋值  如果想在原有基础上新增则使用add
article.tags.add(1,2,3)

6.6 查询

  • 大部分检索是懒惰执行的,只在真实获取值的时候,才会去连接数据库获取数据
  • 查询集通过过滤器进行查询,允许链式调用
  • pk是主键的别名(primary key),如果真实主键是id,那么 pk 和 id 使用是一样的效果
  1. 过滤器

    # get 获取一个对象
    # 查询主键等于 1 的 , 如果主键是ID,也可以使用 id=1
    # 如果条件找不到对应的记录,会抛出 DoesNotExist 错误
    # 如果条件找到多个记录,会抛出 MultipleObjectsReturned 错误
    person = Person.objects.get(pk=1)
    
    # all 获取所有对象
    # 查询所有,得到的QuerySets对象
    all_persons = Person.objects.all()
    
    #过滤使用filter
    #查询user_name='lili'的Account
    Account.objects.filter(user_name='lili') 
    
    
  2. 字段查找

    • 字段检索,是在字段名后加 '__' 双下划线,再加关键字,类似 SQL 语句中的 where 后面的部分, 如: 字段名__关键字

    • 在查找中指定的字段必须是模型字段的名称,但有一个例外,如果是ForeignKey字段,则是属性名+ ‘_id’: Entry.objects.filter(blog_id=4) , 定义的 ForeignKey是 blog

    • 完整的字段检索文档:

      https://docs.djangoproject.com/en/2.2/ref/models/querysets/#field-lookups

      常见的字段检索:

      exact :判断是否等于value,一般不使用,而直接使用 '='

      contains:是否包含,大小写敏感,如果需要不敏感的话,使用icontains

      startswith:以value开头,大小写敏感

      endwith:以value结尾,大小写敏感

      in:是否包含在范围内

      isnull:是否为null, 如:filter(name__isnull=Flase)

      gt:大于,如:filter(sage__gt=30) , 年龄大于30

      gte:大于等于

      lt:小于

      lte:小于等于

6.6.1 确定的搜索 ,SQL: where id = 14

 Blog.objects.get(id__exact=14)
 # 等同于
 Blog.objects.get(id=14)
 
 # 不区分大小写的确定搜索,匹配 beatles blog 、Beatles blog等
 Blog.objects.get(name__iexact="beatles blog")
 
 # 包含,contains ,SQL:WHERE headline LIKE '%Lennon%'
 Entry.objects.get(headline__contains='Lennon')
 # 不区分大小写的包含
 Entry.objects.get(headline__icontains='Lennon')
 
 # 以什么开头, SQL: WHERE headline LIKE 'Lennon%'
 # 还有 不区分大小写的 istartwith
 Entry.objects.get(headline__startswith='Lennon')
 
 # 同样有 endswith ,SQL : WHERE headline LIKE '%Lennon'
 # 还有 不区分大小写的 iendswith
 Entry.objects.get(headline__endswith='Lennon')
 
 # in
 Entry.objects.get(headline__in=['Lennon', 'Terry'])
 
 #isnull
 Account.objects.filter(signature__isnull=True)

6.6.2 gt gte

Account.objects.filter(id__gt=1)
Account.objects.filter(id__gte=1)

6.6.3 gt gte

Account.objects.filter(id__gt=1)
Account.objects.filter(id__gte=1)

6.6.4 日期时间的过滤

year/month/day/week_day/hour/minute/second:时间查询,如: filter(pub_date__year=2015) 年份是2015的, filter(pub_date__day=15) 天数是15的

#时间可以直接使用gt gte lt lte
Account.objects.filter(register_date__gt='2021-7-1')

#__range查询某个时间段
Account.objects.filter(register_date__range=('2021-7-1','2021-7-7'))

#查询某年某月某日 __date
Account.objects.filter(register_date__date='2021-7-6')
<QuerySet [<Account: Account object (2)>, <Account: Account object (3)>]>

#查询某年 __year
# exclude 例外
# 查询 日期年份 不是2006的
persons = Person.objects.exclude(pub_date__year=2006)

# filter 获取对象列表
# 查询 日期年份 是 2006 的
persons = Person.objects.filter(pub_date__year=2006)

# filter 获取对象列表,支持切片,但是不支持负数切片
# limit  5 :前5个
persons = Person.objects.filter(pub_date__year=2006)[:5]
# limit  5,5 : 第6个到10个
persons = Person.objects.filter(pub_date__year=2006)[5:10]
# Entry.objects.all()[-1] 不支持

# 返回前10个记录的, 0 ,2 , 4, 6, 8, 10 ,并且会立刻执行,而不是懒惰执行
Entry.objects.all()[:10:2]

#查询某月 __month
Account.objects.filter(register_date__month=7)

#查询某天 __day
Account.objects.filter(register_date__year=2021,register_date__day=6)
Account.objects.filter(register_date__year=2021).filter(register_date__day=6)

#查询星期几__week_day  注意 from 1(sunday)to 7(saturday)
Account.objects.filter(register_date__week_day=3)

6.6.5 排序

# order_by() 对结果集排序
person_li = Person.objects.filter(pub_date__year=2006).order_by('pub_date')
# 支持多个字段, 类似 SQL:  order by pub_date, headline
person_li = Person.objects.filter(pub_date__year=2006).order_by('pub_date', 'headline')
#降序排序
person_li = Person.objects.filter(pub_date__year=2006).order_by('-pub_date')

6.6.6 其它方法

# count() 对结果集统计
count = Person.objects.filter(pub_date__year=2006).count()

#reverse() 对结果集进行反转 注意使用reverse必须先排序
#比如要取数据的最后10条,可以先反转再切片
Account.objects.all().order_by('id').reverse()[:1]



# first() 返回结果集的第一个对象
person = Person.objects.filter(pub_date__year=2006).first()
person = Person.objects.first()
# last() 返回结果集的最后一个对象
person = Person.objects.filter(pub_date__year=2006).last()

# values() 返回一个 字典对象 列表
person_dict_li = Person.objects.filter(pub_date__year=2006).values()

#values()传入字段
Account.objects.all().values('user_name')
<QuerySet [{'user_name': 'lili'}, {'user_name': 'tom'}, {'user_name': '李四'}]>

Account.objects.all().values('user_name','register_date')
<QuerySet [{'user_name': '李四', 'register_date': datetime.datetime(2021, 7, 1, 3, 18, 18, tzinfo=<UTC>)}, {'user_name': 'tom', 'register_date': datetime.datetime(2021, 7, 6, 1, 52, 54, 5896, tzinfo=<UTC>)}, {'user_name': 'lili', 'register_date': datetime.datetime(2021, 7, 6, 2, 10, 42, 927481, tzinfo=<UTC>)}]>

大部分检索是懒惰的,只在真实获取值的时候,才会真正去连接数据库获取数据:

# 懒惰执行
q = Entry.objects.filter(headline__startswith="What")
q = q.filter(pub_date__lte=datetime.date.today())
q = q.exclude(body_text__icontains="food")
print(q)   # 此处才会真的去连接数据库获取记录

# 返回前10个记录的, 0 ,2 , 4, 6, 8, 10 ,并且会马上执行,而不是懒惰执行
q = Entry.objects.all()[:10:2]  # 已经获取到数据了
print(q)

6.6.7 多对象关联查询

通过Blog模型中,关联的另一个模型对象entry的属性进行过滤:

PS: entry__headline__contains ,即使是访问模型对象entry的属性 headline,也必须使用 '__'

# 检索所有Blog具有至少一个对象Entry ,其headline包含'Lennon'
Blog.objects.filter(entry__headline__contains='Lennon')

# Blog中 有一个对象 entry  的 headline 中包含“Lennon”并且 是 2008年发布的
Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)
# 通过 , 分割多个条件, 相当于数据库中的 'and'
Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

# 取上面相反的值
Blog.objects.exclude(entry__in=Entry.objects.filter(headline__contains='Lennon', pub_date__year=2008, ))

一对一关系中,通过一个模型获取另一个模型:

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

class Restaurant(models.Model):
    place = models.OneToOneField(
        Place,
        on_delete=models.CASCADE,
        primary_key=True,
    )
    # BooleanField 在数据库使用 tinyint 类型
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)
    
    
# 通过 Place 查找 Restaurant
place = Place.objects.first()
restaurant = place.restaurant

# 通过 定义了 OneToOneField 的模型 Restaurant 查找 Place
restaurant = Restaurant.objects.first()
place = restaurant.place

一对多关系中,通过一个模型获取另一个模型:

from django.db import models

class Manufacturer(models.Model):
    name = models.CharField(max_length=20)

class Car(models.Model):
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
    
    name = models.CharField(max_length=20)
    
# 从 一的模型 查找 多的模型
# 通过 '多的模型小写名_set' 查找
manufacturer = Manufacturer.objects.first()
cars = manufacturer.car_set.all()

# 从 多的模型 查找 一的模型
car = Car.objects.first()
manufacturer = car.manufacturer

多对多关系中,通过一个模型获取另一个模型:

class SchoolClass(models.Model):
    name = models.CharField(max_length=20)

class Teacher(models.Model):
    name = models.CharField(max_length=10)
    school_class = models.ManyToManyField(SchoolClass)
    
# 从 没有写 ManyToManyField 的模型查找另一 写了 ManyToManyField 的模型
# 需要在 查询的模型名的小写后 加 _set
schoolClass = SchoolClass.objects.first()
teachers = schoolClass.teacher_set.all()

# 从 写了 ManyToManyField 的模型查找另一个模型
teacher = Teacher.objects.first()
schoolClasses = teacher.school_class.all()

6.6.8 聚合函数

使用aggregate()函数返回聚合函数的值

常用的聚合函数有:Avg、Count、Max、Min、Sum

# Max 找出最大的
from django.db.models import Max
Person.objects.aggregate(Max('age'))
# 结果是一个字典 {'age__max': 30}
# 可以使用 max=Max('age') 指定 别名为 max,而不使用  age__max
Person.objects.aggregate(max=Max('age'))

# 多个聚合函数一起使用
Person.objects.aggregate(Max('age'), Min('age'), Avg('age'))

6.6.9 分组查询

使用annotate()函数实现分组查询,得配合其他函数:

  • annotate:用于分组,配合 Avg,Count等聚合函数,如:annotate(max=Max('age'))
  • filter: 用于过滤,在 annotate之前使用表示 where 条件,在annotate之后使用表示having条件
  • values:在annotate之前使用表示分组字段,在annotate之后使用表示取值
# 基本应用
# 以 group_id 分组,找出level的最大值,最小值,和平均值
Membership.objects.values('group_id').annotate(max=Max('level'), min=Min('level'), avg=Avg('level'))

# 以 group_id 分组 并且 group_id 大于 2 ,找出level的最大值,最小值,和平均值
Membership.objects.values('group_id').annotate(max=Max('level'), min=Min('level'), avg=Avg('level')).filter(group_id__gt=2)
# 和下面这句等效
# 推荐使用下面这种方式
Membership.objects.filter(group_id__gt=2).values('group_id').annotate(max=Max('level'), min=Min('level'), avg=Avg('level'))

6.6.10 修改

#单条记录修改 save
p = Person.objects.get(pk=1)
p.first_name = 'James'
p.save()

obj = MyModel.objects.create(val=1)
# 需要使用F来保证不会出现并发冲突
from django.db.models import F
MyModel.objects.filter(pk=obj.pk).update(age=F('age') + 1)

# 更新多个值  update
Entry.objects.filter(pub_date__year='2007').update(headline='Everything is the same')

6.6.11 删除

delete方法

person = Person.objects.get(pk=1)
person.delete()

6.6.12 刷新对象

​ 通过 refresh_from_db 从数据库中重新获取对象的内容

person = Person.objects.get(pk=1)
person.refresh_from_db()

6.6.13 Q对象

​ filter() 等方法中的关键字参数查询都是并且('AND')的, 如果你需要执行更复杂的查询(例如or语句),那么可以使用Q 对象。

​ Q 对象 (django.db.models.Q) 对象用于封装一组关键字参数,可以使用 & 和 | 操作符组合起来,当一个操作符在两个Q 对象上使用时,它产生一个新的Q 对象。

from django.db.models import Q

# 等同于 select * from poll where question like 'Who%' or question like 'What%'
poll = Poll.objects.filter(Q(question__startswith='Who') | Q(question__startswith='What'))

# 等同于 select * from poll WHERE question like 'Who%' and (pub_date = '2005-05-02' or pub_date = '2005-05-06')
poll = Poll.objects.filter(
    Q(question__startswith='Who'),
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)

# Q对象可以使用 ~ 操作符取反, 相当于SQL中 not 
poll = Poll.objects.filter(
    Q(question__startswith='Who'),
    ~Q(pub_date__year=2005)
)

# Q对象可以和一般的关键字参数混用, 但是Q对象必须在一般关键字参数的前面
Poll.objects.filter(
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)), 	 		             question__startswith='Who'
)

6.6.14 F对象

​ 简单记忆:模型的属性名出现在操作符的右边,就使用F对象进行包裹

  • 可以用模型的A属性与B属性进行比较

    # 找出女生数量大于男生数量的年级
    # 对应sql:select * from grade where girlnum > boynum
    grades = Grade.objects.filter(girlnum__gt=F('boynum'))
    
  • 支持算术运算

    # 找出女生数量大于 男生数量+10 的年级
    # 对应的sql: select * from grade where girlnum > boynum + 10
    Grade.objects.filter(girlnum__gt=F('boynum') + 10)
    
    # 所有书籍的价格 +1
    # 对应的 sql: update book set price = price + 1 
    Book.objects.update(price=F("price")+1)
    

6.6.15 使用sql语句

  1. 通过模型使用sql:

    通过raw函数执行原始SQL语句进行查询,主键字段必须包含在查询的字段中,不然会引发错误 :

    # 定义个 person 模型
    class Person(models.Model):
        first_name = models.CharField()
        last_name = models.CharField()
        birth_date = models.DateField()
    
    # 执行 原始 SQL
    # 表名前面必须加 应用名myapp,即在数据库中的真实表名,否则会抛出异常
    for p in Person.objects.raw('SELECT * FROM myapp_person'):
        print(p)
    
    # 字段先后顺序没关系
    Person.objects.raw('SELECT id, first_name, last_name, birth_date FROM myapp_person')
    # 等同于
    Person.objects.raw('SELECT last_name, birth_date, first_name, id FROM myapp_person')
    
    # 可以从其他表格中查询出匹配 person 模型的记录集
    # 总之最终的数据集的结构必须和 Person一样
    Person.objects.raw('SELECT first AS first_name, last AS last_name,bd AS birth_date,pk AS id,FROM some_other_table')
    
    # 返回的结果集一样可以执行切片
    first_person = Person.objects.raw('SELECT * FROM myapp_person')[0]
    # 但是上述语句会返回所有结果,基于节省传输的需要,在数据库缩小结果集范围更正确
    first_person = Person.objects.raw('SELECT * FROM myapp_person LIMIT 1')[0]
    
    
    # 传递参数
    lname = 'Doe'
    Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = %s', [lname])
    
  2. 避开模型使用sql

    不应用模型,直接使用sql语句进行增删改查

    from django.db import connection
    
    def my_custom_sql(obj):
        with connection.cursor() as cursor:
            cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [obj.baz])
            cursor.execute("SELECT foo FROM bar WHERE baz = %s", [obj.baz])
            row = cursor.fetchone()
    
        return row
    

七、视图

7.1 简单应用

  1. FBV (Function base views)

    就是在视图里使用函数处理请求

    # urlconf 中
    urlpatterns = [
    	path('fbv/', views.current_datetime),
    ]
    
    # views 中
    from django.http import HttpResponse
    import datetime
    
    def current_datetime(request):
        now = datetime.datetime.now()
        html = "<html><body>It is now %s.</body></html>" % now
        return HttpResponse(html)
    
    • 视图函数 current_datetime,每个视图函数都将一个HttpRequest 对象作为其第一个参数,该参数通常被命名request
    • 视图函数的名称无关紧要,它不必以某种方式命名,以便Django能够识别它,但是函数命名一定要能够清晰的描述它的功能
    • 视图函数返回一个HttpResponse响应的对象,每个视图函数负责返回一个HttpResponse对象(有例外,但我们在稍后讨论)
  2. CBV (class base views)

    就是在视图里使用类处理请求

    # urlconf 中
    urlpatterns = [
        # 一定要使用 as_view() ,记住 小括号
    	path('cbv/', views.MyView.as_view()),
    ]
    
    # views中
    from django.http import HttpResponse
    from django.views import View
      
    class MyView(View):
    
        def get(self, request):
            return HttpResponse('get OK')
    
        def post(self, request):
            return HttpResponse('post OK')
    
    • CBV提供了一个as_view()静态方法(也就是类方法),调用这个方法,会创建一个类的实例,然后通过实例调用dispatch()方法,dispatch()方法会根据request的method的不同调用相应的方法来处理request(如get(),post()等)
    • 提高了代码的复用性,可以使用面向对象的技术,比如Mixin(多继承)
    • 可以用不同的函数针对不同的HTTP方法处理,而不是通过很多 if 判断,可以提高代码可读性

7.2 返回错误响应

# 使用 HttpResponseNotFound
def my_view(request):
    # ...
    if foo:
        return HttpResponseNotFound('<h1>Page not found</h1>')
    else:
        return HttpResponse('<h1>Page was found</h1>')


# 还可以直接返回状态码
from django.http import HttpResponse

def my_view(request):
    # ...
    # Return a "created" (201) response code.
    return HttpResponse(status=201)


# 特殊的 404  错误
from django.http import Http404
from django.shortcuts import render
from polls.models import Poll

def detail(request, poll_id):
    try:
        p = Poll.objects.get(pk=poll_id)
    except Poll.DoesNotExist:
        raise Http404("Poll does not exist")
    return render(request, 'polls/detail.html', {'poll': p})

7.3 视图装饰器

@require_http_methods,要求视图只接收指定的http方法

@require_GET():仅仅允许GET方法

@require_POST():仅仅允许POST方法.

@require_safe():仅仅允许GET和HEAD方法

from django.views.decorators.http import require_http_methods

# 允许 GET和POST方法, 默认就是所有方法都支持
@require_http_methods(["GET", "POST"])
def my_view(request):
    # I can assume now that only GET or POST requests make it this far
    # ...
    pass

@login_required : 必须登录才能访问装饰的视图函数

用户未登录,则重定向到settings.LOGIN_URL,除非指定了login_url参数,例如:@login_required(login_url='/polls/login/')

@login_required
def my_view(request):
    # I can assume now that only GET or POST requests make it this far
    # ...
    pass

7.4 请求与响应

  1. HttpRequest

    每一个用户请求在到达视图函数的同时,django会自动创建一个HttpRequest对象并把这个对象当做第一个参数传给要调用的views方法。HttpRequest对象里封装了本次请求所涉及的用户浏览器端数据、服务器端数据等,在views里可以通过request对象来调用相应的属性。

    所有视图函数的第一个参数都是HttpRequest实例

    属性(除非另有说明,否则所有属性均应视为只读):

    • HttpRequest.scheme:

      表示请求使用的协议(http或https)

    • HttpRequest.body:

      原始HTTP请求主体,类型是字节串。处理数据一些非html表单的数据类型很有用,譬如:二进制图像,XML等;

      • 取form表单数据,请使用 HttpRequest.POST

      • 取url中的参数,用HttpRequest.GET

    • HttpRequest.path:

      表示请求页面的完整路径的字符串,不包括scheme和域名。

      例: "/music/bands/the_beatles/"

    • HttpRequest.path_info:

      在某些Web服务器配置下,主机名后的URL部分被分成脚本前缀部分和路径信息部分。path_info无论使用什么Web服务器,该属性始终包含路径的路径信息部分。使用此代替path可以使代码更容易在测试和部署服务器之间移动。

      例如,如果WSGIScriptAlias你的应用程序设置为 "/minfo",则path可能是"/minfo/music/bands/the_beatles/" , path_info 会是 "/music/bands/the_beatles/"。

    • HttpRequest.method:

      表示请求中使用的HTTP方法的字符串,是大写的。例如:

      if request.method == 'GET':
          do_something()
      elif request.method == 'POST':
          do_something_else()
      
    • HttpRequest.encoding:

      表示当前编码的字符串,用于解码表单提交数据(或者None,表示使用该DEFAULT_CHARSET设置)。

      可以设置此属性来更改访问表单数据时使用的编码,修改后,后续的属性访问(例如读取GET或POST)将使用新encoding值。

    • HttpRequest.content_type:

      表示请求的MIME类型的字符串,从CONTENT_TYPE解析 。

    • HttpRequest.content_params:

      包含在CONTENT_TYPE 标题中的键/值参数字典。

    • HttpRequest.GET:

      包含所有给定的HTTP GET参数的类似字典的对象。请参阅QueryDict下面的文档。

    • HttpRequest.POST:

      包含所有给定HTTP POST参数的类似字典的对象,前提是请求包含表单数据。请参阅QueryDict文档。POST不包含文件信息,文件信息请见FILES。

    • HttpRequest.COOKIES:

      包含所有Cookie的字典,键和值是字符串。

    • HttpRequest.FILES:

      包含所有上传文件的类似字典的对象

    • HttpRequest.META:

      包含所有可用HTTP meta的字典

    中间件设置的属性

    ​ Django的contrib应用程序中包含的一些中间件在请求中设置了属性。如果在请求中看不到该属性,请确保使用了相应的中间件类MIDDLEWARE。

    • HttpRequest.session:

      来自SessionMiddleware:代表当前会话的可读写字典对象。

    • HttpRequest.site:

      来自CurrentSiteMiddleware: 代表当前网站的实例Site或 RequestSite返回get_current_site()

    • HttpRequest.user:

      来自AuthenticationMiddleware:AUTH_USER_MODEL代表当前登录用户的实例

  2. QueryDict

    在一个 HttpRequest对象中,GET和 POST属性是django.http.QueryDict实例,该对象定义为相同的键的多个值的字典状类,继承自MultiValueDict, 而MultiValueDict则继承自dict

    qd = request.GET
    
    # QueryDict('a=1&a=2&c=3')
    #<QueryDict: {'a': ['1', '2'], 'c': ['3']}>
    
    # 获取属性c
    print(qd['c'])
    
    # 有多个值的key,会获取最后一个值
    print(qd['a'])  # 打印 2
    
    # 获取key a 对应的所有value:['1', '2']
    print(qd.getlist('a'))
    

    QueryDict.items()

    等同于 Dict的items,但是同一个key,多个值时,最后一个值起作用

    qd = QueryDict('a=1&a=2&a=3&b=4')
    print(list(qd.items()))  # 打印 [('a', '3'), ('b', '4')]
    
  3. HttpResponse

    返回给浏览器端的响应对象

    属性:

    • HttpResponse.content:

      表示响应的字符串

    • HttpResponse.charset:

      表示响应将被编码的字符集,如果在HttpResponse实例化时没有给出,则会从中提取 content_type,如果没有设置content_type,则使用settings.DEFAULT_CHARSET

    • HttpResponse.status_code:

      该响应的 HTTP 状态码

    • HttpResponse.reason_phrase:

      响应的HTTP原因描述语,使用HTTP标准的默认原因描述语
      除非明确设置,否则reason_phrase由 status_code 决定。

    • HttpResponse.streaming:

      总是False,中间件通过此属性可以区分流式响应与常规响应

    • HttpResponse.closed:

      如果response已经结束,则返回True,否则返回False

  4. JsonResponse

    包含json格式内容的响应

    from django.http import JsonResponse
    
    response = JsonResponse({'foo': 'bar'})
    
    print(response.content)  # 打印  b'{"foo": "bar"}'
    
    
  5. FileResponse

    文件内容的响应

    from django.http import FileResponse
    
    path = os.getcwd() + '/' + 'myfile.png'  # os.getcwd() 得到的是django项目根目录
    response = FileResponse(open(path, 'rb'))
    

7.5 快捷方式

  1. render 方法:

    ​ 必需的参数:

    ​ request:用于生成此响应的请求对象。

    ​ template_name:要使用的模板的全名或模板名称的列表。

    ​ 可选参数:

    ​ context:要添加到模板上下文的值的字典。默认情况下,这是一个空字典。如果字典中的值是可调用的,视图将在渲染模板之前调用它。

    ​ content_type:用于生成文档的MIME类型。默认为DEFAULT_CONTENT_TYPE设置的值。

    ​ status:响应的状态码。默认为200。

    ​ using:用于加载模板的模板引擎名。

    from django.shortcuts import render
    
    def my_view(request):
        # View code here...
        return render(request, 'myapp/index.html', {
            'foo': 'bar',
        }, content_type='application/xhtml+xml')
    
    # 等同于
    from django.http import HttpResponse
    from django.template import loader
    
    def my_view(request):
        # View code here...
        t = loader.get_template('myapp/index.html')
        c = {'foo': 'bar'}
        return HttpResponse(t.render(c, request), content_type='application/xhtml+xml')
    
  2. redirect 方法

    1. 通过传递一些对象, 该对象的 get_absolute_url()方法将被调用以找出重定向URL:

      from django.shortcuts import redirect
      
      def my_view(request):
          ...
          object = MyModel.objects.get(...)
          return redirect(object)
      
    2. 通过传递一个URLConf调度器中配置path或re_path的名称,以及可选的一些位置或关键字参数,该URL将使用reverse()方法反向解析 :

      def my_view(request):
          ...
          return redirect('polls:index', foo='bar')
      
    3. 通过传递硬编码的URL重定向:

      # 使用绝对路径
      def my_view(request):
          ...
          return redirect('/some/url/')
      
      # 可以使用 ./ 和 ../ 的相对路径
      def my_view(request):
          return redirect('../../blog/')
      
      
    4. 默认情况下,redirect()返回一个临时重定向(302)。所有上述形式都接受permanent参数; 如果设置为True永久重定向(301)将被返回:

      def my_view(request):
          ...
          object = MyModel.objects.get(...)
          return redirect(object, permanent=True)
      
  3. get_object_or_404,get_list_or_404

    ​ 必需的参数:

    ​ klass:一Model类或一个Manager或QuerySet从哪个实例来获取对象

    ​ **kwargs:查找参数,应该采用get()或filter()的参数。

    from django.shortcuts import get_object_or_404
    
    def my_view(request):
        poll = get_object_or_404(MyModel, pk=1)
    
    #等同于
    def my_view(request):
        try:
            poll = Poll.objects.get(pk=1)
        except Poll.DoesNotExist:
            raise Http404()
    

7.6 内置通用视图

包含在 from django.views import generic 中

​ 具体视图类型有:

__all__ = [
    'View', 'TemplateView', 'RedirectView', 'ArchiveIndexView',
    'YearArchiveView', 'MonthArchiveView', 'WeekArchiveView', 'DayArchiveView',
    'TodayArchiveView', 'DateDetailView', 'DetailView', 'FormView',
    'CreateView', 'UpdateView', 'DeleteView', 'ListView', 'GenericViewError',
]

​ 比较常用的:RedirectView、DetailView、ListView

​ 用法:

DetailView:

# urls中:
path('<int:pk>/', views.MyDetailView.as_view(), name='detail'),

# views:
class MyDetailView(generic.DetailView):
    """
        使用通用的详情视图
    """
    # 对应的模型对象
    model = Question
    # 使用的模板名
    template_name = 'detail.html'

# template(detail.html):   
<h1>{{ object.question_text }}</h1>

八、模板

作为一个Web框架,Django需要一种方便的方式来动态生成HTML。最常用的方法依赖于模板。模板包含所需HTML输出的静态部分以及描述如何插入动态内容的特殊语法。

​ 对模板引擎的一般支持和Django模板语言的实现都存在于 django.template 命名空间中

8.1 django默认模版

1、配置

​ 在settings中配置:

# 配置模板
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',
            ],
        },
    },
]

笔记:

  • BACKEND:是实现Django模板后端API的模板引擎类的路径。内置是django.template.backends.django.DjangoTemplates和 django.template.backends.jinja2.Jinja2(使用这个需要额外安装jinja2库)
  • DIRS :按搜索顺序定义引擎应该查找模板源文件的目录列表
  • APP_DIRS:告诉引擎是否应该在已安装的应用程序中查找模板,每个后端为其模板应存储在的应用程序内的子目录定义一个常规名称。
  • OPTIONS:包含后端特定的设置

2、用法

​ 假如settings中配置如下:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            '/home/html/example.com',
            '/home/html/default',
        ],
    },
    {
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'DIRS': [
            '/home/html/jinja2',
        ],
    },
]

  1. get_template

    该函数使用给定名称加载模板并返回一个 Template对象,找到第一个匹配的模板即返回

    template = get_template('story_detail.html')
    

    Django将要查找的文件,依次为:

    • /home/html/example.com/story_detail.html('django'引擎)
    • /home/html/default/story_detail.html('django'引擎)
    • /home/html/jinja2/story_detail.html('jinja2'引擎)

    示例:

    # 最常用的render
    from django.shortcuts import render
    
    def my_view(request):
        # View code here...
        return render(request, 'myapp/index.html', {
            'foo': 'bar',
        }, content_type='application/xhtml+xml')
    
    
    # 相当于基础的get_template:
    from django.http import HttpResponse
    from django.template import loader
    
    def my_view(request):
        # View code here...
        template = loader.get_template('myapp/index.html')
        context = {'foo': 'bar'}
        # 注意,这个 render 和 快捷键 render 不是一个对象
        return HttpResponse(template.render(context, request), content_type='application/xhtml+xml')
    
    
  2. select_template

    select_template() 用法类似 get_template() ,除了它需要一个模板名称的列表。它按顺序尝试每个名称并返回存在的第一个模板

    template = select_template(['story_253_detail.html','story_detail.html'])
    

    Django将要查找的文件,依次为:

    • /home/html/example.com/story_253_detail.html('django'引擎)
    • /home/html/default/story_253_detail.html('django'引擎)
    • /home/html/jinja2/story_253_detail.html('jinja2'引擎)
    • /home/html/example.com/story_detail.html('django'引擎)
    • /home/html/default/story_detail.html('django'引擎)
    • /home/html/jinja2/story_detail.html('jinja2'引擎)

3、模板语言

​ Django模板只是一个文本文档或使用Django模板语言标记的Python字符串。一些结构被模板引擎识别和解释,主要的是变量( {{ 变量 }} )和标签( {% 标签 %} )。

1、变量

​ 一个变量从上下文中输出一个值,这是一个类似字典的对象将键映射到值。

​ 变量被这样包围 :{{变量}} 譬如:

# 模板中这样写:
My first name is {{ first_name }}. My last name is {{ last_name }}.

# 传入到 模板中的变量为:
{'first_name': 'John', 'last_name': 'Doe'}

# 在浏览器中显示如下:
My first name is John. My last name is Doe.

​ 字典查找,属性查找和列表索引查找 都用点符号实现:

# 获取字典的 value
{{ my_dict.key }}

# 获取对象的 属性
{{ my_object.attribute }}

# 获取列表的 元素
{{ my_list.0 }}

2、标签

​ 标签在渲染过程中提供任意的逻辑。

​ 例如,标签可以输出内容,用作控制结构,

​ 例如“if”语句或“for”循环,从数据库中获取内容,甚至可以访问其他模板标签。

​ 所有标签见:

https://docs.djangoproject.com/en/2.2/ref/templates/builtins/#ref-templates-builtins-tags

  1. 基本用法

    标签被包围 {% 代码 %} 譬如:

    # 无参数标签 csrf_token ,这个标签是用于html进行 form 表单提交时,包含一个 随机变化的字符串,在html页面中,其实就是一个 input type='hidden'
    # 作用是用于防止跨域攻击
    {% csrf_token %}
    
    # 传参的标签 cycle  , 参数 'odd' 'even' ,空格间隔
    # 标签本身也支持 关键字参数 {% 标签 key1='value' key1='even' %}
    {% cycle 'odd' 'even' %}
    
  2. 常见标签

    由于在模板中,没有办法通过代码缩进判断代码块,所以控制标签都需要有结束的标签

    1. if

      判断标签 if  endif :

      # athlete_list 不为空
      {% if athlete_list %}
      	# 输出 athlete_list 的长度    | 是过滤器
          Number of athletes: {{ athlete_list|length }}
      {% elif athlete_in_locker_room_list %}
          Athletes should be out of the locker room soon!
      {% else %}
          No athletes.
      {% endif %}
      
      
    2. firstof

      输出不是False的第一个参数,所有参数都为False,则什么都不输出

      {% firstof var1 var2 var3 %}
      等同于
      {% if var1 %}
          {{ var1 }}
      {% elif var2 %}
          {{ var2 }}
      {% elif var3 %}
          {{ var3 }}
      {% endif %}
      
      使用默认值:
      {% firstof var1 var2 var3 "默认值" %}
      
      
    3. for

      循环遍历,for endfor结束 ,可以遍历列表,字典等

      <ul>
      {% for athlete in athlete_list %}
          <li>{{ athlete.name }}</li>
      {% endfor %}
      </ul>
      
      反向循环:
      {% for athlete in athlete_list reversed%}
      
      字典:  data.items 这种调用方法,是不加 () 的
      {% for key, value in data.items %}
          {{ key }}: {{ value }}
      {% endfor %}
      
      设置默认值, for empty  ,类似python的  for else:
      <ul>
      {% for athlete in athlete_list %}
          <li>{{ athlete.name }}</li>
      {% empty %}
          <li>Sorry, no athletes in this list.</li>
      {% endfor %}
      </ul>
      等同于,但是比下面的写法更加简便:
      <ul>
        {% if athlete_list %}
          {% for athlete in athlete_list %}
            <li>{{ athlete.name }}</li>
          {% endfor %}
        {% else %}
          <li>Sorry, no athletes in this list.</li>
        {% endif %}
      </ul>
      
      

      for循环在循环中设置了许多变量:

      变量 描述
      forloop.counter 循环的当前迭代(1索引)
      forloop.counter0 循环的当前迭代(0索引)
      forloop.revcounter 循环结束时的迭代次数(1索引)
      forloop.revcounter0 循环结束时的迭代次数(0索引)
      forloop.first 如果这是通过循环的第一次,则为真
      forloop.last 如果这是通过循环的最后一次,则为真
      forloop.parentloop 对于嵌套循环,这是围绕当前循环的循环
    4. 布尔运算符

      and、or和not

      {% if athlete_list and coach_list %}
          Both athletes and coaches are available.
      {% endif %}
      
      {% if not athlete_list %}
          There are no athletes.
      {% endif %}
      
      {% if athlete_list or coach_list %}
          There are some athletes or some coaches.
      {% endif %}
      
      {% if not athlete_list or coach_list %}
          There are no athletes or there are some coaches.
      {% endif %}
      
      {% if athlete_list and not coach_list %}
          There are some athletes and absolutely no coaches.
      {% endif %}
      
      

      允许在同一个标签中使用两个and和or子句, and优先级高于or例如:

      {% if athlete_list and coach_list or cheerleader_list %}
      将被解释为:
      if (athlete_list and coach_list) or cheerleader_list
      
      

      但是在if标签中使用实际的括号是无效的语法。如果你需要它们来表示优先级,你应该使用嵌套if标签

    5. 逻辑运算符

      ==, !=, <, >, <=, >=, in, not in, is, 和 is not

      {% if somevar == "x" %}
        This appears if variable somevar equals the string "x"
      {% endif %}
      
      {% if "bc" in "abcdef" %}
        This appears since "bc" is a substring of "abcdef"
      {% endif %}
      
      {% if somevar is not True %}
        This appears if somevar is not True, or if somevar is not found in the
        context.
      {% endif %}
      
      

      优先级,从低到高:

      • or
      • and
      • not
      • in
      • ==,!=,<,>,<=,>=
    6. url

      给定视图和可选参数匹配的绝对路径引用(没有域名的URL)

      {% url 'some-url-name' v1 v2 %}
      第一个参数是url模式名称,后面跟着的是参数,以空格分隔
      
      可以使用关键字:
      {% url 'some-url-name' arg1=v1 arg2=v2 %}
      
      如果您想检索命名空间的URL,请指定完全限定的名称:
      {% url 'myapp:view-name' %}
      
      
    7. widthratio:乘法和除法

      {% widthratio this_value max_value max_width %}
      结果是: this_value/max_value*max_width
      如果要计算  value * 10 : widthratio value 1 10
      如果要计算 value / 8  : widthratio 8 1
      还可以这样使用:
      
      {% widthratio this_value max_value max_width as width %}
      The width is: {{ width }}
      
      
    8. with

      简单的名称缓存复杂变量,当访问多次耗时的方法(例如操作数据库的方法):

      {% with total=business.employees.count%}
          {{ total }} employee
      {% endwith %}
      
      上述的:{% with total=business.employees.count%}
          等同于:{% with business.employees.count as total %}
      
      
    9. 过滤器

      通过 | 来使用

      示例:{{ value|add:"2" }}

      value: 是模板中获取的对象

      add: 是过滤器

      “2”: 是过滤器add的参数,只支持一个参数

      length:messages|length这里判断 messages 不为空,并且长度大于等于100

      {% if messages|length >= 100 %}
         You have lots of messages today!
      {% endif %}
      

      add:相加(减法:|add:-2 ) , 可以应用于 列表等

      {{ value|add:"2" }}
      如果value是4,那么输出6
      
      {{ first|add:second }}
      如果first=[1,2,3] ,second=[4,5,6],那么输出:[1,2,3,4,5,6]
      
      

      divisibleby:能否整除,返回 True和False

      {{ value|divisibleby:"2" }}
      

      addslashes:单引号前加 \

      {{ value|addslashes }}
      value="I'm using Django",输出:"I\'m using Django".
      
      

      capfirst:首字母大写,只对字符串的第一个单词的首字母大写

      {{ value|capfirst }}
      如果value是"django is good",输出将是"Django is good"。
      
      

      center:将值置于给定宽度的字段中

      "{{ value|center:"15" }}"
      如果value是"Django",输出将是。"     Django    ",前面5个,后面4个空格
      
      

      cut:删除给定字符

      {{ value|cut:" " }}
      如果value是"String with spaces",输出将是:"Stringwithspaces"
      
      

      date:日期字符串

      格式字符  	描述	示例              输出
      b	月,文字,3个字母,小写。	'jan'
      d	每月的一天,2位数字前导零。	'01' 至 '31'
      D	星期几,文字,3个字母。	'Fri'
      e	时区名称。可能是任何格式,或者可能会返回一个空字符串,具体取决于日期时间。	'','GMT','-500','US/Eastern',等。
      E	月,通常用于长日期表示的区域设置特定备选表示。	'listopada'(对于波兰语区而言'Listopad')
      f	时间在12小时和分钟之内,如果它们为零,则分钟时间不再。专有扩展。	'1', '1:30'
      F	月,文字,长。	'January'
      g	小时,12小时制,无前导零。	'1' 至 '12'
      G	小时,24小时制,无前导零。	'0' 至 '23'
      h	小时,12小时制。	'01' 至 '12'
      H	小时,24小时制。	'00' 至 '23'
      i  	分钟。	'00' 至 '59'
      I 	夏令时,无论是否有效。	'1'  or  '0'
      J	没有前导零的月份的一天。	'1' 至 '31'
      n	月没有前导零。	'1' 至 '12'
      N   月份             'Jan.', 'Feb.', 'March', 'May'
      s    秒                  00-59
      t	给定月份的天数。	28 至 31
      T	这台机器的时区。	'EST', 'MDT'
      u	微秒。	000000 至 999999
      U	Unix时代以来的秒数(1970年1月1日00:00:00 UTC)。	 
      w ^	没有前导零的数字。	'0'(星期日)至'6'(星期六)
      W ^	ISO-8601周数,周数从周一开始。	1, 53
      y	年,2位数字。	'99'
      Y	年,4位数字。	'1999'
      z	一年中的一天。	0 至 365
      Z	以秒为单位的时区偏移量。UTC以西时区的偏移总是负值,而UTC以东的偏移总是正值。	-43200 至 43200
      
      
      {{ value|date:"D d M Y" }}
      {{ value|date:"Y-m-d H:i:s" }}
      value:datetime.datetime.now()
      
      
    10. include

    包含另外一个模板

    模板名称可以是变量,也可以是单引号或双引号的硬编码(带引号)的字符串

    {% include "foo/bar.html" %}
    
    {% include template_name %}
    该变量也可以是任何具有render()接受上下文的方法的对象
    
    

    include传参数:

    ​ 下面这个例子在页面中显示:"Hello, John!"

    • context:

      context = {'greeting': 'hello', 'person': 'John'
      
    • name_snippet.html模板:

      {{ greeting }}, {{ person|default:"friend" }}!
      
    • 原模板通过with传递参数

      {% include "name_snippet.html" with person="John" greeting="Hello" %}
      

      如果只想使用include传递的参数(甚至没有参数,不使用父模板的上下文变量)渲染模板,使用only:

      {% include "name_snippet.html" with greeting="Hi" only %}
      

4、静态文件

​ 主要使用 css、javascript和图片文件

  1. settings中配置

    # staticfiles 只在 DEBUG=True 的情况下有效,生产环境中使用 服务器(如nginx)的静态文件管理
    INSTALLED_APPS = [
        'django.contrib.staticfiles',
    ]
    
    # "/static/" or "http://static.example.com/" 这种url访问的请求都会交给 staticfiles 处理
    STATIC_URL = '/static/'
    
    # 指定静态文件保存的目录,默认是各个子应用下的 static 文件夹
    # 以下是 linux系统, windows下使用: "C:/Users/user/mysite/extra_static_content"
    STATICFILES_DIRS = [
        "/home/special.polls.com/polls/static",
        "/home/polls.com/polls/static",
        "/opt/webfiles/common",
    ]
    
  2. 在子应用下建立目录 static, 存放 静态文件

  3. 模板文件中

    {% load static %}  # 写在模板中第一行
    
    # 图片
    <img src="{% static "images/hi.jpg" %}" alt="Hi!">
    
    # css
    <link rel="stylesheet" href="{% static 'css/mystyle.css' %}" type="text/css" media="screen">
    
    # javascript
    <script type='text/javascript' src='{% static 'js/myjs.js' %}'></script>
    
    # 设置变量
    {% static "images/hi.jpg" as myphoto %}
    <img src="{{ myphoto }}">
    
  4. get_static_prefix

    在模板中指定 settings 中配置的 STATIC_URL

    {% load static %}
    
    <img src="{% get_static_prefix  %}images/hi.jpg" alt="Hi!">
    # 等同于
    <img src="{% static "images/hi.jpg" %}" alt="Hi!">
    

5、模板继承

​ Django模板引擎中最强大,也是最复杂的部分是模板继承。模板继承允许构建一个基本“骨架”模板,其中包含您网站的所有常见元素,并定义子模板可以覆盖的 blocks
​ 如下,定义一个base.html, 如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="style.css" />
    <title>{% block title %}My amazing site{% endblock %}</title>
</head>

<body>
    <div id="sidebar">
        {% block sidebar %}
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/blog/">Blog</a></li>
        </ul>
        {% endblock %}
    </div>
    <div id="content">
        {% block content %}{% endblock %}
    </div>
</body>
</html>

上面这个模板中,定义了3个 block :

  • title
  • sidebar
  • content

block的基本使用方法:{% block title %} {% endblock %}

​ 子页面detail.html继承上面这个base.html,内容如下:

{% extends "base.html" %}

{% block title %}My amazing blog{% endblock %}

{% block content %}
{% for entry in blog_entries %}
    <h2>{{ entry.title }}</h2>
    <p>{{ entry.body }}</p>
{% endfor %}
{% endblock %}

​ 使用{% extends "base.html" %} 继承 base.html,本页面有2个block,对应base.html的2个block,会在base.html中,使用本detail.html的对应内容进行替换,而block sidebar 并没有对应的block,因此继续使用base.html的 sidebar 的内容

​ 继承的技巧:

  • 如果在模板中使用,{% extends %}必须是该模板中的第一个模板标签。否则模板继承将不起作用。
  • 基本模板中的越多{% block %}标签越好。子模板不必定义所有父块,因此可以在多个块中填写合理的默认值
  • 如果发现自己在多个模板中复制了内容,则可能意味着您应该将该内容移至父模板的 {% block %}
  • 如果需要从父模板中获取块的内容,则使用 {{ block.super }}
{% block sidebar %}
    {{ block.super }}
    新的内容
{% endblock %}

  • 在使用模板标签语法之外创建的变量不能在块{% block% }内使用。例如,该模板不会呈现任何内容
{% trans "Title" as title %}
{% block content %}{{ title }}{% endblock %}

  • 为了增加可读性,您可以选择为您的结束标签 命名。例如:{% endblock content %}
{% block content %}

{% endblock content %}

九、admin后台

Django框架提供了一个自动化后台管理功能,对网站数据的后台维护,仅仅需要进行非常简单的配置和编写极少的代码即可实现。

9.1 配置

settings.py中:

INSTALLED_APPS = [
    'django.contrib.admin',
]

如果需要实现后台管理的中文显示,则修改以下配置:

LANGUAGE_CODE = 'zh-Hans'
USE_I18N = True

9.2 URL 路由

​ urls.py中:

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

9.3 创建管理员账户

创建管理员账户之前,确保项目的数据库已经正确连接,并且已经将admin应用的模型进行了迁移

​ 另外,如果settings中配置了 AUTH_PASSWORD_VALIDATORS,那么会对用户名和密码进行对应的检测,譬如:不能太简单,不能是纯数字等

  1. 项目根目录下 cmd 命令窗口运行以下命令:

    python manage.py createsuperuser

  2. 输入用户名并按回车

    Username: admin

  3. 提示 电子邮件地址

    Email address: [email protected]

  4. 最后一步是输入密码,将被要求输入两次密码,第二次作为第一次确认:

    Password: **********
    
    Password (again): *********
    
    Superuser created successfully.
    

9.4 基本模型管理

  1. 建立模型

    在model.py中

    class Student(models.Model):
        name = models.CharField(max_length=20)
        age = models.IntegerField()
    
  2. 添加模型管理

    在admin.py中

    from django.contrib import admin
    
    from .models import Student
    
    admin.site.register(Student)
    
    
  3. 在浏览器中访问

    访问path为 /admin, 譬如:http://127.0.0.1:8000/admin

  4. 进一步完善显示

    class Student(models.Model):
    	
        # 增加 verbose_name
        name = models.CharField(max_length=20, verbose_name='姓名')
        # 增加 help_text
        age = models.IntegerField(help_text='大于18', verbose_name='年龄')
    
        # 增加 Meta 类
        class Meta:
    	    # verbose_name_plural : 复数形式
            verbose_name_plural = verbose_name = '学生'
    
        # 模型类的 字符串化
        def __str__(self):
            return f'{self.name}({self.pk})'
    

    在子应用下的apps.py中:

    from django.apps import AppConfig
    
    
    class PersonConfig(AppConfig):
        default_auto_field = 'django.db.models.BigAutoField'
        name = 'person'
    
  5. 特殊下拉属性管理

    class Student(models.Model):
    	
        SEX_CHOICES = ((1,'男')), (2, '女')
        
        # 增加 verbose_name
        name = models.CharField(max_length=20, verbose_name='姓名')
        # 增加 help_text
        age = models.IntegerField(help_text='大于18', verbose_name='年龄')
    	
        # 修改已有模型,增加新字段的话,都需要设置默认值或者设置 null=True
        sex = models.IntegerField(choices=SEX_CHOICES, default=1, verbose_name='性别')
        
        # 增加 Meta 类
        class Meta:
    	    # verbose_name_plural : 复数形式
            verbose_name_plural = verbose_name = '学生'
    
        # 模型类的 字符串化
        def __str__(self):
            return f'{self.name}({self.pk})'
    

9.5 关系模型管理

  1. 建立模型

    class Place(models.Model):
        name = models.CharField(max_length=50, verbose_name='地名')
        address = models.CharField(max_length=80, verbose_name='门牌')
    
        def __str__(self):
            return f'{self.name}-{self.address}'
    
        class Meta:
    
            verbose_name_plural = verbose_name = '地址'
    
    class Restaurant(models.Model):
        place = models.OneToOneField(
            Place,
            on_delete=models.CASCADE,
            primary_key=True,
            verbose_name='地址'
        )
    
        name = models.CharField(max_length=50, verbose_name='名字')
        # BooleanField 在数据库使用 tinyint 类型
        serves_hot_dogs = models.BooleanField(default=False, verbose_name='经营热狗')
        serves_pizza = models.BooleanField(default=False, verbose_name='经营披萨')
    
        def __str__(self):
            return self.name
    
        class Meta:
    
            verbose_name_plural = verbose_name = '餐馆'
    
    class Waiter(models.Model):
        restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE, verbose_name='餐馆')
        name = models.CharField(max_length=50, verbose_name='姓名')
    
        def __str__(self):
            return self.name
    
        class Meta:
    
            verbose_name_plural = verbose_name = '服务员'
    
    class SchoolClass(models.Model):
        name = models.CharField(max_length=20, verbose_name='班级名')
    
        class Meta:
    
            verbose_name_plural = verbose_name = '班级'
    
        def __str__(self):
            return self.name
    
    class Teacher(models.Model):
        name = models.CharField(max_length=10, verbose_name='姓名')
        school_class = models.ManyToManyField(SchoolClass, verbose_name='班级')
    
        class Meta:
    
            verbose_name_plural = verbose_name = '老师'
    
        def __str__(self):
            return self.name
    
  2. 添加模型管理

    admin.site.register(Place)
    admin.site.register(Restaurant)
    admin.site.register(Waiter)
    admin.site.register(Teacher)
    admin.site.register(SchoolClass)
    

9.6 自定义模型管理类

之前使用的 admin.site.register(Student) 是用的 Django 默认的管理类,也可以自定义:

class StudentAdmin(admin.ModelAdmin):
    pass

admin.site.register(Student, StudentAdmin)

自定义管理类的属性:

class StudentAdmin(admin.ModelAdmin):
	
    # 显示的属性列表, 值是 属性名
    list_display = ['name', 'age']
    # 排序的 属性 列表 , 默认是升序,如果需要降序:['-age']
    ordering = ['age']

admin.site.register(Student, StudentAdmin)

使用装饰器注册

# 该装饰器使用的是 admin.register , 不是 admin.site.register
@admin.register(Student)
class StudentAdmin(admin.ModelAdmin):
	
    list_display = ['name', 'age']
    ordering = ['age']

自定义模型管理类的属性:

@admin.register(Student)
class StudentAdmin(admin.ModelAdmin):
    
    # 属性为空时,在网页上显示的内容,默认是: -
    empty_value_display = '-empty-'
    # 管理的字段, 和 exclude冲突
    fields = ('name',)
    # 不管理的字段,和 fields冲突
    exclude = ('age',)

多对多关系,默认是 select多选框,一般使用 filter_horizontal 或者 filter_vertical

@admin.register(Teacher)
class TeacherAdmin(admin.ModelAdmin):

    ordering = ('name', )

    filter_horizontal = ('school_class', )

9.7 增加额外批量操作

在admin.py中:

  1. 动作函数是单独的模块函数:

    # 定义动作函数
    def age_add_one(modeladmin, request, queryset):
        queryset.update(age=F('age')+1)
    # 给动作函数添加描述
    age_add_one.short_description = "年龄增加一岁"
    
    class StudentAdmin(admin.ModelAdmin):
        # 在当前自定义管理类中,添加新的动作:age_add_one
        actions = [age_add_one]
    
    admin.site.register(Student, StudentAdmin)
    
  2. 动作函数写在自定义管理类中作为一个方法:

    class StudentAdmin(admin.ModelAdmin):
        # 这里必须是函数名的字符串
        actions = ['age_add_one']
    	
        # 类方法也同样是3个参数!
        def age_add_one(modeladmin, request, queryset):
            queryset.update(age=F('age') + 1)
    
        age_add_one.short_description = "年龄增加一岁"
    
    admin.site.register(Student, StudentAdmin)
    

9.8 覆盖admin默认模板

​ 如果我们要在admin管理页面中,增加自己的功能,那么我们需要覆盖admin的默认模板,通过以下步骤实现:

1、admin允许覆盖的页面

  • app_index.html
  • change_form.html
  • change_list.html
  • delete_confirmation.html
  • object_history.html
  • popup_response.html

2、admin管理模板目录

​ django库下的 contrib/admin/templates/admin 目录,可以查看django自带的所有模板

3、自定义模板

​ 需要修改admin的哪个内置模板,则继承哪个模板,并且在其基础上进行修改,我们以app_index.html为例:

​ 原始模板是:

{% extends "admin/index.html" %}
{% load i18n %}

{% block bodyclass %}{{ block.super }} app-{{ app_label }}{% endblock %}

{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo;
{% for app in app_list %}
{{ app.name }}
{% endfor %}
</div>
{% endblock %}
{% endif %}

{% block sidebar %}{% endblock %}

4、建立对应模板

​ 在settings中配置的templates目录下,建立admin目录,并且在admin目录下,建立mysite目录(mysite是你的应用名,假设mysite是children,那么目录结构如下图)

我们不需要修改原始模板所有的block,只需要在我们要添加功能的地方,进行继承,并且修改就行了,如下:

{% extends "admin/app_index.html" %}

{% block sidebar %}
    <a href="{% url 'children:customize' %}">自定义功能页面</a>
{% endblock %}

5、修改 urls

path('customize/', views.customize, name='customize'),

6、修改 views

def customize(request):
    
    """
        编写你自己的自定义管理功能
    :param request: 
    :return: 
    """
    
    return redirect('/admin/children/')

十、session&cookie

10.1 背景介绍

HTTP协议有一个特性就是无状态的,是指协议对于交互性场景没有记忆能力。

​ 随着动态交互的web应用的出现,HTTP的无状态特性严重阻碍了动态交互应用程序的发展,例如一些购物网站在进行购物时候都会进行了页面跳转/刷新,按照HTTP的无状态协议岂不是登录一次又一次。于是,两种用于保持http连接状态的技术就应运而生了,这个就是Session和Cookie。

10.2 cookie简介

Cookie,有时也用Cookies,是指web程序为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密),一般是以键值对的形式存在,Cookie具有不可跨域名性

​ Cookie是http协议中定义在 header 中的字段

  1. cookie解决无状态问题原理

    Cookie 技术通过在请求和响应报文中写入Cookie 信息来控制客户端的状态。
    Cookie 会根据从服务器端发送的响应报文内的一个叫做Set-Cookie的首部字段信息,通知客户端保存Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入Cookie 值后发送出去。
    服务器端发现客户端发送过来的Cookie 后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前的状态信息。

  2. cookie的常见属性:

    • name:cookie的名称

    • value:cookie的值

    • domain:该cookie的所属域名,具有继承性,只允许本域名及子域名访问

      ​ 譬如:test.com 这个是顶级域名, 二级域名 aaa.test.com 就是 test.com 的子域名,三级域名 bbb.aaa.test.com 是 aaa.test.com 的子域名

      ​ 如果设置一个 cookie: user=terry ,domain = aaa.test.com

      ​ 那么:

      ​ aaa.test.com 这个域名下的url都可以访问 该cookie

      ​ bbb.aaa.test.com 下的url也可以访问 该cookie

      ​ 但是 test.com 下的url不可以访问该cookie,兄弟域名ccc.test.com 也不可以访问该cookie

    • path:该cookie的所属的路径,具有继承性,只允许本路径及子路径域名访问,设置为根路径 path='/' ,则所有路径都可以访问

    • expires/Max-Age:expires设置为一个失效时间值,HTTP1.1 中,expires 被弃用并且被Max-Age替代,设置为cookie多久时间之后失效,是整型,表示秒数

          response.set_cookie('num',123,max_age=24*3600)
          response.set_cookie('num',123,expires=(datetime.now()+timedelta(hours=1,seconds=30)))
      
    • size:cookie的内容大小

    • http:httponly属性,默认为False,若此属性为true,则只有在http请求头中会带有此cookie的信息,而不能通过JavaScript(document.cookie)来访问此cookie

    • secure:默认为False,设置是否只能通过https来传递此cookie

10.3 django中应用cookie

  1. 在django设置cookie

    # 只设置key和value两个参数,默认关闭浏览器失效
    response.set_cookie('key', "value")  
    
    # 10秒后失效
    response.set_cookie('key', "value", max_age=10)
    
  2. django中读取cookie

    request.COOKIES['key']
    # 或
    request.COOKIES.get('key')
    
  3. 签名的cookie

    签名指的就是加密

    # 设置签名cookie, salt 的内容可以是任意的字符串, 不要泄露
    response.set_signed_cookie('key2', 'value2', salt='test')
    
    # 获取签名cookie, 第二个参数 None是默认值
    request.get_signed_cookie('key2', None, salt='test')
    
  4. cookie实现三天免登录

    1. urls.py

       path('login_html/',views.login_html,name='login_html'),
       path('dologin/',views.dologin,name='dologin'),
      
    2. 登录页面

      <!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.0">
          <title>Document</title>
      </head>
      <body>
          <form action="{%url 'dologin' %}"  method="post">
              {% csrf_token %}
              <p>用户名:<input type="text" name="username" value="{{ username }}"/></p>
              <p>密  码:
                <input type="password" name="password" value="{{pwd}}"/>
              </p>
              <p>
                  <input type="checkbox" name="rember" value="rember" {% if username %}checked{%endif%}/>记住密码
                  <input type="submit" value="登录"/>
              </p>
          </form>
      </body>
      </html>
      
    3. 处理视图函数

      def login_html(request):
          #从cookie中获取用户名和密码
          username = request.COOKIES.get('username')
          password = request.get_signed_cookie('pwd',None,salt='pwdsalt')
          if username and password:
              return render(request,'cookie_app/login.html',{'username':username,'pwd':password})
          else:
              return render(request,'cookie_app/login.html')
      
      def dologin(request):
          #从登录表单中获取用户名和密码及是否勾选了记住密码
          data = request.POST
          username = data.get('username')
          password = data.get('password')
          rember = data.get('rember')
          response = HttpResponse()
          if 'root' == username and '123' == password:
              response.content='登录成功'
              if rember == 'rember':
                  #勾选了记住用户名和密码
                  #将用户名和密码保存到cookie中
                  response.set_cookie('username',username,max_age=3*24*3600)
                  response.set_signed_cookie('pwd',password,salt='pwdsalt',max_age=3*24*3600)
              else:
                  #删除cookie中的之前保存用户名和密码
                  response.delete_cookie('username')
                  response.delete_cookie('pwd')
              return response
          else:
              response.delete_cookie('username')
              response.delete_cookie('pwd')
              return redirect('login_html')
      

10.4 Session简介

Session,在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话 。

​ 会话状态仅在支持 cookie 的浏览器中保留!及session是依赖于cookie的。

​ django框架中的session管理允许存储和检索任意数据,它在服务器端存储数据并抽象cookie的发送和接收。Cookie包含session的ID,而不是数据本身(除非使用基于cookie的session管理类型)

10.5 django中应用session

  1. 启用session

    要应用session,必须开启session中间层,在settings中:

    MIDDLEWARE = [
        # 启用 Session 中间层
        'django.contrib.sessions.middleware.SessionMiddleware',
    ]
    
  2. 五中类型的session

    Django中默认支持Session,其内部提供了5种类型供开发者使用:

    • 数据库(默认)
    • 缓存
    • 文件
    • 缓存+数据库
    • 加密cookie

    5种方式的启动配置各异,但是启动完成后,在程序中使用的方式都相同

    1. 数据库方式

      默认的方式,最简单

      # 数据库方式(默认):
      SESSION_ENGINE = 'django.contrib.sessions.backends.db'   
      
      # 数据库类型的session引擎需要开启此应用,启用 sessions 应用
      INSTALLED_APPS = [
          'django.contrib.sessions',
      ]
      
    2. 缓存

      速度最快,但是由于数据是保存在内存中,所以不是持久性的,服务器重启或者内存满了就会丢失数据

      PS:有其他的方式实现持久性的缓存方式,官网查阅:Memcached缓存后端

      SESSION_ENGINE = 'django.contrib.sessions.backends.cache'   
      
    3. 缓存+数据库

      速度次于单纯缓存方式,但是实现了持久性,每次写入高速缓存也将写入数据库,并且如果数据尚未存在于缓存中,则使用数据库

      SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'   
      
    4. 文件

      SESSION_ENGINE = 'django.contrib.sessions.backends.file' 
      
      # 设置文件位置, 默认是 tempfile.gettempdir(),
      # linux下是:/tmp
      # windows下是: C:\Users\51508\AppData\Local\Temp
      SESSION_FILE_PATH = 'd:\session_dir'
      
    5. 加密cookie

      基于cookie的session,所有数据都保存在cookie中,一般情况下不建议使用这种方式

      1. cookie有长度限制,4096个字节
      2. cookie不会因为服务端的注销而无效,那么可能造成攻击者使用已经登出的cookie模仿用户继续访问网站
      3. SECRET_KEY这个配置项绝对不能泄露,否则会让攻击者可以远程执行任意代码
      4. cookie过大,会影响用户访问速度
      SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
      
  3. session的参数配置

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

    # 在django程序中通过 request.session 来获取session对象,并且这个session就是一个字典对象,并且 key 只能是 字符串
    request.session['has_commented'] = True
    
    # 获取某个session变量
    has_commented = request.session.get('has_commented')
    
    # 删除某个key
    del request.session['has_commented']
    
  5. session对象常见方法

    # 获取session对象
    session = request.session
    
    # 除常见的字典方法外
    
    # 从会话中删除当前会话数据并删除会话cookie
    flush()
    
    # 设置测试cookie以确定用户的浏览器是否支持cookie
    set_test_cookie()
    # 返回True或者False,取决于用户的浏览器是否接受测试cookie
    test_cookie_worked()
    # 删除测试cookie
    delete_test_cookie()
    
    # 设置会话的到期时间
    # 如果value是整数,则session将在多少秒不活动后到期
    # 如果value是一个datetime或timedelta,该session将在相应的日期/时间到期
    # 如果value是0,用户的会话cookie将在用户的Web浏览器关闭时到期
    # 如果value是None,则会话将恢复为使用全局会话到期策略
    set_expiry(value)
    
    # 返回此会话到期之前的秒数
    # kwargs 为 `modification` 和 `expiry`,一般不指定
    # modification:最后一次访问日期,默认当前时间, now
    # expiry: 到期剩余秒数,默认全局配置时间
    get_expiry_age(**kwargs)
    
    # 返回此会话将过期的日期
    # 参数同 get_expiry_age
    get_expiry_date(**kwargs)
    
    # 返回True或者False,取决于用户的Web浏览器关闭时用户的会话cookie是否会过期
    get_expire_at_browser_close()
    
    # 从会话存储中删除过期的会话,这是个类方法。
    clear_expired()
    
    # 在保留当前会话数据的同时创建新的会话密钥
    cycle_key()
    
  6. 示例代码

    我们以实现登录来应用session功能(需要使用使用默认的auth应用):

    urls:

    urlpatterns = [
        path('', views.index, name='index'),
        path('login/', views.login, name='login'),
        path('logout/', views.logout, name='logout'),
    ]
    

    views:

    @login_required(login_url='test_app:login')
    def index(request):
        return render(request, 'index.html')
    
    def logout(request):
        auth.logout(request)
        return redirect('test_app:login')
    
    def login(request):
        if request.method == 'GET':
            error_message = request.session.get('error_message')
            request.session['error_message'] = None
            return render(request, 'login.html', {'error_message': error_message})
        else:
            username = request.POST['username']
            password = request.POST['password']
    
            # 从django的authenticate中获取对应的用户名和密码的 ORM 对象
            user = auth.authenticate(username=username, password=password)
    
            if user:
                # 将登陆的用户写入到session中, 在其它视图函数中用 request.user 获取用户对象
                auth.login(request, user)
                return redirect('test_app:index')
            else:
                request.session['error_message'] = '用户名或密码错误!'
                return redirect('test_app:login')
    

    templates:

    ​ index.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        欢迎 {{ request.user.username  }}  光临
        <br>
        <a href="{% url 'test_app:logout' %}">退出登录</a>
    </body>
    </html>
    

    login.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>登录页面</title>
    </head>
    <body>
        <form action="{% url 'test_app:login'  %}" method="POST">
            {% csrf_token %}
            用户名:<input type="text" name="username">
            <br>
            密码:<input type="password" name="password">
            <br>
            {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
            <br>
            <input type="submit" value="登录" >
        </form>
    </body>
    </html>
    

十一、django框架分页器

11.1 概述

分页,就是当我们在页面中显示一些信息列表,内容过多,一个页面显示不完,需要分成多个页面进行显示时,使用的技术就是分页技术。

​ 在django项目中,一般是使用3种分页的技术:

  1. 自定义分页功能,所有的分页功能都是自己实现
  2. django的插件 django-pagination 实现
  3. django自带的分页器 paginator

11.2 分页器相关对象

分页器的对象在 django/core/paginator.py 模块中,主要包括Paginator类和Page类:

  1. Paginator类

    1. 初始化方法_init_(self, object_list, per_page, orphans=0,allow_empty_first_page=True):

      • object_list:可以是QuerySet、带有count()或_len_()方法的列表、元组或其它可切片的对象,如果是QuerySet,应该进行排序,使用order_by()子句或者有默认的ordering
      • per_page:每个页面上显示的项目数目,不包括orphans部分
      • orphans:默认为0,如果最后一页显示的数目小于等于该数值,那么则将最后一页的内容(假设数为N)添加到倒数第二页中去,这样的话倒数第二页就成了最后一页,显示的数目为:per_page+N
      • allow_empty_first_page:默认为True,允许第一页显示空白内容,如果设置为False,那么当object_list为空时,抛出EmptyPage错误
    2. 方法

      1. get_page(self, number)

        • numer:指定页码数,正确值应该是大于等于1的整数

        返回指定number的Page对象,同时还处理超出范围和无效页码,如果number不是数字,则返回第一页,如果number为负数或大于最大页数,则返回最后一页。

      2. page(self, number)

        • numer:指定页码数,正确值应该是大于等于1的整数

        返回指定number的Page对象,不处理异常,如果number无效,则抛出 InvalidPage 错误

    3. 属性

      1. count:项目总条数,调用该属性时,优先调用object_list的count()方法,没有count()方法才尝试len(object_list)方法
      2. num_pages:总页码数
      3. page_range:从1开始的页码迭代器,代码:range(1, self.num_pages + 1)
  2. Page类

    ​ 一般情况下,不会手动实例化该类,而是通过Paginator的page或者get_page方法获取

    1. 初始化方法_init_(self, object_list, number, paginator):

      • object_list:当页显示的object_list对象,object_list可以是QuerySet、带有count()或_len_()方法的列表、元组或其它可切片的对象
      • number:页码数
      • paginator:Paginator类的实例
    2. 方法

      主要的方法都是用来做逻辑判断的,以此来决定页面中显示的诸如:上一页、下一页,首页,末页等

      1. has_next():如果有下一页则返回True

      2. has_previous():如果有上一页,则返回True

      3. has_other_pages():如果有上一页或者下一页,则返回True

      4. next_page_number():返回下一页编号,如果下一页不存在则引发 InvalidPage 错误

      5. previous_page_number():返回上一页编号,如果上一页不存在则引发 InvalidPage 错误

      6. start_index() :返回页面上第一个对象的从1开始的索引,相对于分页器列表中的所有对象。例如,当为每页包含2个对象的5个对象的列表进行分页时,第二个页面Page对象的start_index返回3

      7. end_index() :返回页面上最后一个对象的从1开始的索引,相对于分页器列表中的所有对象。例如,当为每页包含2个对象的5个对象的列表进行分页时,第二个页面Page对象的end_index返回4

    3. 属性

      其实就是初始化方法中的3个参数

      1. object_list:对应的object_list
      2. number:该对象的所处的页码数,从1开始
      3. paginator:关联的Paginator实例

11.3 项目中应用

  1. 新建django项目:paginator_study,子应用:paginator_app

  2. 在mysql数据库新建 paginator_study 库

  3. settings中配置数据库:

    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'NAME': 'paginator_study',
            'USER': 'root',
            'PASSWORD': '123456',
            'HOST': '127.0.0.1',
            'PORT': '3306',
        }
    }
    
  4. 建立模型

    class Student(models.Model):
        name = models.CharField(max_length=20)
        age = models.IntegerField()
        sex = models.IntegerField(choices=((1, '男'), (2, '女')), default=1)
        card_no = models.CharField(max_length=18)
    
  5. 迁移数据库

    python manage.py makemigrations
    
    python manage.py migrate paginator_app
    
  6. 生成测试数据

    model中增加批量插入测试数据的方法:

    @classmethod
    def insert_test_data(cls, num=100):
        def random_str(raw_ite, length):
            return ''.join(random.choices(raw_ite, k=length))
    
        obj_list = []
        for _ in range(num):
            obj_list.append(Student(
                name=random_str(string.ascii_lowercase, 8),
                age=random.randint(18, 50),
                sex=random.choice([1, 2]),
                card_no=random_str(string.digits, 18)
            ))
    
        Student.objects.bulk_create(obj_list)
    

    然后在 python manage.py shell 中调用该方法

    python manage.py shell
    
    from paginator_app.models import *
    
    Student.insert_test_data()
    
  7. urls增加路由配置

    urlpatterns = [
        path('students/', views.students, name='students')
    ]
    
  8. views中实现函数:

    每页显示的页码数,一般保存在settings中

    def students(request):
        page = request.GET.get('page')
        all_students = Student.objects.all()
        paginator = Paginator(all_students, settings.PER_PAGE_NUMBER)
        students_page = paginator.get_page(page)
    
        return render(request, 'paginator_app/students.html', {'students_page':students_page})
    
  9. 增加students.html模板

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>学生列表</title>
    </head>
    <body>
        <table>
            <tr>
                <th>序号</th>
                <th>姓名</th>
                <th>年龄</th>
                <th>性别</th>
                <th>身份证</th>
            </tr>
            {% for student in students_page.object_list %}
                <tr>
                    <td>{{ students_page.start_index|add:forloop.counter0 }}</td>
                    <td>{{ student.name }}</td>
                    <td>{{ student.age }}</td>
                    <td>{{ student.get_sex_display }}</td>
                    <td>{{ student.card_no }}</td>
                </tr>
            {% empty %}
                <tr>
                    <td colspan="5">当前没有学生</td>
                </tr>
            {% endfor %}
        </table>
    </body>
    </html>
    
  10. 增加table的样式

    使用css样式

    1. students.html页面加载static

      {% load static %}
      
    2. 新建目录结构如下: paginator_app>>static>>paginator_app>>css>>paginator.css

      1. paginator.css内容如下:

        table
        {
            border-collapse: collapse;
            text-align: center;
        }
        
        table td, table th
        {
            border: 1px solid #cad9ea;
            color: #666;
            height: 35px;
        }
        
        table thead th
        {
            background-color: #CCE8EB;
            width: 100px;
        }
        
        table tr:nth-child(odd)
        {
            background: #fff;
        }
        
        table tr:nth-child(even)
        {
            background: #F5FAFA;
        }
        
      2. 页面中导入css文件

        <link rel="stylesheet" href="{% static 'paginator_app/css/paginator.css' %}">
        
  11. 增加分栏选项

    1. 目标中增加:

      	{% if students_page.object_list %}
              <nav>
                  <ul>
                      <li>
                          <a href="?page=1">
                              <span>首页</span>
                          </a>
                      </li>
                      {% if students_page.has_previous %}
                          <li>
                              <a href="?page={{ students_page.previous_page_number }}">
                                  <span>上一页</span>
                              </a>
                          </li>
                      {% endif %}
                      {% for pg in page_range %}
                          {% if pg == students_page.number %}
                              <li><strong>{{ pg }}</strong></li>
                          {% else %}
                              <li><a class="pagination__number" href="?page={{ pg }}">{{ pg }}</a></li>
                          {% endif %}
                      {% endfor %}
                      {% if students_page.has_next %}
                          <li>
                              <a href="?page={{ students_page.next_page_number }}">
                                  <span>下一页</span>
                              </a>
                          </li>
                      {% endif %}
                      <li>
                          <a href="?page={{ students_page.paginator.num_pages }}">
                              <span>末页</span>
                          </a>
                      </li>
                  </ul>
              </nav>
          {% endif %}
      
    2. orphans参数的使用

      在setting.py中添加

      PAGE_ORPHANS = 5

      在视图函数中,创建Paginator时,添加orphans参数

      paginator = Paginator(students_li, settings.PER_PAGE_NUMBER, orphans=settings.PAGE_ORPHANS)
      

      可以看到最后一页如果记录条数小于5,则会自动加到倒数第二页中

    3. 视图函数中增加:

      为了在模板使用诸如: 1 2 3 4 5 6 7 8 这样的分页栏,需要判断相对于当前页的前后页码数,这种功能的实现,可以在自定义过滤器中实现,也可以在 views中实现后传入一个 变量

      # 新增函数
      def get_page_range_by_page_and_max_page(page, max_page, num=10):
          min = page-int(num/2)
          min = min if min > 1 else 1
          max = min + num - 1
          max = max if max < max_page else max_page
      
          return range(min, max + 1)
      
      # 返回值增加
      return render(request, 'paginator_app/students.html',
                        {
                            'students_page': students_page,
                            'page_range': get_page_range_by_page_and_max_page(students_page.number, paginator.num_pages)
                        })
      
  12. 分页栏增加样式

    ul,li{ padding:0; margin:0;list-style:none}
    .nav{border:1px solid #000; width:510px; overflow:hidden}
    .nav li{ line-height:22px; float:left; padding:0 5px;}
    .nav li a:hover{ color:#F00}
    nav li{line-height:22px; float:left; padding:0 6px;}
    nav li a{ color:#009900}
    
  13. 可以将分页栏提取到一个模板文件中,成为公共的文件

    新建paginator_common.html

    <nav>
        <ul>
            <li>
                <a href="?page=1">
                    <span>首页</span>
                </a>
            </li>
            {% if students_page.has_previous %}
                <li>
                    <a href="?page={{ students_page.previous_page_number }}">
                        <span>上一页</span>
                    </a>
                </li>
            {% endif %}
            {% for pg in page_range %}
                {% if pg == students_page.number %}
                    <li><strong>{{ pg }}</strong></li>
                {% else %}
                    <li><a class="pagination__number" href="?page={{ pg }}">{{ pg }}</a></li>
                {% endif %}
            {% endfor %}
            {% if students_page.has_next %}
                <li>
                    <a href="?page={{ students_page.next_page_number }}">
                        <span>下一页</span>
                    </a>
                </li>
            {% endif %}
            <li>
                <a href="?page={{ students_page.paginator.num_pages }}">
                    <span>末页</span>
                </a>
            </li>
        </ul>
    </nav>
    

    在student.html 中修改

    {% if students_page.object_list %}
    {% include 'paginator_app/paginator_common.html' %}
    {% endif %}
    

    注意事项:在 paginator_common.html 中使用的 students_page和page_range需要统一命名,否则会报错

    运行结果

十二、django项目中使用验证码

12.1 概述

​ 验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。可以防止:恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试等。

12.2 类别

当今验证码各种不同的类别很多,常见的如下:

  1. 普通型:随机多个(一般是4个)字母、数字和中文的图片,可能加一些干扰项

  2. 问答型:图片中显示一个问题,譬如3+3=?

  3. 拖动行为型:拖动一个小图片到一个拼图中

  4. 点击行为型:按照顺序点击图片中的特定位置

12.3 实现思路

大部分的验证码验证的思路都是这样的:

  1. 客户端发送获取验证码的请求
  2. 服务端接收到验证码请求后,生成对应的验证码和正确答案
  3. 服务端将验证码的正确答案保存到会话对象当中
  4. 服务端将验证码返回到客户端
  5. 客户端看到验证码后:
    • 如果看不清等原因,可以重新获取,那么就重新回到第1步
    • 正确显示后,输入答案,提交答案到服务端
  6. 服务端接收到验证码答案后,和保存在会话对象中的正确答案比对,正确就通过验证,失败则返回错误提示

12.4 django 项目中实现验证码

本文档中以普通的4个字母的验证码作为演示

  1. 实现登录功能(未使用验证码)

    借用之前 session学习 课程中的部分的登录模块代码

  2. 新建django项目:captcha_study,子应用:captcha_app

  3. 在mysql数据库新建 captcha_study 库

  4. settings中配置数据库:

    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'NAME': 'captcha_study',
            'USER': 'root',
            'PASSWORD': '123456',
            'HOST': '127.0.0.1',
            'PORT': '3306',
        }
    }
    
  5. 迁移数据库

    由于只使用了django自带的应用的数据库模型,所以直接 migrate 就可以

    python manage.py migrate
    
  6. 创建 superuser

    python manager.py createsuperuser
    
  7. 修改主应用的urls.py:

    path('captcha/', include('captcha_app.urls')),
    
  8. 新增子应用的urls.py

    from django.urls import path
    from . import views
    
    app_name = 'captcha_app'
    
    urlpatterns = [
        path('', views.index, name='index'),
        path('login/', views.login, name='login'),
        path('logout/', views.logout, name='logout'),
    ]
    
  9. views中修改:

    from django.contrib import auth
    from django.contrib.auth.decorators import login_required
    from django.shortcuts import render, redirect
    
    # Create your views here.
    @login_required(login_url='captcha_app:login')
    def index(request):
        return render(request, 'captcha_app/index.html')
    
    def logout(request):
        # 登出
        auth.logout(request)
        return redirect('captcha_app:login')
    
    def login(request):
        """ 本应用的登录请求
    
            登录请求一般有2个不同的http的method
            get: 显示的就是登录页面
            post: 在登录页面输入用户名和密码之后,点击登录提交
    
        :param request:
        :return:
        """
        # get请求,对一个 登录的页面
        if request.method == 'GET':
            # 通过 session获取 error_message
            error_message = request.session.get('error_message')
            request.session['error_message'] = None
            return render(request, 'captcha_app/login.html', {'error_message':error_message})
        else:
            username = request.POST.get('username')
            password = request.POST.get('password')
    
            # 验证用户名和密码
            user = auth.authenticate(username=username, password=password)
    
            # 用户名和密码正确
            if user:
                # 使用auth应用的话,登录成功必须调用 login 方法
                # 在其他 函数中 使用 request.user 获取 用户对象实例
                auth.login(request, user)
                return redirect('captcha_app:index')
            else:
                # 在不同的 视图函数中传递参数,使用 session
                error_message = '用户名或者密码错误!!'
                request.session['error_message'] = error_message
                return redirect('captcha_app:login')
    
  10. 新增template

    在子应用中建立 templates 文件夹,再建立一个子文件夹:captcha_app

    新增index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        欢迎光临, 用户:{{ request.user.username }}, email:{{ request.user.email }}
        <a href="{% url 'captcha_app:logout' %}">退出登录</a>
    </body>
    </html>
    

    新建login.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>登录页面</title>
    </head>
    <body>
        <form method="post" action="{% url 'captcha_app:login' %}">
            {% csrf_token %}
            <table>
                <tr>
                    <td>用户名:</td>
                    <td><input type="text" value="" name="username" id="username"></td>
                </tr>
                <tr>
                    <td>密码:</td>
                    <td><input type="password" value="" name="password" id="password"></td>
                </tr>
                {% if error_message %}
                <tr>
                    <td colspan="2"><strong>{{ error_message }}</strong></td>
                </tr>
                {% endif %}
                <tr>
                    <td colspan="2">
                        <input type="submit" value="登录">
                    </td>
                </tr>
            </table>
        </form>
    </body>
    </html>
    
    1. 生成图片

      需要安装pillow库

      pip install Pillow
      

      还需要下载一个字体文件,譬如:ubuntu.ttf

    新建captcha.py,代码如下:

    import os
    from PIL import Image, ImageDraw, ImageFont, ImageFilter
    import random
    import string
    
    def random_str(length=4):
        """ 随机字符串 默认长度 4
    
        :param length: 默认长度 4
        :return:
        """
        return ''.join(random.sample(string.ascii_letters, length))
    
    def random_color(s=1, e=255):
        """ 随机 RGB 颜色
    
        :param s:  起始值, 0-255
        :param e:  结束时, 0-255
        :return:  (r, g, b)
        """
        return random.randint(s, e), random.randint(s, e), random.randint(s, e)
    
    def veri_code(length=4, width=160, height=40, size=28):
        """ 生成验证码图片
    
        :param length:  验证码字符串长度
        :param width:  图片宽度
        :param height:  图片高度
        :param size:  字体大小
        :return:  (验证码图片, 验证码字符串)
        """
        # 创建Image对象
        image = Image.new('RGB', (width, height), (255, 255, 255))
        # 创建Font对象
        file = os.path.dirname(os.path.abspath(__file__))
        font = ImageFont.truetype(f'{file}/ubuntu.ttf', size)
        # 创建Draw对象
        draw = ImageDraw.Draw(image)
        # 随机颜色填充每个像素
        for x in range(0, width, 2):
            for y in range(height):
                draw.point((x, y), fill=random_color(64, 255))
        # 验证码
        code = random_str(length)
        # 随机颜色验证码写到图片上
        for t in range(length):
            draw.text((40 * t + 5, 5), code[t], font=font, fill=random_color(32, 127))
        # 模糊滤镜
        image = image.filter(ImageFilter.BLUR)
        return image, code
    
    if __name__ == '__main__':
        img, code = veri_code()
        with open('test.png', 'wb') as f:
            img.save(f)
    

    login.html增加验证码

    1. 增加验证码图片标签和输入验证码内容的标签

      <tr>
          <td>验证码:</td>
          <td>
          	<input type="text" placeholder="请输入验证码" name="check_code">
          	<img src="{% url 'captcha_app:captcha_img' %}">
          </td>
      </tr>
      
    2. urls增加

      path('captcha_img/', views.captcha_img, name='captcha_img'),
      
    3. views中增加

      def captcha_img(request):
          stream = BytesIO()
          img, code = veri_code()
          img.save(stream, 'PNG')
          request.session['check_code'] = code
          return HttpResponse(stream.getvalue())
      
    4. 在图片标签上增加点击刷新的功能

      login.html:

      <img src="{% url 'captcha_app:captcha_img' %}" onclick="changeCheckCode(this);">
      
      <script>
          function changeCheckCode(ths){
              <!--改变URL,刷新图片。-->
              ths.src = "{% url 'captcha_app:captcha_img' %}?r=" + Math.random();
          }
      </script>
      

    在views中增加验证码效验

    1. 修改 login 函数

      def login(request):
          """ 本应用的登录请求
      
              登录请求一般有2个不同的http的method
              get: 显示的就是登录页面
              post: 在登录页面输入用户名和密码之后,点击登录提交
      
          :param request:
          :return:
          """
          # get请求,对一个 登录的页面
          if request.method == 'GET':
              # 通过 session获取 error_message
              error_message = request.session.get('error_message')
              request.session['error_message'] = None
              return render(request, 'captcha_app/login.html', {'error_message':error_message})
          else:
              check_code = request.POST.get('check_code')
      
              # 验证码正确
              if check_code and check_code.lower() == request.session.get('check_code').lower():
                  username = request.POST.get('username')
                  password = request.POST.get('password')
      
                  # 验证用户名和密码
                  user = auth.authenticate(username=username, password=password)
      
                  # 用户名和密码正确
                  if user:
                      # 使用auth应用的话,登录成功必须调用 login 方法
                      # 在其他 函数中 使用 request.user 获取 用户对象实例
                      auth.login(request, user)
                      return redirect('captcha_app:index')
                  else:
                      # 在不同的 视图函数中传递参数,使用 session
                      error_message = '用户名或者密码错误!!'
                      request.session['error_message'] = error_message
                      return redirect('captcha_app:login')
              # 验证码错误
              else:
                  error_message = '验证码错误!'
                  request.session['error_message'] = error_message
                  return redirect('captcha_app:login')
      

十三、中间件

13.1 概述

AOP,面向切面编程,是对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。可以实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。

面向切面编程,就是将交叉业务逻辑封装成切面,利用AOP的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的,与主业务逻辑无关的代码,如安全检查,事物,日志等。若不使用AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使业务逻辑变得混杂不清。
举个例子:银行系统取款会有一个流程查询也会有一个流程。

Django的中间件,就是应用AOP技术来实现的,它是django请求/响应处理的钩子框架,是一个轻巧的低级“插件”系统,在不修改django项目原有代码的基础上,可以全局改变django的输入或输出,每个中间件组件负责执行某些特定功能。

​ PS:因为中间件改变的是全局,所以需要谨慎实用,滥用的话,会影响到服务器的性能

13.2 django默认中间件

django项目默认有以下自带的中间件,如下:

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',
]

一般情况下这些中间件都会启用(最少CommonMiddleware会启用)

13.3 自定义中间件说明

如果需要增加自定义的中间件(该中间件类必须继承MiddlewareMixin(django.utils.deprecation)),一般是添加在系统的中间件之下,如:

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',
    
    # 自定义中间件
    'my_app.middleware.MyMiddleware',
]

​ 中间件中主要有以下方法(一个中间件类最少需要实现下列方法中的一个):

  • process_request:处理请求对象,请求到达django框架时,第一时间调用

    多个中间件之间顺序调用

    参数:request

    返回:

    • response:调用当前中间件的process_response处理
    • None:调用下一个中间件的process_request处理
  • process_response:处理响应对象,视图函数返回response后,调用

    多个中间件之间倒序调用

    参数:request, response

    返回:

    • response:调用上一个中间件的process_response处理
  • process_view:视图预处理,在视图函数处理之前调用,即请求在urlconf当中匹配到对应的视图函数之后,先不调用视图函数,而是先调用此方法

    多个中间件之间顺序调用

    参数:request,view_func,view_args,view_kwargs

    ​ view_func:url路由匹配到的视图函数, 不是字符串,是函数对象

    ​ view_args:视图函数的可变参数

    ​ view_kwargs:视图函数的可变关键字参数

    返回:

    • response:调用最后一个中间件的process_response开始处理
    • None:调用下一个中间件的process_view处理
  • process_exception:在视图函数处理过程抛出异常时调用,中间件的方法(除了process_template_response)中抛出异常不会触发

    多个中间件之间倒序调用

    参数:request,exception

    ​ exception:是处理过程中抛出的异常对象

    返回:

    • response:之后的process_exception都不会触发,而是直接调用最后一个中间件的process_response处理
    • None:调用上一个中间件的process_exception处理
  • process_template_response:默认不执行,在视图函数完成操作后调用,除非视图函数返回的response中有render方法

    多个中间件之间倒序调用

    参数:request,response

    ​ response:不是HttpReponse,而是具有render方法的对象,譬如:SimpleTemplateResponse对象,在(django.template.response中)

    返回:

    • response:具有render方法的对象,继续调用上一个中间件的process_template_response处理,最后一个process_template_response处理完成后,会自动调用 response对象中的render方法,得到一个HttpResponse对象,进行返回,再调用process_response操作

    中间件方法的执行时有顺序的,process_request与process_view是按照顺序去执行的,而process_response、process_exception和process_template_response是反序的 :

总结:用户请求 >> process_request >> urlconf路由匹配,找到对应的视图函数 >> process_view >> 视图函数 >> process_template_response(如果视图函数返回的response,有render方法,否则这一步不会执行) >> process_response >> 返回response到用户

​ 其中,在 视图函数 和 process_template_response 处理过程中,如果出现 exception ,那么就会倒序执行 中间件的process_exception

13.4 常见自定义中间件功能

总之,你如果有对全局request或response的操作需求,那么就可以使用中间件,譬如:

  1. IP过滤:对一些特定IP地址返回特定响应
  2. URL过滤:如果用户访问的是login视图,则通过;如果访问其他视图,需要检测是不是有session已经有了就通过,没有就返回login页面。这样就不用在多个视图函数上写装饰器login_required
  3. 内容压缩:response内容实现gzip的压缩,返回压缩后的内容给前端
  4. CDN:内容分发网络,实现缓存,如果缓存中有数据直接返回,没有找到缓存再去请求视图
  5. URL过滤:某个子应用升级暂停使用,某个特定的path路径下的请求,返回一个特定页面

13.5 实例项目

  1. 新建django项目:middleware_study,子应用:middleware_app

  2. urls增加路由配置

    urlpatterns = [
        path('', views.index, name='index')
    ]
    
  3. views中实现函数:

    def index(request):
        return render(request, 'middleware_app/index.html')
    
  4. 增加index.html模板

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>中间件学习的首页</title>
    </head>
    <body>
        欢迎学习中间件!
    </body>
    </html>
    
  5. 新建middleware.py

    from django.http import HttpResponse
    from django.template.response import SimpleTemplateResponse
    from django.utils.deprecation import MiddlewareMixin
    
    
    class FirstMiddleware(MiddlewareMixin):
    
        def process_request(self, request):
            print('FirstMiddleware process_request')
    
        def process_response(self, request, response):
            print('FirstMiddleware process_response')
    
            return response
    
        def process_view(self, request, view_func, view_args, view_kwargs):
            print('FirstMiddleware process_view')
    
        def process_exception(self, request, exception):
            print('FirstMiddleware process_exception')
    
        def process_template_response(self, request, response):
            print('FirstMiddleware process_template_response')
    
            return response
    
    class SecondMiddleware(MiddlewareMixin):
    
        def process_request(self, request):
            print('SecondMiddleware process_request')
    
            # 触发当前中间件的 process_response
            # return HttpResponse('SecondMiddleware 测试process_request直接返回response')
    
        def process_response(self, request, response):
            print('SecondMiddleware process_response')
    
            return response
    
        def process_view(self, request, view_func, view_args, view_kwargs):
            print('SecondMiddleware process_view')
    
            # 直接返回HttpResponse, 触发最后一个中间件的 process_response
            # return HttpResponse('SecondMiddleware process_view 返回HttpResponse')
    
            # 在 这里直接返回具有render方法的 response,
            # 依然会触发最后一个中间件的process_template_response方法
            # return SimpleTemplateResponse('middleware_app/test_template_response.html')
    
        def process_exception(self, request, exception):
            print('SecondMiddleware process_exception')
    
            # 捕获异常,直接返回HttpResponse, 触发最后一个中间件的process_response
            return HttpResponse('SecondMiddleware process_exception捕获异常,返回response')
    
        def process_template_response(self, request, response):
            print('SecondMiddleware process_template_response')
    
            # 直接返回HttpResponse,会导致后续抛出异常,因为HttpResponse没有render方法了
            # return HttpResponse('SecondMiddleware process_template_response 返回HttpResponse')
            return response
    
    class ThridMiddleware(MiddlewareMixin):
    
        def process_request(self, request):
            print('ThridMiddleware process_request')
    
        def process_response(self, request, response):
            print('ThridMiddleware process_response')
    
            return response
    
        def process_view(self, request, view_func, view_args, view_kwargs):
            print('ThridMiddleware process_view')
    
        def process_exception(self, request, exception):
            print('ThridMiddleware process_exception')
    
        def process_template_response(self, request, response):
            print('ThridMiddleware process_template_response')
    
            return response
    
  6. settings中导入

    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',
    
        'middleware_app.middleware.FirstMiddleware',
        'middleware_app.middleware.SecondMiddleware',
        'middleware_app.middleware.ThridMiddleware',
    ]
    
  7. urls中增加

    urlpatterns = [
        path('', views.index, name='index'),
    ]
    
  8. views中增加

    from django.http import HttpResponse
    from django.shortcuts import render
    
    # Create your views here.
    from django.template.response import SimpleTemplateResponse
    
    
    class MyTemplateTest:
    
        def render(self):
            return HttpResponse('MyTemplateTest render 内容')
    
    def index(request):
        print('views index function')
    
        # raise Exception('手动抛出异常')
    
        return MyTemplateTest()
        # return SimpleTemplateResponse('middleware_app/index.html')
    
        # 这个render 返回的是一个普通的HttpResponse,该对象没有render方法
        # return render(request, 'middleware_app/index.html')
    

13.6 示例-URL过滤

  1. setting.py文件中的配置

    MIDDLEWARE = [
        'middleware_app.middlewares.UpgradeMiddleware',
    ]
    
  2. 增加upgrade.html模板

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>升级维护中</title>
    </head>
    <body>
        当前系统正在升级,预计2022年1月1日 0点0分0秒升级完成,到时欢迎使用!
    </body>
    </html>
    
  3. middleware.py

    class UpgradeMiddleware(MiddlewareMixin):
    
        def process_request(self, request):
            if request.path.startswith('/middleware/'):
                return render(request, 'middleware_app/upgrade.html')
    
  4. 浏览器中访问

    http://127.0.0.1:8000/middleware