python 编程-从入门到实践:项目3 Web应用程序—学习笔记 以及uwsgi+nginx部署到centos7服务器上

 

1 建立项目

1.1 指定规范

我们要编写一个名为“学习笔记”的Web应用程序, 让用户能够记录感兴趣的主题, 并在学习每个主题的过程中添加日志条目。 “学习笔记”的主页对这个网站进行描述,并邀请用户注册或登录。 用户登录后, 就可创建新主题、 添加新条目以及阅读既有的条目。

学习新的主题时, 记录学到的知识可帮助跟踪和复习这些知识。 优秀的应用程序让这个记录过程简单易行。

1.2 建立虚拟环境

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

将项目的库与其他项目分类是有益的也是将其部署到服务器上所必须的。

新建一个learning_log目录,在终端中切换到这个目录,用下面语句创建一个名为ll_env的虚拟环境(运用venv模块)

learning_log$ python -m venv ll_env
learning_log$

1.3 激活虚拟环境

windows系统:

D:\PythonWork\learning_log>ll_env\Scripts\activate
(ll_env) D:\PythonWork\learning_log>

环境处于活动状态时,环境名将包含在括号内,此时可以在坏境中安装包,并使用已安装的包。(在ll_env中安装的包仅在该环境处于活动状态时才可用)

linux系统:

learning_log$ source ll_env/bin/activate
(ll_env)learning_log$

要停止虚拟环境,可执行deactivate:

 

(ll_env) D:\PythonWork\learning_log>deactivate
D:\PythonWork\learning_log>

1.4 安装Django

 

创建并激活虚拟环境后,安装Django

 

(ll_env) D:\PythonWork\learning_log>pip3 install Django==1.11
Collecting Django==1.11
  Using cached https://files.pythonhosted.org/packages/47/a6/078ebcbd49b19e22fd560a2348cfc5cec9e5dcfe3c4fad8e64c9865135bb/Django-1.11-py2.py3-none-any.whl
Collecting pytz (from Django==1.11)
  Using cached https://files.pythonhosted.org/packages/4f/a4/879454d49688e2fad93e59d7d4efda580b783c745fd2ec2a3adf87b0808d/pytz-2020.1-py2.py3-none-any.whl
Installing collected packages: pytz, Django
Successfully installed Django-1.11 pytz-2020.1

由于我们是在虚拟环境中工作, 因此在所有的系统中, 安装Django的命令都相同: 不需要指定标志--user , 也无需使用python -m pip install package_name 这样较长的命令。

注意 Django仅在虚拟环境处于活动状态时才可用。

 

1.5 在Django中创建项目

 

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

 

❶ (ll_env)learning_log$ django-admin.py startproject learning_log .
❷ (ll_env)learning_log$ ls
learning_log ll_env manage.py
❸ (ll_env)learning_log$ ls learning_log
__init__.py settings.py urls.py wsgi.py

问题:django-admin.py startproject learning_log .该语句用于使用django来创建一个名为“learning_log ”的工程名。但在cmd中运行这个语句,并不能达到效果。执行完直接跳过或弹出IDE。

解决:django-admin startproject [项目名称] Windows运行时去掉.py

分析:pip在安装Django的时候会自动生成一个django-admin的可运行文件。

 

❶处的命令让Django新建一个名为learning_log的项目。 这个命令末尾的句点让新项目使用合适的目录结构, 这样开发完成后可轻松地将应用程序部署到服务器。

注意 千万别忘了这个句点, 否则部署应用程序时将遭遇一些配置问题。 如果忘记了这个句点,就将创建的文件和文件夹删除(ll_env除外) , 再重新运行这个命令

 

1.6 创建数据库

 

Django将大部分与项目相关的信息都存储在数据库中, 因此我们需要创建一个供Django使用的数据库。 为给项目“学习笔记”创建数据库, 请在处于活动虚拟环境中的情况下执行下面的命令:

 

(ll_env) D:\PythonWork\learning_log>python manage.py migrate

问题:报错SyntaxError: Generator expression must be parenthesized

分析:Django1.11版本和Python3.7版本不兼容。2.0版本以后的Django修复了这个问题

解决:升级Django版本 pip3 install -U Django

 

(ll_env) D:\PythonWork\learning_log>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying sessions.0001_initial... OK

成功之后查看目录又创建了一个文件db.sqlite3。

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

 

1.7 查看项目

 

下面来核实Django是否正确地创建了项目。 为此, 可执行命令runserver :

 

(ll_env) D:\PythonWork\learning_log>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
June 03, 2020 - 14:19:21
Django version 3.0.6, using settings 'learning_log.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

现在打开一款Web浏览器, 并输入URL: http://localhost:8000/; 如果这不管用, 请输入http://127.0.0.1:8000/。

 

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

 

2 创建应用程序

 

当前, 在前面打开的终端窗口中应该还运行着runserver 。 请再打开一个终端窗口(或标签页) , 并切换到manage.py所在的目录。 激活该虚拟环境, 再执行命令startapp :

learning_log$ source ll_env/bin/activate
(ll_env)learning_log$ python manage.py startapp learning_logs
❶ (ll_env)learning_log$ ls
db.sqlite3 learning_log learning_logs ll_env manage.py
❷ (ll_env)learning_log$ ls learning_logs/
admin.py __init__.py migrations models.py tests.py views.py

 

命令startapp appname 让Django建立创建应用程序所需的基础设施。

 

2.1 定义模型

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

打开models.py。(用models.py来定义我们要在应用程序中管理的数据)

from django.db import models
# 在这里创建模型

models模型介绍:https://www.cnblogs.com/Vera-y/p/11914233.html

django中models模块为各类数据库提供了统一的api,可根据不同的业务需求配置数据库。

models模块开发流程:

配置数据库 详情:https://www.cnblogs.com/Vera-y/p/11492314.html

定义模型类: 一个模型类在数据库中对应一张数据表

生成迁移文件

执行迁移文件生成数据表

使用模型类进行增删改查

 

django中的增删改查,在orm中转换为create/insert等语句再和各种数据库进行交互,之后,在数据库中得到的数据集等在通过orm转换为python中的数据表

orm的任务:

    • 根据对象的类型生成表结构

    • 将对象、列表的操作转换为sql语句

    • 将sql语句查询到的结构转换为对象、列表

模型告诉Django如何处理应用程序中存储的数据。 在代码层面, 模型就是一个类, 就像前面讨论的每个类一样, 包含属性和方法。 下面是表示用户将要存储的主题的模型:

 

from django.db import models
class Topic(models.Model):
    """用户学习的主题"""
    ❶ text = models.CharField(max_length=200)
    ❷ date_added = models.DateTimeField(auto_now_add=True)
    ❸ def __str__(self):
    """返回模型的字符串表示"""
    return self.text

我们需要告诉Django, 默认应使用哪个属性来显示有关主题的信息。 Django调用方法__str__() 来显示模型的简单表示。 在这里, 我们编写了方法__str__() , 它返回存储在属性text 中的字符串(见❸) 。

 

2.2 激活模型

要使用模型, 必须让Django将应用程序包含到项目中。为此,打开settings.py(它位于目录learning_log/learning_log中), 你将看到一个这样的片段, 即告诉Django哪些应用程序安装在项目中:settings.py

 

--snip--
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
)
--snip--

这是一个元组, 告诉Django项目是由哪些应用程序组成的。 请将INSTALLED_APPS 修改成下面这样, 将前面的应用程序添加到这个元组中:

 

--snip--
INSTALLED_APPS = (
    --snip--
    'django.contrib.staticfiles',
    # 我的应用程序
    'learning_logs',
)
--snip--

接下来, 需要让Django修改数据库, 使其能够存储与模型Topic 相关的信息。 为此, 在终端窗口中执行下面的命令:

 

(ll_env)learning_log$ python manage.py makemigrations learning_logs
Migrations for 'learning_logs':
0001_initial.py:
- Create model Topic
(ll_env)learning_log$

命令makemigrations 让Django确定该如何修改数据库, 使其能够存储与我们定义的新模型相关联的数据。 输出表明Django创建了一个名为0001_initial.py的迁移文件, 这个文件将在数据库中为模型Topic 创建一个表。

下面来应用这种迁移, 让Django替我们修改数据库:

(ll_env)learning_log$ python manage.py migrate
--snip--Running migrations:
Rendering model states... DONE
❶ Applying learning_logs.0001_initial... OK

 

这个命令的大部分输出都与我们首次执行命令migrate的输出相同。 我们需要检查的是❶处的输出行, 在这里, Django确认为learning_logs 应用迁移时一切正常(OK ) 。

每当需要修改“学习笔记”管理的数据时, 都采取如下三个步骤: 修改models.py; 对learning_logs 调用makemigrations ; 让Django迁移项目。

 

2.3 Django管理网站

 

为应用程序定义模型时, Django提供的管理网站(admin site) 让你能够轻松地处理模型。 网站的管理员可使用管理网站, 但普通用户不能使用。 在本节中, 我们将建立管理网站,并通过它使用模型Topic 来添加一些主题。

1、创建超级用户

 

(ll_env) D:\PythonWork\learning_log>python manage.py createsuperuser
Username (leave blank to use 'administrator'): ll_admin
Email address:
Password:
Password (again):
This password is too short. It must contain at least 8 characters.
This password is too common.
This password is entirely numeric.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

电子邮件地址可以为空

注意:可能会对网站管理员隐藏有些敏感信息。 例如, Django并不存储你输入的密码, 而存储从该密码派生出来的一个字符串——散列值。 每当你输入密码时, Django都计算其散列值, 并将结果与存储的散列值进行比较。 如果这两个散列值相同, 就通过了身份验证。 通过存储散列值, 即便黑客获得了网站数据库的访问权, 也只能获取其中存储的散列值, 而无法获得密码。 在网站配置正确的情况下, 几乎无法根据散列值推导出原始密码。

 

2、向管理网站注册模型

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

我们创建应用程序learning_logs 时, Django在models.py所在的目录中创建了一个名为admin.py的文件:

 

from django.contrib import admin
# 在这里注册你的模型

为向管理网站注册Topic , 请输入下面的代码

 

from django.contrib import admin
❶ from learning_logs.models import Topic
❷ admin.site.register(Topic)

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

现在, 使用超级用户账户访问管理网站: 访问http://localhost:8000/admin/ , 并输入你刚创建的超级用户的用户名和密码。 这个网页让你能够添加和修改用户和用户组, 还可以管理与刚才定义的模型Topic 相关的数据。

 

注意:如果你在浏览器中看到一条消息, 指出访问的网页不可用, 请确认你在终端窗口中运行着Django服务器。 如果没有, 请激活虚拟环境, 并执行命令python manage.py runserver 。

 

3、添加主题

为此, 单击Topics进入主题网页, 它几乎是空的, 这是因为我们还没有添加任何主题。 单击Add, 你将看到一个用于添加新主题的表单。 在第一个方框中输入Chess , 再单击Save, 这将返回到主题管理页面, 其中包含刚创建的主题。

下面再创建一个主题, 以便有更多的数据可供使用。 再次单击Add, 并创建另一个主题Rock Climbing 。 当你单击Save时, 将重新回到主题管理页面, 其中包含主题Chess和Rock Climbing。

2.4 定义模型Entry

要记录学到的国际象棋和攀岩知识, 需要为用户可在学习笔记中添加的条目定义模型。 每个条目都与特定主题相关联, 这种关系被称为多对一关系, 即多个条目可关联到同一个主题。

下面是模型Entry 的代码:

models.py

 

from django.db import models
class Topic(models.Model):
    --snip--
❶ 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] + "..."

第一个属性topic 是一个ForeignKey 实例(见❷) 。 外键是一个数据库术语, 它引用了数据库中的另一条记录; 这些代码将每个条目关联到特定的主题。 每个主题创建时, 都给它分配了一个键(或ID) 。 需要在两项数据之间建立联系时, Django使用与每项信息相关联的键。 稍后我们将根据这些联系获取与特定主题相关联的所有条目。【在创建一对多的关系的,需要在ForeignKey的第二参数中加入on_delete=models.CASCADE 主外关系键中,级联删除,也就是当删除主表的数据的时候从表中的数据也随着一起删除。这是数据库外键定义的一个可选项,用来设置当主键表中的被参考列的数据发生变化时,外键表中响应字段的变换规则的。】

在❹处, 我们在Entry 类中嵌套了Meta 类。 Meta 存储用于管理模型的额外信息, 在这里, 它让我们能够设置一个特殊属性, 让Django在需要时使用Entries 来表示多个条目。 如果没有这个类, Django将使用Entrys来表示多个条目。 最后, 方法__str__() 告诉Django, 呈现条目时应显示哪些信息。 由于条目包含的文本可能很长, 我们让Django只显示text 的前50个字符(见❺) 。 我们还添加了一个省略号, 指出显示的并非整个条目。

 

2.5 迁移模型(Entry)

