曾经沧海难为水,除却巫山不是云|

Joey-Wang

园龄:4年3个月粉丝:17关注:0

📂Python
🔖Python
2021-04-26 21:19阅读: 308评论: 0推荐: 0

Python项目实践3——Web应用程序

第十八章 Django入门

Django是一个Web框架——用于帮助开发交互式网站的工具。能响应网页请求,还能读写数据库、管理用户等。

本章将介绍如何使用 Django 开发一个”学习笔记“项目,这是个在线日志系统,让你能记录学习到的有关特定主题的知识。

18.1 建立项目

建立项目时,首先需以规范的方式对项目进行描述,再建立虚拟环境,以便在其中创建项目。

18.1.1 制定规范

  1. 完整的规范详细说明了项目的目标,阐述了项目的功能,并讨论了项目的外观和用户界面。
  2. 规范应突出重点,帮助避免项目偏离轨道。

这里不会制定完整的项目规范,只列出一些明确的目标,以突出开发的终点。我们制定的规范如下:

我们要编写一个名为“学习笔记”的Web应用程序,让用户能够记录感兴趣的主题,并在学习每个主题的过程中添加日志条目。

“学习笔记”的主页对这个网站进行描述,并邀请用户注册或登录。用户登录后,就可创建新主题、添加新条目以及阅读既有的条目。

18.1.2 建立虚拟环境

要使用 Django,首先需要建立一个虚拟工作环境。

虚拟环境:是系统的一个位置,可以在其中安装包,并将其与其他Python包隔离。

(将项目的库和其他项目分离是有益的,且对部署到服务器是必须的。)

# 为项目新建一个目录learning_log, 在终端中切换到这个目录,输入以下终端命令创建一个虚拟环境
# 这里运行模块venv,并使用它创建一个名为ll_venv的虚拟环境【这时learning_log目录下会出现ll_env目录】
python -m venv ll_env
image-20200511232229409

18.1.3 激活虚拟环境

  1. 环境处于活动状态时,环境名将包含在括号内。此时可在环境中安装包,并使用已安装的包。
  2. 在环境中安装的包仅在该环境处于活动状态时才可用。
source ll_env/bin/activate
# 若要停止使用虚拟环境,执行命令:
# deactivate
image-20200511232313890

PS:若使用Windows系统,使用命令ll_env\Scripts\activate激活虚拟环境。

18.1.4 安装Django

虚拟环境激活后,执行命令安装 Django:

pip install Django
image-20200511232643297

由于是在虚拟环境(独立的环境)中工作,在各种系统中安装 Django 的命令都相同。

⚠️ 此 Django 仅在虚拟环境 ll_env 处于活动状态下才可用。

18.1.5 在Django中创建项目

在虚拟环境仍处于活动状态的情况下(ll_env包含在圆括号内),执行如下命令新建一个项目:

# 让Django新建一个名为learning_log的项目
django-admin.py startproject learning_log .
image-20200511232918017
  • 命令末尾的句点让新项目使用合适的目录结构,这样开发完成后可轻松将应用程序部署到服务器。
    • 省略句点将导致部署应用程序时遭遇一些配置问题。
    • 若写命令时忘记了句点,则需将创建的文件和文件夹删除(ll_env除外),再重新运行此命令。
  • 运行此命令后,Django创建一个 learning_log 目录以及 manage.py 文件。目录 learning_log 包含四个文件,其中:
    • settings.py —— 指定 Django 如何与系统交互以及如何管理项目。

    • urls.py —— 告诉 Django 应创建哪些网页来响应浏览器请求。

    • wsgi.py —— 帮助 Django 提供它创建的文件。

      【此文件名是Web服务器网关接口(web server gateway interface)首次字母缩写】

18.1.6 创建数据库

Django将大部分与项目相关的信息都存储在数据库中,故需要创建一个供 Django 使用的数据库。

python manage.py migrate
image-20200511234100606
  • 我们将 修改数据库 又称为 迁移数据库

    首次执行命令 migrate 时,将让 Django 确保数据库与项目的当前状态匹配。

    在使用 SQLite 的新项目中首次执行此命令时,将新建一个数据库。

  • 图中Operations to perform:Django 指出它将准备好数据库用于存储执行管理和身份验证任务所需的信息。(Apply all migrations,应用所有的迁移)

  • 使用 ls 命令,通过输出可见 Django 又创建 db.sqlite3 文件。

    SQLite是一种使用单个文件的数据库,它让你不用太关注数据库管理的问题,是编写简单应用程序的理想选择。

18.1.7 查看项目

下面来核实 Django 正确地创建了项目。为此可使用命令 runserver 查看项目状态:

python manage.py runserver
image-20200511234858641
  • Django启动一个名为 development server 的服务器,让你能查看系统中的项目,当你在浏览器中输入URL 以请求网页时,该 Django 服务器将进行响应:生成合适的网页,并将其发送给浏览器。

    image-20200511235050427
  • URL http://127.0.0.1:8000/ 表明项目将在你的计算机(即 localhost)的8000端口上侦听请求。

    localhost是一种只处理当前系统发出的请求,而不允许其他任何人查看你正在开发的网页的服务器

  • 如果出现错误消息 “That port is already in use”(指定端口已被占用),请执行命令 python manage.py runserver 8001,让Diango使用另一个端口;如果这个端口也不可用,请不断执行上述命令,并逐渐增大其中的端口号,直到找到可用的端口。

18.2 创建应用程序

Django项目由一系列应用程序组成,他们协同工作,让项目成为一个整体。

本章只创建一个应用程序,它将完成项目的大部分工作。第19章将添加一个管理用户账户的应用程序。

保持18.1中的终端窗口仍运行着 runserver,再打开一个终端窗口,切换到 manage.py 所在目录,激活虚拟环境,并执行命令startapp:

# 激活虚拟环境
source ll_env/bin/activate
# 创建learning_logs项目
python manage.py startapp learning_logs
image-20200512001401752
  1. startapp appname 让 Django 建立创建应用程序所需的基础设施。
  2. 通过 ls 发现 Django 创建文件夹 learning_logs,此文件夹中最重要文件:models.py、admin.py、views.py
    • models.py —— 定义要在应用程序中管理的数据。
    • admin.py —— 注册自定义的模型 model,让 Django 通过管理网站管理模型。
    • views.py —— 编写视图函数,获取并处理页面所需数据。

当前目录结构:

image-20210320083704801

18.2.1 定义模型

🌰 考虑下涉及的数据。每位用户都需要在学习笔记中创建很多主题。用户输入的每个条目都与特定主题相关联,这些条目将以文本的方式显示。我们还需要存储每个条目的时间戳,以便能够告诉用户各个条目都是什么时候创建的。

打开learning_logs目录中的文件models.py,这里为我们导入了模块 models,还让我们可以创建自己的模型。

from django.db import models

# Create your models here.

模型:告诉Django如何处理应用程序中存储的数据。模型在代码层面,就是一个类,把包含属性和方法。

🌰 下面表示用户将要存储的主题模型:

from django.db import models

# Create your models here.
class Topic(models.Model):
    """用户学习的主题"""
    text = models.CharField(max_length=200)
    date_added = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        """返回模型的字符串表示"""
        return self.text
  • Model —— Django 中定义了模块基本功能的类

  • CharField —— 由字符组成的数据,即文本。定义 CharField 属性时,必须告诉 Django 该在数据库中预留多少空间

    • 参数 max_length :最大字符长度
  • DateTimeField —— 记录日期和时间的数据

    • 参数 auto_now_add:是否将此属性自动设置成当前日期和时间

      (此处为True,表示每当用户创建新主题时,都让Django将此属性自动设置成当前日期和时间)

  • 需告诉Django默认应使用哪个属性显示有关主题的信息。Django调用方法 __str__() 显示模型的简单表示。

    (此处返回存储在属性text中的字符串)

    • 若使用python2.7 应调用方法__unicode__()

18.2.2 激活模型

  1. 要使用模型,必须让Django将应用程序包含到项目中:

    打开 setting.py 文件,其中 INSTALLED_APPS 片段告诉 Django 项目是由哪些应用程序组成。

    ⚠️ 务必将自己创建的应用程序放在默认应用程序前面,这样能覆盖默认应用程序的行为。

    # 此例中setting.py文件在learning_log/learning_log中
    
    --略--
    # Application definition
    
    INSTALLED_APPS = [
        # 我的应用程序
        'learning_logs',
        
        # 默认应用程序
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
    ]
    --略--
    
  2. 让Django修改数据库,使其能存储与模型相关的信息。

    • 终端执行命令makemigrations 让 Django 确定该如何修改数据库,使其能够存储与我们定义的新模型相关联的数据。
    python manage.py makemigrations learning_logs
    
    image-20200512105908665
    • 🌰 输出表明Django创建了一个名为 0001_initial.py 的迁移文件,此文件在数据库中为模型 Topic 创建一个表。
  3. 应用迁移,让 Django 替我们修改数据库:

    python manage.py migrate
    
    image-20200512110520096
    • 此命令的大部分输出与首次执行命令 migrate 用于创建数据库时的输出相同,需检查的是打红框的输出行。此处Django确认learning_logs应用迁移时一切正常(OK)。

