允许用户输入数据

在建立创建账户的身份验证系统之前,我们首先要添加一些页面,允许用户输入自己的数据。我们将为用户提供添加新主题、添加新条目和编辑以前条目的功能。目前,只有超级用户才能通过管理员站点输入数据。我们不希望用户与管理员站点进行交互,因此我们将使用 Django 的表单创建工具来创建允许用户输入数据的页面。

添加新主题

让我们从允许用户添加新主题开始。添加基于表单的页面的方法与添加我们已经创建的页面的方法大致相同:我们定义一个 URL、编写一个视图函数并编写一个模板。唯一不同的是添加了一个名为 forms.py 的新模块,其中将包含表单。

主题 ModelForm

任何让用户在网页上输入和提交信息的页面都涉及到一个称为表单的 HTML 元素。当用户输入信息时,我们需要验证所提供的信息是正确的数据类型,并且不是恶意的,例如用于中断服务器的代码。然后,我们需要处理有效信息并将其保存到数据库的适当位置。Django 可以自动完成大部分工作。

在 Django 中创建表单的最简单方法是使用 ModelForm,它使用我们在第 18 章中定义的模型信息来自动创建表单。在文件 forms.py 中编写第一个表单,该文件应与 models.py 创建在同一目录下:

forms.py
from django import forms
from .models import Topic

class TopicForm(forms.ModelForm): # 1
    class Meta:
        model = Topic # 2
        fields = ['text'] # 3
        labels = {'text': ''} # 4

我们首先导入表单模块和我们要使用的模型 Topic。然后,我们定义一个名为 TopicForm 的类,它继承于 forms.ModelForm ❶。

最简单的 ModelForm 由一个嵌套的 Meta 类组成,该类告诉 Django 表单应基于哪个模型,以及表单中应包含哪些字段。在这里,我们指定表单应基于 Topic 模型 ❷,并且只包含文本字段 ❸。标签字典中的空字符串告诉 Django 不要为文本字段 ❹ 生成标签。

new_topic URL

新页面的 URL 应该简短并具有描述性。当用户要添加新主题时,我们会将其发送到 http://localhost:8000/new_topic/ 。下面是 new_topic 页面的 URL 模式;将其添加到 learning_logs/urls.py :

learning_logs/urls.py
--snip--
urlpatterns = [
    --snip--
    # Page for adding a new topic.
    path('new_topic/', views.new_topic, name='new_topic'),
]

这种 URL 模式会将请求发送到视图函数 new_topic(),我们接下来将编写该函数。

new_topic() 视图函数

new_topic() 函数需要处理两种不同的情况:对 new_topic 页面的初始请求,在这种情况下,它应该显示一个空白表单;以及处理在表单中提交的任何数据。在处理完提交表单中的数据后,它需要将用户重定向到主题页面:

views.py
from django.shortcuts import render, redirect
from .models import Topic
from .forms import TopicForm

--snip--
def new_topic(request):
    """Add a new topic."""
    if request.method != 'POST': # 1
        # No data submitted; create a blank form.
        form = TopicForm() # 2
    else:
        # POST data submitted; process data.
        form = TopicForm(data=request.POST) # 3
        if form.is_valid(): # 4
            form.save() # 5
            return redirect('learning_logs:topics') # 6
    # Display a blank or invalid form.
    context = {'form': form} # 7
    return render(request, 'learning_logs/new_topic.xhtml', context)

我们导入函数 redirect,在用户提交主题后将其重定向到主题页面。我们还导入了刚刚编写的表单 TopicForm

GET and POST Requests

在构建应用程序时,您会用到 GET 和 POST 两种主要类型的请求。GET 请求用于只从服务器读取数据的页面。当用户需要通过表单提交信息时,通常会使用 POST 请求。我们将指定 POST 方法来处理所有表单。(还存在一些其他类型的请求,但我们不会在本项目中使用它们)。

new_topic() 函数接收请求对象作为参数。当用户首次请求该页面时,浏览器会发送一个 GET 请求。用户填写并提交表单后,浏览器将提交一个 POST 请求。根据请求的不同,我们可以知道用户是请求一个空白表单(GET),还是要求我们处理一个已完成的表单(POST)。