由于我们添加了一个新模型, 因此需要再次迁移数据库。 你将慢慢地对这个过程了如指掌: 修改models.py, 执行命令python manage.py makemigrations app_name ,再执行命令python manage.py migrate 。

 

(ll_env) D:\PythonWork\learning_log>python manage.py makemigrations learning_logs
Migrations for 'learning_logs':
  learning_logs\migrations\0002_entry.py
    - Create model Entry

(ll_env) D:\PythonWork\learning_log>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, learning_logs, sessions
Running migrations:
  Applying learning_logs.0002_entry... OK

生成了一个新的迁移文件——0002_entry.py, 它告诉Django如何修改数据库, 使其能够存储与模型Entry 相关的信息(见❶) 。 执行命令migrate , 我们发现Django应用了这种迁移且一切顺利(见❷)

 

2.6 向管理网站注册Entry

我们还需要注册模型Entry 。 为此, 需要将admin.py修改成类似于下面这样:

admin.py

 

from django.contrib import admin
from learning_logs.models import Topic, Entry
admin.site.register(Topic)
admin.site.register(Entry)

返回到http://localhost/admin/ , 你将看到learning_logs下列出了Entries。 单击Entries的Add链接, 或者单击Entries再选择Add entry。 你将看到一个下拉列表, 让你能够选择要为哪个主题创建条目, 还有一个用于输入条目的文本框。 从下拉列表中选择Chess, 并添加一个条目。 下面是我添加的第一个条目。

2.7 Django shell

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

(ll_env) D:\PythonWork\learning_log>python manage.py shell
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from learning_logs.models import Topic
>>> Topic.objects.all()
<QuerySet [<Topic: The opening is the first part of the game, roughly the first ten moves or so. In the opening, it's a good idea to do three things— bring out your bishops and knights, try to control the center of the>, <Topic: Chess>, <Topic: Rock Climbing>]>
>>> topics = Topic.objects.all()
>>> for topic in topics:
... print(topic.id, topic)
  File "<console>", line 2
    print(topic.id, topic)
        ^
IndentationError: expected an indented block
>>> for topic in topics:
...     print(topic.id, topic)
...
1 The opening is the first part of the game, roughly the first ten moves or so. In the opening, it's a good idea to do three things— bring out your bishops and knights, try to control the center of the
2 Chess
3 Rock Climbing
>>> del topic[0]
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: 'Topic' object doesn't support item deletion
>>> t = Topic.objects.get(id=1)
>>> t.text
"The opening is the first part of the game, roughly the first ten moves or so. In the opening, it's a good idea to do three things— bring out your bishops and knights, try to control the center of the"
>>> t = Topic.objects.get(id=2)
>>> t.text
'Chess'
>>> t.date_added
datetime.datetime(2020, 6, 3, 9, 18, 27, 645347, tzinfo=<UTC>)
>>> t.entry_set.all()
<QuerySet [<Entry: The opening is the first part of the game, roughly...>, <Entry: In the opening phase of the game, it's important t...>]>
>>> ^Z

now exiting InteractiveConsole...

(ll_env) D:\PythonWork\learning_log>

 

编写用户可请求的网页时, 我们将使用这种语法。 确认代码能获取所需的数据时, shell很有帮助。 如果代码在shell中的行为符合预期, 那么它们在项目文件中也能正确地工作。 如果代码引发了错误或获取的数据不符合预期, 那么在简单的shell环境中排除故障要比在生成网页的文件中排除故障容易得多。 我们不会太多地使用shell, 但应继续使用它来熟悉对存储在项目中的数据进行访问的Django语法。

注意:每次修改模型后, 你都需要重启shell, 这样才能看到修改的效果。 要退出shell会话, 可按Ctr + D; 如果你使用的是Windows系统, 应按Ctr + Z, 再按回车键。

 

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

用Django创建网页的过程通常分三个阶段: 定义URL、 编写视图和编写模板。 首先, 你必须定义URL模式。 URL模式描述了URL是如何设计的, 让Django知道如何将浏览器请求与网站URL匹配, 以确定返回哪个网页。

每个URL都被映射到特定的视图 ——视图函数获取并处理网页所需的数据。 视图函数通常调用一个模板, 后者生成浏览器能够理解的网页。 为明白其中的工作原理, 我们来创建学习笔记的主页。 我们将定义该主页的URL、 编写其视图函数并创建一个简单的模板。

3.1 映射URL

用户通过在浏览器中输入URL以及单击链接来请求网页, 因此我们需要确定项目需要哪些URL。 主页的URL最重要, 它是用户用来访问项目的基础URL。 当前, 基础URL(http://localhost:8000/) 返回默认的Django网站, 让我们知道正确地建立了项目。 我们将修改这一点, 将这个基础URL映射到“学习笔记”的主页。

打开项目主文件夹learning_log中的文件urls.py, 你将看到如下代码:

urls.py

 

❶ from django.conf.urls import include, url
from django.contrib import admin
❷ urlpatterns = [
    ❸ url(r'^admin/', admin.site.urls),
]

前两行导入了为项目和管理网站管理URL的函数和模块(见❶) 。 这个文件的主体定义了变量urlpatterns (见❷) 。 在这个针对整个项目的urls.py文件中, 变量urlpatterns 包含项目中的应用程序的URL。 ❸处的代码包含模块admin.site.urls , 该模块定义了可在管理网站中请求的所有URL。

我们需要包含learning_logs的URL:

 

from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    ❶ url(r'', include('learning_logs.urls', namespace='learning_logs')),
]

 

在❶处, 我们添加了一行代码来包含模块learning_logs.urls 。 这行代码包含实参namespace , 让我们能够将learning_logs 的URL同项目中的其他URL区分开来, 这在项目开始扩展时很有帮助。

默认的urls.py包含在文件夹learning_log中, 现在我们需要在文件夹learning_logs中创建另一个urls.py文件:

 

urls.py

 

❶ """定义learning_logs的URL模式"""
❷ from django.conf.urls import url
❸ from . import views
❹ urlpatterns = [
    # 主页
    ❺ url(r'^$', views.index, name='index'),
]

为弄清楚当前位于哪个urls.py文件中, 我们在这个文件开头添加了一个文档字符串(见❶) 。 接下来, 我们导入了函数url , 因为我们需要使用它来将URL映射到视图(见❷) 。我们还导入了模块views (见❸) , 其中的句点让Python从当前的urls.py模块所在的文件夹中导入视图。 在这个模块中, 变量urlpatterns 是一个列表, 包含可在应用程序learning_logs 中请求的网页(见❹) 。

实际的URL模式是一个对函数url() 的调用, 这个函数接受三个实参(见❸) 。 第一个是一个正则表达式。 Django在urlpatterns 中查找与请求的URL字符串匹配的正则表达式, 因此正则表达式定义了Django可查找的模式。

我们来看看正则表达式r'^$' 。 其中的r 让Python将接下来的字符串视为原始字符串, 而引号告诉Python正则表达式始于和终于何处。 脱字符(^ ) 让Python查看字符串的开头,而美元符号让Python查看字符串的末尾。 总体而言, 这个正则表达式让Python查找开头和末尾之间没有任何东西的URL。 Python忽略项目的基础URL(http://localhost:8000/) , 因此这个正则表达式与基础URL匹配。 其他URL都与这个正则表达式不匹配。 如果请求的URL不与任何URL模式匹配, Django将返回一个错误页面。

url() 的第二个实参(见❺) 指定了要调用的视图函数。 请求的URL与前述正则表达式匹配时, Django将调用views.index (这个视图函数将在下一节编写) 。 第三个实参将这个URL模式的名称指定为index, 让我们能够在代码的其他地方引用它。 每当需要提供到这个主页的链接时, 我们都将使用这个名称, 而不编写URL。

 

3.2 编写视图

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

learning_logs中的文件views.py是你执行命令python manage.py startapp 时自动生成的, 当前其内容如下:

views.py

 

from django.shortcuts import render

# Create your views here.

当前, 这个文件只导入了函数render() , 它根据视图提供的数据渲染响应。 下面的代码演示了该如何为主页编写视图:

 

from django.shortcuts import render

# Create your views here.
def index(request):
    '''学习笔记的主页'''
    return render(request, 'learning_logs/index.html')

URL请求与我们刚才定义的模式匹配时, Django将在文件views.py中查找函数index() , 再将请求对象传递给这个视图函数。 在这里, 我们不需要处理任何数据, 因此这个函数只包含调用render() 的代码。 这里向函数render() 提供了两个实参: 原始请求对象以及一个可用于创建网页的模板。 下面来编写这个模板。

3.3 编写模板

模板定义了网页的结构。 模板指定了网页是什么样的, 而每当网页被请求时, Django将填入相关的数据。 模板让你能够访问视图提供的任何数据。 我们的主页视图没有提供任何数据, 因此相应的模板非常简单。

在文件夹learning_logs中新建一个文件夹, 并将其命名为templates。 在文件夹templates中, 再新建一个文件夹, 并将其命名为learning_logs。 这好像有点多余(我们在文件夹learning_logs中创建了文件夹templates, 又在这个文件夹中创建了文件夹learning_logs) , 但建立了Django能够明确解读的结构, 即便项目很大, 包含很多应用程序亦如此。 在最里面的文件夹learning_logs中, 新建一个文件, 并将其命名为index.html, 再在这个文件中编写如下代码:

index.html

<p>Learning Log</p>
<p>Learning Log helps you keep track of your learning, for any topic you're
learning about.</p>

 

这个文件非常简单。 对于不熟悉HTML的读者, 这里解释一下: 标签<p></p> 标识段落; 标签<p> 指出了段落的开头位置, 而标签</p> 指出了段落的结束位置。 这里定义了两个段落: 第一个充当标题, 第二个阐述了用户可使用“学习笔记”来做什么。

问题:NameError: name 'include' is not defined

解决:没有在urls.py文件中引入include

from django.conf.urls import include, url

问题:django.core.exceptions.ImproperlyConfigured: Specifying a namespace in include() without providing an app_name is not supported. Set the app_name attribute in the included module, or pass a 2-tuple containing the list of patterns and app_name instead.

分析:查找错误出处,为conf.py文件

源代码如下:

 

def include(arg, namespace=None):
    app_name = None
    if isinstance(arg, tuple):
        # Callable returning a namespace hint.
        try:
            urlconf_module, app_name = arg
        except ValueError:
            if namespace:
                raise ImproperlyConfigured(
                    'Cannot override the namespace for a dynamic module that '
                    'provides a namespace.'
                )
            raise ImproperlyConfigured(
                'Passing a %d-tuple to include() is not supported. Pass a '
                '2-tuple containing the list of patterns and app_name, and '
                'provide the namespace argument to include() instead.' % len(arg)
            )
    else:
        # No namespace hint - use manually provided namespace.
        urlconf_module = arg
 
    if isinstance(urlconf_module, str):
        urlconf_module = import_module(urlconf_module)
    patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)
    app_name = getattr(urlconf_module, 'app_name', app_name)
    if namespace and not app_name:
        raise ImproperlyConfigured(
            'Specifying a namespace in include() without providing an app_name '
            'is not supported. Set the app_name attribute in the included '
            'module, or pass a 2-tuple containing the list of patterns and '
            'app_name instead.',
        )
    namespace = namespace or app_name
    # Make sure the patterns can be iterated through (without this, some
    # testcases will break).
    if isinstance(patterns, (list, tuple)):
        for url_pattern in patterns:
            pattern = getattr(url_pattern, 'pattern', None)
            if isinstance(pattern, LocalePrefixPattern):
                raise ImproperlyConfigured(
                    'Using i18n_patterns in an included URLconf is not allowed.'
                )
    return (urlconf_module, app_name, namespace)

从include()函数可以看出来,这个函数有两个参数,一个arg,一个namespace,我在代码中也是两个参数,但是异常中提示了,没有提供app_name,还提示需要传入一个两元元组,从第六行代码。

urlconf_module, app_name = arg

 

可以看出来,arg就是那个元组,且给app_name赋值了,所以我们这里修改代码为:

 

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'', include(('learning_logs.urls', "learning_logs"), namespace='learning_logs')),
]

现在, 如果你请求这个项目的基础URL——http://localhost:8000/, 将看到刚才创建的网页, 而不是默认的Django网页。 Django接受请求的URL, 发现该URL与模式r'^$' 匹配, 因此调用函数views.index() , 这将使用index.html包含的模板来渲染网页, 结果如图【47】所示。

 

【47】

4 创建其他网页

制定创建网页的流程后, 可以开始扩充“学习笔记”项目了。 我们将创建两个显示数据的网页, 其中一个列出所有的主题, 另一个显示特定主题的所有条目。 对于每个网页, 我们都将指定URL模式, 编写一个视图函数, 并编写一个模板。 但这样做之前, 我们先创建一个父模板, 项目中的其他模板都将继承它。

4.1 模板继承