⚠️ 每当需要修改模型管理的数据时or添加新模型时,都采取步骤:

  • 修改 models.py

  • 对app_name(此处为learning_logs)调用命令makemigrations

    python manage.py makemigrations app_name
    
  • 让 Django 迁移项目

    python manage.py migrate
    

18.2.3 Django 管理网站

  • 为应用程序定义模型时,Django提供的管理网站可轻松处理模型。
  • 网站的管理员可使用管理网站,普通用户不能使用。
  • 本节示例中 ,我们将建立管理网站,并通过它使用模型Topic来添加一些主题。(Topic是18.2.1中中示例创建的模型)
  1. 创建超级用户

    • Django允许创建具有所有权限的用户——超级用户。权限决定了用户可执行的操作。
    • 最严格的权限设置只允许用户阅读网站的公开信息;注册了的用户通常可阅读自己的私有数据,还可查看一些只有会员才能查看的信息。

    终端执行👇命令,在 Django 中创建超级用户:

    python manage.py createsuperuser
    
    image-20200515105731182

    此处输入用户名为 ll_admin,电子邮件字段可为空,密码需输入2次,至少包含8位。

    • 可能会对网站管理员隐藏有些敏感信息。例如,Django不存储你输入的密码,而是存储从此密码派生出来的一个字符串——散列值
      • 每当你输入密码时,Django都计算其散列值,并将结果于存储的散列值进行比较,若二者相同,则通过身份验证。

      • 通过存储散列值,即使黑客获得了网站数据库的访问权,也只能获取其中存储的散列值,而无法获取密码。在网站配置正确的情况下,几乎无法根据散列值推导出原始密码。

  2. 向管理网站注册模型

    Django自动在管理网站中添加了一些模型,如 User 和 Group,但对于我们创建的模型,必须手工进行注册。

    • 🌰 我们在创建应用程序 learning_logs 时,Django 在 models.py 所在目录中创建了 admin.py 文件。

      image-20200515231058883

      为管理网站注册 Topic,在 admin.py 文件中输入代码:

      from learning_logs.models import Topic
      admin.site.register(Topic)
      # 或 from .models import Topic
      # models 前面的句点让 Django 在当前文件所在目录查找 models.py
      

      这些代码导入我们要注册的模型 Topic,admin.site.register() 让Django通过管理网站管理我们的模型。

    • 此时,使用超级用户账户访问管理网站,访问 http://localhost:8000/admin/,输入刚创建的超级用户的用户名密码:

      image-20200515232034952

      登录后,可看到以下页面👇,此页面能让你添加和修改用户 User 和用户组 Group,还能管理与刚才定义的模型 Topic相关的数据。

      image-20200515232538328
  3. 添加主题

    向管理网站注册 Topic 后,我们添加第一个主题。

    单击 Topic 进入主题页面,点击 add,将出现一个用于添加新主题的表单。

    此处我们添加了 Chess、Rock Climbing 两个主题。

    image-20200515233003679

18.2.4 定义模型 Entry

本例中,要记录学到的国际象棋和攀岩知识,用户必须能在学习笔记中添加条目。

每个条目都与特定主题相关联,这种关系被称为多对一关系,即多个条目可关联到同一个主题。

在learning_logs目录中的文件 models.py 中添加模型 Entry:

from django.db import models
# Create your models here.

class Topic(models.Model):
    """用户学习的主题"""
    text = models.CharField(max_length=200)
    date_added = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        """返回模型的字符串表示"""
        return self.text

class Entry(models.Model):
    """学到的有关某个主题的具体知识"""
    topic = models.ForeignKey(Topic, on_delete=models.CASCADE)
    text = models.TextField()
    date_added = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name_plural = 'entries'

    def __str__(self):
        """返回模型的字符串表示"""
        return self.text[:50] + "..."
  • Entry 模型也继承了 Django 基类 Model。

  • 第一个属性 topic 是一个 ForeignKey实例

    • 外健是一个数据库术语,它指向数据库中的另一条记录;

    • 🌰 在此例中,这些将每个条目关联到特定的主题。每个主题创建时,都给它分配了一个键(或ID)需要在这两项数据之间建立联系时,Django使用与每项信息相关联的键。我们稍后将根据这些联系获取与特定主题相关联的所有条目。

    • 实参on_delete=models.CASCADE 级联删除。

    🌰 此处条目 Entry 与主题 Topic 为多对一,让 Django 在删除主题时同时删除所有与之相关联的条目。

  • 属性text是一个 TextField 实例。此字段无需长度限制,因为我们不想限制条目的长度。

  • 属性data_added属性让我们能按创建顺序呈现条目,并在每个条目旁边放置时间戳。

  • 在Entry类中嵌套了 Meta类,Meta存储用于管理模型的额外信息

    • 🌰 此处,它让我们能设置一个特殊属性,让 Django 在需要时使用 Entries 来表示多个条目。若没有这个类,则 Django 将使用多个 Entry 表示多个条目。
  • 方法 __str__() 告诉 Django,呈现条目时应显示那些信息。

    🌰 此处,由于条目包含的文本可能很长,故只显示text前50个字符

18.2.5 迁移模型 Entry

因为我们添加了一个新模型Entry,故需要再次迁移数据库。

python manage.py makemigrations learning_logs
python manage.py migrate
image-20200516004725064

输出表明生成了一个新的迁移文件 0002_entry.py,它告诉 Django 如何修改数据库,使其能存储与模型 Entry 相关的信息。

⚠️ Entry 模型中 topic = models.ForeignKey(Topic, on_delete=models.CASCADE),一定要写 on_delete 参数。

  • 因为django 升级到2.0之后,表与表之间关联的时候,必须要写on_delete 参数,否则会报异常:

    TypeError: init() missing 1 required positional argument: ‘on_delete’

    image-20200516004550965
  • on_delete各个参数的含义如下:

    on_delete=None,               # 删除关联表中的数据时,当前表与其关联的field的行为
    on_delete=models.CASCADE,     # 删除关联数据,与之关联也删除
    on_delete=models.DO_NOTHING,  # 删除关联数据,什么也不做
    on_delete=models.PROTECT,     # 删除关联数据,引发错误ProtectedError
    # models.ForeignKey('关联表', on_delete=models.SET_NULL, blank=True, null=True)
    on_delete=models.SET_NULL,    # 删除关联数据,与之关联的值设置为null(前提FK字段需要设置为可空,一对一同理)
    # models.ForeignKey('关联表', on_delete=models.SET_DEFAULT, default='默认值')
    on_delete=models.SET_DEFAULT, # 删除关联数据,与之关联的值设置为默认值(前提FK字段需要设置默认值,一对一同理)
    on_delete=models.SET,         # 删除关联数据,
    	a. 与之关联的值设置为指定值,设置:models.SET(值)
    	b. 与之关联的值设置为可执行对象的返回值,设置:models.SET(可执行对象)
    

    由于多对多 (ManyToManyField) 没有 on_delete 参数,所以以上只针对外键 (ForeignKey) 和一对一 (OneToOneField)

18.2.6 向管理网站注册 Entry

修改 models.py,执行命令python manage.py makemigrations app_name,再执行命令python manage.py migrate后。

还需注册模型 Entry —— 修改 admin.py 文件

from django.contrib import admin

# Register your models here.
from learning_logs.models import Topic, Entry
admin.site.register(Topic)
admin.site.register(Entry)

此时访问http://localhost:8000/admin/,可看到 learning_logs 下列出了 Entries。点击Add,添加条目。

image-20200516010242680 image-20200516010634835

点击 save 按钮,将返回主条目管理页面。

在这里,你将发现使用 text[:50] 作为条目的字符串表示的好处:管理界面中,只显示了条目的开头部分而不是其所有文本,这使得管理多个条目容易得多。

image-20200516010802118

18.2.7 Django shell

输入一些数据后,可通过交互式终端会话以编程方式查看这些数据。这种交互式环境称为 Django shell,是测试项目和排除其故障的理想之地。

  1. 在活动的虚拟环境中执行时,命令python manage.py shell启动一个Python解释器,可使用它来探索存储在项目数据库中的数据。

  2. 此处我们导入了模块 learning_logs.models 中的模型 Topic,然后使用方法 Topic.objects.all() 来获取模型 Topic 的所有实例;它返回的是一个列表,称为查询集

    python manage.py shell
    from learning_logs.models import Topic
    Topic.objects.all()
    
    image-20200516012109837
  3. 可像遍历列表一样遍历查询集。

    topics = Topic.objects.all()
    for topic in topics:
    	print(topic.id, topic)
    
    image-20200516012619792
  4. 排序查询集。

    topics = Topic.objects.order_by('date_added')
    for topic in topics:
    	print(topic.id, topic)
    
  5. 过滤查询集。

    topics = Topic.objects.filter(owner=1).order_by('date_added')
    for topic in topics:
    	print(topic.id, topic)
    
  6. 在知道对象ID后,可获取该对象并查看任何属性。

    t = Topic.objects.get(id=1)
    t.text
    t.date_added
    
    image-20200516013430156
  7. 我们还可查看与主题相关联的条目。

    前面我们给模型 Entry 定义了属性 topic。这是一个 ForeignKey,将条目与主题相关联。利用这种关联,Django能获取与特定主题相关联的所有条目。

    • 为通过外键关系获取数据,可使用相关模型的小写名称、下划线和单词set。

      🌰 此处有模型 Topic 和 Entry,Entry 通过外键关联到 Topic。t 为 Topic 的一个对象,表示一个主题,则可用t.entry_set.all() 获取此主题的所有条目。

    t.entry_set.all()
    
    image-20200516013752061

