义。就目前而言,主页只显示标题和简单的描述。

18.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/', include(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/', include(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。

注意  正则表达式通常被称为regex,几乎每种编程语言都使用它。它们的用途多得难以置信,但需要经过一定的练习才能熟悉。如果你不明白前面介绍的内容,也不

用担心,你在完成这个项目的过程中,将会看到很多正则表达式。

18.3.2  编写视图

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

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

views.py

from django.shortcuts import render

在这里创建视图

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

from django.shortcuts import render

def index(request):

"""学习笔记的主页"""

return render(request, 'learning_logs/index.html')

URL请求与我们刚才定义的模式匹配时,Django将在文件views.py中查找函数index() ,再将请求对象传递给这个视图函数。在这里,我们不需要处理任何数据,因此这个函数只

包含调用render() 的代码。这里向函数render() 提供了两个实参:原始请求对象以及一个可用于创建网页的模板。下面来编写这个模板。

08 - 图1

18.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> 指出了段落的结束位置。这里定义了两

个段落:第一个充当标题,第二个阐述了用户可使用“学习笔记”来做什么。

现在,如果你请求这个项目的基础URL——http://localhost:8000/,将看到刚才创建的网页,而不是默认的Django网页。Django接受请求的URL,发现该URL与模式r'^$&#39; 匹配,因此

调用函数views.index() ,这将使用index.html包含的模板来渲染网页,结果如图18-3所示。

图18-3  学习笔记的主页

创建网页的过程看起来可能很复杂,但将URL、视图和模板分离的效果实际上很好。这让我们能够分别考虑项目的不同方面,且在项目很大时,让各个参与者可专注于其最擅长

的方面。例如,数据库专家可专注于模型,程序员可专注于视图代码,而Web设计人员可专注于模板。

动手试一试

18-5 膳食规划程序 :假设你要创建一个应用程序,帮助用户规划一周的膳食。为此,新建一个文件夹,并将其命名为meal_planner,再在这个文件夹中新建一个Django 项目。接下来,新建一个名为meal_plans 的应用程序,并为这个项目创建一个简单的主页。

18-6 比萨店主页 :在你为完成练习18-4而创建的项目Pizzeria中,添加一个主页。

18.4  创建其他网页

制定创建网页的流程后,可以开始扩充“学习笔记”项目了。我们将创建两个显示数据的网页,其中一个列出所有的主题,另一个显示特定主题的所有条目。对于每个网页,我们

都将指定URL模式,编写一个视图函数,并编写一个模板。但这样做之前,我们先创建一个父模板,项目中的其他模板都将继承它。

18.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文件,模板文件的缩进层级更多,因此每个层级通常只缩进两个空格。

  1. 子模板

现在需要重新编写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're learning about.</p> ❸ {% endblock content %}

如果将这些代码与原来的index.html进行比较,可发现我们将标题Learning Log替换成了从父模板那里继承的代码(见❶)。子模板的第一行必须包含标签{% extends %} ,让

Django知道它继承了哪个父模板。文件base.html位于文件夹learning_logs中,因此父模板路径中包含learning_logs。这行代码导入模板base.html的所有内容,让index.html能够指定要

在content 块预留的空间中添加的内容。

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

段落。在❸处,我们使用标签{% endblock content %} 指出了内容定义的结束位置。

模板继承的优点开始显现出来了:在子模板中,只需包含当前网页特有的内容。这不仅简化了每个模板,还使得网站修改起来容易得多。要修改很多网页都包含的元素,只需在

父模板中修改该元素,你所做的修改将传导到继承该父模板的每个页面。在包含数十乃至数百个网页的项目中,这种结构使得网站改进起来容易而且快捷得多。

注意  在大型项目中,通常有一个用于整个网站的父模板——base.html,且网站的每个主要部分都有一个父模板。每个部分的父模板都继承base.html,而网站的每个网

页都继承相应部分的父模板。这让你能够轻松地修改整个网站的外观、网站任何一部分的外观以及任何一个网页的外观。这种配置提供了一种效率极高的工作方式,

让你乐意不断地去改进网站。

18.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() 进行处理。

  1. 视图

函数topics() 需要从数据库中获取一些数据,并将其发送给模板。我们需要在views.py中添加的代码如下:

views.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() (见❺)。

  1. 模板

显示所有主题的页面的模板接受字典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>

08 - 图2

❹ {% 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。单击这个链接,将看到类似于图18-4所示的网页。

图18-4  显示所有主题的网页

18.4.3  显示特定主题的页面

接下来,我们需要创建一个专注于特定主题的页面——显示该主题的名称及该主题的所有条目。同样,我们将定义一个新的URL模式,编写一个视图并创建一个模板。我们还将

修改显示所有主题的网页,让每个项目列表项都是一个链接,单击它将显示相应主题的所有条目。

  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 的值来获取相应的主

题。

  1. 视图

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

08 - 图3

views.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 shel中所做的那样。在❸处,我们获取与该主题相关联的条目,并将它们按date_added 排序:date_added 前面的减号

指定按降序排列,即先显示最近的条目。我们将主题和条目都存储在字典context 中(见❹),再将这个字典发送给模板topic.html(见❺)。

注意  ❷处和❸处的代码被称为查询,因为它们向数据库查询特定的信息。在自己的项目中编写这样的查询时,先在Django shel中进行尝试大有裨益。相比于编写视

图和模板,再在浏览器中检查结果,在shel中执行代码可更快地获得反馈。

  1. 模板

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

topic.html

{% 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 %} 打

印一条消息,告诉用户当前主题还没有条目。

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

在浏览器中查看显示特定主题的页面前,我们需要修改模板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/。

如果你刷新显示所有主题的页面,再单击其中的一个主题,将看到类似于图18-5所示的页面。

08 - 图4

图18-5  特定主题的详细页面,其中显示了该主题的所有条目

动手试一试

18-7 模板文档 :请浏览Django模板文档,其网址为https://docs.djangoproject.com/en/1.8/ref/templates/ 。自己开发项目时,可再回过头来参考该文档。

18-8 比萨店页面 :在练习18-6中开发的项目Pizzeria中添加一个页面,它显示供应的比萨的名称。然后,将每个比萨名称都设置成一个链接,单击这种链接将显示一个

页面,其中列出了相应比萨的配料。请务必使用模板继承来高效地创建页面。

18.5  小结

在本章中,你首先学习了如何使用Django框架来创建Web应用程序。你制定了简要的项目规范,在虚拟环境中安装了Django,创建了一个项目,并核实该项目已正确地创建。你学

习了如何创建应用程序,以及如何定义表示应用程序数据的模型。你学习了数据库,以及在你修改模型后,Django可为你迁移数据库提供什么样的帮助。你学习了如何创建可访

问管理网站的超级用户,并使用管理网站输入了一些初始数据。

你还探索了Django shel,它让你能够在终端会话中处理项目的数据。你学习了如何定义URL、创建视图函数以及编写为网站创建网页的模板。最后,你使用了模板继承,它可简化

各个模板的结构,并使得修改网站更容易。

在第19章,我们将创建对用户友好而直观的网页,让用户无需通过管理网站就能添加新的主题和条目,以及编辑既有的条目。我们还将添加一个用户注册系统,让用户能够创建

账户和自己的学习笔记。让任意数量的用户都能与之交互,是Web应用程序的核心所在。

第 19 章 用户账户

Web应用程序的核心是让任何用户都能够注册账户并能够使用它,不管用户身处何方。在本章中,你将创建一些表单,让用户能够添加主题和条目,以及编辑既有的

条目。你还将学习Django如何防范对基于表单的网页发起的常见攻击,这让你无需花太多时间考虑确保应用程序安全的问题。

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

使得用户只能看到自己的数据。你将学习如何确保用户数据的安全。

19.1  让用户能够输入数据

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

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

19.1.1  添加新主题

首先来让用户能够添加新主题。创建基于表单的页面的方法几乎与前面创建网页一样:定义一个URL,编写一个视图函数并编写一个模板。一个主要差别是,需要导入包含表单

的模块forms.py。

  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 生成标签。

  1. 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'),

]

这个URL模式将请求交给视图函数new_topic() ,接下来我们将编写这个函数。

  1. 视图函数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)

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

  1. 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 中,用户将在主题列表中看到他刚输入的主题。

  1. 模板new_topic