可编写一个包含通用元素的父模板, 并让每个网页都继承这个模板, 而不必在每个网页中重复定义这些通用元素。 这种方法能让你专注于开发每个网页的独特方面, 还能让修改项目的整体外观容易得多。

4.1.1 父模板

首先来创建一个名为base.html的模板, 并将其存储在index.html所在的目录中。 这个文件包含所有页面都有的元素; 其他的模板都继承base.html。 当前, 所有页面都包含的元素只有顶端的标题。 我们将在每个页面中包含这个模板, 因此我们将这个标题设置为到主页的链接:

base.html

 

<p>
❶   <a href="{% url 'learning_logs:index' %}">Learning Log</a>
</p>
❷ {% block content %}{% endblock content %}

这个文件的第一部分创建一个包含项目名的段落, 该段落也是一个到主页的链接。 为创建链接, 我们使用了一个模板标签 , 它是用大括号和百分号({% %} ) 表示的。 模板标签是一小段代码, 生成要在网页中显示的信息。 在这个实例中, 模板标签{% url 'learning_logs:index' %} 生成一个URL, 该URL与learning_logs/urls.py中定义的名为index 的URL模式匹配(见❶) 。 在这个示例中, learning_logs 是一个命名空间 , 而index 是该命名空间中一个名称独特的URL模式。

在简单的HTML页面中, 链接是使用锚 标签定义的:

<a href="link_url">link text</a>

 

让模板标签来生成URL, 可让链接保持最新容易得多。 要修改项目中的URL, 只需修改urls.py中的URL模式, 这样网页被请求时, Django将自动插入修改后的URL。 在我们的项目中, 每个网页都将继承base.html, 因此从现在开始, 每个网页都包含到主页的链接。

在❶处, 我们插入了一对块标签。 这个块名为content , 是一个占位符, 其中包含的信息将由子模板指定。

子模板并非必须定义父模板中的每个块, 因此在父模板中, 可使用任意多个块来预留空间, 而子模板可根据需要定义相应数量的块。

注意 在Python代码中, 我们几乎总是缩进四个空格。 相比于Python文件, 模板文件的缩进层级更多, 因此每个层级通常只缩进两个空格。

4.1.2 子模板

现在需要重新编写index.html, 使其继承base.html, 如下所示:

index.html

 

❶ {% extends "learning_logs/base.html" %}
❷ {% block content %}
  <p>Learning Log helps you keep track of your learning, for any topic you'relearning about.</p>
❸ {% endblock content %

我们将标题Learning Log 替换成了从父模板那里继承的代码(见❶) 。子模板的第一行必须包含标签{% extends %},让Django知道它继承了哪个父模板。文件base.html位于文件夹learning_logs中, 因此父模板路径中包含learning_logs。 这行代码导入模板base.html的所有内容, 让index.html能够指定要在content 块预留的空间中添加的内容。

在❷处, 我们插入了一个名为content 的{% block %} 标签, 以定义content 块。 不是从父模板继承的内容都包含在content 块中, 在这里是一个描述项目“学习笔记”的段落。 在❸处, 我们使用标签{% endblock content %} 指出了内容定义的结束位置。

模板继承的优点开始显现出来了: 在子模板中, 只需包含当前网页特有的内容。 这不仅简化了每个模板, 还使得网站修改起来容易得多。 要修改很多网页都包含的元素, 只需在父模板中修改该元素, 你所做的修改将传导到继承该父模板的每个页面。 在包含数十乃至数百个网页的项目中, 这种结构使得网站改进起来容易而且快捷得多。

4.2 显示所有主题的页面

所有主题页面显示用户创建的所有主题, 它是第一个需要使用数据的网页。

4.2.1 URL模式

首先, 我们来定义显示所有主题的页面的URL。 通常, 使用一个简单的URL片段来指出网页显示的信息; 我们将使用单词topics, 因此URL http://localhost:8000/topics/将返回显示所有主题的页面。 下面演示了该如何修改learning_logs/urls.py.

 

urls.py

"""为learning_logs定义URL模式"""
--snip--
urlpatterns = [
    # 主页
    url(r'^$', views.index, name='index'),
    # 显示所有的主题
    ❶ url(r'^topics/$', views.topics, name='topics'),
]

 

我们只是在用于主页URL的正则表达式中添加了topics/ (见❶) 。 Django检查请求的URL时, 这个模式与这样的URL匹配: 基础URL后面跟着topics 。 可以在末尾包含斜杠, 也可以省略它, 但单词topics 后面不能有任何东西, 否则就与该模式不匹配。 其URL与该模式匹配的请求都将交给views.py中的函数topics() 进行处理。

4.2.2 视图

view.py

 

from django.shortcuts import render
❶ from .models import Topic
def index(request):
    --snip--
❷ def topics(request):
    """显示所有的主题"""
    ❸ topics = Topic.objects.order_by('date_added')
    ❹ context = {'topics': topics}
    ❺ return render(request, 'learning_logs/topics.html', context)

我们首先导入了与所需数据相关联的模型(见❶) 。 函数topics() 包含一个形参: Django从服务器那里收到的request 对象(见❷) 。 在❸处, 我们查询数据库——请求提供Topic 对象, 并按属性date_added 对它们进行排序。 我们将返回的查询集存储在topics 中。

在❹处, 我们定义了一个将要发送给模板的上下文。 上下文是一个字典, 其中的键是我们将在模板中用来访问数据的名称, 而值是我们要发送给模板的数据。 在这里, 只有一个键—值对, 它包含我们将在网页中显示的一组主题。 创建使用数据的网页时, 除对象request 和模板的路径外, 我们还将变量context 传递给render() (见❺) 。

4.2.3 模板

显示所有主题的页面的模板接受字典context , 以便能够使用topics() 提供的数据。 请创建一个文件, 将其命名为topics.html, 并存储到index.html所在的目录中。 下面演示了如何在这个模板中显示主题:

topics.html

 

{% extends "learning_logs/base.html" %}
{% block content %}
  <p>Topics</p>
❶ <ul>
❷   {% for topic in topics %}
❸     <li>{{ topic }}</li>
❹   {% empty %}
      <li>No topics have been added yet.</li>
❺   {% endfor %}
❻ </ul>
{% endblock content %}

就像模板index.html一样, 我们首先使用标签{% extends %} 来继承base.html, 再开始定义content 块。 这个网页的主体是一个项目列表, 其中列出了用户输入的主题。 在标准HTML中, 项目列表被称为无序列表, 用标签<ul></ul> 表示。 包含所有主题的项目列表始于❶处。

在❷处, 我们使用了一个相当于for 循环的模板标签, 它遍历字典context 中的列表topics 。 模板中使用的代码与Python代码存在一些重要差别: Python使用缩进来指出哪些代码行是for 循环的组成部分, 而在模板中, 每个for 循环都必须使用{% endfor %} 标签来显式地指出其结束位置。 因此在模板中, 循环类似于下面这样:

 

{% for item in list %}
  do something with each item
{% endfor %}

在循环中, 我们要将每个主题转换为一个项目列表项。 要在模板中打印变量, 需要将变量名用双花括号括起来。 每次循环时, ❸处的代码{{ topic }} 都被替换为topic 的当前值。 这些花括号不会出现在网页中, 它们只是用于告诉Django我们使用了一个模板变量。 HTML标签<li></li> 表示一个项目列表项, 在标签对<ul></ul> 内部, 位于标签<li> 和</li> 之间的内容都是一个项目列表项。

在❹处, 我们使用了模板标签{% empty %} , 它告诉Django在列表topics 为空时该怎么办: 这里是打印一条消息, 告诉用户还没有添加任何主题。 最后两行分别结束for 循环(见❺) 和项目列表(见❻) 。

现在需要修改父模板, 使其包含到显示所有主题的页面的链接:

base.html

<p>
❶   <a href="{% url 'learning_logs:index' %}">Learning Log</a> -
❷   <a href="{% url 'learning_logs:topics' %}">Topics</a>
</p>
{% block content %}{% endblock content %}

 

我们在到主页的链接后面添加了一个连字符(见❶) , 然后添加了一个到显示所有主题的页面的链接——使用的也是模板标签url (见❷) 。 这一行让Django生成一个链接, 它与learning_logs/ urls.py中名为topics 的URL模式匹配。

现在如果你刷新浏览器中的主页, 将看到链接Topics。 单击这个链接, 将看到类似于图【48】所示的网页。

 

【48】

4.3 显示特定主题的页面

显示该主题的名称及该主题的所有条目。 同样, 我们将定义一个新的URL模式, 编写一个视图并创建一个模板。 我们还将修改显示所有主题的网页, 让每个项目列表项都是一个链接, 单击它将显示相应主题的所有条目。

4.3.1 URL模式

显示特定主题的页面的URL模式与前面的所有URL模式都稍有不同, 因为它将使用主题的id 属性来指出请求的是哪个主题。 例如, 如果用户要查看主题Chess(其id 为1) 的详细页面, URL将为http://localhost:8000/topics/1/。 下面是与这个URL匹配的模式, 它包含在learning_logs/urls.py中:

urls.py

 

--snip--
urlpatterns = [
    --snip--
    # 特定主题的详细页面
    url(r'^topics/(?P<topic_id>\d+)/$', views.topic, name='topic'),
]

我们来详细研究这个URL模式中的正则表达式——r'^topics/(?P<topic_id>\d+)/$' 。 r 让Django将这个字符串视为原始字符串, 并指出正则表达式包含在引号内。 这个表达式的第二部分(/(?P<topic_id>\d+)/ ) 与包含在两个斜杠内的整数匹配, 并将这个整数存储在一个名为topic_id 的实参中。 这部分表达式两边的括号捕获URL中的值; ?P<topic_id> 将匹配的值存储到topic_id 中; 而表达式\d+ 与包含在两个斜杆内的任何数字都匹配, 不管这个数字为多少位。

发现URL与这个模式匹配时, Django将调用视图函数topic() , 并将存储在topic_id 中的值作为实参传递给它。 在这个函数中, 我们将使用topic_id 的值来获取相应的主题。

4.3.2 视图

函数topic() 需要从数据库中获取指定的主题以及与之相关联的所有条目, 如下所示:

view.py

 

--snip--
❶ 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)

这是第一个除request 对象外还包含另一个形参的视图函数。 这个函数接受正则表达式(?P<topic_id>\d+) 捕获的值, 并将其存储到topic_id 中(见❶) 。 在❷处, 我们使用get() 来获取指定的主题, 就像前面在Django shell中所做的那样。 在❸处, 我们获取与该主题相关联的条目, 并将它们按date_added 排序: date_added 前面的减号指定按降序排列, 即先显示最近的条目。 我们将主题和条目都存储在字典context 中(见❹) , 再将这个字典发送给模板topic.html(见❺) 。

注意 ❷处和❸处的代码被称为查询, 因为它们向数据库查询特定的信息。 在自己的项目中编写这样的查询时, 先在Django shell中进行尝试大有裨益。 相比于编写视图和模板, 再在浏览器中检查结果, 在shell中执行代码可更快地获得反馈。

4.3.3 模板

这个模板需要显示主题的名称和条目的内容; 如果当前主题不包含任何条目, 我们还需向用户指出这一点:

 

{% extends 'learning_logs/base.html' %}
{% block content %}
❶   <p>Topic: {{ topic }}</p>
    <p>Entries:</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>
       There are no entries for this topic yet.
     </li>
    {% endfor %}
    </ul>
{% endblock content %}

像这个项目的其他页面一样, 这里也继承了base.html。 接下来, 我们显示当前的主题(见❶) , 它存储在模板变量{{ topic }} 中。 为什么可以使用变量topic 呢? 因为它包含在字典context 中。 接下来, 我们开始定义一个显示每个条目的项目列表(见❷) , 并像前面显示所有主题一样遍历条目(见❸) 。

每个项目列表项都将列出两项信息: 条目的时间戳和完整的文本。 为列出时间戳(见❹) , 我们显示属性date_added 的值。 在Django模板中, 竖线(| ) 表示模板过滤器——对模板变量的值进行修改的函数。 过滤器date: 'M d, Y H:i' 以这样的格式显示时间戳: January 1, 2015 23:00。 接下来的一行显示text 的完整值, 而不仅仅是entry 的前50个字符。 过滤器linebreaks (见❺) 将包含换行符的长条目转换为浏览器能够理解的格式, 以免显示为一个不间断的文本块。 在❻处, 我们使用模板标签{% empty %} 打印一条消息, 告诉用户当前主题还没有条目。

4.3.4 将显示所有主题的页面中的每个主题都设置为链接

在浏览器中查看显示特定主题的页面前, 我们需要修改模板topics.html, 让每个主题都链接到相应的网页, 如下所示:

topics.html

 

--snip--
  {% for topic in topics %}
    <li>
      <a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a>
    </li>
  {% empty %}
--snip--

