Django表单
HTML表单是交互式网站的支柱,从Google的单一搜索框的简单性到无处不在的博客评论提交表单,到复杂的自定义数据输入界面。 本章将介绍如何使用Django来访问用户提交的表单数据,并对其进行验证和处理。 一路走来,我们将介绍HttpRequest和Form对象。
从请求对象获取数据
我在第二章介绍了HttpRequest对象,当时我们首先介绍了视图函数,但是当时我没有太多的话要说。 回想一下,在我们的hello()视图中,每个视图函数都将HttpRequest对象作为其第一个参数:
from django.http import HttpResponse
def hello(request):
return HttpResponse("Hello world")
HttpRequest对象,比如这里的变量请求,有一些你应该熟悉的有趣的属性和方法,以便你知道什么是可能的。 在执行view函数的时候,你可以使用这些属性来获取关于当前请求的信息(即,在Django支持的网站上加载当前页面的用户/ Web浏览器)。
关于URL的信息
HttpRequest对象包含有关当前请求的URL的几条信息(表6-1)。
表6-1:HttpRequest方法和属性![](https://img.haomeiwen.com/i1621231/248d0fb58f361136.png)
始终使用这些属性/方法,而不是在视图中对网址进行硬编码。 这使得更灵活的代码可以在其他地方重复使用。 一个简单的例子:
# BAD!
def current_url_view_bad(request):
return HttpResponse("Welcome to the page at /current/")
# GOOD
def current_url_view_good(request):
return HttpResponse("Welcome to the page at %s"
% request.path)
有关请求的其他信息
request.META是一个Python字典,包含给定请求的所有可用的HTTP包头,包括用户的IP地址和用户代理(通常是Web浏览器的名称和版本)。 请注意,可用包头的完整列表取决于用户发送了哪些包头以及您的Web服务器设置了哪些包头。 本词典中的一些常用键是:
- HTTP_REFERER - 引用的URL,如果有的话。 (注意REFERER的拼写错误。)
- HTTP_USER_AGENT - 用户浏览器的用户代理字符串(如果有)。 这看起来像是:“Mozilla / 5.0(X11; U; Linux`` i686; fr-FR; rv:1.8.1.17)Gecko / 20080829 Firefox / 2.0.0.17”。
- REMOTE_ADDR - 客户端的IP地址,例如“12.345.67.89”。 (如果请求已经通过任何代理,那么这可能是以逗号分隔的IP地址列表,例如“12.345.67.89,23.456.78.90”)。
请注意,因为request.META只是一个基本的Python字典,所以如果您尝试访问不存在的密钥,将会得到一个KeyError异常。 (因为HTTP包头是外部数据 - 也就是说,它们是由用户的浏览器提交的 - 它们不应该被信任,并且如果特定标头为空或者不存在,您应该始终将应用程序设计为优雅地失败。 )您应该使用try / except子句或get()方法来处理未定义键的情况:
# BAD!
def ua_display_bad(request):
ua = request.META['HTTP_USER_AGENT'] # Might raise KeyError!
return HttpResponse("Your browser is %s" % ua)
# GOOD (VERSION 1)
def ua_display_good1(request):
try:
ua = request.META['HTTP_USER_AGENT']
except KeyError:
ua = 'unknown'
return HttpResponse("Your browser is %s" % ua)
# GOOD (VERSION 2)
def ua_display_good2(request):
ua = request.META.get('HTTP_USER_AGENT', 'unknown')
return HttpResponse("Your browser is %s" % ua)
我鼓励你写一个小视图,显示所有的请求。META数据,所以你可以知道里面有什么。 以下是这个观点:
def display_meta(request):
values = request.META
html = []
for k in sorted(values):
html.append('<tr><td>%s</td><td>%s</td></tr>' % (k, values[k]))
return HttpResponse('<table>%s</table>' % '\n'.join(html))
查看请求对象包含哪些信息的另一个好方法是在系统崩溃时仔细查看Django错误页面 - 这里有大量有用的信息,包括所有HTTP头和其他请求对象(请求 例如.path)。
关于提交数据的信息
除了关于请求的基本元数据外,HttpRequest对象还有两个属性,包含用户提交的信息:request.GET和request.POST。 这两个都是类似字典的对象,可以让您访问GET和POST数据。 POST数据通常是从HTML <form>提交的,而GET数据可以来自<form>或页面URL中的查询字符串。
字典式的对象
当我说request.GET和request.POST是“类似字典”的对象时,我的意思是它们的行为与标准的Python字典类似,但是在技术上并不是字面的。 例如,request.GET和request.POST都有get(),keys()和values()方法,并且可以通过在request.GET中执行键遍历键。
那为什么这个区别呢?
因为request.GET和request.POST都有正常字典没有的附加方法。 我们会在短时间内进入这些。 您可能遇到类似于“类文件对象”的术语 - 具有几个基本方法的Python对象,比如read(),它们可以作为“真实”文件对象的替身。
一个简单的Django表单处理示例
继续正在进行的书籍,作者和出版商的例子,让我们创建一个简单的视图,让用户按标题搜索我们的图书数据库。 通常,开发表单有两部分:处理提交数据的HTML用户界面和后端视图代码。 第一部分很简单, 让我们建立一个显示搜索表单的视图。 当您使用startapp创建书籍应用程序时,Django会在\ books文件夹中为您创建一个新的views.py文件。 继续,并添加一个新的视图到这个文件:
# \books\views.py
from django.shortcuts import render
def search_form(request):
return render(request, 'books/search_form.html')
下一步是创建模板,但是我们首先需要为模板创建一些新的文件夹。 如果你没有改变它,你的设置文件中的“APP_DIRS”被设置为True。 这意味着Django将搜索您所有的应用程序以查找名为\ templates的文件夹。
在书籍应用程序文件夹中创建一个新的\ templates文件夹。 然后继续在新的\ templates文件夹中创建另一个文件夹并将其称为书籍。 您的最终文件夹结构将为books \ templates \ books \。
这个内部书籍文件夹对命名空间模板很重要。 由于Django将搜索所有应用程序的匹配模板,为应用程序模板创建一个名称空间可确保Django使用正确的模板(如果两个应用程序使用相同的模板名称)。
创建以下search_form.html文件并将其保存到新文件夹中:
# mysite_project\mysite\books\templates\books\search_form.html
<html>
<head>
<title>Search</title>
</head>
<body>
<form action="/search/" method="get">
<input type="text" name="q">
<input type="submit" value="Search">
</form>
</body>
</html>
现在我们需要创建一个URLconf,以便Django可以找到我们的新视图。 在您的books文件夹中,创建一个新的urls.py文件(startapp不会创建该文件)。 将以下网址格式添加到这个新的urls.py中:
# mysite\books\urls.py
from django.conf.urls import url
from books import views
urlpatterns = [
url(r'^search-form/$', views.search_form),
]
(请注意,我们直接导入views模块,而不是从books.views导入search_form类似的东西,因为前者不那么冗长,我们将在第7章详细介绍这个导入方法。
最后一件事 - 当Django搜索URL模式时,除非我们明确地包含来自其他应用程序的URL模式,否则它只会搜索基本的mysite \ urls.py文件。 所以,让我们继续并修改我们的网站urlpatterns:
# mysite\urls.py
from django.conf.urls import include, url
urlpatterns = [
# ...
url(r'^', include('books.urls')),
]
这个新的URL模式必须被添加到urlpatterns列表的末尾。 这是因为r'^'正则表达式将所有内容发送到books.urls,因此我们要确保其他模式都不匹配,然后再发送Django来检查books \ urls.py中是否有匹配的模式。
现在,如果您运行开发服务器并访问http://127.0.0.1:8000/search-form/,您将看到搜索界面(图6.1)。 很简单。
![](https://img.haomeiwen.com/i1621231/25810c3e9bc5fe3f.png)
# books/urls.py
urlpatterns = [
url(r'^search-form/$', views.search_form),
url(r'^search/$', views.search),
]
# books/views.py
from django.http import HttpResponse
# ...
def search(request):
if 'q' in request.GET:
message = 'You searched for: %r' % request.GET['q']
else:
message = 'You submitted an empty form.'
return HttpResponse(message)
目前,这只是显示用户的搜索词,所以我们可以确保数据正确提交给Django,这样你就可以感觉到搜索词是如何流经系统的(图6-2)。
![](https://img.haomeiwen.com/i1621231/49fdcb74357aa821.png)
简而言之:
- HTML <form>定义了一个变量q。 提交时,q的值通过GET(method =“get”)发送到URL / search /。
- 处理URL / search /(search())的Django视图可以访问request.GET中的q值。
这里指出的一个重要的事情是,我们明确地检查request.GET中是否存在“q”。 正如我在上面的META部分中所指出的那样,您不应该相信用户提交的任何内容,甚至不会认为他们已经提交了任何东西。 如果我们没有添加这个检查,任何空表单的提交都会在视图中引发KeyError:
# BAD!
def bad_search(request):
# The following line will raise KeyError if 'q' hasn't been submitted!
message = 'You searched for: %r' % request.GET['q']
return HttpResponse(message)
查询字符串参数
因为GET数据在查询字符串中传递(例如,/ search /?q = django),所以可以使用request.GET来访问查询字符串变量。在第2章介绍Django的URLconf系统时,我将Django的漂亮的URL与更传统的PHP / Java URL(比如/ time / plus?hours = 3)进行了比较,并在第6章介绍了如何做到这一点。
现在你知道如何在视图中访问查询字符串参数(例如在这个例子中hours = 3) - 使用request.GET。 POST数据与GET数据的工作方式相同 - 只需使用request.POST而不是request.GET。 GET和POST有什么区别?
当提交表单的行为只是一个“获取”数据的请求时使用GET。每当提交表单的行为都会产生一些副作用 - 更改数据,发送电子邮件或其他不简单显示数据的事情时,请使用POST。在我们的图书搜索示例中,我们使用GET,因为查询不会更改我们服务器上的任何数据。 (如果您想了解有关GET和POST的更多信息,请参阅w3.org网站。)
现在我们已经验证了request.GET正在被正确传递,让我们把用户的搜索查询连接到我们的书籍数据库(再次,在views.py中):
from django.http import HttpResponse
from django.shortcuts import render
from books.models import Book
def search(request):
if 'q' in request.GET and request.GET['q']:
q = request.GET['q']
books = Book.objects.filter(title__icontains=q)
return render(request, 'books/search_results.html',
{'books': books, 'query': q})
else:
return HttpResponse('Please submit a search term.')
关于我们在这里所做的一些说明:
-
除了检查request.GET中是否存在“q”,我们还要确保request.GET ['q']在传递给数据库查询之前是非空值。
-
我们使用Book.objects.filter(title__icontains = q)来查询我们的图书表,查找所有包含给定提交的书籍。 icontains是一种查找类型(如第4章和附录B中所述),该语句可以大致翻译为“获取标题包含q的书籍,而不区分大小写”。
这是一个非常简单的书籍搜索方式。 我不建议在大型生产数据库上使用简单的图标查询,因为它可能会很慢。 (在现实世界中,你想要使用某种自定义的搜索系统,在网络上搜索开源全文搜索,以了解可能性。)
我们将书籍(Book对象列表)传递给模板。 要使我们的新搜索表单正常工作,我们来创建search_results.html文件:
# \books\templates\books\search_results.html
<html>
<head>
<title>Book Search</title>
</head>
<body>
<p>You searched for: <strong>{{ query }}</strong></p>
{% if books %}
<p>Found {{ books|length }} book{{ books|pluralize }}.</p>
<ul>
{% for book in books %}
<li>{{ book.title }}</li>
{% endfor %}
</ul>
{% else %}
<p>No books matched your search criteria.</p>
{% endif %}
</body>
</html>
请注意复数模板过滤器的使用情况,根据找到的书籍数量输出适当的“s”。
现在,当您运行开发服务器并访问http://127.0.0.1:8000/search-form/时,您的搜索词应该返回更有用的结果(图6-3)。
![](https://img.haomeiwen.com/i1621231/376f851a97c35dc4.png)
改进我们简单的形式处理示例
正如前面的章节,我已经向您展示了可能可行的最简单的事情。 现在我将指出一些问题,并告诉你如何改进它。 首先,我们的search()视图对一个空查询的处理很差 - 我们只是显示一个“请提交一个搜索词”消息,要求用户点击浏览器的后退按钮。 这是可怕的和不专业的,如果你真的在野外实现这样的东西,你的Django权限将被撤销。
重新显示表单要好得多,并且上面有一个错误,以便用户可以立即再次尝试。 最简单的方法是再次渲染模板,如下所示:
from django.shortcuts import render
from django.http import HttpResponse
from books.models import Book
def search_form(request):
return render(request, 'books/search_form.html')
def search(request):
error = False
if 'q' in request.GET:
q = request.GET['q']
if not q:
error = True
else:
books = Book.objects.filter(title__icontains=q)
return render(request, 'books/search_results.html', {'books': books, 'query': q})
return render(request, 'books/search_form.html', {'error': error})
(请注意,我在这里包含了search_form(),所以你可以在一个地方看到两个视图。)在这里,我们改进了search()来再次渲染search_form.html模板,如果查询是空的。 而且因为我们需要在该模板中显示错误消息,所以我们传递一个模板变量。 现在我们可以编辑search_form.html来检查错误变量:
<html>
<head>
<title>Search</title>
</head>
<body>
{% if error %}
<p style="color: red;">Please submit a search term.</p>
{% endif %}
<form action="/search/" method="get">
<input type="text" name="q">
<input type="submit" value="Search">
</form>
</body>
</html>
我们仍然可以在原始视图search_form()中使用这个模板,因为search_form()不会将错误传递给模板 - 所以在这种情况下错误信息不会显示出来(图6-4)。
![](https://img.haomeiwen.com/i1621231/aba2677c57b84f79.png)
有了这个改变,这是一个更好的应用程序,但它现在提出了一个问题:是否有必要专门的search_form()视图? 就目前而言,对URL / search /(不带任何GET参数)的请求将显示空的表单(但有错误)。 我们可以删除search_form()视图及其相关的URLpattern,只要我们改变search()来隐藏错误消息,当有人访问/search/没有GET参数时:
def search(request):
error = False
if 'q' in request.GET:
q = request.GET['q']
if not q:
error = True
else:
books = Book.objects.filter(title__icontains=q)
return render(request, 'books/search_results.html', {'books': books, 'query': q})
return render(request, 'books/search_form.html', {'error': error})
在这个更新后的视图中,如果用户访问/搜索/没有GET参数,他们将看到没有错误信息的搜索表单。 如果用户提交的表单中包含'q'的空值,他们将会看到带有错误信息的搜索表单。 最后,如果用户提交的表单中包含“q”的非空值,他们将看到搜索结果。
我们可以对此应用程序进行最后的改进,以消除一些冗余。 现在我们已经将两个视图和URL合并为一个,而search / handle同时处理搜索表单显示和结果显示,search_form.html中的HTML <form>不需要对URL进行硬编码。 而不是这样:
<form action="/search/" method="get">
可以更改为:
<form action="" method="get">
action =""表示“将表单提交到当前页面所在的URL”。通过这种更改,如果您将search()视图挂接到另一个URL,则不必记住更改操作。