下面来创建新模板new_topic.html,用于显示我们刚创建的表单:

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 %}

08 - 图5

这个模板继承了base.html,因此其基本结构与项目“学习笔记”的其他页面相同。在❶处,我们定义了一个HTML表单。实参action 告诉服务器将提交的表单数据发送到哪里,这

里我们将它发回给视图函数new_topic() 。实参method 让浏览器以POST请求的方式提交数据。

Django使用模板标签{% csrf_token %} (见❷)来防止攻击者利用表单来获得对服务器未经授权的访问(这种攻击被称为跨站请求伪造 )。在❸处,我们显示表单,从中可

知Django使得完成显示表单等任务有多简单:我们只需包含模板变量{{ form.as_p }} ,就可让Django自动创建显示表单所需的全部字段。修饰符as_p 让Django以段落格式渲

染所有表单元素,这是一种整洁地显示表单的简单方式。

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

  1. 链接到页面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 %}

这个链接放在了既有主题列表的后面。图19-1显示了生成的表单。请使用这个表单来添加几个新主题。

图19-1  用于添加新主题的页面

19.1.2  添加新条目

现在用户可以添加新主题了,但他们还想添加新条目。我们将再次定义URL,编写视图函数和模板,并链接到添加新条目的网页。但在此之前,我们需要在forms.py中再添加一个

类。

  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列。这给用户提供了足够的空间,可以

编写有意义的条目。

  1. 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() 。

  1. 视图函数new_entry()

视图函数new_entry() 与函数new_topic() 很像:

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() 将用户重定向到显示新增条目所属主题的页面,用户将在该页面的

条目列表中看到新添加的条目。

  1. 模板new_entry

从下面的代码可知,模板new_entry 类似于模板new_topic :

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完全相同。

  1. 链接到页面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> 08 - 图6

</p>

<ul>

—snip—

</ul>

{% endblock content %}

我们在显示条目前添加链接,因为在这种页面中,执行的最常见的操作是添加新条目。图19-2显示了页面new_entry 。现在用户可以添加新主题,还可以在每个主题中添加任

意数量的条目。请在一些既有主题中添加一些新条目,尝试使用一下页面new_entry 。

图19-2  页面new_entry

19.1.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() 。

  1. 视图函数edit_entry()

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

数据库中:

views.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创建一个表单,并使用既有条目对象中的信息填充它。用户将看到既有的数据,并能够编辑它们。

08 - 图7

处理POST请求时,我们传递实参instance=entry 和data=request.POST (见❸),让Django根据既有条目对象创建一个表单实例,并根据request.POST 中的相关数

据对其进行修改。然后,我们检查表单是否有效,如果有效,就调用save() ,且不指定任何实参(见❹)。接下来,我们重定向到显示条目所属主题的页面(见❺),用户将

在其中看到其编辑的条目的新版本。

  1. 模板edit_entry

下面是模板edit_entry.html,它与模板new_entry.html类似:

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,以提醒用户:单击该按钮将保存所做的编辑,而不是创建一个新条目(见❷)。

  1. 链接到页面edit_entry

现在,在显示特定主题的页面中,需要给每个条目添加到页面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" ,它出现在页面中每个条目的后面。图19-3显示了包含这些链接时,显示特定主题的页面是什么样的。

图19-3  每个条目都有一个用于对其进行编辑的链接

至此,“学习笔记”已具备了需要的大部分功能。用户可添加主题和条目,还可根据需要查看任何一组条目。在下一节,我们将实现一个用户注册系统,让任何人都可向“学习笔

记”申请账户,并创建自己的主题和条目。

动手试一试

19-1 博客 :新建一个Django项目,将其命名为Blog。在这个项目中,创建一个名为blogs的应用程序,并在其中创建一个名为BlogPost 的模型。这个模型应包

含title 、text 和date_added 等字段。为这个项目创建一个超级用户,并使用管理网站创建几个简短的帖子。创建一个主页,在其中按时间顺序显示所有的帖

子。

创建两个表单,其中一个用于发布新帖子,另一个用于编辑既有的帖子。

尝试填写这些表单,确认它们能够正确地工作。

19.2  创建用户账户

在这一节,我们将建立一个用户注册和身份验证系统,让用户能够注册账户,进而登录和注销。我们将创建一个新的应用程序,其中包含与处理用户账户相关的所有功能。我们

还将对模型Topic 稍做修改,让每个主题都归属于特定用户。

19.2.1  应用程序users

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

(ll_env)learning_log$ python manage.py startapp users

(ll_env)learning_log$ ls

❶ db.sqlite3 learning_log learning_logs ll_env manage.py users

(ll_env)learning_log$ ls users

❷ admin.py init.py migrations models.py tests.py views.py

这个命令新建一个名为users的目录(见❶),其结构与应用程序learning_logs 相同(见❷)。

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

在settings.py中,我们需要将这个新的应用程序添加到INSTALLED_APPS 中,如下所示:

settings.py

—snip—

INSTALLED_APPS = (

—snip—

我的应用程序

'learning_logs',

'users',

)

—snip—

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

  1. 包含应用程序users 的URL

接下来,我们需要修改项目根目录中的urls.py,使其包含我们将为应用程序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')),

]