我们使用模板标签url 根据learning_logs中名为topic 的URL模式来生成合适的链接。 这个URL模式要求提供实参topic_id , 因此我们在模板标签url 中添加了属性topic.id。 现在, 主题列表中的每个主题都是一个链接, 链接到显示相应主题的页面, 如http://localhost:8000/topics/1/。

 

用户账户

我们将实现一个用户身份验证系统。 你将创建一个注册页面, 供用户创建账户, 并让有些页面只能供已登录的用户访问。 接下来, 我们将修改一些视图函数,使得用户只能看到自己的数据。 你将学习如何确保用户数据的安全。

5 让用户能够输入数据

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

当前, 只有超级用户能够通过管理网站输入数据。 我们不想让用户与管理网站交互, 因此我们将使用Django的表单创建工具来创建让用户能够输入数据的页面。

5.1 添加新主题

5.1.1 用于添加主题的表单

让用户输入并提交信息的页面都是表单, 那怕它看起来不像表单。 用户输入信息时, 我们需要进行验证, 确认提供的信息是正确的数据类型, 且不是恶意的信息, 如中断服务器的代码。 然后, 我们再对这些有效信息进行处理, 并将其保存到数据库的合适地方。 这些工作很多都是由Django自动完成的。

在Django中, 创建表单的最简单方式是使用ModelForm, 它根据我们在第18章定义的模型中的信息自动创建表单。 创建一个名为forms.py的文件, 将其存储到models.py所在的目录中, 并在其中编写你的第一个表单:

forms.py

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

 

我们首先导入了模块forms 以及要使用的模型Topic 。 在❶处, 我们定义了一个名为TopicForm 的类, 它继承了forms.ModelForm 。

最简单的ModelForm 版本只包含一个内嵌的Meta 类, 它告诉Django根据哪个模型创建表单, 以及在表单中包含哪些字段。 在❷处, 我们根据模型Topic 创建一个表单, 该表单只包含字段text (见❸) 。 ❹处的代码让Django不要为字段text 生成标签。

5.1.2 URL模式new_topic

这个新网页的URL应简短而具有描述性, 因此当用户要添加新主题时, 我们将切换到http://localhost:8000/new_topic/。 下面是网页new_topic 的URL模式, 我们将其添加learning_logs/urls.py中:

urls.py

 

--snip--
urlpatterns = [
    --snip--
    # 用于添加新主题的网页
    url(r'^new_topic/$', views.new_topic, name='new_topic'),
]

5.1.3 视图函数new_topic()

函数new_topic() 需要处理两种情形: 刚进入new_topic 网页(在这种情况下, 它应显示一个空表单) ; 对提交的表单数据进行处理, 并将用户重定向到网页topics :

views.py

 

from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse

from .models import Topic
from .forms import TopicForm
--snip--
def new_topic(request):
    """添加新主题"""
    ❶ if request.method != 'POST':
        # 未提交数据: 创建一个新表单
        ❷ form = TopicForm()
    else:
        # POST提交的数据,对数据进行处理
        ❸ form = TopicForm(request.POST)
        ❹ if form.is_valid():
            ❺ form.save()
            ❻ return HttpResponseRedirect(reverse('learning_logs:topics'))
    ❼ context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)

问题:pycharm中提示错误,或者说,根本找不到django.core中的urlresolvers

分析:Django1.10.*版本开始把原来的 django.core.urlresolvers 包 更改为了 django.urls包

解决:from django.core.urlresolvers import reverse 改成 from django.urls import reverse

 

我们导入了HttpResponseRedirect 类, 用户提交主题后我们将使用这个类将用户重定向到网页topics 。 函数reverse() 根据指定的URL模型确定URL, 这意味着Django将在页面被请求时生成URL。 我们还导入了刚才创建的表单TopicForm 。

5.1.4 GET请求和POST请求

创建Web应用程序时, 将用到的两种主要请求类型是GET请求和POST请求。

GET请求:对于只是从服务器读取数据的页面

POST请求:在用户需要通过表单提交信息时, 通常使用

处理所有表单时, 我们都将指定使用POST方法。 还有一些其他类型的请求, 但这个项目没有使用。

函数new_topic() 将请求对象作为参数。 用户初次请求该网页时, 其浏览器将发送GET请求; 用户填写并提交表单时, 其浏览器将发送POST请求。 根据请求的类型, 我们可以确定用户请求的是空表单(GET请求) 还是要求对填写好的表单进行处理(POST请求) 。

❶处的测试确定请求方法是GET还是POST。 如果请求方法不是POST, 请求就可能是GET, 因此我们需要返回一个空表单(即便请求是其他类型的, 返回一个空表单也不会有任何问题) 。 我们创建一个TopicForm 实例(见❷) , 将其存储在变量form 中, 再通过上下文字典将这个表单发送给模板(见❼) 。 由于实例化TopicForm 时我们没有指定任何实参, Django将创建一个可供用户填写的空表单。

如果请求方法为POST, 将执行else 代码块, 对提交的表单数据进行处理。 我们使用用户输入的数据(它们存储在request.POST 中) 创建一个TopicForm 实例(见❸) ,这样对象form 将包含用户提交的信息。

要将提交的信息保存到数据库, 必须先通过检查确定它们是有效的(见❹) 。 函数is_valid() 核实用户填写了所有必不可少的字段(表单字段默认都是必不可少的) , 且输入的数据与要求的字段类型一致(例如, 字段text 少于200个字符, 这是我们在第18章中的models.py中指定的) 。 这种自动验证避免了我们去做大量的工作。 如果所有字段都有效, 我们就可调用save() (见❺) , 将表单中的数据写入数据库。 保存数据后, 就可离开这个页面了。 我们使用reverse() 获取页面topics 的URL, 并将其传递给HttpResponseRedirect() (见❻) , 后者将用户的浏览器重定向到页面topics。在页面topics 中, 用户将在主题列表中看到他刚输入的主题。

5.1.5 模板new_topic

new_topic.html

{% extends "learning_logs/base.html" %}
{% block content %}
    <p>Add a new topic:</p>
    ❶ <form action="{% url 'learning_logs:new_topic' %}" method='post'>
        ❷ {% csrf_token %}
        ❸ {{ form.as_p }}
        ❹ <button name="submit">add topic</button>
    </form>
{% endblock content %}

这个模板继承了base.html, 因此其基本结构与项目“学习笔记”的其他页面相同。 在❶处, 我们定义了一个HTML表单。 实参action 告诉服务器将提交的表单数据发送到哪里, 这里我们将它发回给视图函数new_topic() 。 实参method 让浏览器以POST请求的方式提交数据。

Django使用模板标签{% csrf_token %} (见❷) 来防止攻击者利用表单来获得对服务器未经授权的访问(这种攻击被称为跨站请求伪造 ) 。 在❸处, 我们显示表单, 从中可知Django使得完成显示表单等任务有多简单: 我们只需包含模板变量{{ form.as_p }} , 就可让Django自动创建显示表单所需的全部字段。 修饰符as_p 让Django以段落格式渲染所有表单元素, 这是一种整洁地显示表单的简单方式。

Django不会为表单创建提交按钮, 因此我们在❹处定义了一个这样的按钮。

5.1.6 链接到页面new_topic

接下来, 我们在页面topics 中添加一个到页面new_topic 的链接:

topics.html

 

{% extends "learning_logs/base.html" %}
{% block content %}
    <p>Topics</p>
    <ul>
    --snip--
    </ul>
    <a href="{% url 'learning_logs:new_topic' %}">Add a new topic:</a>
{% endblock content %}

5.2 添加新条目

将再次定义URL, 编写视图函数和模板, 并链接到添加新条目的网页。 但在此之前, 我们需要在forms.py中再添加一个类。

5.2.1 用于添加新条目的表单

我们需要创建一个与模型Entry 相关联的表单, 但这个表单的定制程度比TopicForm 要高些:

forms.py

 

from django import forms
from .models import Topic, Entry
class TopicForm(forms.ModelForm):
--snip--
class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry
        fields = ['text']
        ❶ labels = {'text': ''}
       ❷ widgets = {'text': forms.Textarea(attrs={'cols': 80})}

我们首先修改了import 语句, 使其除导入Topic 外, 还导入Entry 。 新类EntryForm 继承了forms.ModelForm , 它包含的Meta 类指出了表单基于的模型以及要在表单中包含哪些字段。 这里也给字段'text' 指定了一个空标签(见❶) 。

在❷处, 我们定义了属性widgets 。 小部件 (widget) 是一个HTML表单元素, 如单行文本框、 多行文本区域或下拉列表。 通过设置属性widgets , 可覆盖Django选择的默认小部件。 通过让Django使用forms.Textarea , 我们定制了字段'text' 的输入小部件, 将文本区域的宽度设置为80列, 而不是默认的40列。 这给用户提供了足够的空间, 可以编写有意义的条目。

5.2.2 URL模式new_entry

在用于添加新条目的页面的URL模式中, 需要包含实参topic_id , 因为条目必须与特定的主题相关联。 该URL模式如下, 我们将它添加到了learning_logs/urls.py中:\

urls.py

--snip--
urlpatterns = [
    --snip--
    # 用于添加新条目的页面
    url(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry, name='new_entry'),
]

 

这个URL模式与形式为http://localhost:8000/new_entry/id / 的URL匹配, 其中 id 是一个与主题ID匹配的数字。 代码(?P<topic_id>\d+) 捕获一个数字值, 并将其存储在变量topic_id 中。 请求的URL与这个模式匹配时, Django将请求和主题ID发送给函数new_entry() 。

5.2.3 视图函数new_entry()

views.py

 

from django.shortcuts import render
--snip--

from .models import Topic
from .forms import TopicForm, EntryForm

--snip--
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 HttpResponseRedirect(reverse('learning_logs:topic',args=[topic_id]))
    context = {'topic': topic, 'form': form}
    return render(request, 'learning_logs/new_entry.html', context)

我们修改了import 语句, 在其中包含了刚创建的EntryForm 。 new_entry() 的定义包含形参topic_id , 用于存储从URL中获得的值。 渲染页面以及处理表单数据时, 都需要知道针对的是哪个主题, 因此我们使用topic_id 来获得正确的主题(见❶) 。

在❷处, 我们检查请求方法是POST还是GET。 如果是GET请求, 将执行if 代码块: 创建一个空的EntryForm 实例(见❸) 。 如果请求方法为POST, 我们就对数据进行处理:创建一个EntryForm 实例, 使用request 对象中的POST数据来填充它(见❹) ; 再检查表单是否有效, 如果有效, 就设置条目对象的属性topic , 再将条目对象保存到数据库。

调用save() 时, 我们传递了实参commit=False (见❺) , 让Django创建一个新的条目对象, 并将其存储到new_entry 中, 但不将它保存到数据库中。 我们将new_entry的属性topic 设置为在这个函数开头从数据库中获取的主题(见❻) , 然后调用save() , 且不指定任何实参。 这将把条目保存到数据库, 并将其与正确的主题相关联。

在❼处, 我们将用户重定向到显示相关主题的页面。 调用reverse() 时, 需要提供两个实参: 要根据它来生成URL的URL模式的名称; 列表args , 其中包含要包含在URL中的所有实参。 在这里, 列表args 只有一个元素——topic_id 。 接下来, 调用HttpResponseRedirect() 将用户重定向到显示新增条目所属主题的页面, 用户将在该页面的条目列表中看到新添加的条目。

5.2.4 模板new_entry

new_entry.html

 

{% extends "learning_logs/base.html" %}

{% block content %}
    
    ❶ <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
    
    <p>Add a new entry:</p>
    ❷ <form action="{% url 'learning_logs:new_entry' topic.id %}" method='post'>
        {% csrf_token %}
       {{ form.as_p }}
       <button name='submit'>add entry</button>
    </form>
    
{% endblock content %}

我们在页面顶端显示了主题(见❶) , 让用户知道他是在哪个主题中添加条目; 该主题名也是一个链接, 可用于返回到该主题的主页面。

表单的实参action 包含URL中的topic_id 值, 让视图函数能够将新条目关联到正确的主题(见❷) 。 除此之外, 这个模板与模板new_topic.html完全相同。

5.2.5 链接到页面new_entry

需要在显示特定主题的页面中添加到页面new_entry 的链接:

topic.html

{% extends "learning_logs/base.html" %}
{% block content %}
    <p>Topic: {{ topic }}</p>
    <p>Entries:</p>
    <p>
        <a href="{% url 'learning_logs:new_entry' topic.id %}">add new entry</a>
    </p>
    <ul>
    --snip—
    </ul>
{% endblock content %}

我们在显示条目前添加链接, 因为在这种页面中, 执行的最常见的操作是添加新条目。 图【52】显示了页面new_entry 。 现在用户可以添加新主题, 还可以在每个主题中添加任意数量的条目。 请在一些既有主题中添加一些新条目, 尝试使用一下页面new_entry 。

    【52】

5.3 编辑条目

5.3.1 URL模式edit_entry

