19.1 让用户输入数据
建立用于创建用户账户的身份验证系统之前,我们先来添加几个页面,让用户能够输入数据。我们将让用户添加新主题,添加新条目以及编辑既有条目。
当前,只有超级用户能够通过管理网站输入数据。我们不想让用户与管理网站交互,因此我们将使用Django的表单创建工具来创建让用户能够输入数据的页面。
19.1.1 添加新主题
首先来让用户能够添加新主题。创建基于表单的页面的方法几乎与前面创建页面一样:定义URL,编写视图函数并编写一个模板。一个主要差别是,需要导入包含表单的模块forms.py。
- 用于添加主题的表单
让用户输入并提交信息的页面都是表单,那怕看起来不像。用户输入信息时,我们需要进行验证,确认提供的信息是正确的数据类型,而不是恶意的信息,如中断服务器的代码。然后,对这些有效信息进行处理,并将其保存到数据库的合适地方。这些工作很多都是由Django自动完成的。
在Diango中,创建表单的最简单方式是使用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 生成标签。
- URL模式new_topic
新页面的URL应简短且具有描述性,因此当用户要添加新主题时,我们切换到http://localhost:8000/new_topic/。下面是页面new_topic 的URL模式,请将其添加到learning_logs/ urls.py中:
urls.py
—snip—
urlpatterns = [
—snip—
# 用于添加新主题的页面。
path('new_topic/', views.new_topic, name='new_topic'),
]
这个URL模式将请求交给视图函数new_topic() ,下面来编写这个函数。
- 视图函数new_topic()
函数new_topic() 需要处理两种情形。一是刚进入new_topic 页面(在这种情况下应显示空表单);二是对提交的表单数据进行处理,并将用户重定向到页面topics :
views.py
from django.shortcuts import render, redirect
from .models import Topic
from .forms import TopicForm
—snip—
def new_topic(request):
"""添加新主题。"""
❶ if request.method != 'POST':
# 未提交数据:创建一个新表单。
❷ form = TopicForm()
else:
# POST提交的数据:对数据进行处理。
❸ form = TopicForm(data=request.POST)
❹ if form.is_valid():
❺ form.save()
❻ return redirect('learning_logs:topics')
# 显示空表单或指出表单数据无效。
❼ context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
我们导入了函数redirect ,用户提交主题后将使用这个函数重定向到页面topics 。函数redirect 将视图名作为参数,并将用户重定向到这个视图。我们还导入了刚创建的表单TopicForm 。
- 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() (见❺),将表单中的数据写入数据库。
保存数据后,就可离开这个页面了。为此,使用redirect() 将用户的浏览器重定向到页面topics (见❻)。在页面topics 中,用户将在主题列表中看到他刚输入的主题。
我们在这个视图函数的末尾定义了变量context ,并使用稍后将创建的模板new_topic.html来渲染页面。这些代码不在if 代码块内,因此无论是用户刚进入new_topic 页面还是提交的表单数据无效,这些代码都将执行。用户提交的表单数据无效时,将显示一些默认的错误消息,帮助用户提供有效的数据。
- 模板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 %}
这个模板继承了base.html,因此其基本结构与项目“学习笔记”的其他页面相同。在❶处,定义了一个HTML表单。实参action 告诉服务器将提交的表单数据发送到哪里。这里将它发回给视图函数new_topic() 。实参method 让浏览器以POST请求的方式提交数据。
Django使用模板标签{% csrf_token %} (见❷)来防止攻击者利用表单来获得对服务器未经授权的访问(这种攻击称为跨站请求伪造 )。❸处显示表单,从中可知Django使得完成显示表单等任务有多简单:只需包含模板变量{{ form.as_p }} ,就可让Django自动创建显示表单所需的全部字段。修饰符as_p 让Django以段落格式渲染所有表单元素,这是一种整洁地显示表单的简单方式。
Django不会为表单创建提交按钮,因此我们在❹处定义了一个。
- 链接到页面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中再添加一个类。
- 用于添加新条目的表单
我们需要创建一个与模型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' 指定了标签'Entry:' (见❶)。
在❷处,我们定义了属性widgets 。小部件 (widget)是一个HTML表单元素,如单行文本框、多行文本区域或下拉列表。通过设置属性widgets ,可覆盖Django选择的默认小部件。通过让Django使用forms.Textarea ,我们定制了字段'text' 的输入小部件,将文本区域的宽度设置为80列,而不是默认的40列。这给用户提供了足够的空间来编写有意义的条目。
- URL模式new_entry
在用于添加新条目的页面的URL模式中,需要包含实参topic_id ,因为条目必须与特定的主题相关联。该URL模式如下,请将它添加到learning_logs/urls.py中:
urls.py
—snip—
urlpatterns = [
—snip—
# 用于添加新条目的页面。
path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry'),
]
这个URL模式与形如http://localhost:8000/new_entry/id / 的URL匹配,其中的 id 是一个与主题ID匹配的数。代码
- 视图函数new_entry()
视图函数new_entry() 与函数new_topic() 很像,请在views.py中添加如下代码:
views.py
from django.shortcuts import render, redirect
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 redirect('learning_logs:topic', topic_id=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() 且不指定任何实参。这将把条目保存到数据库,并将其与正确的主题相关联。
在❼处,调用redirect() ,它要求提供两个参数:要重定向到的视图和要给视图函数提供的参数。这里重定向到topic() ,而这个视图函数需要参数topic_id 。视图函数topic() 渲染新增条目所属主题的页面,其中的条目列表包含新增的条目。
在视图函数new_entry() 的末尾,我们创建了一个上下文字典,并使用模板new_entry.html渲染页面。这些代码将在用户刚进入页面或提交的表单数据无效时执行。
- 模板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完全相同。
- 链接到页面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 %}
我们将这个链接放在条目列表前面,因为在这种页面中,执行的最常见的操作是添加新条目。图19-2显示了页面new_entry 。现在用户可添加新主题,还可在每个主题中添加任意数量的条目。请在一些主题中添加新条目,尝试使用一下页面new_entry 。
图19-2 页面new_entry
19.1.3 编辑条目
下面来创建让用户能够编辑既有条目的页面。
- URL模式edit_entry
这个页面的URL需要传递要编辑的条目的ID。修改后的learning_logs/urls.py如下:
urls.py
—snip—
urlpatterns = [
—snip—
# 用于编辑条目的页面。
path('edit_entry/<int:entry_id>/', views.edit_entry, name='edit_entry'),
]
在URL(如http://localhost:8000/edit_entry/1/)中传递的ID存储在形参entry_id 中。这个URL模式将与其匹配的请求发送给视图函数edit_entry() 。
- 视图函数edit_entry()
页面edit_entry 收到GET请求时,edit_entry() 将返回一个表单,让用户能够对条目进行编辑;收到POST请求(条目文本经过修订)时,则将修改后的文本保存到数据库:
views.py
from django.shortcuts import render, redirect
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 redirect('learning_logs:topic', topic_id=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() 且不指定任何实参(见❹),因为条目已关联到特定的主题。然后,重定向到显示条目所属主题的页面(见❺),用户将在其中看到其编辑的条目的新版本。
如果要显示表单让用户编辑条目或者用户提交的表单无效,就创建上下文字典并使用模板edit_entry.html渲染页面。
- 模板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作为一个实参,让视图函数edit_entry() 能够修改正确的条目对象。在❷处,将提交按钮的标签设置成Save changes,旨在提醒用户:单击该按钮将保存所做的编辑,而不是创建一个新条目。
- 链接到页面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 等字段。为这个项目创建一个超级用户,并使用管理网站创建几个简短的帖子。创建一个主页,在其中按时间顺序显示所有的帖子。
创建两个表单,其中一个用于发布新帖子,另一个用于编辑既有的帖子。尝试填写这些表单,确认它们能够正确工作。