我们添加了一行代码,以包含应用程序users 中的文件urls.py。这行代码与任何以单词users打头的URL(如http://localhost:8000userslogin/)都匹配。我们还创建了命名空

间'users' ,以便将应用程序learning_logs 的URL同应用程序users 的URL区分开来。

19.2.2  登录页面

我们首先来实现登录页面的功能。为此,我们将使用Django提供的默认登录视图,因此URL模式会稍有不同。在目录learning_logusers中,新建一个名为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:8000userslogin/匹配(见❷)。这个URL中的单词users让Django在users/urls.py中查找,而单词

login让它将请求发送给Django默认视图login (请注意,视图实参为login ,而不是views.login )。鉴于我们没有编写自己的视图函数,我们传递了一个字典,告诉Django 去哪里查找我们将编写的模板。这个模板包含在应用程序users 而不是learning_logs 中。

  1. 模板login.html

用户请求登录页面时,Django将使用其默认视图login ,但我们依然需要为这个页面提供模板。为此,在目录learning_logusers中,创建一个名为templates的目录,并在其中创建一

个名为users的目录。以下是模板login.html,你应将其存储到目录learning_loguserstemplatesusers中:

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 }}

08 - 图8

❹ <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在用户成功登录后将其重定向到什么地方——在这里是主页。

  1. 链接到登录页面

下面在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 ,我们使用这个属性来个性化问候语,让用户知道他已登录(见

❷)。在❸处,对于还未通过身份验证的用户,我们再显示一个到登录页面的链接。

  1. 使用登录页面

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

它。

注销后,访问http://localhost:8000userslogin/,你将看到类似于图19-4所示的登录页面。输入你在前面设置的用户名和密码,将进入页面index。。在这个主页的页眉中,显示了一条

个性化问候语,其中包含你的用户名。

图19-4  登录页面

19.2.3  注销

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

视图函数,并在base.html中添加一个注销链接。

  1. 注销URL

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

urls.py

—snip—

urlpatterns = [

登录页面

—snip—

注销

url(r'^logout/$', views.logout_view, name='logout'),

]

这个URL模式将请求发送给函数logout_view() 。这样给这个函数命名,旨在将其与我们将在其中调用的函数logout() 区分开来(请确保你修改的是users/urls.py,而不是

learning_log/ urls.py)。

  1. 视图函数logout_view()

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

views.py

08 - 图9

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 对象作为实参。然后,我们重定向到主页(见❸)。

  1. 链接到注销视图

现在我们需要添加一个注销链接。我们在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—

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

其看起来更专业。

图19-5  包含个性化问候语和注销链接的主页

19.2.4  注册页面

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

  1. 注册页面的URL 模式

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

urls.py

—snip—

urlpatterns = [

登录页面

—snip—

注册页面

url(r'^register/$', views.register, name='register'),

]

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

  1. 视图函数register()

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

录。请在users/views.py中添加如下代码:

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 传递给它(见❻),这将为新用户创建有效的会话。最后,我们将用户重定向到主页(见❼),其页眉中显示了

一条个性化的问候语,让用户知道注册成功了。

  1. 注册模板

注册页面的模板与登录页面的模板类似,请务必将其保存到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在表单中正确地显示所有的字段,包括错误消息——如果用户没有正确地填写表单。

  1. 链接到注册页面

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

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—

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

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

注意  这里的注册系统允许用户创建任意数量的账户。有些系统要求用户确认其身份:发送一封确认邮件,用户回复后其账户才生效。通过这样做,系统生成的垃圾

账户将比这里使用的简单系统少。然而,学习创建应用程序时,完全可以像这里所做的那样,使用简单的用户注册系统。

动手试一试

19-2 博客账户 :在你为完成练习19-1而开发的项目Blog中,添加一个用户身份验证和注册系统。让已登录的用户在屏幕上看到其用户名,并让未注册的用户看到一个

到注册页面的链接。

19.3  让用户拥有自己的数据

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

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

19.3.1  使用@login_required 限制访问

Django提供了装饰器@login_required ,让你能够轻松地实现这样的目标:对于某些页面,只允许已登录的用户访问它们。装饰器 (decorator)是放在函数定义前面的指

令,Python在函数运行前,根据它来修改函数代码的行为。下面来看一个示例。

  1. 限制对topics 页面的访问

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

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 = 'userslogin/'

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

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

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

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,都应限制对它们的访问。

19.3.2  将数据关联到用户

现在,需要将数据关联到提交它们的用户。我们只需将最高层的数据关联到用户,这样更低层的数据将自动关联到用户。例如,在项目“学习笔记”中,应用程序的最高层数据是

主题,而所有条目都与特定主题相关联。只要每个主题都归属于特定用户,我们就能确定数据库中每个条目的所有者。

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

联的数据。

  1. 修改模型Topic

对models.py的修改只涉及两行代码:

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 的外键关系。

  1. 确定当前有哪些用户

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

有主题都关联到同一个用户,如超级用户。为此,我们需要知道该用户的ID。

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