这个页面的URL需要传递要编辑的条目的ID。 修改后的learning_logs/urls.py如下:

urls.py

--snip--
urlpatterns = [
    --snip--
    # 用于编辑条目的页面
    url(r'^edit_entry/(?P<entry_id>\d+)/$', views.edit_entry, name='edit_entry'),
]

在URL(如http://localhost:8000/edit_entry/1/) 中传递的ID存储在形参entry_id 中。 这个URL模式将预期匹配的请求发送给视图函数edit_entry() 。

5.3.2 视图函数edit_entry()

页面edit_entry 收到GET请求时, edit_entry() 将返回一个表单, 让用户能够对条目进行编辑。 该页面收到POST请求(条目文本经过修订) 时, 它将修改后的文本保存到数据库中:

view.py

from django.shortcuts import render
--snip--

from .models import Topic, Entry
from .forms import TopicForm, EntryForm
--snip--

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 HttpResponseRedirect(reverse('learning_logs:topic',args=[topic.id]))
    context = {'entry': entry, 'topic': topic, 'form': form}
    return render(request, 'learning_logs/edit_entry.html', context)

我们首先需要导入模型Entry 。 在❶处, 我们获取用户要修改的条目对象, 以及与该条目相关联的主题。 在请求方法为GET时将执行的if 代码块中, 我们使用实参instance=entry 创建一个EntryForm 实例(见❷) 。 这个实参让Django创建一个表单, 并使用既有条目对象中的信息填充它。 用户将看到既有的数据, 并能够编辑它们。处理POST请求时, 我们传递实参instance=entry 和data=request.POST (见❸) , 让Django根据既有条目对象创建一个表单实例, 并根据request.POST 中的相关数据对其进行修改。 然后, 我们检查表单是否有效, 如果有效, 就调用save() , 且不指定任何实参(见❹) 。 接下来, 我们重定向到显示条目所属主题的页面(见❺) , 用户将在其中看到其编辑的条目的新版本。

5.3.3 模板edit_entry

edit_entry.html

{% extends "learning_logs/base.html" %}
{% block content %}
    <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
    <p>Edit entry:</p>
    ❶ <form action="{% url 'learning_logs:edit_entry' entry.id %}" method='post'>
        {% csrf_token %}
        {{ form.as_p }}
        ❷ <button name="submit">save changes</button>
    </form>
{% endblock content %}

在❶处, 实参action 将表单发回给函数edit_entry() 进行处理。 在标签{% url %} 中, 我们将条目ID作为一个实参, 让视图对象能够修改正确的条目对象。 我们将提交按钮命名为save changes, 以提醒用户: 单击该按钮将保存所做的编辑, 而不是创建一个新条目(见❷)。

5.3.4 链接到页面edit_entry

topic.html

--snip--
    {% 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 %}">edit entry</a>
            </p>
         </li>
--snip--

我们将编辑链接放在每个条目的日期和文本后面。 在循环中, 我们使用模板标签{% url %} 根据URL模式edit_entry 和当前条目的ID属性(entry.id ) 来确定URL。 链接文本为"edit entry" , 它出现在页面中每个条目的后面。 图【53】显示了包含这些链接时, 显示特定主题的页面是什么样的。

【53】

问题(19-1遇到):django 项目运行出现了问题,问题如下 django.urls.exceptions.NoReverseMatch: ‘xxx’ is not a registered namespace

解决:在对应的app的urls下加上app_name=‘xxx’

#polls.url 下添加一个app_name就好:
from . import views
app_name = 'xxx'
urlpatterns = [...]

6 创建用户账户

立一个用户注册和身份验证系统, 让用户能够注册账户, 进而登录和注销。 我们将创建一个新的应用程序, 其中包含与处理用户账户相关的所有功能。 我们还将对模型Topic 稍做修改, 让每个主题都归属于特定用户。

6.1 应用程序users

首先使用命令startapp来创建一个名为users的应用程序

(ll_env) D:\PythonWork\learning_log>python manage.py startapp users

(ll_env) D:\PythonWork\learning_log>dir
 驱动器 D 中的卷是 新加卷
 卷的序列号是 6667-B823

 D:\PythonWork\learning_log 的目录

2020/06/29  11:31    <DIR>          .
2020/06/29  11:31    <DIR>          ..
2020/06/22  18:45    <DIR>          .idea
2020/06/12  16:48           143,360 db.sqlite3
2020/06/08  11:58    <DIR>          learning_log
2020/06/12  15:51    <DIR>          learning_logs
2020/06/03  13:47    <DIR>          ll_env
2020/06/03  13:49               832 manage.py
2020/06/29  11:31    <DIR>          users
               2 个文件        144,192 字节
               7 个目录 31,096,836,096 可用字节

6.1.1 将应用程序users添加到settings.py中

setting.py

--snip--
INSTALLED_APPS = (
    --snip--
    # 我的应用程序
    'learning_logs',
    'users',
)
--snip--

6.1.2 包含应用程序users的URL

urls.py

from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    # url(r'^users/', include('users.urls', namespace='users')),
    # url(r'', include('learning_logs.urls', namespace='learning_logs')),
    url(r'^users/',include(('users.urls','users'),namespace='users')),
    url(r'', include(('learning_logs.urls', "learning_logs"), namespace='learning_logs')),
]

问题:之前遇到过django.core.exceptions.ImproperlyConfigured: Specifying a namespace in include() without providing an app_name is not supported. Set the app_name attribute in the included module, or pass a 2-tuple containing the list of patterns and app_name instead.

解决:include()函数有两个参数,一个arg,一个namespace,我在代码中也是两个参数,但是异常中提示了,没有提供app_name,还提示需要传入一个两元元组。

6.2 登录页面

我们将使用Django提供的默认登录视图, 因此URL模式会稍有不同。 在目录learning_log/users/中, 新建一个名为urls.py的文件, 并在其中添加如下代码:

urls.py

"""为应用程序users定义URL模式"""
from django.conf.urls import url
❶ from django.contrib.auth.views import login
from . import views

urlpatterns = [
    # 登录页面
    ❷ url(r'^login/$', login, {'template_name': 'users/login.html'},
        name='login'),
]

我们首先导入了默认视图login(❶)登录页面的URL模式与URL http://localhost:8000/users/login/匹配(见❷) 。 这个URL中的单词users让Django在users/urls.py中查找, 而单词login让它将请求发送给Django默认视图login (请注意, 视图实参为login , 而不是views.login ) 。 鉴于我们没有编写自己的视图函数, 我们传递了一个字典, 告诉Django去哪里查找我们将编写的模板。 这个模板包含在应用程序users 而不是learning_logs 中。

问题:Django2.0 里 django.contrib.auth.views没有login

分析:将login改为LoginView

将url(r'^login/$', login, {'template_name': 'users/login.html'}, name = 'login'),

改为url(r'^login/$', LoginView.as_view(template_name='users/login.html'), name="login"),

也就是说Django2.0中login的导入方法和使用方法都进行了更新

'''为应用程序users定义URL模式'''

from django.conf.urls import url
from django.contrib.auth.views import LoginView

from . import views

urlpatterns = [
    # 登录页面
    # url(r'^login/$', login, {'template_name': 'users/login.html'}, name='login'),
    url(r'^login/$',LoginView.as_view(template_name= 'users/login.htm'),name='login')
]

6.2.1 模板login.html

用户请求登录页面时, Django将使用其默认视图login , 但我们依然需要为这个页面提供模板。 为此, 在目录learning_log/users/中, 创建一个名为templates的目录, 并在其中创建一个名为users的目录。 以下是模板login.html, 你应将其存储到目录learning_log/users/templates/users/中:

login.html

{% extends "learning_logs/base.html" %}
{% block content %}
    ❶ {% if form.errors %}
    <p>Your username and password didn't match. Please try again.</p>
    {% endif %}
    
    ❷ <form method="post" action="{% url 'users:login' %}">
    {% csrf_token %}
    ❸ {{ form.as_p }}
   ❹ <button name="submit">log in</button>
   ❺ <input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
   </form>
{% endblock content %}

这个模板继承了base.html, 旨在确保登录页面的外观与网站的其他页面相同。 请注意, 一个应用程序中的模板可继承另一个应用程序中的模板。

如果表单的errors 属性被设置, 我们就显示一条错误消息(见❶) , 指出输入的用户名—密码对与数据库中存储的任何用户名—密码对都不匹配。

我们要让登录视图处理表单, 因此将实参action 设置为登录页面的URL(见❷) 。 登录视图将一个表单发送给模板, 在模板中, 我们显示这个表单(见❸) 并添加一个提交按钮(见❹) 。 在❺处, 我们包含了一个隐藏的表单元素——'next' , 其中的实参value 告诉Django在用户成功登录后将其重定向到什么地方——在这里是主页。

6.2.2 链接到登录页面

下面在base.html中添加到登录页面的链接, 让所有页面都包含它。 用户已登录时, 我们不想显示这个链接, 因此将它嵌套在一个{% if %} 标签中:

base.html

<p>
    <a href="{% url 'learning_logs:index' %}">Learning Log</a> -
    <a href="{% url 'learning_logs:topics' %}">Topics</a> -
    ❶ {% if user.is_authenticated %}
        ❷ Hello, {{ user.username }}.
    {% else %}
        ❸ <a href="{% url 'users:login' %}">log in</a>
    {% endif %}
</p>
{% block content %}{% endblock content %}

在Django身份验证系统中, 每个模板都可使用变量user , 这个变量有一个is_authenticated 属性: 如果用户已登录, 该属性将为True , 否则为False 。 这让你能够向已通过身份验证的用户显示一条消息, 而向未通过身份验证的用户显示另一条消息。

在这里, 我们向已登录的用户显示一条问候语(见❶) 。 对于已通过身份验证的用户, 还设置了属性username , 我们使用这个属性来个性化问候语, 让用户知道他已登录(见❷) 。 在❸处, 对于还未通过身份验证的用户, 我们再显示一个到登录页面的链接。

6.2.3 使用登录页面

前面建立了一个用户账户,登录一下,看看登录页面是否管用.请访问http://localhost:8000/admin/, 如果你依然是以管理员的身份登录的, 请在页眉上找到注销链接并单击它。

注销后, 访问http://localhost:8000/users/login/, 你将看到类似于图【58】所示的登录页面。 输入你在前面设置的用户名和密码, 将进入页面index。 。 在这个主页的页眉中, 显示了一条个性化问候语, 其中包含你的用户名。

【58】

6.3 注销

现在需要提供一个让用户注销的途径。 我们不创建用于注销的页面, 而让用户只需单击一个链接就能注销并返回到主页。 为此, 我们将为注销链接定义一个URL模式, 编写一个视图函数, 并在base.html中添加一个注销链接。

6.3.1 注销URL

下面的代码为注销定义了URL模式, 该模式与URL http://locallwst:8000/users/logout/匹配。 修改后的users/urls.py如下:

users/urls.py

--snip--
urlpatterns = [
    # 登录页面
    --snip--
    # 注销
    url(r'^logout/$', views.logout_view, name='logout'),
]

这个URL模式将请求发送给函数logout_view() 。 这样给这个函数命名, 旨在将其与我们将在其中调用的函数logout() 区分开来。

6.3.2 视图函数logout_view()

函数logout_view() 很简单: 只是导入Django函数logout() , 并调用它, 再重定向到主页。 请打开users/views.py, 并输入下面的代码:

users/views.py

from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
❶ from django.contrib.auth import logout

def logout_view(request):
    """注销用户"""
    ❷ logout(request)
    ❸ return HttpResponseRedirect(reverse('learning_logs:index'))

我们从django.contrib.auth中导入了函数logout() (见❶) 。 在❷处, 我们调用了函数logout() , 它要求将request 对象作为实参。 然后, 我们重定向到主页(见❸) 。

问题:导入reverse失败

分析:django2.0 把原来的 django.core.urlresolvers 包 更改为了 django.urls包,所以我们需要把导入的包都修改一下就可以了。

解决:

# from django.core.urlresolvers import reverse
from django.urls import reverse

6.3.3 链接到注销视图

现在我们需要添加一个注销链接。 我们在base.html中添加这种链接, 让每个页面都包含它; 我们将它放在标签{% if user.is_authenticated %} 中, 使得仅当用户登录后才能看到它:

base.html

--snip—
    {% if user.is_authenticated %}
        Hello, {{ user.username }}.
        <a href="{% url 'users:logout' %}">log out</a>
    {% else %}
        <a href="{% url 'users:login' %}">log in</a>
    {% endif %}
--snip--

图【59】显示了用户登录后看到的主页。 这里的重点是创建能够正确工作的网站, 因此几乎没有设置任何样式。 确定所需的功能都能正确运行后, 我们将设置这个网站的样式, 使其看起来更专业。

【59】

6.4 注册页面

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

6.4.1 注册页面的URL模式

下面的代码定义了注册页面的URL模式, 它也包含在users/urls.py中:

users/urls.py

--snip--
urlpatterns = [
    # 登录页面
    --snip--
    # 注册页面
    url(r'^register/$', views.register, name='register'),
]

这个模式与URL http://localhost:8000/users/register/匹配, 并将请求发送给我们即将编写的函数register() 。

6.4.2 视图函数register()

在注册页面首次被请求时, 视图函数register() 需要显示一个空的注册表单, 并在用户提交填写好的注册表单时对其进行处理。 如果注册成功, 这个函数还需让用户自动登录。 请在users/views.py中添加如下代码:

from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.forms import UserCreationForm

def logout_view(request):
--snip--
def register(request):
    """注册新用户"""
    if request.method != 'POST':
        # 显示空的注册表单
        ❶ form = UserCreationForm()
    else:
        # 处理填写好的表单
        ❷ form = UserCreationForm(data=request.POST)
        ❸ if form.is_valid():
            ❹ new_user = form.save()
            # 让用户自动登录, 再重定向到主页
            ❺ authenticated_user = authenticate(username=new_user.username,password=request.POST['password1'])
            ❻ login(request, authenticated_user)
            ❼ return HttpResponseRedirect(reverse('learning_logs:index'))
    context = {'form': form}
    return render(request, 'users/register.html', context)

我们首先导入了函数render() , 然后导入了函数login() 和authenticate() , 以便在用户正确地填写了注册信息时让其自动登录。 我们还导入了默认表单UserCreationForm 。 在函数register() 中, 我们检查要响应的是否是POST请求。 如果不是, 就创建一个UserCreationForm 实例, 且不给它提供任何初始数据(见❶) 。

如果响应的是POST请求, 我们就根据提交的数据创建一个UserCreationForm 实例(见❷) , 并检查这些数据是否有效: 就这里而言, 是用户名未包含非法字符, 输入的两个密码相同, 以及用户没有试图做恶意的事情。

如果提交的数据有效, 我们就调用表单的方法save() , 将用户名和密码的散列值保存到数据库中(见❹) 。 方法save() 返回新创建的用户对象, 我们将其存储在new_user中。

保存用户的信息后, 我们让用户自动登录, 这包含两个步骤。 首先, 我们调用authenticate() , 并将实参new_user.username 和密码传递给它(见❺) 。 用户注册时,被要求输入密码两次; 由于表单是有效的, 我们知道输入的这两个密码是相同的, 因此可以使用其中任何一个。 在这里, 我们从表单的POST数据中获取与键'password1' 相关联的值。 如果用户名和密码无误, 方法authenticate() 将返回一个通过了身份验证的用户对象, 而我们将其存储在authenticated_user 中。 接下来, 我们调用函数login() , 并将对象request 和authenticated_user 传递给它(见❻) , 这将为新用户创建有效的会话。 最后, 我们将用户重定向到主页(见❼) , 其页眉中显示了一条个性化的问候语, 让用户知道注册成功了。

6.4.3 注册模板

注册页面的模板与登录页面的模板类似, 请务必将其保存到login.html所在的目录中:

register.html

{% extends "learning_logs/base.html" %}
{% block content %}
    <form method="post" action="{% url 'users:register' %}">
        {% csrf_token %}
        {{ form.as_p }}
    <button name="submit">register</button>
    <input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
    </form>
{% endblock content %}

这里也使用了方法as_p , 让Django在表单中正确地显示所有的字段, 包括错误消息——如果用户没有正确地填写表单。

6.3.4 链接到注册页面

接下来, 我们添加这样的代码, 即在用户没有登录时显示到注册页面的链接:

base.html

--snip--
    {% if user.is_authenticated %}
        Hello, {{ user.username }}.
        <a href="{% url 'users:logout' %}">log out</a>
    {% else %}
        <a href="{% url 'users:register' %}">register</a> -
        <a href="{% url 'users:login' %}">log in</a>
    {% endif %}
--snip--

现在, 已登录的用户看到的是个性化的问候语和注销链接, 而未登录的用户看到的是注册链接和登录链接。 请尝试使用注册页面创建几个用户名各不相同的用户账户。

在下一节, 我们将对一些页面进行限制, 仅让已登录的用户访问它们, 我们还将确保每个主题都属于特定用户。

7 让用户拥有自己的数据

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

在本节中, 我们将修改模型Topic , 让每个主题都归属于特定用户。 这也将影响条目, 因为每个条目都属于特定的主题。 我们先来限制对一些页面的访问。

7.1 使用@login_required限制访问

Django提供了装饰器@login_required , 让你能够轻松地实现这样的目标: 对于某些页面, 只允许已登录的用户访问它们。 装饰器 (decorator) 是放在函数定义前面的指令, Python在函数运行前, 根据它来修改函数代码的行为。 下面来看一个示例。

7.1.1 限制对topic页面的访问

每个主题都归特定用户所有, 因此应只允许已登录的用户请求topics 页面。 为此, 在learning_logs/views.py中添加如下代码:

learning_logs/views.py

--snip--
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required

from .models import Topic, Entry
--snip--

@login_required
def topics(request):
    """显示所有的主题"""
    --snip--

首先导入了函数login_required() 。 我们将login_required() 作为装饰器用于视图函数topics() ——在它前面加上符号@ 和login_required , 让Python在运行topics() 的代码前先运行login_required() 的代码。

login_required() 的代码检查用户是否已登录, 仅当用户已登录时, Django才运行topics() 的代码。 如果用户未登录, 就重定向到登录页面。

为实现这种重定向, 我们需要修改settings.py, 让Django知道到哪里去查找登录页面。 请在settings.py末尾添加如下代码:

settings.py

