本章将学习如何通过web表单获取数据
Django提供了一些优秀的表单处理功能,使得从用户那里收集信息并通过模型保存到数据库成为一个非常简单的过程。Django表单处理功能允许你:
- 显示带有自动生成的表单小部件(如文本字段或日期选择器)的HTML表单;
- 根据一组验证规则检查提交的数据;
- 在验证错误时重新显示表单;
- 将提交的表单数据转换为相关的Python数据类型。
使用Django表单功能的一个主要优点是,它可以为您节省大量创建HTML表单的时间和麻烦。
1. 基本流程
处理 表单 & 用户输入 的基本步骤:
(1) 如果你还没有,在rango中创建一个forms.py模块来存储与表单相关的类
(2) 为希望表示为表单的每个模型创建ModelForm类
(3) 根据您的需要定制表单
(4) 创建/更新一个view来处理表单
• 显示表单,
• 保存表单数据
• 标记当用户在表单中输入不正确的数据(或根本没有数据)时可能发生的错误
(5) 创建/更新template以显示表单
(6) 添加一个urlpattern以映射到新view(如果您创建了一个新视图)
2. Page & Category Forms
这里,我们将实现允许用户通过form向DB添加page & category
首先,在rango目录中创建一个 form.py(也可以将form放在model.py中,但是统一放在form.py中让代码库更加整洁、容易使用)
(1) 创建ModelForm
类
在rango/form.py中,我们将创建几个继承自Django ModelForm的类。ModelForm是个helper class,允许从现有的模型中创建Django表单。我们已经为Category & Page定义了两个模型,所以为这两个模型都创建ModelForms。
在 rango/forms.py 中添加以下代码
from django import forms
from rango.models import Page, Category
class CategoryForm(forms.ModelForm):
name = forms.CharField(max_length=128, help_text="Please enter the category name.")
views = forms.IntegerField(widget=forms.HiddenInput(), initial=0) likes = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
slug = forms.CharField(widget=forms.HiddenInput(), required=False)
# An inline class to provide additional information on the form.
class Meta:
# 提供ModelForm和model之间的关联
model = Category
fields = ('name',)
class PageForm(forms.ModelForm):
title = forms.CharField(max_length=128, help_text="Please enter the title of the page.")
url = forms.URLField(max_length=200, help_text="Please enter the URL of the page.")
views = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
class Meta:
# Provide an association between the ModelForm and a model
model = Page
# 通过这种方式,我们不需要显示模型中的每个字段。某些字段允许NULL值;我们可能不想包括他们。
#这里,我们隐藏了外键;可以从表单中排除类别字段,
exclude = ('category',)
#或指定要包含的字段(不要包含类别字段)
# fields = ('title', 'url', 'views')
通过fields
指定表单中包含哪些字段,或者通过exclude
指定要排除哪些字段。
Django为我们提供了几种方法来定制为我们创建的表单。
在上面的代码示例中,我们指定了希望为每个要显示的字段使用的小部件。
• CharField
用于标题字段和表单, 为用户提供文本输入;
• URLField
为url字段, 为用户提供文本输入;
• EmailField
用于电子邮件地址条目;
• ChoiceField
用于单选输入按钮;
• DateField
用于日期/时间条目;
字段提供的max_length
参数—我们指定的长度与我们在底层数据模型中指定的每个字段的最大长度相同。
我们在每个表单中为views和likes字段包含了几个IntegerField
条目。
参数设置widget=forms.HiddenInput()
将小部件设置为隐藏,然后将初始值设置为0。这是将字段默认设置为零的一种方法。由于字段将被隐藏,用户将无法为这些字段输入值。
然而,即使PageForm
中有一个隐藏字段,我们仍然需要在表单中包含该字段。如果在字段中我们排除了views,那么表单将不包含该字段(尽管它被指定了)。这意味着表单不会返回该字段的值0。这可能会根据模型的设置方式产生错误。如果在模型中我们指定这些字段的默认值为0,那么我们可以依赖模型自动地用默认值填充字段——从而避免非空错误。在这种情况下,没有必要使用这些隐藏字段。
但是,我们没有指定初始值或默认值,而是说表单不需要该字段。这是因为我们的模型将负责在表单最终保存时填充字段。基本上,在定义模型和表单时需要小心,以确保表单将包含并传递正确填充模型所需的所有数据。
从ModelForm
继承的类最重要的方面可能是需要定义要为哪个模型提供表单。我们通过嵌套的class meta
来处理这个问题。将嵌套Class Meta
的model
属性设置为您希望使用的模型,fields
元组指定表单中希望包含哪些字段。使用字段名的元组指定要包含的字段。
(2) 创建 Add Category View
创建一个新view来显示表单并处理表单数据的发布。将以下代码添加到rango/views.py中
from rango.forms import CategoryForm
from django.shortcuts import redirect
def add_category(request):
form = CategoryForm()
# A HTTP POST?
if request.method == 'POST':
form = CategoryForm(request.POST)
# 验证form是否有效
if form.is_valid():
form.save(commit=True) # 将新的category存储在DB中
return redirect('/rango/') # 将用户重定向到index view
else:
print(form.errors) # 将form中的错误显示在终端
# Render the form with error messages (if any)
return render(request, 'rango/add_category.html', {'form': form})
新的add_category()
添加了几个处理表单的关键功能。首先,我们创建一个CategoryForm()
,然后检查HTTP请求是否为POST(用户是否通过表单提交数据?)然后,我们可以通过相同的URL处理POST请求。
add_category()
可以处理三种不同的场景:
•显示一个新的空白表单添加一个类别;
•保存用户提供的表单数据到关联模型,并重定向到Rango主页;
•如果有错误,用错误信息重新显示表单
Django的表单处理机制处理用户浏览器通过HTTP POST请求返回的数据。它不仅处理将表单数据保存到所选的模型中,而且还会为每个表单字段(如果需要的话)自动生成任何错误消息。这意味着Django不会存储任何缺少信息的提交表单,因为这些信息可能会对数据库的参考完整性造成问题。例如,在类别名称字段中不提供值将返回错误,因为该字段不能为空。
在render()调用中,引用了一个新的模板add_category.html(我们在下面定义它)这将包含相关的Django模板代码以及表单和页面的HTML。
(3) 创建Add Category Template
创建文件templates/rango/add_category.html,添加以下HTML标记和Django模板代码
<!DOCTYPE html>
<html>
<head>
<title>Rango</title>
</head>
<body>
<h1>Add a Category</h1>
<div>
<form id="category_form" method="post" action="/rango/add_category/">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% for field in form.visible_fields %}
{{ field.errors }}
{{ field.help_text }}
{{ field }}
{% endfor %}
<input type="submit" name="submit" value="Create Category"/>
</form>
</div>
</body>
</html>
HTTP是一种无状态协议,因此需要隐藏和可见表单字段。你不能在不同的HTTP请求之间保持状态,这可能使web应用程序的某些部分难以实现。为了克服这个限制,创建了隐藏的HTML表单字段,它允许web应用程序通过HTML表单将重要的信息传递给客户端(在呈现的页面上看不到),只有当用户提交表单时才会发送回原始服务器。
visible fields可见字段(将显示给用户的那些字段)由ModelForm Meta Class
中的fields
属性控制。这些模板循环为每个表单元素生成必要的HTML标记。对于可见表单字段,我们还添加了特定字段可能出现的任何错误和帮助文本,帮助文本可用于向用户解释他或她需要输入什么。
{% csrf_token %} – 跨站点请求伪造令牌 (Cross Site Request Forgery Tokens),它有助于保护和保护在随后提交表单时发起的HTTP POST请求。Django框架要求CSRF令牌出现。如果您忘记在表单中包含CSRF令牌,那么用户在提交表单时可能会遇到错误。
(4) 映射Add Category View
现在我们需要将add_category()
视图映射到URL。在模板中,我们在表单的action
属性中使用了URL /rango/add_category/。我们现在需要创建一个从URL到view的映射。在rango/urls.py中修改urls模式:
urlpatterns = [
path('', views.index, name='index'),
path('about/', views.about, name='about'),
path('category/<slug:category_name_slug>/', views.show_category, name='show_category'),
path('add_category/', views.add_category, name='add_category'),
]
(5) 修改Index Page View
作为最后一步,我们可以在index页面上放一个链接,这样用户就可以轻松地导航到允许他们添加类别的页面。编辑模板rango/index.html,并在
<a href="/rango/add_category/">Add a New Category</a><br />
(6) 清除 Forms
我们的Page模型有一个url属性设置为URLField类型的实例。在相应的HTML表单中,Django会合理地期望任何输入到url字段的文本都是格式正确、完整的url。然而,用户可能会发现输入http://www.url.com之类的东西很麻烦——实际上,用户甚至可能不知道正确的URL是指哪些东西构成的。
大多数现代浏览器现在都会检查URL是否格式良好,因此这个示例只适用于较老的浏览器。但是,它向您展示了如何在试图将数据保存到数据库之前清理数据。如果您没有旧的浏览器来尝试这个示例,请尝试将URLField更改为CharField。然后,呈现的HTML将不会指示浏览器代表您执行检查,而您实现的代码将被执行。
在用户输入可能不完全正确的情况下,我们可以重写在ModelForm中实现的clean()方法。该方法在将表单数据保存到新模型实例之前被调用,从而为我们提供了一个插入代码的逻辑位置,该代码可以验证甚至修复用户输入的任何表单数据。我们可以检查用户输入的url字段的值是否以http://开头,如果不是,我们可以将http://放在用户输入的前面。
class PageForm(forms.ModelForm):
...
def clean(self):
cleaned_data = self.cleaned_data
url = cleaned_data.get('url')
# If url is not empty and doesn't start with 'http://',then prepend 'http://'.
if url and not url.startswith('http://'):
url = f'http://{url}'
cleaned_data['url'] = url
return cleaned_data
在clean()方法中,可以观察到一个简单的模式,你可以在自己的Django表单处理代码中复制这个模式。
a. 表单数据从ModelForm字典属性cleaned_data获得。
b. 您希望检查的表单字段可以从cleaned_data字典中获取。使用dictionary对象提供的.get()方法获取表单的值。如果用户没有在表单字段中输入值,则其条目将不存在于cleaned_data字典中。在这种情况下,.get()将返回None而不是引发KeyError异常。这有助于你的代码看起来更干净一点!
c.对于要处理的每个表单字段,请检查是否检索了值。如果输入了某些内容,请检查其值。如果这不是您所期望的,那么您可以添加一些逻辑来修复这个问题,然后在cleaned_data字典中重新分配值。
d. 必须始终通过返回对cleaned_data字典的引用来结束clean()方法。否则,更改将不会被应用。