我们使用 if 测试来确定请求方法是 GET 还是 POST ❶。如果请求方法不是 POST,那么请求很可能是 GET,因此我们需要返回一个空白表单。(我们创建一个 TopicForm ❷实例,将其赋值给变量 form,然后将表单发送到上下文字典❼ 中的模板。因为我们在实例化 TopicForm 时没有包含参数,所以 Django 创建了一个空白表单供用户填写。

如果请求方法是 POST,else 模块将运行并处理表单中提交的数据。我们创建一个 TopicForm ❸ 实例,并将用户输入的数据传递给它,这些数据被分配给 request.POST。返回的表单对象包含用户提交的信息。

在检查提交的信息是否有效❹ 之前,我们无法将其保存到数据库中。is_valid() 方法会检查所有必填字段是否都已填写(表单中的所有字段默认都是必填字段),以及输入的数据是否与预期的字段类型相匹配—​例如,文本长度是否小于 200 个字符,正如我们在第 18 章的 models.py 中指定的那样。这种自动验证为我们节省了大量工作。如果一切正常,我们就可以调用 save() ❺,将表单中的数据写入数据库。

保存数据后,我们就可以离开这个页面了。redirect() 函数接收视图名称,并将用户重定向到与该视图相关的页面。在这里,我们使用 redirect() 将用户的浏览器重定向到主题页面❻,在主题列表中,用户可以看到刚刚输入的主题。

上下文变量定义在视图函数的末尾,页面使用 new_topic.xhtml 模板渲染,我们接下来将创建该模板。这段代码位于任何 if 代码块之外;如果创建的是空白表单,它就会运行;如果确定提交的表单无效,它也会运行。无效表单将包含一些默认的错误信息,以帮助用户提交可接受的数据。

new_topic 模板

现在,我们将制作一个名为 new_topic.xhtml 的新模板,用于显示刚刚创建的表单:

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

该模板扩展了 base.xhtml,因此它与学习日志中的其余页面具有相同的基本结构。 我们使用 <form></form> 标签来定义 HTML 表单❶。 action 参数告诉浏览器将表单中提交的数据发送到哪里; 在这种情况下,我们将其发送回视图函数 new_topic()。方法参数告诉浏览器以 POST 请求的形式提交数据。

Django 使用模板标签 {% csrf_token %} ❷ 来防止攻击者利用表单获得对服务器的未授权访问。 (这种攻击称为跨站请求伪造。)接下来,我们显示表单; 在这里您可以看到 Django 可以多么简单地完成某些任务,例如显示表单。 我们只需要包含模板变量 {{ form.as_div }} ,Django 就可以创建自动显示表单所需的所有字段❸。 as_div 修饰符告诉 Django 将所有表单元素渲染为 HTML <div></div> 元素; 这是整齐地显示表单的简单方法。

Django 不会为表单创建提交按钮,因此我们在关闭表单之前定义一个提交按钮❹。

链接到 new_topic 页面

接下来,我们在主题页面上添加一个指向 new_topic 页面的链接:

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

将链接放在现有主题列表之后。图 19-1 显示了生成的表单;请尝试使用该表单添加一些自己的新主题。

image 2023 12 05 11 40 53 350
Figure 1. Figure 19-1: The page for adding a new topic

添加新实体条目

既然用户可以添加新主题,他们也会希望添加新条目。我们将再次定义一个 URL,编写一个视图函数和一个模板,并链接到页面。但首先,我们要在 forms.py 中添加另一个类。

ModelForm 实体

我们需要创建一个与 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': ''} # 1
        widgets = {'text': forms.Textarea(attrs={'cols': 80})} # 2

我们更新导入语句,使其包括 Entry 和 Topic。我们创建一个名为 EntryForm 的新类,该类继承自 forms.ModelForm。EntryForm 类有一个嵌套的 Meta 类,列出了它所基于的模型,以及要包含在表单中的字段。我们再次给字段 "text "加上一个空白标签❶。

对于 EntryForm,我们加入了 widgets 属性❷。widget 是一种 HTML 表单元素,如单行文本框、多行文本区域或下拉列表。通过包含 widgets 属性,您可以覆盖 Django 的默认 widget 选择。在这里,我们告诉 Django 使用宽度为 80 列的 forms.Textarea 元素,而不是默认的 40 列。这样用户就有足够的空间写出有意义的条目。

new_entry URL

新条目必须与特定主题相关联,因此我们需要在添加新条目的 URL 中加入 topic_id 参数。下面是添加到 learning_logs/urls.py 中的 URL:

learning_logs/urls.py
--snip--
urlpatterns = [
    --snip--
    # Page for adding a new entry.
    path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry'),
]

此 URL 模式匹配任何形式为 http://localhost:8000/new_entry/id/ 的 URL,其中 id 是与主题 ID 匹配的数字。代码 <int:topic_id> 捕获一个数值并将其赋值给变量 topic_id。当请求匹配此模式的 URL 时,Django 会将请求和主题 ID 发送给 new_entry() 视图函数。

new_entry() 视图函数

new_entry 的视图函数与添加新主题的函数非常相似。请在 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):
    """Add a new entry for a particular topic."""
    topic = Topic.objects.get(id=topic_id) # 1

    if request.method != 'POST': # 2
        # No data submitted; create a blank form.
        form = EntryForm() # 3
    else:
        # POST data submitted; process data.
        form = EntryForm(data=request.POST) # 4
        if form.is_valid():
            new_entry = form.save(commit=False) # 5
            new_entry.topic = topic # 6
            new_entry.save()
            return redirect('learning_logs:topic', topic_id=topic_id) # 7

    # Display a blank or invalid form.
    context = {'topic': topic, 'form': form}
    return render(request, 'learning_logs/new_entry.xhtml', context)