(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

>>>

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

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

  1. 迁移数据库

知道用户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让我们输入默认值(见❺)。

为将所有既有主题都关联到管理用户l_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$

Django应用新的迁移,结果一切顺利(见❶)。

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

❶ >>> 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 (见❶),再遍历所有的既有主题,并打印每个主题及其所属的用户(见❷)。正如你看到的,现在每个主题都属于用户

l_admin。

注意  你可以重置数据库而不是迁移它,但如果这样做,既有的数据都将丢失。一种不错的做法是,学习如何在迁移数据库的同时确保用户数据的完整性。如果你确

实想要一个全新的数据库,可执行命令python manage.py flush ,这将重建数据库的结构。如果你这样做,就必须重新创建超级用户,且原来的所有数据都将

丢失。

19.3.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页面将不会列出任何主题。

19.3.4  保护用户的主题

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

你自己试一试就明白了。以拥有所有主题的用户的身份登录,访问特定的主题,并复制该页面的URL,或将其中的ID记录下来。然后,注销并以另一个用户的身份登录,再输入

显示前述主题的页面的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章,我们将对这个项目进行配置,让用户看到更合适的错误页面。

19.3.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 异常。

19.3.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—

08 - 图10

我们首先调用form.save() ,并传递实参commit=False ,这是因为我们先修改新主题,再将其保存到数据库中(见❶)。接下来,将新主题的owner 属性设置为当前用户

(见❷)。最后,对刚定义的主题实例调用save() (见❸)。现在主题包含所有必不可少的数据,将被成功地保存。

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

动手试一试

19-3 重构 :在views.py中,我们在两个地方核实主题关联到的用户为当前登录的用户。请将执行这种检查的代码放在一个名为check_topic_owner() 的函数中,

并在恰当的地方调用这个函数。

19-4 保护页面new_entry :一个用户可在另一个用户的学习笔记中添加条目,方法是输入这样的URL,即其中包含输入另一个用户的主题的ID。为防范这种攻击,

请在保存新条目前,核实它所属的主题归当前用户所有。

19-5 受保护的博客 :在你创建的项目Blog中,确保每篇博文都与特定用户相关联。确保任何用户都可访问所有的博文,但只有已登录的用户能够发表博文以及编辑既

有博文。在让用户能够编辑其博文的视图中,在处理表单前确认用户编辑的是他自己发表的博文。

19.4  小结

在本章中,你学习了如何使用表单来让用户添加新主题、添加新条目和编辑既有条目。接下来,你学习了如何实现用户账户。你让老用户能够登录和注销,并学习了如何使用

Django提供的表单UserCreationForm 让用户能够创建新账户。

建立简单的用户身份验证和注册系统后,你通过使用装饰器@login_required 禁止未登录的用户访问特定页面。然后,你通过使用外键将数据关联到特定用户,还学习了如何

执行要求指定默认数据的数据库迁移。

最后,你学习了如何修改视图函数,让用户只能看到属于他的数据。你使用方法filter() 来获取合适的数据,并学习了如何将请求的数据的所有者同当前登录的用户进行比

较。

该让哪些数据可随便访问,该对哪些数据进行保护呢?这可能并非总是那么显而易见,但通过不断地练习就能掌握这种技能。在本章中,我们就该如何保护用户数据所做的决策

表明,与人合作开发项目是个不错的主意:有人对项目进行检查的话,更容易发现其薄弱环节。

至此,我们创建了一个功能齐备的项目,它运行在本地计算机上。在本书的最后一章,我们将设置这个项目的样式,使其更漂亮;我们还将把它部署到一台服务器上,让任何人

都可通过互联网注册并创建账户。

第 20 章 设置应用程序的样式并对其进行部署

当前,项目“学习笔记”功能已齐备,但未设置样式,也只是在本地计算机上运行。在本章中,我们将以简单而专业的方式设置这个项目的样式,再将其部署到一台服

务器上,让世界上的任何人都能够建立账户。

为设置样式,我们将使用Bootstrap库,这是一组工具,用于为Web应用程序设置样式,使其在任何现代设备上都看起来很专业,无论是大型的平板显示器还是智能手

机。为此,我们将使用应用程序django-bootstrap3,这也让你能够练习使用其他Django开发人员开发的应用程序。

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

项目所做的修改。

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

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

我们一直专注于项目“学习笔记”的功能,而没有考虑样式设置的问题,这是有意为之的。这是一种不错的开发方法,因为能正确运行的应用程序才是有用的。当然,应用程序能

够正确运行后,外观就显得很重要了,因为漂亮的应用程序才能吸引用户使用它。

在本节中,我将简要地介绍应用程序django-bootstrap3,并演示如何将其继承到项目中,为部署项目做好准备。

08 - 图11

20.1.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的末尾添加如下代码:

settings.py

—snip—

我的设置

LOGIN_URL = 'userslogin/'

django-bootstrap3的设置

BOOTSTRAP3 = {

'include_jquery': True,

}

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

20.1.2  使用Bootstrap 来设置项目“ 学习笔记” 的样式

Bootstrap基本上就是一个大型的样式设置工具集,它还提供了大量的模板,你可将它们应用于项目以创建独特的总体风格。对Bootstrap初学者来说,这些模板比各个样式设置工具

使用起来要容易得多。要查看Bootstrap提供的模板,可访问http://getbootstrap.com/ ,单击Getting Started,再向下滚动到Examples部分,并找到Navbars in action。我们将使用模板Static top navbar,它提供了简单的顶部导航条、页面标题和用于放置页面内容的容器。

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

图20-1  项目“ 学习笔记” 的主页—— 使用Bootstrap 设置样式后

知道要获得的效果后,接下来的内容理解起来将更容易。

20.1.3  修改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> 。

  1. 定义导航栏

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

—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 设置一组链接的样式,使其出现在导航栏右边——登录链接和注册链

接通常出现在这里。在这里,我们要么显示问候语和注销链接,要么显示注册链接和登录链接。这部分余下的代码结束包含导航栏的元素(见❽)。

  1. 定义页面的主要部分

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所示的专业级导航栏。请尝试调整窗口的大小,使其非常窄;此时导航栏将变成一个按钮,如果你单击这个按

钮,将打开一个下拉列表,其中包含所有的导航链接。

注意  这个简化的Bootstrap模板适用于最新的浏览器,而较早的浏览器可能不能正确地渲染某些样式。完整的模板可在http://getbootstrap.com/getting-started/#examples/

到,它几乎在所有浏览器中都管用。

20.1.4  使用jumbotron 设置主页的样式

下面来使用新定义的header 块及另一个名为jumbotron的Bootstrap元素修改主页。jumbotron元素是一个大框,相比于页面的其他部分显得鹤立鸡群,你想在其中包含什么东西都可

以;它通常用于在主页中呈现项目的简要描述。我们还可以修改主页显示的消息。index.html的代码如下:

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 %}

在❶处,我们告诉Django,我们要定义header 块包含的内容。在一个jumbotron 元素(见❷)中,我们放置了一条简短的标语——Track your Learning,让首次访问者大致知

道“学习笔记”是做什么用的。

在❸处,我们通过添加一些文本,做了更详细的说明。我们邀请用户建立账户,并描述了用户可执行的两种主要操作:添加新主题以及在主题中创建条目。现在的主页类似于图

20-1所示,与设置样式前相比,有了很大的改进。

20.1.5  设置登录页面的样式

我们改进了登录页面的整体外观,但还未改进登录表单,下面来让表单与页面的其他部分一致:

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 %}

在❶处,我们在这个模板中加载了bootstrap3模板标签。在❷处,我们定义了header 块,它描述了这个页面是做什么用的。注意,我们从这个模板中删除了{% if form.errors %} 代码块,因为django-bootstrap3会自动管理表单错误。

在❸处,我们添加了属性class="form" ;然后使用模板标签{% bootstrap_form %} 来显示表单(见❹);这个标签替换了我们在第19章使用的标签{{ form.as_p }} 。模板标签{% booststrap_form %} 将Bootstrap样式规则应用于各个表单元素。❺处是bootstrap3起始模板标签{% buttons %} ,它将Bootstrap样式应用于按钮。

图20-2显示了现在渲染的登录表单。这个页面比以前整洁得多,其风格一致,用途明确。如果你尝试使用错误的用户名或密码登录,将发现消息的样式与整个网站也是一致的,

毫无违和感。

08 - 图12