编写用户可请求的网页时,我们将使用这种语法。确认代码能获取所需的数据时,shell有很大帮助。

  • 若代码在shell的行为符合预期,则它们在项目文件中也能正确地工作。

  • 若代码引发了错误或获取的数据不符合预期,则在简单的shell环境中排除故障要比在生成网页的文件中排除故障容易得多。

    (我们不会太多地使用shell,但应继续使用它来熟悉对存储在项目中的数据进行访问的Django语法。)

⚠️ 每次修改模型后,都需要重启shell,才能看到修改后的效果。

  • 按Ctr + D退出shell会话,所使用Windows系统,应按Ctr + Z,最后回车。

18.3 创建网页:学习笔记主页

Django创建网页的过程通常分三个阶段:定义URL、编写视图和编写模板。

  1. URL模式描述了URL是如何设计的,让 Django 知道如何将浏览器请求与网站URL匹配,以确定返回哪个网页。
  2. 每个URL都被映射到特定的视图 —— 视图函数获取并处理网页所需的数据。
  3. 视图函数通常调用模版来渲染页面 —— 模板定义页面的总体结构。

(三个阶段的顺序无关紧要,但本项目中,总是先定义 URL模式)

🌰 为明白其中的工作原理,我们来创建学习笔记的主页。这包括定义该主页的URL、编写其视图函数并创建一个简单的模版。