"""
项目learning_log的Django设置
--snip--
# 我的设置
LOGIN_URL = '/users/login/'

现在, 如果未登录的用户请求装饰器@login_required 的保护页面, Django将重定向到settings.py中的LOGIN_URL 指定的URL。

要测试这个设置, 可注销并进入主页。 然后, 单击链接Topics, 这将重定向到登录页面。 接下来, 使用你的账户登录, 并再次单击主页中的Topics链接, 你将看到topics页面。

7.1.2 全面限制对项目“学习笔记”的访问

Django让你能够轻松地限制对页面的访问, 但你必须针对要保护哪些页面做出决定。 最好先确定项目的哪些页面不需要保护, 再限制对其他所有页面的访问。 你可以轻松地修改过于严格的访问限制, 其风险比不限制对敏感页面的访问更低。

在项目“学习笔记”中, 我们将不限制对主页、 注册页面和注销页面的访问, 并限制对其他所有页面的访问。

在下面的learning_logs/views.py中,对除index()外的每个视图都应用了装饰器@login_required:

views.py

--snip--
@login_required
def topics(request):
    --snip--
    
@login_required
def topic(request, topic_id):
    --snip--

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

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

@login_required
def edit_entry(request, entry_id):
    --snip--

如果你在未登录的情况下尝试访问这些页面, 将被重定向到登录页面。 另外, 你还不能单击到new_topic 等页面的链接。 但如果你输入URL http://localhost:8000/new_topic/, 将重定向到登录页面。 对于所有与私有用户数据相关的URL, 都应限制对它们的访问。

7.2 将数据关联到用户

现在, 需要将数据关联到提交它们的用户。 我们只需将最高层的数据关联到用户, 这样更低层的数据将自动关联到用户。 例如, 在项目“学习笔记”中, 应用程序的最高层数据是主题, 而所有条目都与特定主题相关联。 只要每个主题都归属于特定用户, 我们就能确定数据库中每个条目的所有者。

下面来修改模型Topic , 在其中添加一个关联到用户的外键。 这样做后, 我们必须对数据库进行迁移。 最后, 我们必须对有些视图进行修改, 使其只显示与当前登录的用户相关联的数据。

7.2.1 修改模型Topic

models.py

from django.db import models
from django.contrib.auth.models import User

class Topic(models.Model):
    """用户要学习的主题"""
    text = models.CharField(max_length=200)
    date_added = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(User)
    def __str__(self):
        """返回模型的字符串表示"""
        return self.text
class Entry(models.Model):
    --snip--

我们首先导入了django.contrib.auth 中的模型User , 然后在Topic 中添加了字段owner , 它建立到模型User 的外键关系。

问题:File "D:\PythonWork\learning_log\learning_logs\models.py", line 11, in Topic owner = models.ForeignKey(User)

TypeError: __init__() missing 1 required positional argument: 'on_delete'

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

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

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)

解决:改为owner = models.ForeignKey(User, on_delete=models.CASCADE)

7.2.2 确定当前有哪些用户

我们迁移数据库时, Django将对数据库进行修改, 使其能够存储主题和用户之间的关联。 为执行迁移, Django需要知道该将各个既有主题关联到哪个用户。 最简单的办法是, 将既有主题都关联到同一个用户, 如超级用户。 为此, 我们需要知道该用户的ID。

下面来查看已创建的所有用户的ID。 为此, 启动一个Django shell会话, 并执行如下命令:

(venv)learning_log$ python manage.py shell
❶ >>> from django.contrib.auth.models import User
❷ >>> User.objects.all()
[<User: ll_admin>, <User: eric>, <User: willie>]
❸ >>> for user in User.objects.all():
... print(user.username, user.id)
...
ll_admin 1
eric 2
willie 3
>>>

在❶处, 我们在shell会话中导入了模型User 。 然后, 我们查看到目前为止都创建了哪些用户(见❷) 。 输出中列出了三个用户: ll_admin、 eric和willie。

在❸处, 我们遍历用户列表, 并打印每位用户的用户名和ID。 Django询问要将既有主题关联到哪个用户时, 我们将指定其中的一个ID值。

(ll_env) D:\PythonWork\learning_log>python manage.py shell
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> User.objects.all()
<QuerySet [<User: ll_admin>, <User: linannan>]>
>>> for user in User.objects.all():
...     print(user.username,user.id)
...
ll_admin 1
linannan 2
>>>

7.2.3 迁移数据库

知道用户ID后, 就可以迁移数据库了。

❶ (venv)learning_log$ python manage.py makemigrations learning_logs
❷ You are trying to add a non-nullable field 'owner' to topic without a default;
we can't do that (the database needs something to populate existing rows).
❸ Please select a fix:
    1) Provide a one-off default now (will be set on all existing rows)
    2) Quit, and let me add a default in models.py
❹ Select an option: 1
❺ Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now()
❻ >>> 1
Migrations for 'learning_logs':
    0003_topic_owner.py:
        - Add field owner to topic

我们首先执行了命令makemigrations (见❶) 。 在❷处的输出中, Django指出我们试图给既有模型Topic 添加一个必不可少(不可为空) 的字段, 而该字段没有默认值。 在❸处, Django给我们提供了两种选择: 要么现在提供默认值, 要么退出并在models.py中添加默认值。 在❹处, 我们选择了第一个选项, 因此Django让我们输入默认值(见❺) 。

为将所有既有主题都关联到管理用户ll_admin, 我输入了用户ID值1(见❻) 。 并非必须使用超级用户, 而可使用已创建的任何用户的ID。 接下来, Django使用这个值来迁移数据库, 并生成了迁移文件0003_topic_owner.py, 它在模型Topic 中添加字段owner 。

现在可以执行迁移了。 为此, 在活动的虚拟环境中执行下面的命令:

(venv)learning_log$ python manage.py migrate
Operations to perform:
    Synchronize unmigrated apps: messages, staticfiles
    Apply all migrations: learning_logs, contenttypes, sessions, admin, auth
--snip--
Running migrations:
    Rendering model states... DONE
    ❶ Applying learning_logs.0003_topic_owner... OK
(venv)learning_log$

 为验证迁移符合预期, 可在shell会话中像下面这样做:

❶ >>> from learning_logs.models import Topic
❷ >>> for topic in Topic.objects.all():
    ... print(topic, topic.owner)
...
Chess ll_admin
Rock Climbing ll_admin
>>>

我们从learning_logs.models 中导入Topic (见❶) , 再遍历所有的既有主题, 并打印每个主题及其所属的用户(见❷) 。 正如你看到的, 现在每个主题都属于用户ll_admin。

注意:你可以重置数据库而不是迁移它, 但如果这样做, 既有的数据都将丢失。 一种不错的做法是, 学习如何在迁移数据库的同时确保用户数据的完整性。 如果你确实想要一个全新的数据库, 可执行命令python manage.py flush , 这将重建数据库的结构。 如果你这样做, 就必须重新创建超级用户, 且原来的所有数据都将丢失。

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

当前, 不管你以哪个用户的身份登录, 都能够看到所有的主题。 我们来改变这种情况, 只向用户显示属于自己的主题。

在views.py中, 对函数topics() 做如下修改:

views.py

--snip--
@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)
--snip--

用户登录后, request 对象将有一个user 属性, 这个属性存储了有关该用户的信息。 代码Topic.objects.filter(owner=request.user) 让Django只从数据库中获

取owner 属性为当前用户的Topic 对象。 由于我们没有修改主题的显示方式, 因此无需对页面topics的模板做任何修改。

要查看结果, 以所有既有主题关联到的用户的身份登录, 并访问topics页面, 你将看到所有的主题。 然后, 注销并以另一个用户的身份登录, topics页面将不会列出任何主题。

7.4 保护用户主题

我们还没有限制对显示单个主题的页面的访问, 因此任何已登录的用户都可输入类似于http://localhost:8000/topics/1/的URL, 来访问显示相应主题的页面。

为修复这种问题, 我们在视图函数topic() 获取请求的条目前执行检查:

views.py

from django.shortcuts import render
❶ from django.http import HttpResponseRedirect, Http404
from django.core.urlresolvers import reverse
--snip--

@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)
--snip--

服务器上没有请求的资源时, 标准的做法是返回404响应。 在这里, 我们导入了异常Http404 (见❶) , 并在用户请求它不能查看的主题时引发这个异常。 收到主题请求后, 我们在渲染网页前检查该主题是否属于当前登录的用户。 如果请求的主题不归当前用户所有, 我们就引发Http404 异常(见❷) , 让Django返回一个404错误页面。

现在, 如果你试图查看其他用户的主题条目, 将看到Django发送的消息Page Not Found。 在第20章, 我们将对这个项目进行配置, 让用户看到更合适的错误页面。

7.5 保护页面edit_entry

页面edit_entry 的URL为http://localhost:8000/edit_entry/entry_id / , 其中 entry_id 是一个数字。 下面来保护这个页面, 禁止用户通过输入类似于前面的URL来访问其他用户的条目:

views.py

--snip--
@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
    if request.method != 'POST':
        # 初次请求, 使用当前条目的内容填充表单
        --snip--

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

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

当前, 用于添加新主题的页面存在问题, 因此它没有将新主题关联到特定用户。 如果你尝试添加新主题, 将看到错误消息IntegrityError , 指出learning_logs_topic.user_id 不能为NULL 。 Django的意思是说, 创建新主题时, 你必须指定其owner 字段的值。

由于我们可以通过request 对象获悉当前用户, 因此存在一个修复这种问题的简单方案。 请添加下面的代码, 将新主题关联到当前用户:

views.py

--snip--
@login_required
def new_topic(request):
    """添加新主题"""
    if request.method != 'POST':
        # 没有提交的数据,创建一个空表单
        form = TopicForm()
    else:
        # POST提交的数据,对数据进行处理
       form = TopicForm(request.POST)
       if form.is_valid():
            ❶ new_topic = form.save(commit=False)
            ❷ new_topic.owner = request.user
            ❸ new_topic.save()
            return HttpResponseRedirect(reverse('learning_logs:topics'))
    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)
--snip--

我们首先调用form.save() , 并传递实参commit=False , 这是因为我们先修改新主题, 再将其保存到数据库中(见❶) 。 接下来, 将新主题的owner 属性设置为当前用户(见❷) 。 最后, 对刚定义的主题实例调用save() (见❸) 。 现在主题包含所有必不可少的数据, 将被成功地保存。

现在, 这个项目允许任何用户注册, 而每个用户想添加多少新主题都可以。 每个用户都只能访问自己的数据, 无论是查看数据、 输入新数据还是修改旧数据时都如此。

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

为设置样式, 我们将使用Bootstrap库, 这是一组工具, 用于为Web应用程序设置样式, 使其在任何现代设备上都看起来很专业, 无论是大型的平板显示器还是智能手机。 为此, 我们将使用应用程序django-bootstrap3, 这也让你能够练习使用其他Django开发人员开发的应用程序。

我们将把项目“学习笔记”部署到Heroku, 这个网站让你能够将项目推送到其服务器, 让任何有网络连接的人都可使用它。 我们还将使用版本控制系统Git来跟踪对这个项目所做的修改。

完成项目“学习笔记”后, 你将能够开发简单的Web应用程序, 让它们看起来很漂亮, 再将它们部署到服务器。 你还能够利用更高级的学习资源来提高技能。

8.1 应用程序django-bootstrap3

我们将使用django-bootstrap3来将Bootstrap继承到项目中。 这个应用程序下载必要的Bootstrap文件, 将它们放到项目的合适位置, 让你能够在项目的模板中使用样式设置指令。为安装django-bootstrap3, 在活动的虚拟环境中执行如下命令:

(ll_env)learning_log$ pip install django-bootstrap3
--snip--
Successfully installed django-bootstrap3

接下来, 需要在settings.py的INSTALLED_APPS 中添加如下代码, 在项目中包含应用程序django-boostrap3:

settings.py

--snip--
INSTALLED_APPS = (
    --snip--
    'django.contrib.staticfiles',
    # 第三方应用程序
    'bootstrap3',
    # 我的应用程序
    'learning_logs',
    'users',
)
--snip--

新建一个用于指定其他开发人员开发的应用程序的片段, 将其命名为“第三方应用程序”, 并在其中添加'bootstrap3' 。 大多数应用程序都需要包含在INSTALLED_APPS 中,为确定这一点, 请阅读要使用的应用程序的设置说明。

我们需要让django-bootstrap3包含jQuery, 这是一个JavaScript库, 让你能够使用Bootstrap模板提供的一些交互式元素。 请在settings.py的末尾添加如下代码:

--snip--
# 我的设置
LOGIN_URL = '/users/login/'
# django-bootstrap3的设置
BOOTSTRAP3 = {
    'include_jquery': True,
}

这些代码让你无需手工下载jQuery并将其放到正确的地方。

8.2 使用Bootstrap来设置项目“学习笔记”的样式,修改base.html

Bootstrap基本上就是一个大型的样式设置工具集, 它还提供了大量的模板, 你可将它们应用于项目以创建独特的总体风格。 对Bootstrap初学者来说, 这些模板比各个样式设置工具使用起来要容易得多。 要查看Bootstrap提供的模板, 可访问http://getbootstrap.com/ , 单击Getting Started, 再向下滚动到Examples部分, 并找到Navbars in action。 我们将使用模板Static top navbar, 它提供了简单的顶部导航条、 页面标题和用于放置页面内容的容器。

图【60】显示了对base.html应用这个Bootstrap模板并对index.html做细微修改后的主页。

【60】

8.2.1 修改base.html

我们需要修改模板base.html, 以使用前述Bootstrap模板。 我们把新的base.html分成几个部分进行介绍。

1、定义HTML头部

对base.html所做的第一项修改是, 在这个文件中定义HTML头部, 使得显示“学习笔记”的每个页面时,浏览器标题栏都显示这个网站的名称。我们还将添加一些在模板中使用Bootstrap所需的信息。 删除base.html的全部代码, 并输入下面的代码:

base.html

❶ {% load bootstrap3 %}

❷ <!DOCTYPE html>
❸ <html lang="en">
    ❹ <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        ❺ <title>Learning Log</title>
        ❻ {% bootstrap_css %}
        {% bootstrap_javascript %}
    ❼ </head>

在❶处, 我们加载了django-bootstrap3中的模板标签集。 接下来, 我们将这个文件声明为使用英语(见❸) 编写的HTML文档(见❷) 。 HTML文件分为两个主要部分: 头部(head) 和主体 (body) ; 在这个文件中, 头部始于❹处。 HTML文件的头部不包含任何内容: 它只是将正确显示页面所需的信息告诉浏览器。 在❺处, 我们包含了一个title元素, 在浏览器中打开网站“学习笔记”的页面时, 浏览器的标题栏将显示该元素的内容。

在❻处, 我们使用了django-bootstrap3的一个自定义模板标签, 它让Django包含所有的Bootstrap样式文件。 接下来的标签启用你可能在页面中使用的所有交互式行为, 如可折叠的导航栏。 ❼处为结束标签</head> 。

8.2.2 定义导航栏

下面来定义页面顶部的导航栏

--snip--
</head>
<body>

    <!-- Static navbar -->
    ❶ <nav class="navbar navbar-default navbar-static-top">
        <div class="container">
            
            <div class="navbar-header">
                ❷ <button type="button" class="navbar-toggle collapsed"
                        data-toggle="collapse" data-target="#navbar"
                        aria-expanded="false" aria-controls="navbar">
                </button>
                ❸ <a class="navbar-brand" href="{% url 'learning_logs:index' %}">
                        Learning Log</a>
            </div>

            ❹ <div id="navbar" class="navbar-collapse collapse">
                ❺ <ul class="nav navbar-nav">
                    ❻ <li><a href="{% url 'learning_logs:topics' %}">Topics</a></li>
                </ul>

                ❼ <ul class="nav navbar-nav navbar-right">
                    {% if user.is_authenticated %}
                        <li><a>Hello, {{ user.username }}.</a></li>
                        <li><a href="{% url 'users:logout' %}">log out</a></li>
                    {% else %}
                        <li><a href="{% url 'users:register' %}">register</a></li>
                        <li><a href="{% url 'users:login' %}">log in</a></li>
                    {% endif %}
                ❽ </ul>
            </div><!--/.nav-collapse -->
        </div>
    </nav>

第一个元素为起始标签<body> 。 HTML文件的主体包含用户将在页面上看到的内容。 ❶处是一个<nav> 元素, 表示页面的导航链接部分。 对于这个元素内的所有内容, 都将根据选择器 (selector) navbar 、 navbar-default 和navbar-static-top 定义的Bootstrap样式规则来设置样式。 选择器决定了特定样式规则将应用于页面上的哪些元素。

在❷处, 这个模板定义了一个按钮, 它将在浏览器窗口太窄、 无法水平显示整个导航栏时显示出来。 如果用户单击这个按钮, 将出现一个下拉列表, 其中包含所有的导航元素。在用户缩小浏览器窗口或在屏幕较小的移动设备上显示网站时, collapse 会使导航栏折叠起来。

在❸处, 我们在导航栏的最左边显示项目名, 并将其设置为到主页的链接, 因为它将出现在这个项目的每个页面中。

在❹处, 我们定义了一组让用户能够在网站中导航的链接。 导航栏其实就是一个以<ul> 打头的列表(见❺) , 其中每个链接都是一个列表项(<li> ) 。 要添加更多的链接, 可插入更多使用下述结构的行:

<li><a href="{% url 'learning_logs:title' %}">Title</a></li>

这行表示导航栏中的一个链接。 这个链接是直接从base.html的前一个版本中复制而来的。

在❼处, 我们添加了第二个导航链接列表, 这里使用的选择器为navbar-right 。 选择器navbar-right 设置一组链接的样式, 使其出现在导航栏右边——登录链接和注册链接通常出现在这里。 在这里, 我们要么显示问候语和注销链接, 要么显示注册链接和登录链接。 这部分余下的代码结束包含导航栏的元素(见❽) 。

8.2.3 定义页面的主要部分

base.html的剩余部分包含页面的主要部分:

--snip--
    </nav>
    ❶ <div class="container">

        <div class="page-header">
            ❷ {% block header %}{% endblock header %}
        </div>
        <div>
            ❸ {% block content %}{% endblock content %}
       </div>
       
    </div> <!-- /container -->
  </body>
</html>

❶处是一个<div> 起始标签, 其class属性为container 。 div是网页的一部分, 可用于任何目的, 并可通过边框、 元素周围的空间(外边距) 、 内容和边框之间的间距(内边距) 、 背景色和其他样式规则来设置其样式。 这个div是一个容器, 其中包含两个元素: 一个新增的名为header 的块(见❷) 以及我们在第18章使用的content 块(见❸) 。 header 块的内容告诉用户页面包含哪些信息以及用户可在页面上执行哪些操作; 其class属性值page-header 将一系列样式应用于这个块。 content 块是一个独立的div, 未使用class属性指定样式。

如果你在浏览器中加载“学习笔记”的主页, 将看到一个类似于图20-1所示的专业级导航栏。 请尝试调整窗口的大小, 使其非常窄; 此时导航栏将变成一个按钮, 如果你单击这个按钮, 将打开一个下拉列表, 其中包含所有的导航链接。

8.3 使用jumbotron设置主页的样式

下面来使用新定义的header 块及另一个名为jumbotron的Bootstrap元素修改主页。 jumbotron元素是一个大框, 相比于页面的其他部分显得鹤立鸡群, 你想在其中包含什么东西都可以; 它通常用于在主页中呈现项目的简要描述。 我们还可以修改主页显示的消息。 index.html的代码如下:

{% extends "learning_logs/base.html" %}

❶ {% block header %}
    ❷ <div class='jumbotron'>
        <h1>Track your learning.</h1>
    </div>
{% endblock header %}

{% block content %}
    ❸ <h2>
        <a href="{% url 'users:register' %}">Register an account</a> to make
        your own Learning Log, and list the topics you're learning about.
    </h2>
    <h2>
        Whenever you learn something new about a topic, make an entry
        summarizing what you've learned.
    </h2>
{% endblock content %}

8.4 设置登录页面的样式

login.html

{% extends "learning_logs/base.html" %}
❶ {% load bootstrap3 %}

❷ {% block header %}
    <h2>Log in to your account.</h2>
{% endblock header %}

{% block content %}
    ❸ <form method="post" action="{% url 'users:login' %}" class="form">
        {% csrf_token %}
        ❹ {% bootstrap_form form %}

        ❺ {% buttons %}
            <button name="submit" class="btn btn-primary">log in</button>
        {% endbuttons %}
    
        <input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
    </form>
{% endblock content %}

8.5 设置new_topic页面样式

new_topic.html

{% extends "learning_logs/base.html" %}
{% load bootstrap3 %}

❶ {% block header %}
    <h2>Add a new topic:</h2>
{% endblock 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">add topic</button>
    {% endbuttons %}
    </form>
    
{% endblock content %}

这里的大多数修改都类似于对login.html所做的修改: 在❶处加载bootstrap3, 添加header 块并在其中包含合适的消息; 接下来, 我们在标签<form> 中添加属性class="form"见❷) , 使用模板标签{% bootstrap_form %} 代替{{ form.as_p }} (见❸) , 并使用bootstrap3结构来定义提交按钮(见❹) 。 如果你现在登录并导航到new_topic 页面, 将发现其外观类似于登录页面。

8.6 设置topics页面的样式

topic.html

{% extends "learning_logs/base.html" %}

❶ {% block header %}
    <h1>Topics</h1>
{% endblock header %}

{% block content %}
    <ul>
        {% for topic in topics %}
            <li>
                ❷ <h3>
                    <a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a>
                </h3>
            </li>
        {% empty %}
            <li>No topics have been added yet.</li>
        {% endfor %}
    </ul>

    ❸ <h3><a href="{% url 'learning_logs:new_topic' %}">Add new topic</h3>
{% endblock content %}

我们不需要标签{% load bootstrap3 %} , 因为我们在这个文件中没有使用任何bootstrap3自定义标签.我们在header 块中添加了标题Topics(见❶).为设置每个主题的样式, 我们将它们都设置为<h3> 元素,让它们在页面上显得大些(见❷);对于添加新主题的链接,也做了同样的处理(见❸)。

8.7 设置topic页面中条目的样式

topic页面包含的内容比其他大部分页面都多, 因此需要做的样式设置工作要多些。 我们将使用Bootstrap面板 (panel) 来突出每个条目。 面板是一个带预定义样式的div, 非常适合用于显示主题的条目:

topic.html

{% extends 'learning_logs/base.html' %}

❶ {% block header %}
    <h2>{{ topic }}</h2>
{% endblock header %}

{% block content %}
    <p>
        <a href="{% url 'learning_logs:new_entry' topic.id %}">add new entry</a>
    </p>

    {% for entry in entries %}
        ❷ <div class="panel panel-default">
            ❸ <div class="panel-heading">
                ❹ <h3>
                    {{ entry.date_added|date:'M d, Y H:i' }}
                    ❺ <small>
                        <a href="{% url 'learning_logs:edit_entry' entry.id %}">
                            edit entry</a>
                    </small>
                </h3>
            </div>
            ❻ <div class="panel-body">
                {{ entry.text|linebreaks }}
            </div>
        </div> <!-- panel -->
    {% empty %}
        There are no entries for this topic yet.
    {% endfor %}

{% endblock content %}

我们首先将主题放在了header 块中(见❶) 。 然后, 我们删除了这个模板中以前使用的无序列表结构。 在❷处, 我们创建了一个面板式div元素(而不是将每个条目作为一个列表项) , 其中包含两个嵌套的div: 一个面板标题(panel-heading ) div(见❸) 和一个面板主体(panel-body ) div(见❹) 。 其中面板标题div包含条目的创建日期以及用于编辑条目的链接, 它们都被设置为<h3> 元素, 而对于编辑条目的链接, 还使用了标签<small> , 使其比时间戳小些(见❺) 。

❻处是面板主体div, 其中包含条目的实际文本。 注意, 只修改了影响页面外观的元素, 对在页面中包含信息的Django代码未做任何修改。图【62】显示了修改后的topic页面。 “学习笔记”的功能没有任何变化, 但显得更专业了, 对用户会更有吸引力。

【62】

9 部署“学习笔记”

参考:https://www.cnblogs.com/liudinglong/p/12185180.html

至此, 项目“学习笔记”的外观显得很专业了, 下面来将其部署到一台服务器, 让任何有网络连接的人都能够使用它。

yum update -y

接下来安装一些基础库,软件管理包和可能使用的依赖关系,一般编译软件的时候需要,个人习惯装完系统就安装一下:

yum -y install gcc gcc-c++ openssl-devel zlib-devel pcre pcre-devel
yum -y groupinstall 
yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel    (这是从网上直接copy的,有些不是必须的,可根据自己的需求选择下载)

安装python3.7.3

yum install wget
wget https://www.python.org/ftp/python/3.5.3/Python-3.5.3.tgz
tar -zxvf Python-3.5.3.tgz
cd Python-3.5.3
./configure --prefix=/usr/local/python3 --enable-shared
make && make install
ln -s /usr/local/python3/bin/python3 /usr/bin/python3
python3 -v

问题:make && make install报错ModuleNotFoundError: No module named '_ctypes'

解决:1、执行如下命令:yum install libffi-devel

2、从"./configure ..."重新安装

问题:运行python3 -v时报错

[root@localhost Python-3.7.3]# python3 -v
python3: error while loading shared libraries: libpython3.7m.so.1.0: cannot open shared object file: No such file or directory

原因:是因为python运行时没有加载到libpython3.5m.so.1.0 这个库文件 将其复制到响应目录OK

解决:

[root@www Python-3.5.0]# cd /install/Python-3.7.3 进入解压后的编译目录
[root@localhost Python-3.7.3]# cp libpython3.7m.so /usr/local/lib64/
[root@localhost Python-3.7.3]# cp libpython3.7m.so /usr/lib
[root@localhost Python-3.7.3]# cp libpython3.7m.so /usr/lib64/

还是不行:将需要python库的路径写到/etc/ld.so.conf配置中。执行以下命令

1 cd /etc/ld.so.conf.d

2 vim python3.conf # 新增文件

3 #添加python库路径,因为我们开始把python的编译后的路径设置在/usr/local/python3,所以python库的路径为/usr/local/python3/lib

4 #按i输入,输入成功后按Esc建,输入:wq保存退出

修改完成后,输入ldconfig 启动配置。

现在python3 -v 执行成功

安装setuptools

wget --no-check-certificate  https://pypi.python.org/packages/source/s/setuptools/setuptools-19.6.tar.gz#md5=c607dd118eae682c44ed146367a17e26
tar -zxvf setuptools-19.6.tar.gz
cd setuptools-19.6
python3 setup.py build
python3 setup.py install
   
   (如有报错: RuntimeError: Compression requires the (missing) zlib module,则需要安装yum install zlib-devel,安装后要重新编译 python3.5:  make && make install)

安装pip

wget --no-check-certificate  https://pypi.python.org/packages/source/p/pip/pip-8.0.2.tar.gz#md5=3a73c4188f8dbad6a1e6f6d44d117eeb
tar -zxvf pip-8.0.2.tar.gz
cd pip-8.0.2
python3 setup.py build
python3 setup.py install

使用pip安装包测试:如报错,则缺少yum install openssl-devel,安装完成后一样需要重新编译python3.5(make&&make install)

ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3

安装django

pip3 install django==3.0.6 -i https://pypi.mirrors.ustc.edu.cn/simple/

国内pip源

阿里云 http://mirrors.aliyun.com/pypi/simple/

中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/ 

豆瓣(douban) http://pypi.douban.com/simple/ 
 
清华大学 https://pypi.tuna.tsinghua.edu.cn/simple/

中国科学技术大学 http://pypi.mirrors.ustc.edu.cn/simple/

使用方法很简单,直接 -i 加 url 即可!如下:

pip install web.py -i http://pypi.douban.com/simple

部署项目:

将项目考到/www目录下

进入项目并启动

[root@localhost ~]# cd /www/learning_log/
[root@localhost learning_log]# source ll_env/bin/activate
(ll_env) [root@localhost learning_log]# python3 manage.py runserver

项目开发完毕,在部署之前需要再配置文件setting.py中将 ALLOWED_HOSTS配置设置为:当前服务器IP或*,如:

ALLOWED_HOSTS = ["*",]

安装uwsgi:

先简单了解一下uwsgi,uWSGI:是一个web服务器,实现了WSGI协议、uwsgi协议、http协议等。它是线路协议,是实现服务器与其他网络服务器通信的协议,可以看作Tomcat。

Django框架运行依赖wsgi(本质提供socket服务端),众多模块实现了wsgi规范,而django框架中默认使用wsigiref模块来实现,他由于性能比较低,所以用于本地开发和测试,而线上部署时需要使用uwsgi来代替。

yum install libxml*
export LDFLAGS="-Xlinker --no-as-needed"
pip3 install uwsgi
ln -s /usr/local/python3/bin/uwsgi /usr/bin/uwsgi

注:如有报错需重新安装时需

pip uninstall uwsgi
pip install uwsgi

注意删除 ~/.cache/pip pip安装缓存文件

测试uwsgi是否可用

vim app.py
#python中
def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World"]
[root@lxh ~]# uwsgi --http :9001 --wsgi-file app.py

这时访问127.0.0.1:9001 得到

在项目的根目录下,也就是manage.py同级目录下,新建一个" uwsgi.ini "文件。文件名可以随便,但扩展名必须是".ini"

[uwsgi]
http-socket = 172.29.248.77:8888
# 项目地址
chdir = /www/learning_log
# 项目名.wsgi
module =  learning_log.wsgi:application
# 是否以主进程模式允许
master = true
# 进程数
processes = 4
#日志文件路径,前提是该文件要存在,且可写
daemonize = /www/learning_log/run.log
# ... with appropriate permissions - may be needed
chmod-socket = 664
#表示不记录正常信息,只记录错误信息,否则你的日志可能很快就爆满
disable-logging = true
#当服务器退出的时候自动清理环境
vacuum = true
#进程信息文件路径(这里指项目的根目录)
pidfile=%(chdir)/uwsgi.pid
# 注意:一定要指定路径否则会报有的安装了的模块不存在
pythonpath = /www/learning_log/ll_env/lib/python3.7/site-packages
socket-timeout=30
#plugin=python

配置好了就可以启动

uwsgi --ini uwsgi.ini

输入ip+端口,通过浏览器可以访问

敲黑板:这里也有个坑,只用uwsgi作为web访问,在之前uwsgi.ini文件里,配置的服务器地址一定要是服务器真实IP地址,不能是localhost或127.0.0.1,不然你本机无法从浏览器访问了。

总结一下常用操作命令:

# 启动uwsgi
uwsgi --ini uwsgi.ini

# 关闭uwsgi
uwsgi --stop ./uwsgi.pid

# 重启
uwsgi --reload ./uwsgi.pid

#查看确认是否uwsgi启动
ps -ef|grep uwsgi 

#查看端口是否起来
netstat -anp|grep 9527

Uwsgi + Nginx的部署

上面是uwsgi的启动方式,一般不考虑安全、负载均衡和代理的话,就不需要结合nginx来部署。

安装nginx 修改conf文件

user  root;
worker_processes  4;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';


    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;
    fastcgi_connect_timeout 300;
    fastcgi_send_timeout 300;
    fastcgi_read_timeout 300;
    fastcgi_buffer_size 256k;
    fastcgi_buffers 16 256k;
    fastcgi_busy_buffers_size 512k;
    fastcgi_temp_file_write_size 512k;

    server {
        listen       8899;
        server_name  127.0.0.1;

    	charset utf-8;
	# root         /usr/share/nginx/html;
	# Load configuration files for the default server block.
	# include /etc/nginx/default.d/*.conf;
	# access_log off;
	access_log      /usr/local/nginx/access.log; 
        #error_log       nginx_error.log;
        #gzip_types text/plain application/x-javascript text/css text/javascript application/x-httpd-php application/json text/json image/jpeg image/gif image/png application/octet-stream;
        error_page 404 /404.html;
	error_page 500 502 503 504 /50x.html;
	
	location / {
            include     uwsgi_params;
            #uwsgi_pass  unix:/www/learning_log/uwsgi.ini;
            uwsgi_pass  172.29.248.77:8888;
	    uwsgi_param UWSGI_CHDIR  /www/learning_log;
	    uwsgi_param UWSGI_SCRIPT learning.wsgi;
            uwsgi_read_timeout 1800;
            uwsgi_send_timeout 300;
            proxy_read_timeout 300;
	    uwsgi_connect_timeout 30;
    	}

    }

}

现在报错502 尚未解决  如果有人解决麻烦留言告知  万分感谢!!!

 

 

 

 

 

 

 

 

 

 

 

 

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值