图20-2  使用Bootstrap 设置样式后的登录页面

20.1.6  设置new_topic 页面的样式

下面来让其他网页的风格也一致。首先来修改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 页面,将发现其外观类似于登录页面。

20.1.7  设置topics 页面的样式

下面来确保用于查看信息的页面的样式也是合适的,首先来设置topics页面的样式:

topics.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> 元素,让它们在页面上显得大些(见❷);对于添加新主题的链接,也做了同样的处理(见❸)。

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

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

08 - 图13

用于显示主题的条目:

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代码未做任何修改。

图20-3显示了修改后的topic页面。“学习笔记”的功能没有任何变化,但显得更专业了,对用户会更有吸引力。

图20-3  使用Bootstrap 设置样式后的topic 页面

注意  要使用其他Bootstrap模板,可采用与本章类似的流程:将这个模板复制到base.html中,并修改包含实际内容的元素,以使用该模板来显示项目的信息;然后,

使用Bootstrap的样式设置工具来设置各个页面中内容的样式。

动手试一试

20-1 其他表单 :我们对登录页面和add_topic 页面应用了Bootstrap样式。请对其他基于表单的页面做类似的修改:new_entry 页面、edit_entry 页面和注册页

面。

20-2 设置博客的样式 :对于你在第19章创建的项目Blog,使用Bootstrap来设置其样式。

20.2  部署“ 学习笔记”

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

够管理Web应用程序的部署。我们将让“学习笔记”在Heroku上运行。

在Windows系统上的部署过程与在Linux和OS X系统上稍有不同。如果你使用的是Windows,请阅读各节的“注意”,它们指出了在Windows系统上需要采取的不同做法。

20.2.1  建立Heroku 账户

要建立账户,请访问https://heroku.com/ ,并单击其中的一个注册链接。注册账户是免费的,Heroku提供了免费试用服务,让你能够将项目部署到服务器并对其进行测试。

注意  Heroku提供的免费试用服务存在一些限制,如可部署的应用程序数量以及用户访问应用程序的频率。但这些限制都很宽松,让你完全能够在不支付任何费用的

情况下练习部署应用程序。

20.2.2  安装Heroku Toolbelt

要将项目部署到Heroku的服务器并对其进行管理,需要使用Heroku Toolbelt提供的工具。要安装最新的Heroku Toolbelt版本,请访问https://toolbelt.heroku.com/ ,并根据你使用的操作

系统按相关的说明做:使用只包含一行的终端命令,或下载并运行安装程序。

20.2.3  安装必要的包

你还需安装很多包,以帮助在服务器上支持Django项目提供的服务。为此,在活动的虚拟环境中执行如下命令:

(ll_env)learning_log$ pip install dj-database-url

(ll_env)learning_log$ pip install dj-static

(ll_env)learning_log$ pip install static3

(ll_env)learning_log$ pip install gunicorn

务必逐个地执行这些命令,这样你就能知道哪些包未能正确地安装。dj-database-url 包帮助Django与Heroku使用的数据库进行通信,dj-static 和static3 包帮助Django 正确地管理静态文件,而gunicorn 是一个服务器软件,能够在在线环境中支持应用程序提供的服务。(静态文件包括样式规则和JavaScript文件。)

注意  在Windows系统中,有些必不可少的包可能无法安装,因此如果在你尝试安装有些这样的包时出现错误消息,也不用担心。重要的是让Heroku在部署中安装这

些包,下一节就将这样做。

20.2.4  创建包含包列表的文件requirements.txt Heroku需要知道我们的项目依赖于哪些包,因此我们将使用pip来生成一个文件,其中列出了这些包。同样,进入活动虚拟环境,并执行如下命令:

(ll_env)learning_log$ pip freeze > requirements.txt

命令freeze 让pip将项目中当前安装的所有包的名称都写入到文件requirements.txt中。请打开文件requirements.txt,查看项目中安装的包及其版本(如果你使用的是Windows系统,

看到的内容可能不全):

requirements.txt

Django==1.8.4

dj-database-url==0.3.0

dj-static==0.0.6

django-bootstrap3==6.2.2

gunicorn==19.3.0

static3==0.6.1

“学习笔记”依赖于6个特定版本的包,因此需要在相应的环境中才能正确地运行。我们部署“学习笔记”时,Heroku将安装requirements.txt列出的所有包,从而创建一个环境,其中包

含我们在本地使用的所有包。有鉴于此,我们可以信心满满,深信项目部署到Heroku后,行为将与它在本地系统上的完全相同。当你在自己的系统上开发并维护各种项目时,这

将是一个巨大的优点。

接下来,我们需要在包列表中添加psycopg2 ,它帮助Heroku管理活动数据库。为此,打开文件requirements.txt,并添加代码行psycopg2>=2.6.1 。这将安装2.6.1版的psycopg2

——如果有更高的版本,则安装更高的版本:

requirements.txt

Django==1.8.4

dj-database-url==0.3.0

dj-static==0.0.6

django-bootstrap3==6.2.2

gunicorn==19.3.0

static3==0.6.1

psycopg2>=2.6.1

如果有必不可少的包在你的系统中没有安装,请将其添加到文件requirements.txt中。最终的文件requirements.txt应包含上面列出的每个包。如果在你的系统中,requirements.txt列出的

包的版本与上面列出的不同,请保留原来的版本号。

注意  如果你使用的是Windows系统,请确保文件requirements.txt的内容与前面列出的一致,而不要管你在系统中能够安装哪些包。

20.2.5  指定Python 版本

如果你没有指定Python版本,Heroku将使用其当前的Python默认版本。下面来确保Heroku使用我们使用的Python版本。为此,在活动的虚拟环境中,执行命令python —version :

(ll_env)learning_log$ python —version

Python 3.5.0

上面的输出表明,我使用的是Python 3.5.0。请在manage.py所在的文件夹中新建一个名为runtime.txt的文件,并在其中输入如下内容:

runtime.txt

python3.5.0

这个文件应只包含一行内容,以上面所示的格式指定了你使用的Python版本;请确保输入小写的python ,在它后面输入一个连字符,再输入由三部分组成的版本号。

注意  如果出现错误消息,指出不能使用你指定的Python版本,请访问https://devcenter.heroku.com/ 并单击Python,再单击链接Specifying a Python Runtime。浏览打开的文

章,了解支持的Python版本,并使用与你使用的Python版本最接近的版本。

20.2.6  为部署到Herohu 而修改settings.py 现在需要在settings.py末尾添加一个片段,在其中指定一些Heroku环境设置:

settings.py

—snip—

django-bootstrap3设置

BOOTSTRAP3 = {

'include_jquery': True,

}

Heroku设置

❶ if os.getcwd() == '/app':

❷ import dj_database_url