我们更新导入语句,将刚刚制作的 EntryForm 包括在内。new_entry() 的定义中有一个 topic_id 参数,用于存储从 URL 接收到的值。我们需要使用 topic 来渲染页面和处理表单数据,因此我们使用 topic_id 来获取正确的 topic 对象❶。

接下来,我们检查请求方法是 POST 还是 GET ❷。如果是 GET 请求,则执行 if 代码块,然后创建 EntryForm ❸ 的空白实例。

如果请求方法是 POST,我们会创建一个 EntryForm 实例来处理数据,该实例中会填充来自请求对象❹ 的 POST 数据。然后,我们检查表单是否有效。如果有效,我们需要在将其保存到数据库之前设置条目对象的主题属性。在调用 save() 时,我们会加入 commit=False ❺ 参数,告诉 Django 创建一个新的条目对象并将其赋值给 new_entry,而不会将其保存到数据库中。我们将 new_entry 的 topic 属性设置为函数❻ 开始时从数据库中提取的 topic。然后,我们调用不带参数的 save(),用正确的关联主题将条目保存到数据库中。

重定向()调用需要两个参数:我们要重定向到的视图名称和视图函数所需的参数❼。在这里,我们要重定向到 topic(),它需要 topic_id 这个参数。然后,该视图会渲染用户输入条目的主题页面,用户应该会在条目列表中看到自己的新条目。

在函数结束时,我们将创建一个上下文字典,并使用 new_entry.xhtml 模板渲染页面。该代码将在空白表单或已提交但无效的表单中执行。

new_entry 模板

如下代码所示,new_entry 的模板与 new_topic 的模板相似:

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

我们会在页面顶部显示主题❶,这样用户就能看到他们正在向哪个主题添加条目。同时,主题也是返回该主题主页的链接。

表单的操作参数在 URL 中包含 topic.id 值,这样视图函数就能将新条目与正确的主题❷ 关联起来。除此之外,该模板看起来就像 new_topic.xhtml。

链接到 new_entry 页面

接下来,我们需要在每个主题页面的主题模板中加入一个指向 new_entry 页面的链接:

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

我们将添加条目的链接放在显示条目之前,因为添加新条目是该页面上最常见的操作。图 19-2 显示了 new_entry 页面。现在用户可以添加新主题,并为每个主题添加任意数量的条目。在你创建的一些主题中添加一些条目,试试 new_entry 页面。