18.3.1 映射URL

  1. 用户通过在浏览器中输入 URL 以及单击链接来请求页面。因此我们要确定项目需要哪些 URL。

    (主页的 URL 最重要,它是用户用于访问项目的基础 URL)

  2. 默认情况下,当前基础URL(http://localhost:8000/)返回默认的Django网站,让我们知道项目建立正确。

🌰 我们将修改此基础URL,将之映射到“学习笔记(learning_logs)”的主页。

当前目录结构:

image-20210320083704801
  1. 打开项目主文件夹 learning_log 中的文件 urls.py,看到已存在代码如下:

    from django.contrib import admin
    from django.urls import path
    
    urlpatterns = [
        path('admin/', admin.site.urls),
    ]
    
    • 👆导入的模块 admin 和函数 path 是为了对管理网站进行管理。
    • 变量urlpatterns包含项目中应用程序的 URL。
    • 模块admin.site.urls定义了可在管理网站中请求的所有 URL。
  2. 我们需要在此文件中加入包含 learning_logs 应用程序的 URL,修改代码为:

    from django.contrib import admin
    from django.urls import path, include
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('', include('learning_logs.urls')),  # 包含模块 learning_logs.urls
    ]
    
  3. 在 learning_logs 应用程序对应的文件夹 learnings_logs 中创建 urls.py 文件,输入代码:

    """定义 learning_logs 的URL模式"""
    
    from django.urls import path
    from . import views  # 从当前文件所在目录中导入views.py
    
    app_name = 'learning_logs'
    urlpatterns = [
        # 主页
        path('', views.index, name='index')
    ]
    
    • 变量app_name让 Django 能将此 url.py 文件同项目内其他应用程序中的同名文件区分开。

    • 变量urlpatterns是个列表,包含可在应用程序 learning_logs 中请求的页面。

    • 函数path(string, 函数名, name=)—— 将 URL 映射到视图。

      • 参数1:字符串,帮助 Django 正确地路由(route)请求。
      • 参数2:指定了要调用 views.py 中的哪个函数(视图函数)。
      • 参数3:指定此 URL 模式的名称,以便在代码的其他地方引用它。每当需要提供到这个主页的链接时,都将使用此名称,而不编写 URL。

      收到请求的 URL 后,Django 力图将请求路由给一个视图。因此它搜索所有的 URL 模式,找到与当前请求匹配的那个。

      • 当请求 URL 与参数1正则表达式匹配时,将调用参数2指定的函数;
      • 若 URL 与任何既有的 URL 模式都不匹配,Django 将返回一个错误页面。
        ⚠️ Django 忽略项目的基础 URL(http://localhost:8000/),故空字符串('')会与基础 URL 匹配。

18.3.2 编写视图

视图函数接受请求中的信息,准备好生成页面所需的数据,再将这些数据发送给浏览器——这通常是使用定义页面外观的模板实现的。

🌰 我们在18.3.1中,我们做了以下操作:

  • 修改项目 learning_log 的 urls.py 文件,将基础 URL(http://localhost:8000/)映射到应用程序 learning_logs 中。
  • 在应用程序 learning_logs 中创建 urls.py 文件,编写代码将基础 URL 请求路由给了 views.py 中的 index() 视图函数。

因此,我们接下来就要编写此视图函数代码:

from django.shortcuts import render


# Create your views here.
def index(request):
    """学习笔记的主页"""
    return render(request, 'learning_logs/index.html')
  • 函数render(request, 用于创建页面的模板路径)—— 根据视图提供的数据渲染响应。
    • 参数1:对象 request。urls.py 中 URL 请求与定义的模式匹配时,Django将查找对应视图函数,再将对象 request 传递给此视图函数。
    • 参数2:一个可用于创建页面的模板路径。

18.3.3 编写模板

模板定义页面的外观,每当页面被请求时,Django 将填入响应的数据。模板让你能访问视图提供的任何数据。

🌰 我们在18.3.2中视图函数使用了模板地址 'learning_logs/index.html',下面我们就要创建此模板。

在应用程序文件夹 learning_logs 中新建文件夹 templates,在文件夹 templates 中新建文件夹 learning_logs,在此文件夹中新建 index.html。编写其中代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <p>学习笔记</p>
    <p>学习笔记帮助你记录你所学习的任何主题</p>
</body>
</html>

此时的项目目录结构:

image-20210320232148745

此时我们请求项目的基础 URL(http://localhost:8000)将看到此创建的页面。

image-20210320232302414

工作原理:Django接收请求的 URL,发现该URL与模式''匹配,故调用函数 views.index(),这将使用 index.html 包含的模板来渲染页面。

上述创建页面的过程,将 URL、视图、模板分离的很好。

18.4 创建其他页面

我们已经明白了创建页面的流程:定义URL、编写视图和编写模板。

在此基础上创建了“学习笔记”的主页,接下来我们将创建两个显示数据的页面——一个列出所有主题,一个显示特定主题的所有条目。

18.4.1 模板继承

因为创建网站时,一些通用元素几乎会在所有页面出现,因此可编写一个包含通用元素的父模板,并让每个页面都继承这个父模板,而不必在每个页面中重复定义这些通用元素。

  1. 父模板:在 learning_logs/templates/learning_logs 文件夹中新建 base.html 模板

    <p><a href="{% url 'learning_logs:index' %}">学习笔记</a></p>
    {% block content %}{% endblock content %}
    

    模板标签 —— 用 {%%} 表示,是一小段代码,生成要在页面中显示的信息

    • url 标签 {% url '命名空间:URL模式' %} —— 生成一个 URL。{% url 'learning_logs:index' %} 中 URL 与在 learning_logs/urls.py 中定义的名为 'index' 的URL模式匹配。

      • learning_logs 是一个命名空间,来自文件 learning_logs/urls.py 中赋给 app_name 的值。
      • index 是该命名空间中一个名为 index 的 URL模式

      使用模板标签生成的URL能确保链接是最新的。

  • 一对块标签 {% block 块名 %}{% endblock 块名 %} —— 是个占位符,其中包含的信息由子模板决定

    • 标签 {% endblock 块名 %}指明内容定义的结束位置,其中块名非必填,但若模板包含多个块,则指定块名有助于确定结束的是哪个块。
  1. 子模板:重写 index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
    {% extends "learning_logs/base.html" %}
    {% block content %}
        <p>学习笔记帮助你记录你所学习的任何主题,以及相关主题的所有条目</p>
    {% endblock content %}
    </body>
    </html>
    

    标签 {%extends%} —— 让 Django 知道该模板继承自哪个父模板。

在大型项目中,通常有一个用于整个网站的父模板 base.html,且网站的各个主要部分都有一个父模板。

每个部分的父模板都继承 base.html,网站的每个页面都继承相应部分的父模板。

18.4.2 显示所有主题的页面

在18.4.1中我们讲解了高效的页面创建方式,下面我们来创建显示所有主题Topic的页面。

还是按照流程:定义URL、编写视图函数和编写模板。

  1. URL模式:修改 learning_logs/urls.py

    """定义 learning_logs 的URL模式"""
    
    from django.urls import path
    from . import views
    
    app_name = 'learning_logs'
    urlpatterns = [
        # 主页
        path('', views.index, name='index'),
        # 显示所有的主题
        path('topics/', views.topics, name='topics')  # URL中可在末尾包含斜杠,也可省略
    ]
    
  2. 编写视图函数:修改 learning_logs/views.py

    from django.shortcuts import render
    
    from .models import Topic
    
    
    # Create your views here.
    def index(request):
        """学习笔记的主页"""
        return render(request, 'learning_logs/index.html')
    
    
    def topics(request):
        """显示所有主题"""
        topics = Topic.objects.order_by('date_added')
        context = {'topics': topics}
        return render(request, 'learning_logs/topics.html', context)
    
    • 查询数据库 Topic.objects.order_by('date_added'),相关指令见18.2.7。返回查询集。
    • 15行定义一个将发送给模板的上下文context,是个字典,键是将在模板中用于访问数据的名称,值是要发送给模板的数据。
    • 在创建使用数据的页面时,除了request对象和模板路径外,还需传递上下文变量 context 给 render()
  3. 编写模板:创建 learning_logs/templates/learning_logs/topics.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>所有主题</title>
    </head>
    <body>
    {% extends 'learning_logs/base.html' %}
    {% block content %}
        <p>主题</p>
        <ul>
            {% for topic in topics %}
                <li>{{ topic }}</li>
                {% empty %}
                <li>目前没有被添加的主题。</li>
            {% endfor %}
        </ul>
    {% endblock content %}
    </body>
    </html>
    
    • 和模板 index.html 相同,先使用标签 {%extends%} 继承 base.html

    • 标签 {%for%} 用于for循环,标签 {%endfor%} 显式指出循环结束位置

      🆚 Python使用缩进来指出哪些代码是 for 循环的组成部分;模板中,for循环必须通过标签显式指出结束位置

    • 👆标签 {%empty%} 指出列表 topics 为空时该怎么办。

    • 要在模板中打印变量,需将变量名用双花括号括起 —— {{变量}}

      • 此变量来自模板中定义,或视图函数中 上下文 context 的。
  4. 修改父模板,使其包含到 topics.html 的链接:

    <p>
        <a href="{% url 'learning_logs:index' %}">学习笔记</a> -
        <a href="{% url 'learning_logs:topics' %}">主题</a>
    </p>
    {% block content %}{% endblock content %}
    

最终效果:

image-20210321013205813

点击“主题”,会跳转到显示所有主题的页面。

image-20210321013303968

点击“学习笔记”,会跳转到首页。

18.4.3 显示特定主题的页面

下面我们来创建显示特定主题的页面,它将显示此主题的所有条目。

还是按照流程:定义URL、编写视图函数和编写模板。

  1. URL模式:修改 learning_logs/urls.py

    """定义 learning_logs 的URL模式"""
    
    from django.urls import path
    from . import views
    
    app_name = 'learning_logs'
    urlpatterns = [
        # 主页
        path('', views.index, name='index'),
        # 显示所有的主题
        path('topics/', views.topics, name='topics'),
        # 特定主题的详细页面
        path('topics/<int:topic_id>/', views.topic, name='topic')
    ]
    
    • 我们需要通过主题的 id 属性来指出请求的是哪个主题的所有条目。🌰 http://localhost:8080/topics/1/
      • <int:topic_id> 捕获一个数值,并将其赋给变量 topic_id
      • 发现 URL 与这个模式匹配时,Django 将调用视图函数 topic(),并将存储在 topic_id 中的值作为实参传给它
  2. 编写视图函数:修改 learning_logs/views.py

    from django.shortcuts import render
    
    from .models import Topic
    
    
    # Create your views here.
    def index(request):
        """学习笔记的主页"""
        return render(request, 'learning_logs/index.html')
    
    
    def topics(request):
        """显示所有主题"""
        topics = Topic.objects.order_by('date_added')
        context = {'topics': topics}
        return render(request, 'learning_logs/topics.html', context)
    
    
    def topic(request, topic_id):
        """显示单个主题及其所有的条目"""
        topic = Topic.objects.get(id=topic_id)
        entries = topic.entry_set.order_by('date_added')
        context = {'topic': topic, 'entries': entries}
        return render(request, 'learning_logs/topic.html', context)
    
  3. 编写模板:创建 learning_logs/templates/learning_logs/topic.html

    {% extends 'learning_logs/base.html' %}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
    {% block content %}
        <p>主题:{{ topic }}</p>
        <p>相关条目:</p>
        <ul>
        {% for entry in entries %}
            <li>
                <p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
                <p>{{ entry.text|linebreaks }}</p>
            </li>
            {% empty %}
            <li>该主题目前没有被添加的条目。</li>
        {% endfor %}
        </ul>
    {% endblock content %}
    </body>
    </html>
    
    • 👆 变量 topic包含在字典 context 中。
    • 在 Django 模板中,竖线 | 表示模板 过滤器 —— 对模板变量的值进行修改的函数。
      • 过滤器 date:'M d, Y H:i' —— 以类似这样的格式显示时间戳:January 1, 2018 23:00
      • 过滤器 linebreaks —— 将包含换行符的长条目转换为浏览器能理解的格式,以免显示为不间断的文本块。
  4. 将显示所有主题的页面 topics.html 中的主题设置为链接:

    --- 略 ---
            {% for topic in topics %}
                <li>
                    <a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a>
                </li>
                {% empty %}
                <li>目前没有被添加的主题。</li>
            {% endfor %}
    --- 略 ---
    
    • 我们使用模板标签 url 根据 learning_logs 中名为 topic 的 URL 模式来生成合适的链接。这个 URL 模式要求提供实参topic_id,因此我们在模板标签 url 中添加了属性 topic.id。

    ⚠️ 注意 topic_id 和 topic.id 的区别。此处 topic_id 只是个变量名 🆚 topic.id 获取主题的 ID 值。

最终效果:

image-20210321013205813

点击“主题”,会跳转到显示所有主题的页面。

image-20210321015153602

点击某个主题,会跳转到其所拥有的条目页面。比如我点击"Chess"。

image-20210321015354325

第十九章 用户账户

Web应用程序的核心在于:让任何用户都能注册账户并能使用它。

🌰 本章中,我们将创建一些表单,让用户能添加主题和条目,以及编辑既有的条目。

然后我们将实现一个用户身份验证系统。首先创建一个注册页面,供用户创建账户,让有些页面只能供已登录的用户访问。接下来,修改一些视图函数,使用户只能看到自己的数据。

19.1 让用户输入数据

创建用户身份验证系统之前,我们先添加几个页面,让用户能输入数据:添加新主题、添加新条目、编辑既有的条目。

当前,只有超级用户能通过管理网站(http://localhost:8080/admin)输入数据。

我们不想让用户与管理网站交互,仅此我们使用 Django 的表单创建工具创建让用户能输入数据的页面。

19.1.1 添加新主题

创建基于表单的页面与创建普通页面流程相同:定义 URL、编写视图函数、编写模板。

一个主要差别在于:需要导入包含表单的模块 forms.py

  1. 创建用于添加主题的 表单:在 models.py 所在目录 learning_logs 创建 forms.py

    from django import forms
    
    from .models import Topic
    
    
    class TopicForm(forms.ModelForm):
        class Meta:
            model = Topic
            fields = ['text']
            labels = {'text': ''}
    

    Django 中创建表单最简单的方式是使用 ModelForm,最简单的 ModelForm 版本只包含一个内嵌的 Meta类 —— 告诉 Django 根据哪个模型创建表单,以及在表单中包含哪些字段。

    • model —— 指定创建表单根据的模型
    • fields —— 表单包含指定模型的哪些字段
    • labels —— 字典,为各个表单字段生成的标签(空标签对应空字符串 ''
  2. URL 模式:修改 learning_logs/urls.py

    ---略---
    urlpatterns = [
        ---略---
        # 用于添加新主题的页面
        path('new_topic/', views.new_topic, name='new_topic'),
    ]
    
  3. 编写视图函数:修改 learning_logs/views.py

    函数应处理两种情形:

    • 刚进入 new_topic 页面时显示空表单
    • 对提交的表单数据进行处理,并将用户重定向到页面 topics
    ---略---
    def new_topic(request):
        """添加新主题"""
        if request.method != 'POST':
            # 未提交数据:创建一个新表单
            form = TopicForm()
        else:
            # POST 提交的数据:对数据进行处理
            form = TopicForm(data=request.POST)
            if form.is_valid():
                form.save()
                return redirect('learning_logs:topics')
    
        # 显示空表单或指出表单数据无效
        context = {'form': form}
        return render(request, 'learning_logs/new_topic.html', context)
    
    • GET请求 🆚 POST请求

      • 对于只是从服务器读取数据的页面,使用 GET 请求
      • 在用户需通过表单提交信息时,通常使用 POST 请求
      • 处理所有表单时,都指定使用 POST 方法

      🌰 👆new_topic() 函数将请求对象作为参数。

      用户初次请求该页面时,其浏览器发送 GET 请求;用户填写并提交表单时,其浏览器发送 POST 请求。

    • 实例化 TopicForm 时,指定实参 datadata=request.POST,将使用 用户输入的数据(存储在request.POST中)创建实例,则实例化对象会包含用户提交的信息;不指定实参则创建空表单。

    • 方法 is_valid() —— 核实用户填写了所有必填字段(表单字段默认都必填),且输入数据域要求的字段类型一致。

    • 方法 save() —— 将表单中的数据写入数据库。返回值为新建的对象,此处为 Topic 实例。

    • 函数 redict('命名空间:URL模式') —— 参数 URL 模式也就是视图名。将用户重定向到此视图。

    • 用户提交的表单数据无效时,将显示一些默认的错误提示信息。

  4. 编写模板:创建 learning_logs/templates/learning_logs/new_topic.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>添加主题</title>
    </head>
    <body>
    {% extends 'learning_logs/base.html' %}
    {% block  content %}
        <p>添加新主题:</p>
        <form action="{% url 'learning_logs:new_topic' %}" method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button name="submit">添加主题</button>
        </form>
    {% endblock content %}
    </body>
    </html>
    
    • form 表单的实参 action —— 告诉服务器将提交的表单数据发送到哪里;method —— 指定浏览器的请求方式
    • 模板标签 {%csrf_token%} ——防止攻击者利用表单来获取对服务器未经授权的访问(此攻击称为 跨站请求伪造
    • 模板变量 {{form.as_p}} 能让 Django 自动创建显示表单所需的全部字段。修饰符 as_p 让 Django 以段落格式渲染所有表单元素。
  5. 在显示所有主题页面 topics.html 添加此链接:

    ---略---
    {% block content %}
        <p>主题</p>
        <ul>
            ---略---
        </ul>
        <a href="{% url 'learning_logs:new_topic' %}">添加新主题</a>
    {% endblock content %}
    ---略---
    

最终效果:主题页面 topics.html 中可“添加新主题”

image-20210321091043757

点击 “添加新主题” 会跳转到👇 表单页面 new_topic.html,添加后跳转到所有主题页面 topics.html 。

image-20210321091147518

若填写信息有误,会提示错误信息。

image-20210321091323898

19.1.2 添加新条目

与添加新主题的流程相同。

  1. 创建用于添加条目的 表单:在 forms.py 中再再添加一个类

    class EntryForm(forms.ModelForm):
        class Meta:
            model = Entry
            fields = ['text']
            labels = {'text': ''}
            widgets = {'text': forms.Textarea(attrs={'cols': 80})}
    

    Meta类指出此表单基于模型 Entry,表单包含 text 字段,且给此字段指定空标签。

    • 定义属性 widgets —— 字典,定义后可覆盖 Django 选择的默认小部件。
      • 小部件(widget) 是个 HTML 表单元素,Eg:单行文本框、多行文本区域、下拉列表等。
      • 👆定制字段 'text' 的输入小部件为 forms.Textarea,且设置文本区域宽度为80列(默认为40列)
  2. URL 模式:修改 learning_logs/urls.py

    ---略---
    urlpatterns = [
        ---略---
        # 用于添加新条目的页面
        path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry'),
    ]
    

    因为条目必须与特定的主题关联,因此添加新条目的 URL 模式中需要实参 topic_id

  3. 编写视图函数:修改 learning_logs/views.py

    ---略---
    def new_entry(request, topic_id):
        """在特定主题中添加新条目"""
        topic = Topic.objects.get(id=topic_id)
        if request.method != 'POST':
            # 未提交数据:创建一个新表单
            form = EntryForm()
        else:
            # POST 提交的数据:对数据进行处理
            form = EntryForm(data=request.POST)
            if form.is_valid():
                new_entry = form.save(commit=False)
                new_entry.topic = topic
                new_entry.save()
                return redirect('learning_logs:topic', topic_id=topic_id)
    
        # 显示空表单或指出表单数据无效
        context = {'topic': topic, 'form': form}
        return render(request, 'learning_logs/new_entry.html', context)
    
    • 视图函数包含形参 topic_id,用于存储从 URL 中获得的值
    • 函数逻辑:若是 GET 请求,执行 if 代码块,创建一个空的 EntryForm 实例(空表单);若是 POST 请求,就对数据进行处理——创建一个 EntryForm 实例,使用 request 对象的 POS 数据填充它。然后检查表单是否有效,若是,就设置条目对象的 topic 属性,再将条目对象保存到数据库。
    • save() 传递实参 commit=False ——让 Django 创建一个新的条目对象,并赋值给 new_entry,但不保存到数据库中。返回值为新建的对象,此处为 Entry 实例。
    • save() —— 将条目保存到数据库中,并将其与正确的主题相关联。
  4. 编写模板:创建 learning_logs/templates/learning_logs/new_entry.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>添加条目</title>
    </head>
    <body>
    {% extends 'learning_logs/base.html' %}
    {% block content %}
        <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
        <p>添加新条目:</p>
        <form action="{% url 'learning_logs:new_entry' topic.id %}" method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button name="submit">添加条目</button>
        </form>
    {% endblock content %}
    </body>
    </html>
    
  5. 在显示特定主题页面 topic.html 添加此链接:

    {% block content %}
        <p>主题:{{ topic }}</p>
        <p>相关条目:</p>
        <p><a href="{% url 'learning_logs:new_entry' topic.id %}">添加新条目</a></p>
        <ul>
        ---略---
        </ul>
    {% endblock content %}
    

最终效果:某特定的主题页面,显示“添加新条目”

image-20210321093631232

点击此按钮会跳转到👇 表单页面 new_entry.html,添加后跳转到之前的特定主题页面。

image-20210321093712188

19.1.3 编辑条目

  1. URL 模式:修改 learning_logs/urls.py

    urlpatterns = [
        ---略---
        # 用于编辑条目的页面
        path('edit_entry/<int:entry_id>/', views.edit_entry, name='edit_entry'),
    ]
    
  2. 编写视图函数:修改 learning_logs/views.py

    def edit_entry(request, entry_id):
        """编辑既有条目"""
        entry = Entry.objects.get(id=entry_id)
        topic = entry.topic
        if request.method != 'POST':
            # 初次请求:使用当前条目填充表单
            form = EntryForm(instance=entry)
        else:
            # POST 提交的数据:对数据进行处理
            form = EntryForm(instance=entry, data=request.POST)
            if form.is_valid():
                form.save()
                return redirect('learning_logs:topic', topic_id=topic.id)
    
        context = {'entry': entry, 'topic': topic, 'form': form}
        return render(request, 'learning_logs/edit_entry.html', context)
    
    • 传递实参 instance=entrydata=request.POST ,让 Django 根据既有条目对象 Entry 创建一个表单实例,并根据 request.POST 中的相关数据对其进行修改。
    • 此处直接调用 save() 因为这是编辑既有的条目,条目已关联到特定的主题,所以可直接保存到数据库。
  3. 编写模板:创建 learning_logs/templates/learning_logs/edit_entry.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>编辑条目</title>
    </head>
    <body>
    {% extends 'learning_logs/base.html' %}
    {% block content %}
        <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
        <p>编辑条目:</p>
        <form action="{% url 'learning_logs:edit_entry' entry.id %}" method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button name="submit">保存更改</button>
        </form>
    {% endblock content %}
    </body>
    </html>
    
  4. 在显示特定主题页面 topic.html 添加此链接:

    {% block content %}
        ---略---
        {% for entry in entries %}
            <li>
                <p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
                <p>{{ entry.text|linebreaks }}</p>
                <p><a href="{% url 'learning_logs:edit_entry' entry.id %}">编辑条目</a></p>
            </li>
            {% empty %}
            <li>该主题目前没有被添加的条目。</li>
        {% endfor %}
        </ul>
    {% endblock content %}
    

最终效果:某特定的主题页面中,各条目下显示“编辑条目”

image-20210321094903369

点击第二个条目中的编辑钮后会跳转到👇 表单页面 new_entry.html,文本框中显示条目信息,编辑后点击保存,会跳回到之前的特定主题页面。

image-20210321095013320

19.2 创建用户账户

本节将建立用户注册和身份验证系统,让用户能注册账户、登录、注销。

为此我们将新建一个应用程序,其中包含与处理用户账户有关的所有功能。(此应用程序将尽可能使用 Django自带的用户身份验证系统来完成工作)

19.2.1 创建应用程序 users

使用命令 startapp 创建名为 users 的应用程序:

python manage.py startapp appname
image-20210321100446535

此命令会创建目录 users,可见此目录结构与应用程序 learning_logs 相同。

19.2.2 将 users 添加到 settings.py 中

在项目learning_log 的 settings.py 中,需将此新的应用程序添加到 INSTALLED_APPS

---略---
INSTALLED_APPS = [
    # 我的应用程序
    'learning_logs',
    'users',

    # Django 默认创建的应用程序
    ---略---
]
---略---

这样 Django 就将应用程序 users 包含到项目中。

19.2.3 包含 users 的URL

修改项目根目录的 urls.py,使其包含将为应用程序 users 定义的 URL:

from django.contrib import admin
from django.urls import path, include

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

则任何以单词 users 打头的 URL 都能匹配应用程序 users 中的文件 urls.py。🌰 http://localhost:8000/users/login

19.2.4 登录页面

我们首先来实现登录页面的功能。我们将使用 Django 提供的默认视图 login,因此 URL 模式会稍有不同。

  1. URL 模式:新建 learning_log/users/urls.py

    """为应用程序 users 定义URL模式"""
    
    from django.urls import path, include
    from . import views
    
    app_name = 'users'
    urlpatterns = [
        # 包含默认的身份验证 URL
        path('', include('django.contrib.auth.urls'))
    ]
    
    • 一些默认的身份验证 URL django.contrib.auth.urls,这些 URL 包含具名的 URL 模式,如 'login''logout'
    • app_name 设置命名空间,即使是 Django 提供的默认 URL,将其包含在应用程序 users 的文件中后,也能通过命名空间 users 进行访问。
    • 登录页面的 URL 模式与 URL http://localhost:8000/users/login 匹配。此 URL 中的单词 users 让 Django 在 users/urls.py 中查找,单词 login 让它将请求发送给 Django 的默认视图 login。
  2. 编写模板:创建 users/templates/registration/login.html

    用户请求登录页面时,Django 将使用一个默认的视图函数 login,但我们让需为此页面提供模板 login.html

    ⚠️ 模板名是默认视图 login 规定好的。

    默认的身份验证视图在文件夹 registration 中查找模板,故我们创建文件夹 templates,再在其中创建文件夹 registration,最后在文件夹 registration 中创建模板 login.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>登录</title>
    </head>
    <body>
    {% extends 'learning_logs/base.html' %}
    {% block content %}
        {% if form.errors %}
            <p>用户名或密码错误,请重试。</p>
        {% endif %}
        <form action="{% url 'users:login' %}" method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button name="submit">登录</button>
            <input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/>
        </form>
    {% endblock content %}
    </body>
    </html>
    
    • 设置表单的 errors 属性,能显示错误信息

    • 17行写了一个隐藏的表单元素 'next',其实参 value 告诉 Django 在用于成功登录后将重定向到哪里。

      本例中,用户成功登录后,将返回主页

  3. 在 base.html 中添加到登录页面的链接:

    <p>
        <a href="{% url 'learning_logs:index' %}">学习笔记</a> -
        <a href="{% url 'learning_logs:topics' %}">主题</a> -
        {% if user.is_authenticated %}
            你好,{{ user.username }}
        {% else %}
            <a href="{% url 'users:login' %}">登录</a>
        {% endif %}
    </p>
    {% block content %}{% endblock content %}
    
    • 在 Django 身份验证系统中,每个模板都可使用变量 user
      • 此变量有属性 is_authenticated —— 若用户已登录,值为 True,否则为 False
      • 此变量有属性 username —— 已登录用户的用户名

最终效果:

image-20210321162441242

前面建立了一个用户账户 ll_admin,此处可用此账户登录。若登录成功:

image-20210321162607355

若登录失败:

image-20210321162649912

19.2.5 注销

本节提供一个让用户注销的途径。我们在 base.html 中添加一个注销连接,用户单击此链接后,将进入一个确认其已注销的页面。

  1. 在 base.html 中添加注销链接

    <p>
        <a href="{% url 'learning_logs:index' %}">学习笔记</a> -
        <a href="{% url 'learning_logs:topics' %}">主题</a> -
        {% if user.is_authenticated %}
            你好,{{ user.username }}
            <a href="{% url 'users:logout' %}"> 注销</a>
        {% else %}
            <a href="{% url 'users:login' %}">登录</a>
        {% endif %}
    </p>
    {% block content %}{% endblock content %}
    

    默认的具名注销 URL 模式为 'logout'

  2. 编写模板:创建 users/templates/registration/logged_out.html

    默认的注销视图使用模板 logged_out.html 渲染注销确认页面。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>注销</title>
    </head>
    <body>
    {% extends 'learning_logs/base.html' %}
    {% block content %}
        <p>你已经注销登录,谢谢访问!</p>
    {% endblock content %}
    </body>
    </html>
    

最终效果:

image-20210321163347836

点击注销按钮后:

image-20210321163409690

19.2.6 注册页面

本节我们创建一个页面供新用户注册。我们将使用 Django 提供的表单 UserCreationForm,但编写自己的视图函数和模板。

  1. URL 模式:修改 users/urls.py

    """为应用程序 users 定义URL模式"""
    
    from django.urls import path, include
    from . import views
    
    app_name = 'users'
    urlpatterns = [
        # 包含默认的身份验证 URL
        path('', include('django.contrib.auth.urls')),
        # 注册页面
        path('register/', views.register, name='register'),
    ]
    
  2. 编写视图函数:修改 users/views.py

    from django.shortcuts import render, redirect
    from django.contrib.auth import login
    from django.contrib.auth.forms import UserCreationForm
    
    
    # Create your views here.
    
    def register(request):
        """注册新用户"""
        if request.method != 'POST':
            # 显示空的注册表单
            form = UserCreationForm()
        else:
            # 处理填好的表单
            form = UserCreationForm(data=request.POST)
            if form.is_valid():
                new_user = form.save()
                # 让用户自动登录,再重定向到主页
                login(request, new_user)
                return redirect('learning_logs:index')
    
        # 显示空表单或指出表单无效
        context = {'form': form}
        return render(request, 'registration/register.html', context)
    
    • 导入了函数 login() 以便在用户正确填写注册信息后让其自动登录,还导入了默认表单 UserCreationForm
    • 此处 is_valid() 检查数据是否有效,就本例而言,指的是用户名未包含非法字符,输入的两次密码相同,以及用户没有试图做恶意的事情。
    • 调用表单的方法 save() 将用户名和密码的散列值保存到数据库中,返回新创建的用户对象。
    • 调用函数 login(),并传入对象 request 和新建的用户对象,为用户创建有效的会话,从而让其自动登录。
  3. 编写模板:创建 users/templates/registration/register.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>注册</title>
    </head>
    <body>
    {% extends 'learning_logs/base.html' %}
    {% block content %}
        <form action="{% url 'users:register' %}" method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button name="submit">注册</button>
            <input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/>
        </form>
    {% endblock content %}
    </body>
    </html>
    
  4. 在 base.html 中添加注册链接

    <p>
        <a href="{% url 'learning_logs:index' %}">学习笔记</a> -
        <a href="{% url 'learning_logs:topics' %}">主题</a> -
        {% if user.is_authenticated %}
            你好,{{ user.username }}
            <a href="{% url 'users:logout' %}"> 注销</a>
        {% else %}
            <a href="{% url 'users:register' %}">注册</a>
            <a href="{% url 'users:login' %}">登录</a>
        {% endif %}
    </p>
    {% block content %}{% endblock content %}
    

最终效果:

image-20210321164323766

点击注册按钮:

image-20210321164354944

19.3 让用户拥有自己的数据

用户应该能输入其专有的数据,因此我们将建立一个系统,确定各项数据所属的用户,再限制对页面的访问,让用户只能使用自己的数据。

🌰 本章将修改主题模型 Topic,让每个主题都归属于特定用户。这也将影响条目,因为每个条目都属于特定的主题。

我们先来限制对一些页面的访问。

19.3.1 使用 @login_required 限制访问

装饰器:放在函数定义前面的指令,Python 在函数运行前根据它来修改函数代码的行为。

装饰器 @login_required —— 在函数运行前检查用户是否登录,若未登录,Django 将重定向到 settings.py 中的 LOGIN_URL 指定的 URL。

🌰 在项目“学习笔记”中,我们不限制对主页和注册页面的访问,但限制对其他所有页面的访问。则在 learning_logs/view.py 中,对除 index() 外的每个视图都应用装饰器 @login_required

from django.contrib.auth.decorators import login_required

---略---

@login_required
def topics(request):
    ---略---


@login_required
def topic(request, topic_id):
    ---略---

@login_required
def new_topic(request):
    ---略---

@login_required
def new_entry(request, topic_id):
    ---略---

@login_required
def edit_entry(request, entry_id):
    ---略---
  1. 导入函数 login_required()
  2. login_required() 作为装饰器应用于视图函数 topic() —— 在视图函数定义前加上符号 @ 和函数名 login_required,让Python 在运行 topic() 代码前运行 login_required() 代码

login_required() 代码检查用户是否登录,我们希望用户未登录时能重定向到登录页面,故在 settings.py 末尾添加代码:

---略---
# 我的设置
LOGIN_URL = 'users:login'

Django让你能够轻松地限制对页面的访问,但你必须针对要保护哪些页面做出决定。

最好先确定项目的哪些页面不需要保护,再限制对其他所有页面的访问。

你可以轻松地修改过于严格的访问限制,其风险比不限制对敏感页面的访问更低。

19.3.2 将数据关联到用户

本小节我们需将数据关联到提交他们的用户。只需将最高层的数据关联到用户,更低层的数据就会自动关联到用户。
🌰 在项目“学习笔记”中,应用程序 learning_logs 的最高层数据是主题 Topic,所有条目 Entry 都与特定主题相关联。因此我们只需让每个主题都归属于特定用户,就能确定数据库中每个条目的所有者。

  1. 修改模型 Topic,添加一个关联带用户的外键

    修改 models.py

    from django.db import models
    from django.contrib.auth.models import User   # !!!!
    
    
    # Create your models here.
    
    class Topic(models.Model):
        """用户学习的主题"""
        text = models.CharField(max_length=200)
        date_added = models.DateTimeField(auto_now_add=True)
        owner = models.ForeignKey(User, on_delete=models.CASCADE) # !!!!
    
        def __str__(self):
            """返回模型的字符串表示"""
            return self.text
    ---略---
    

    导入 django.contrib.auth 中的模型 User,然后在 Topic 中添加字段 owner,它建立 Topic 到 User 的外键关系。用户被删除时,所有与之关联的主题也会被删除。

  2. 确定当前有哪些用户

    • 因为👆修改了模型,故需对数据库进行迁移,让 Django 修改数据库,使其能存储主题和用户之间的关联。

    • 为执行迁移,Django 需知道如何处理既有主题与用户的关联关系。我们可以将既有主题都关联到既有的一个用户上,如已经创建的超级用户。因此,需知道该用户的 ID。

    为了查看已创建的所有用户的 ID,我们启动一个 Django shell 会话,并执行👇命令:

    python manage.py shell
    from django.contrib.auth.models import User
    User.objects.all()
    for user in User.objects.all():
    	print(user.username, user.id)
    
    image-20210321195953189

    在 shell 会话中导入模型 User。查看目前创建了哪些用户。我们遍历用户列表打印每位用户的用户名和 ID。可见只创建了用户 ll_admin,ID为1。

  3. 迁移数据库

    在直到用户 ID 后,我们进行迁移数据库。

    python manage.py makemigrations learning_logs
    python manage.py migrate
    
    image-20210321200454225 image-20210321201059836
    • 执行 makemigrations 时,输出中 Django 指出你试图给既有模型 Topic 添加一个必不可少(不可为空)的字段,而该字段没有默认值。随后给我们提供了两种选择:要么现在提供默认值,要么退出并在models.py中添加默认值。
    • 我们选择第一个选项,故 Django 会让我们输入默认值。我们打算将既有主题关联到超级用户 ll_admin 上,故输入此用户 ID:1。
    • 接下来,Django使用这个值来迁移数据库,并生成了迁移文件0002_topic_owner.py,它在模型Topic中添加字段owner。
    • 最后执行迁移 migrate

    为验证迁移符合预期,可在 shell 会话中输入:

    from learning_logs.models import Topic
    for topic in Topic.objects.all():
    	print(topic, topic.owner)
    
    image-20210321201507536

    可见每个主题的的所属用户都为 ll_admin。证明迁移顺利。

你也可以重置数据库,而不是迁移它。但这样做既有的数据都将丢失。

重置数据库结构命令 python manage.py flush

19.3.3 只允许用户访问自己的主题

当前我们限制了用户只有登录后才能看到除首页外的其他页面,但不管以哪个用户身份登录,都能看到所有主题。

下面我们改变这一点,只向用户显示属于自己的主题。—— 修改 views.py 中的函数 topics():

---略---
@login_required
def topics(request):
    """显示所有主题"""
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')  # !!!!
    context = {'topics': topics}
    return render(request, 'learning_logs/topics.html', context)
---略---

用户登录后,request 对象将有一个 user 属性,这个属性存储了有关该用户的信息。

查询 Topic.objects.filter(owner=request.user) 让 Django 只从数据库中获取 owner 属性为当前用户的 Topic 对象。

19.3.4 保护用户的主题

我们尚未限制对显示单个主题的页面的访问,故任何已登录的用户都可输入类似 http://localhost:8000/topics/1/ 的URL,来访问显示相应主题所有条目的页面。

故我们修改 views.py 中的函数 topic():

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.http import Http404                  # !!!!
---略---
@login_required
def topic(request, topic_id):
    """显示单个主题及其所有的条目"""
    topic = Topic.objects.get(id=topic_id)
    # 确认请求的主题属于当前用户
    if topic.owner !=request.user:                # !!!!
        raise Http404
    entries = topic.entry_set.order_by('date_added')
    context = {'topic': topic, 'entries': entries}
    return render(request, 'learning_logs/topic.html', context)
---略---

服务器上没有请求的资源时,标准做法是返回404响应。故引入异常 Http404,并在用户请求其不应查看的主题时引发此异常,Django 返回 404 错误页面。

19.3.5 保护页面 edit_entry

同理于19.3.4,编辑条目页面也需被保护,禁止用户通过输入类似于前面的URL来访问其他用户的条目:

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.http import Http404                  # !!!!
---略---
@login_required
def edit_entry(request, entry_id):
    """编辑既有条目"""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic
    if topic.owner != request.user:               # !!!!
        raise Http404
---略---

我们获取指定的条目以及与之相关联的主题,然后检查主题的所有者是否是当前登录的用户,如果不是,就引发 Http404 异常。

19.3.6 将新主题关联到当前用户

当前,用于添加新主题的页面存在问题 —— 没有将新主题关联到特定用户。

如果你尝试添加新主题,将看到错误消息 IntegrityError,指出 learning_logs_topic.user_id 不能为NULL(NOT NULL constraint failed: learning_logs_topic.owner_id)。
Django的意思是说,创建新主题时,你必须指定其owner字段的值。

为此,我们修改 views.py 中的函数 new_topic():

---略---
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 未提交数据:创建一个新表单
        form = TopicForm()
    else:
        # POST 提交的数据:对数据进行处理
        form = TopicForm(data=request.POST)
        if form.is_valid():
            new_topic = form.save(commit=False)      # !!!!
            new_topic.owner = request.user           # !!!!
            new_topic.save()                         # !!!!
            return redirect('learning_logs:topics')

    # 显示空表单或指出表单数据无效
    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)
---略---

调用 form.save() 时传递实参 commit=False,因为要修改新主题,再将其保存到数据库。

第二十章 设置应用程序的样式并部署

当前,项目“学习笔记”功能齐备,但未设置样式,且只能在本地计算机上运行。本章中,我们将设置此项目的样式,再将其部署到一台服务器上,让其他人也能访问它。

20.1 设置项目“学习笔记”的样式

Bootstrap库 :它是一个大型样式设置工具集,还提供了大量模板,可应用于项目以创建独特的总体风格。

我们将使用应用程序 django-bootstrap4,这也让我们能练习使用其他 Django 开发人员开发的应用程序。

对Bootstrap初学者来说,这些模板比各个样式设置工具使用起来要容易得多。

要查看Bootstrap提供的模板,可访问官网 http://getbootstrap.com/,单击 Examples。本章我们将使用 Navbars 的模板Navbars Static,它提供了简单的顶部导航条、页面标题和用于放置页面内容的容器。

官网教程挺好的,但是是英文的。中文教程可看 Bootstrap4菜鸟教程

20.1.1 应用程序 django-bootstrap4

我们将使用应用程序 django-bootstrap4 将 Bootstrap 集成到项目中。

此应用程序下载必要的 Bootstrap 文件,将其放到项目的合适位置,让你能在项目的模板中使用样式设置指令。

  1. 我们在活动状态的虚拟环境执行👇指令,安装 django-bootstrap4:

    pip install django-bootstrap4
    
    image-20210321205818962
  2. 在 settings.py 的 INSTALLED_APPS 中添加👇代码,在项目中包含应用程序 django-bootstrap4:

    ---略---
    INSTALLED_APPS = [
        # 我的应用程序
        'learning_logs',
        'users',
    
        # 第三方应用程序
        'bootstrap4',
    
        # Django默认添加的应用程序
        'django.contrib.admin',
        ---略---
    
    • 新建一个名为“第三方应用程序”的片段,用于指定其他开发人员开发的应用程序,并在其中添加 'bootstrap4'
    • 务必将此片段放在 “我的应用程序” 和 “Django默认添加的应用程序” 之间

20.1.2 使用 Bootstrap 设置项目样式

  1. 修改父模板 base.html

    {% load bootstrap4 %}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>学习笔记</title>
        {% bootstrap_css %}
        {% bootstrap_javascript jquery='full' %}
    </head>
    <body>
    <nav class="navbar navbar-expand-md navbar-light bg-light mb-4 color">
        <!-- Brand -->
        <a href="{% url 'learning_logs:index' %}" class="navbar-brand">学习笔记</a>
    
        <!-- Toggler/collapsibe Button -->
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse"
                aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
    
        <!-- Navbar links -->
        <div class="collapse navbar-collapse" id="navbarCollapse">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a href="{% url 'learning_logs:topics' %}" class="nav-link">主题</a>
                </li>
            </ul>
    
            <ul class="navbar-nav ml-auto">
                {% if user.is_authenticated %}
                    <li class="nav-item">
                        <span class="navbar-text">你好,{{ user.username }}</span>
                    </li>
                    <li class="nav-item">
                        <a href="{% url 'users:logout' %}" class="nav-link">注销</a>
                    </li>
                {% else %}
                    <li class="nav-item">
                        <a href="{% url 'users:register' %}" class="nav-link">注册</a>
                    </li>
                    <li class="nav-item">
                        <a href="{% url 'users:login' %}" class="nav-link">登录</a>
                    </li>
                {% endif %}
            </ul>
        </div>
    </nav>
    
    <main role="main" class="container">
        <div class="pb-2 mb-2 border-bottom">
            {% block page_header %}{% endblock page_header %}
        </div>
        <div>
            {% block content %}{% endblock content %}
        </div>
    </main>
    </body>
    </html>
    
    • {% load bootstrap4 %} —— 加载 django-bootstrap4 中的模板标签集。
    • 导航栏一般放在页面的顶部,创建导航栏:
      • 我们可以使用 .navbar 类来创建一个标准的导航栏,后面紧跟: .navbar-expand-xl|lg|md|sm 类来创建响应式的导航栏 (大屏幕水平铺开,小屏幕垂直堆叠)。
      • 导航栏上的选项可以使用 <ul>元素并添加 class="navbar-nav" 类。 然后在 <li> 元素上添加 .nav-item 类, <a> 元素上使用 .nav-link 类。
      • 可以使用以下类来创建不同颜色导航栏:.bg-primary, .bg-success, .bg-info, .bg-warning, .bg-danger, .bg-secondary, .bg-dark.bg-light
      • .navbar-brand 类用于高亮显示品牌/Logo。
    • 折叠导航栏:
      • 通常,小屏幕上我们都会折叠导航栏,通过点击来显示导航选项。
      • 要创建折叠导航栏,可以在按钮上添加 class="navbar-toggler", data-toggle="collapse" 与 data-target="#thetarget" 类。然后在设置了 class="collapse navbar-collapse" 类的 div 上包裹导航内容(链接), div 元素上的 id 匹配按钮 data-target 的上指定的 id。
    • 选择器 mb 表示下外边距;pb 表示下内边距;border 在周围添加边框,border-bottom 只在下面添加边框;ml-auto 表示自动左边距,👆它根据导航栏包含的其他元素设置左边距,确保这组链接位于屏幕右边。

    最终效果:

    image-20210322113102741

    调整窗口的大小,使其非常窄时,导航栏将变成一个按钮,单击此按钮将打开一个下拉列表,其中包含所有的导航链接。

    image-20210322113222279

    导航栏教程:官网(英文),菜鸟教程(中文)

  2. 修改首页 index.html

    {% extends "learning_logs/base.html" %}
    {% block page_header %}
        <div class="jumbotron">
            <h1 class="display-3">跟踪你的学习。</h1>
    
            <p class="lead">制作你自己的学习日志,并把你正在学习的主题列成一个清单。每当你学到了关于某个主题的新知识时,就把你所学的总结下来。</p>
    
            <a href="{% url 'users:register' %}" class="btn btn-lg btn-primary" role="button">注册 &raquo;</a>
        </div>
    {% endblock page_header %}
    
    • jumbotron 元素是个大框,它可以包含任何东西,通常用于在主页中呈现简要的项目描述和让用户行动起来的元素。它是应用了一系列样式设置指令的 <div> 元素。
    • &raquo; —— HTML实体,表示两个右尖括号 >>
  3. 修改登录页面 login.html

    {% extends 'learning_logs/base.html' %}
    {% load bootstrap4 %}
    
    {% block page_header %}
        <h2>登录到你的账号</h2>
    {% endblock page_header %}
    
    {% block content %}
        <form action="{% url 'users:login' %}" method="post" class="form">
            {% csrf_token %}
            {% bootstrap_form form %}
            {% buttons %}
                <button name="submit" class="btn btn-primary">登录</button>
            {% endbuttons %}
    
        <input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/>
        </form>
    {% endblock content %}
    
    • 我们从这个模板中删除了 {% if form.errors %} 代码块,因为 django-bootstrap4 会自动管理表单错误。

    • <form>标签中添加属性 class="form",再使用模板标签 {% bootstrap_form %} 来显示表单,这个标签替换了我们在第19章使用的标签 {{ form.as_p }}

    • {% buttons %}{% endbuttons %} 将Bootstrap样式应用于按钮。

    image-20210322115940444
  4. 修改注册页面 register.html

    {% extends 'learning_logs/base.html' %}
    {% load bootstrap4 %}
    
    {% block page_header %}
            <h2>注册</h2>
    {% endblock page_header %}
    
    {% block content %}
        <form action="{% url 'users:register' %}" method="post" class="form">
            {% csrf_token %}
            {% bootstrap_form form %}
            {% buttons %}
                <button name="submit" class="btn btn-primary">注册</button>
            {% endbuttons %}
            <input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/>
        </form>
    {% endblock content %}
    
    image-20210322120349800
  5. 修改添加主题页面 new_topic.html

    {% extends 'learning_logs/base.html' %}
    {% block page_header %}
         <h2>添加新主题:</h2>
    {% endblock page_header %}
    
    {% block  content %}
        <form action="{% url 'learning_logs:new_topic' %}" method="post" class="form">
            {% csrf_token %}
            {% bootstrap_form form %}
            {% buttons %}
                <button name="submit" class="btn btn-primary">添加主题</button>
            {% endbuttons %}
        </form>
    {% endblock content %}
    
    image-20210322120046281
  6. 修改添加条目页面 new_entry.html

    {% extends 'learning_logs/base.html' %}
    {% load bootstrap4 %}
    
    {% block page_header %}
            <h2>添加新条目:</h2>
    {% endblock page_header %}
    
    {% block content %}
        <h3><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></h3>
        <form action="{% url 'learning_logs:new_entry' topic.id %}" method="post" class="form">
            {% csrf_token %}
            {% bootstrap_form form %}
            {% buttons %}
            <button name="submit" class="btn btn-primary">添加条目</button>
            {% endbuttons %}
        </form>
    {% endblock content %}
    
    image-20210322120148244
  7. 修改编辑条目页面 edit_entry.html

    {% extends 'learning_logs/base.html' %}
    {% load bootstrap4 %}
    
    {% block page_header %}
        <h2>编辑条目:</h2>
    {% endblock page_header %}
    
    {% block content %}
        <h3><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></h3>
        <form action="{% url 'learning_logs:edit_entry' entry.id %}" method="post" class="form">
            {% csrf_token %}
            {% bootstrap_form form %}
            {% buttons %}
                <button name="submit" class="btn btn-primary">保存更改</button>
            {% endbuttons %}
        </form>
    {% endblock content %}
    
    image-20210322120215940
  8. 修改显示所有主题的页面 topics.html

    {% extends 'learning_logs/base.html' %}
    {% block page_header %}
        <h1>主题</h1>
    {% endblock %}
    {% block content %}
        <ul>
            {% for topic in topics %}
                <li>
                    <h3><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></h3>
                </li>
            {% empty %}
                <li>
                    <h3>目前没有被添加的主题。</h3>
                </li>
            {% endfor %}
        </ul>
        <h3><a href="{% url 'learning_logs:new_topic' %}">添加新主题</a></h3>
    {% endblock content %}
    

    我们不需要标签 {% load bootstrap4 %},因为我们在这个文件中没有使用任何 bootstrap4 自定义标签。

  9. 修改显示特定主题的所有条目页面 topic.html

    {% extends 'learning_logs/base.html' %}
    {% block page_header %}
        <h3>{{ topic }}</h3>
    {% endblock page_header %}
    {% block content %}
        <p><a href="{% url 'learning_logs:new_entry' topic.id %}">添加新条目</a></p>
        <ul>
        {% for entry in entries %}
            <div class="card mb-3">
                <h4 class="card-header">
                    {{ entry.date_added|date:'M d, Y H:i' }}
                    <small><a href="{% url 'learning_logs:edit_entry' entry.id %}">编辑条目</a></small>
                </h4>
                <div class="card-body">
                    {{ entry.text|linebreaks }}
                </div>
            </div>
            {% empty %}
            <p>该主题目前没有被添加的条目。</p>
        {% endfor %}
        </ul>
    {% endblock content %}
    
    • 使用 Bootstrap 的 卡片(card) 组件来突出每个条目,卡片是带灵活的预定义样式的 <div>。
    image-20210322120123073

20.2 部署学习笔记

至此,项目“学习笔记”的外观已非常专业,下面将其部署到服务器,让任何有互联网连接的人都能使用它。

为此,我们将使用 Heroku,这是个基于 Web 的平台,供我们管理 Web 应用程序的部署。

本小节笔者并没有实操,所以就不记录了,看书吧 _(:з」∠)_

《Python编程从入门到实践》第二版 p407 页

参考文献

Django文档

Bootstrap4教程

本文作者:Joey-Wang

本文链接:https://www.cnblogs.com/joey-wang/p/14706588.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Joey-Wang  阅读(308)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开