DATABASES = {

'default': dj_database_url.config(default='postgres://localhost')

}

让request.is_secure()承认X-Forwarded-Proto头

❸ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

支持所有的主机头(host header)

❹ ALLOWED_HOSTS = ['*']

静态资产配置

❺ BASEDIR = os.path.dirname(os.path.abspath(_file))

STATIC_ROOT = 'staticfiles'

STATICFILES_DIRS = (

os.path.join(BASE_DIR, 'static'),

)

在❶处,我们使用了函数getcwd() ,它获取当前的工作目录 (当前运行的文件所在的目录)。在Heroku部署中,这个目录总是/app。在本地部署中,这个目录通常是项目文件

夹的名称(就我们的项目而言,为learning_log)。这个if 测试确保仅当项目被部署到Heroku时,才运行这个代码块。这种结构让我们能够将同一个设置文件用于本地开发环境和

在线服务器。

在❷处,我们导入了dj_database_url ,用于在Heroku上配置服务器。Heroku使用PostgreSQL(也叫Postgres)——一种比SQLite更高级的数据库;这些设置对项目进行配置,

使其在Heroku上使用Postgres数据库。其他设置的作用分别如下:支持HTTPS请求(见❸);让Django能够使用Heroku的URL来提供项目提供的服务(见❹);设置项目,使其能够

在Heroku上正确地提供静态文件(见❺)。

20.2.7  创建启动进程的Procfile

Procfile告诉Heroku启动哪些进程,以便能够正确地提供项目提供的服务。这个文件只包含一行,你应将其命名为Procfile(其中的P为大写),不指定文件扩展名,并保存到

manage.py所在的目录中。

Procfile的内容如下:

Procfile

web: gunicorn learning_log.wsgi —log-file -

这行代码让Heroku将gunicorn用作服务器,并使用learning_log/wsgi.py中的设置来启动应用程序。标志log-file 告诉Heroku应将哪些类型的事件写入日志。

20.2.8  为部署到Herohu 而修改wsgi.py 为部署到Heroku,我们还需修改wsgi.py,因为Heroku需要的设置与我们一直在使用的设置稍有不同:

wsgi.py

—snip—

import os

from django.core.wsgi import get_wsgi_application

from dj_static import Cling

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "learning_log.settings") application = Cling(get_wsgi_application()) 我们导入了帮助正确地提供静态文件的Cling,并使用它来启动应用程序。这些代码在本地也适用,因此无需将其放在if 代码块内。

20.2.9  创建用于存储静态文件的目录

在Heroku上,Django搜集所有的静态文件,并将它们放在一个地方,以便能够高效地管理它们。我们将创建一个用于存储这些静态文件的目录。在文件夹learning_log中,有一个名

称也为learning_log的子文件夹。在这个子文件夹中,新建一个名为static的文件夹,因此这个文件夹的路径为learning_log/learning_log/static/。我们还需在这个文件夹中创建一个占位文

件,因为项目被推送到Heroku时,它将不会包含原来为空的文件夹。在目录static/中,创建一个名为placeholder.txt的文件:

placeholder.txt

This file ensures that learning_log/static/ will be added to the project.

Django will collect static files and place them in learning_log/static/.

上述内容没有什么特别之处,只是指出了在项目中添加这个文件的原因。

20.2.10  在本地使用gunicorn 服务器

如果你使用的是Linux或OS X,可在部署到Heroku前尝试在本地使用gunicorn服务器。为此,在活动的虚拟环境中,执行命令heroku local 以启动Procfile指定的进程:

(ll_env)learning_log$ heroku local

Installing Heroku Toolbelt v4… done —snip—

forego | starting web.1 on port 5000

❶ web.1 | [2015-08-13 22:00:45 -0800] [12875] [INFO] Starting gunicorn 19.3.0

❷ web.1 | [2015-08-13 22:00:45 -0800] [12875] [INFO] Listening at:

http://0.0.0.0:5000 (12875)

❸ web.1 | [2015-08-13 22:00:45 -0800] [12878] [INFO] Booting worker with pid: 12878

首次执行命令heroku local 时,将安装Heroku Toolbelt中的很多包。这里的输出表明启动了gunicorn,其进程id为12875(见❶)。❷处的输出表明,gunicorn在端口5000上侦听请

求。另外,gunicorn还启动了一个工作进程(12878),用于帮助处理请求(见❸)。

为确认一切运行正常,请访问http://localhost:5000/,你将看到“学习笔记”的主页,就像使用Django服务器(runserver)时一样。为停止heroku local 启动的进程,请按Ctrl + C,

你将在本地开发中继续使用runserver 。

注意  gunicorn不能在Windows系统上运行,因此如果你使用的是Windows系统,请跳过这一步。但这不会影响你将项目部署到Heroku。

20.2.11  使用Git 跟踪项目文件

如果你阅读完了第17章,就知道Git是一个版本控制程序,让你能够在每次成功实现新功能后都拍摄项目代码的快照。无论出现什么问题(如实现新功能时不小心引入了bug),你

都可以轻松地恢复到最后一个可行的快照。每个快照都被称为提交 。

使用Git意味着你在试着实现新功能时无需担心破坏项目。将项目部署到服务器时,需要确保部署的是可行版本。如果你想更详细地了解Git和版本控制,请参阅附录D。

  1. 安装Git

Heroku Toolbelt包含Git,因此它应该已经安装到了你的系统中。然而,在安装Heroku Toolbelt之前打开的终端窗口中无法访问Git,因此请打开一个新的终端窗口,并在其中执行命

令git —version :

(ll_env)learning_log$ git —version

git version 2.5.0

如果由于某种原因出现了错误消息,请参阅附录D中的Git安装说明。

  1. 配置Git

Git跟踪谁修改了项目,即便项目由一个人开发时亦如此。为进行跟踪,Git需要知道你的用户名和email。因此,你必须提供用户名,但对于练习项目,可随便伪造一个email:

(ll_env)learning_log$ git config —global user.name "ehmatthes"

(ll_env)learning_log$ git config —global user.email "eric@example.com"

如果你忘记了这一步,当你首次提交时,Git将提示你提供这些信息。

  1. 忽略文件

我们无需让Git跟踪项目中的每个文件,因此将让Git忽略一些文件。为此,在manage.py所在的文件夹中创建一个名为.gitignore的文件。注意,这个文件名以句点打头,且不包含扩

展名。在这个文件中输入如下内容:

.gitignore

ll_env/

pycache/

*.sqlite3

我们让Git忽略目录l env ,因为我们随时都可以自动重新创建它。我们还指定不跟踪目录 \ pycache_,这个目录包含Django运行.py文件时自动创建的.pyc文件。我们没有跟踪对本