image 2023 12 05 11 53 14 966
Figure 2. Figure 19-2: The new_entry page

编辑实体条目

现在,我们将制作一个页面,以便用户编辑他们添加的条目。

edit_entry URL

页面的 URL 需要传递要编辑的条目的 ID。下面是 learning_logs/urls.py:

urls.py
--snip--
urlpatterns = [
    --snip--
    # Page for editing an entry.
    path('edit_entry/<int:entry_id>/', views.edit_entry, name='edit_entry'),
]

这种 URL 模式可与 http://localhost:8000/edit_entry/id/ 等 URL 匹配。这里 id 的值被分配给参数 entry_id。Django 会将符合此格式的请求发送到视图函数 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):
    """Edit an existing entry."""
    entry = Entry.objects.get(id=entry_id) # 1
    topic = entry.topic

    if request.method != 'POST':
        # Initial request; pre-fill form with the current entry.
        form = EntryForm(instance=entry) # 2
    else:
        # POST data submitted; process data.
        form = EntryForm(instance=entry, data=request.POST) # 3
        if form.is_valid():
            form.save() # 4
            return redirect('learning_logs:topic', topic_id=topic.id) # 5

    context = {'entry': entry, 'topic': topic, 'form': form}
    return render(request, 'learning_logs/edit_entry.xhtml',context)

我们首先导入条目模型。然后,我们获取用户想要编辑的条目对象❶ 以及与该条目相关联的主题。在针对 GET 请求运行的 if 代码块中,我们创建一个 EntryForm 实例,参数为 instance= entry ❷。这个参数告诉 Django 创建表单,并预填现有条目对象的信息。用户将看到他们已有的数据,并可以编辑这些数据。

处理 POST 请求时,我们同时传递 instance=entry 和 data=request.POST 参数❸。这些参数告诉 Django 根据与现有条目对象相关的信息创建表单实例,并使用 request.POST 中的相关数据进行更新。然后,我们检查表单是否有效;如果有效,我们就调用 save(),不需要任何参数,因为条目已经与正确的主题❹ 关联。然后我们重定向到主题页面,在那里用户应该能看到他们编辑的条目的更新版本❺。

如果我们显示的是编辑条目的初始表单,或者提交的表单无效,我们就会创建上下文字典,并使用 edit_entry.xhtml 模板渲染页面。

edit_entry 模板

接下来,我们创建一个 edit_entry.xhtml 模板,它与 new_entry.xhtml 类似:

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

action 参数会将表单发送回 edit_entry() 函数进行处理❶。我们在{% url %}标签中加入 entry.id 作为参数,这样视图函数就能修改正确的条目对象。我们将提交按钮标记为 "保存更改",以提醒用户他们是在保存编辑内容,而不是创建新条目❷。

链接到编辑条目页面

现在,我们需要为主题页面上的每个条目添加一个指向 edit_entry 页面的链接:

topic.xhtml
--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 的 URL,以及循环中当前条目的 ID 属性(entry.id)。编辑条目 "链接文本会出现在页面上的每个条目之后。图 19-3 显示了带有这些链接的主题页面。

image 2023 12 05 12 00 13 038
Figure 3. Figure 19-3: Each entry now has a link for editing that entry

学习日志现在已具备其所需的大部分功能。用户可以添加主题和条目,也可以阅读他们想要的任何条目集。在下一部分,我们将建立一个用户注册系统,这样任何人都可以在学习日志上注册一个账户,并创建自己的主题和条目集。

亲身体验

19-1. 博客:新建一个名为 Blog 的 Django 项目。创建一个名为 blogs 的应用程序,其中一个模型代表整个博客,另一个模型代表单个博文。为每个模型分配一组适当的字段。为项目创建一个超级用户,并使用管理站点创建一个博客和一些短文。创建一个主页,以适当的顺序显示所有文章。

创建用于创建博客、发表新文章和编辑现有文章的页面。使用你的页面,确保它们能正常工作。