地数据库的修改,因为这是一个糟糕的做法:如果你在服务器上使用的是SQLite,当你将项目推送到服务器时,可能会不小心用本地测试数据库覆盖在线数据库。

注意  如果你使用的是Python 2.7,请将pycache 替换为*.pyc ,因为Python 2.7不会创建目录pycache

  1. 提交项目

我们需要为“学习笔记”初始化一个Git仓库,将所有必要的文件都加入到这个仓库中,并提交项目的初始状态,如下所示:

❶ (ll_env)learning_log$ git init

Initialized empty Git repository in homeehmatthes/pcc/learning_log/.git/

❷ (ll_env)learning_log$ git add .

❸ (ll_env)learning_log$ git commit -am "Ready for deployment to heroku."

[master (root-commit) dbc1d99] Ready for deployment to heroku.

43 files changed, 746 insertions(+)

create mode 100644 .gitignore

create mode 100644 Procfile

—snip—

create mode 100644 users/views.py

❹ (ll_env)learning_log$ git status

On branch master

nothing to commit, working directory clean

(ll_env)learning_log$

在❶处,我们执行命令git init ,在“学习笔记”所在的目录中初始化一个空仓库。在❷处,我们执行了命令git add . (千万别忘了这个句点),它将未被忽略的文件都添

加到这个仓库中。在❸处,我们执行了命令git commit -am commit message ,其中的标志-a 让Git在这个提交中包含所有修改过的文件,而标志-m 让Git记录一条日志

消息。

在❹处,我们执行了命令git status ,输出表明当前位于分支master中,而工作目录是干净 (clean)的。每当你要将项目推送到Heroku时,都希望看到这样的状态。

20.2.12  推送到Heroku

我们终于为将项目推送到Heroku做好了准备。在活动的虚拟环境中,执行下面的命令:

❶ (ll_env)learning_log$ heroku login Enter your Heroku credentials.

Email: eric@example.com

Password (typing will be hidden):

Logged in as eric@example.com

❷ (ll_env)learning_log$ heroku create

Creating afternoon-meadow-2775… done, stack is cedar-14

https://afternoon-meadow-2775.herokuapp.com/ |

https://git.heroku.com/afternoon-meadow-2775.git

Git remote heroku added

❸ (ll_env)learning_log$ git push heroku master

—snip—

remote: ——-> Launching… done, v6

❹ remote: https://afternoon-meadow-2775.herokuapp.com/ deployed to Heroku remote: Verifying deploy…. done.

To https://git.heroku.com/afternoon-meadow-2775.git

bdb2a35..62d711d master -> master

(ll_env)learning_log$

首先,在终端会话中,使用你在https://heroku.com/ 创建账户时指定的用户名和密码来登录Heroku(见❶)。然后,让Heroku创建一个空项目(见❷)。Heroku生成的项目名由两个

单词和一个数字组成,你以后可修改这个名称。接下来,我们执行命令git push heroku master (见❸),它让Git将项目的分支master推送到Heroku刚才创建的仓库中;

Heroku随后使用这些文件在其服务器上创建项目。❹处列出了用于访问这个项目的URL。

执行这些命令后,项目就部署好了,但还未对其做全面的配置。为核实正确地启动了服务器进程,请执行命令heroku ps :

(ll_env)learning_log$ heroku ps

❶ Free quota left: 17h 40m

❷ === web (Free): gunicorn learning_log.wsgi __log-file -

web.1: up 2015/08/14 07:08:51 (~ 10m ago)

(ll_env)learning_log$

输出指出了在接下来的24小时内,项目还可在多长时间内处于活动状态(见❶)。编写本书时,Heroku允许免费部署在24小时内最多可以有18小时处于活动状态。项目的活动时

间超过这个限制后,将显示标准的服务器错误页面,稍后我们将设置这个错误页面。在❷处,我们发现启动了Procfile指定的进程。

现在,我们可以使用命令heroku open 在浏览器中打开这个应用程序了:

(ll_env)learning_log$ heroku open

Opening afternoon-meadow-2775… done

你也可以启动浏览器并输入Heroku告诉你的URL,但上述命令可实现同样的结果。你将看到“学习笔记”的主页,其样式设置正确无误,但你还无法使用这个应用程序,因为我们还

没有建立数据库。

注意  部署到Heroku的流程会不断变化。如果你遇到无法解决的问题,请通过查看Heroku文档来获取帮助。为此,可访问https://devcenter.heroku.com/ ,单击Python,再

单击链接Getting Started with Django。如果你看不懂这些文档,请参阅附录C提供的建议。

20.2.13  在Heroku 上建立数据库

为建立在线数据库,我们需要再次执行命令migrate ,并应用在开发期间生成的所有迁移。要对Heroku项目执行Django和Python命令,可使用命令heroku run 。下面演示了如

何对Heroku部署执行命令migrate :

❶ (ll_env)learning_log$ heroku run python manage.py migrate

❷ Running `python manage.py migrateòn afternoon-meadow-2775… up, run.2435

—snip—

❸ Running migrations:

—snip—

Applying learning_logs.0001_initial… OK

Applying learning_logs.0002_entry… OK

Applying learning_logs.0003_topic_user… OK

Applying sessions.0001_initial… OK

(ll_env)learning_log$

我们首先执行了命令heroku run python manage.py migrate (见❶);Heroku随后创建一个终端会话来执行命令migrate (见❷)。在❸处,Django应用默认迁移以

及我们在开发“学习笔记”期间生成的迁移。

现在如果你访问这个部署的应用程序,将能够像在本地系统上一样使用它。然而,你看不到你在本地部署中输入的任何数据,因为它们没有复制到在线服务器。一种通常的做法

是不将本地数据复制到在线部署中,因为本地数据通常是测试数据。

你可以分享“学习笔记”的Heroku URL,让任何人都可以使用它。在下一节,我们将再完成几个任务,以结束部署过程并让你能够继续开发“学习笔记”。

20.2.14  改进Heroku 部署

在本节中,我们将通过创建超级用户来改进部署,就像在本地一样。我们还将让这个项目更安全:将DEBUG 设置为False ,让用户在错误消息中看不到额外的信息,以防他们

使用这些信息来攻击服务器。

  1. 在Heroku 上创建超级用户

我们知道可使用命令heroku run 来执行一次性命令,但也可这样执行命令:在连接到了Heroku服务器的情况下,使用命令heroku run bash 来打开Bash终端会话。Bash是

众多Linux终端运行的语言。我们将使用Bash终端会话来创建超级用户,以便能够访问在线应用程序的管理网站:

(ll_env)learning_log$ heroku run bash

Running `bashòn afternoon-meadow-2775… up, run.6244

❶ ~ $ ls

learning_log learning_logs manage.py Procfile requirements.txt runtime.txt users staticfiles ❷ ~ $ python manage.py createsuperuser

Username (leave blank to use 'u41907'): ll_admin

Email address:

Password:

Password (again):

Superuser created successfully.

❸ ~ $ exit

exit

(ll_env)learning_log$

在❶处,我们执行命令ls ,以查看服务器上有哪些文件和目录;服务器包含的文件和目录应该与本地系统相同。你可以像遍历其他文件系统一样遍历这个文件系统。

注意  即便你使用的是Windows系统,也应使用这里列出的命令(如ls 而不是dir ),因为你正通过远程连接运行一个Linux终端。

在❷处,我们执行了创建超级用户的命令,它像第18章在本地系统创建超级用户一样提示你输入相关的信息。在这个终端会话中创建超级用户后,使用命令exit 返回到本地系

统的终端会话(见❸)。

现在,你可以在在线应用程序的URL末尾添加/admin/来登录管理网站了。对我而言,这个URL为https://afternoon-meadow-2775.herokuapp.com/admin/

如果已经有其他人开始使用这个项目,别忘了你可以访问他们的所有数据!千万别不把这当回事,否则用户就不会再将其数据托付给你了。

  1. 在Heroku 上创建对用户友好的URL

你可能希望URL更友好,比https://afternoon-meadow-2775.herokuapp.com/ 更好记。为此,可只需使用一个命令来重命名应用程序:

(ll_env)learning_log$ heroku apps:rename learning-log

Renaming afternoon-meadow-2775 to learning-log… done

https://learning-log.herokuapp.com/ | https://git.heroku.com/learning-log.git Git remote heroku updated (ll_env)learning_log$

给应用程序命名时,可使用字母、数字和连字符;你想怎么命名应用程序都可以,只要指定的名称未被别人使用就行。现在,项目的URL变成了https://learning-log.herokuapp.com/

使用以前的URL再也无法访问它,命令apps:rename 将整个项目都移到了新的URL处。

注意  你使用Heroku提供的免费服务来部署项目时,如果项目在指定的时间内未收到请求或过于活跃,Heroku将让项目进入休眠状态。用户初次访问处于休眠状态的

网站时,加载时间将更长,但对于后续请求,服务器的响应速度将更快。这就是Heroku能够提供免费部署的原因所在。

20.2.15  确保项目的安全

当前,我们部署的项目存在一个严重的安全问题:settings.py包含设置DEBUG=True ,它在发生错误时显示调试信息。开发项目时,Django的错误页面向你显示了重要的调试信

息,如果将项目部署到服务器后依然保留这个设置,将给攻击者提供大量可供利用的信息。我们还需确保任何人都无法看到这些信息,也不能冒充项目托管网站来重定向请求。

下面来修改settings.py,以让我们能够在本地看到错误消息,但部署到服务器后不显示任何错误消息:

settings.py

—snip—

Heroku设置

if os.getcwd() == '/app':

—snip—

让request.is_secure()承认X-Forwarded-Proto头

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

只允许Heroku托管这个项目

❶ ALLOWED_HOSTS = ['learning-log.herokuapp.com']

❷ DEBUG = False

静态资产配置

—snip—

我们只需做两方面的修改。在❶处,修改ALLOWED_HOSTS ,只允许Heroku托管这个项目。你需要使用应用程序的名称,可以是Heroku提供的名称(如afternoon-meadow-2775.herokuapp.com),也可以是你选择的名称。在❷处,我们将DEBUG 设置为False ,让Django不在错误发生时显示敏感信息。

20.2.16  提交并推送修改

现在需要将对settings.py所做的修改提交到Git仓库,再将修改推送到Heroku。下面的终端会话演示了这个过程:

❶ (ll_env)learning_log$ git commit -am "Set DEBUG=False for Heroku."

[master 081f635] Set DEBUG=False for Heroku.

1 file changed, 4 insertions(+), 2 deletions(-)

❷ (ll_env)learning_log$ git status

On branch master

nothing to commit, working directory clean

(ll_env)learning_log$

我们执行命令git commit ,并指定了一条简短而具有描述性的提交消息(见❶)。别忘了,标志-am 让Git提交所有修改过的文件,并记录一条日志消息。Git找出唯一一个修

改过的文件,并将所做的修改提交到仓库。

❷处显示的状态表明我们在仓库的分支master上工作,当前没有任何未提交的修改。推送到Heroku之前,必须检查状态并看到刚才所说的消息。如果你没有看到这样的消息,说明

有未提交的修改,而这些修改将不会推送到服务器。在这种情况下,可尝试再次执行命令commit ,但如果你不知道该如何解决这个问题,请阅读附录D,更深入地了解Git的用

法。

下面来将修改后的仓库推送到Heroku:

(ll_env)learning_log$ git push heroku master

—snip—

remote: ——-> Python app detected

remote: ——-> Installing dependencies with pip

—snip—

remote: ——-> Launching… done, v8

remote: https://learning-log.herokuapp.com/ deployed to Heroku

remote: Verifying deploy…. done.

To https://git.heroku.com/learning-log.git

4c9d111..ef65d2b master -> master

(ll_env)learning_log$

Heroku发现仓库发生了变化,因此重建项目,确保所有的修改都已生效。它不会重建数据库,因此这次无需执行命令migrate 。

现在要核实部署更安全了,请输入项目的URL,并在末尾加上我们未定义的扩展。例如,尝试访问http://learning-log.herokuapp.com/letmein/ 。你将看到一个通用的错误页面,它没有

泄露任何有关该项目的具体信息。如果你尝试向本地的“学习笔记”发出同样的请求——输入URL http://localhost:8000/letmein/,你将看到完整的Django错误页面。这样的结果非常理

想,你接着开发这个项目时,将看到信息丰富的错误消息,但用户看不到有关项目代码的重要信息。

20.2.17  创建自定义错误页面

在第19章,我们对“学习笔记”进行了配置,使其在用户请求不属于他的主题或条目时返回404错误。你可能还遇到过一些500错误(内部错误)。404错误通常意味着你的Django代

码是正确的,但请求的对象不存在。500错误通常意味着你编写的代码有问题,如views.py中的函数有问题。当前,在这两种情况下,Django都返回通用的错误页面,但我们可以编

写外观与“学习笔记”一致的404和500错误页面模板。这些模板必须放在根模板目录中。

  1. 创建自定义模板

在文件夹learning_log/learning_log中,新建一个文件夹,并将其命名为templates;再在这个文件夹中新建一个名为404.html的文件,并在其中输入如下内容:

404.html

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

{% block header %}

<h2>The item you requested is not available. (404)</h2>

{% endblock header %}

这个简单的模板指定了通用的404错误页面包含的信息,并且该页面的外观与网站的其他部分一致。

再创建一个名为500.html的文件,并在其中输入如下代码: