Django 表单
在我们的网站上,我们想做的最后一件事情就是创建一个漂亮的方式来增加和编辑博客文章。 Django 的admin
是很酷,但是自定义和美化更困难了。 使用 forms
,我们可以拥有对界面绝对的权限 — 所有我们能想象到的事情几乎都可以做到!
Django 表单的一个好处就是我们既可以从零开始定义,也可以创建一个 ModelForm
,它将表单的结果保存到模型里。
这正是我们想做的:我们将为我们自己的 Post
模型创建一个表单。
就像 Django 所有重要的部分一样,表单有他们自己的文件 forms.py
。
我们需要在 blog
目录下创建一个文件。
blog
└── forms.py
OK ,让我们在文本编辑器打开它,然后键入以下代码:
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'text',)
首先我们需要导入 Django 表单(from django import forms
),然后导入 Post
模型(from .models import Post
)。
PostForm
, 正如你所猜想的,是我们表单的名字。 我们需要告诉Django,这个表单是一个 ModelForm
(所以 Django 将会为我们变一些魔法) – forms.ModelForm
将会负责这些。
接下来,我们有 class Meta
,在这里我们告诉 Django 哪个模型会被用来创建这个表单(model = Post
)。
最后,我们可以说一说在我们的表单里应该有哪些字段。 在此情景下,我们只想显示 title
和 text
— author
应该是当前登录的人(你!), created_date
应该是我们创建文章时自动设定的(或者说,在代码里设定的),对吗?
搞定了!现在我们需要做的就是在视图里使用表单,然后将它展现在模板里。
所以我们将再次创建一个指向页面的链接,一个 URL,一个视图和一个模板。
链接到一个包含表单的页面
现在应该在代码编辑器打开 blog/templates/blog/base.html
。我们将添加一个链接到名为 page-header
的div
标记:
<a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
请注意我们想要调用我们的新视图 post_new
。类"glyphicon glyphicon-plus"
由我们正在使用的 bootstrap 主题提供,并且它将会向我们展示一个加号。
添加这行代码之后,你的 html 文件现在看起来应该像这样:
{% load static %}
<html>
<head>
<title>Django Girls blog</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
<link href='//fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{% static 'css/blog.css' %}">
</head>
<body>
<div class="page-header">
<a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
<h1><a href="/">Django Girls Blog</a></h1>
</div>
<div class="content container">
<div class="row">
<div class="col-md-8">
{% block content %}
{% endblock %}
</div>
</div>
</div>
</body>
</html>
保存并刷新页面 http://127.0.0.1:8000 ,你可以看到一个熟悉的 NoReverseMatch
错误信息,是这个情况吧?很好!
URL
我们在代码编辑器打开 blog/urls.py
,然后添加一行新代码:
path('post/new/', views.post_new, name='post_new'),
最终代码看起来像这样:
from django.urls import path
from . import views
urlpatterns = [
path('', views.post_list, name='post_list'),
path('post/<int:pk>/', views.post_detail, name='post_detail'),
path('post/new/', views.post_new, name='post_new'),
]
刷新网页后,我们看到一个 AttributeError
,因为我们没有实现 post_new
视图。让我们现在把它加上吧。
post_new 视图
现在,在代码编辑器中打开 blog/views.py
文件,在已有的 from
行中添加下面的代码:
from .forms import PostForm
还有我们的 视图:
def post_new(request):
form = PostForm()
return render(request, 'blog/post_edit.html', {'form': form})
为了创建一个新的 Post
表单,我们需要调用 PostForm()
,然后把它传递给模板。我们会回到这个视图,但是现在,让我们为这个表单快速创建一个模板。
模板
我们需要在 blog/templates/blog
目录下创建一个文件 post_edit.html
,并在代码编辑器中打开。为了创建一个表单,我们需要做几件事情:
- 我们需要展示表单。我们可以使用(比如)
{{ form.as_p }}
来实现。 - 上面的这行需要被 HTML 表单标签包裹:
<form method="POST">...</form>
。 - 我们需要一个
Save
按钮。我们通过使用一个 HTML 按钮来完成:<button type="submit">Save</button>
。 - 最后, 在
<form ...>
标签后,我们需要加上{% csrf_token %}
。 这个非常重要,因为它会使你的表单是安全的! 当你忘记这一点时,Django 会在你试图保存表单时抱怨的:
OK, 让我们看看在 post_edit.html
里 HTML 应该看起来什么样:
{% extends 'blog/base.html' %}
{% block content %}
<h2>New post</h2>
<form method="POST" class="post-form">{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="save btn btn-default">Save</button>
</form>
{% endblock %}
现在刷新!哇!你的表单显示出来了!
但是,请等一下!当你键入诸如 title
和 text
字段,然后尝试保存它 — 会发生什么?
什么都没有!我们再一次回到了同一个页面,然而我们的文本已经消失了... 同时新的文章并没有被添加。所以哪里出错了呢?
答案是:没有错误。我们需要在视图里多做一点点工作。
保存表单
再一次打开 blog/views.py
。现在, post_new
视图中的所有内容如下:
def post_new(request):
form = PostForm()
return render(request, 'blog/post_edit.html', {'form': form})
每次我们提交表单后,都会回到相同的视图,但是这个时候在request
有更多的数据 ,具体是在 request.POST
(命名和博客的 "post" 无关;它只与 "上传" 数据这件事有关)。 还记得在 HTML 文件里,我们的 <form>
定义有一个变量 method="POST"
吗? 所有在表单里定义的字段现在都在 request.POST
。你不应该重命名 POST
为其他任何东西(其他唯一有效的 method
值是 GET
,但是我们没有时间去解释它们两者的区别是什么)。
所以在我们的视图里,我们有了两种不同的情况去处理: 首先:当首次访问一个页面,我们想要得到一个空白的页面;第二,当回到视图,我们要有刚刚键入的所有表单数据。 所以我们需要添加一个条件判断(我们为此使用 if
)。
if request.method == "POST":
[...]
else:
form = PostForm()
现在去填写 [...]
。如果 method
是 POST,那么我们要用表单里的数据构建 PostForm
,对吗?我们会这样实现它:
form = PostForm(request.POST)
下一件事情就是去检查表单是否正确(所有必填字段都要被赋值,并且没有不正确的值被提交)。我们将使用 form.is_valid()
来实现。
我们检查表单是否有效,如果是我们就保存它!
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.published_date = timezone.now()
post.save()
大体上,我们这里做了两件事:使用 form.save
保存表单,添加一个作者(因为 PostForm
中没有 author
字段,然而这个字段是必须的!)。 commit=False
意味着我们还不想保存 Post
模型 — 我们想首先添加作者。 大多数情况下,当你使用 form.save()
时,不会设置 commit=False
,但是在此处下,我们需要这样提供该参数。 post.save()
会保留更改(添加作者),并创建一个新的博客文章!
最后,如果我们能够立即去新创建的博客的 post_detail
页面,那就太棒了,对吗?为了做到这点,我们需要再次导入:
from django.shortcuts import redirect
把它添加到你文件的最开始处。现在我们可以说:去 post_detail
页面看看新创建的博客:
return redirect('post_detail', pk=post.pk)
post_detail
是我们想去的视图的名字。 还记得这个视图需要有一个 pk
变量吗?为了把它传递给视图我们使用 pk=post.pk
, 其中 post
就是我们刚刚创建的博客!
好吧,我们已经说了很多了,但可能我们想看到整个视图现在看起来什么样,对吗?
def post_new(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.published_date = timezone.now()
post.save()
return redirect('post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_edit.html', {'form': form})
让我们看看它是否正常工作。 转到页127.0.0.1:8000/post/new/ ,添加 title
和 text
,将它保存... 看! 新博客文章已经加进来了,并且我们被重定向到 post_detail
页面!
你可能已经注意到在保存博客文章之前我们设置发布日期。稍后,我们将引入在 Django Girls 教程:扩展中出现过的 publish button 。
太棒了!
由于我们最近访问过 Django admin 界面, 系统现在认为我们还在登录状态。有一些情况会导致我们被退出登录 (浏览器关闭,重启数据库,等等)。如果在新建文章的时候得到错误,指向缺少登录状态的用户,去 admin 页面 127.0.0.1:8000/admin 重新登录。这会暂时解决问题。有一个永久解决方案在Homework: add security to your website! 章节等着你,就在主教程之后。
表单验证
现在,我们将向你展现 Django 表单是多么酷。 一篇博客文章需要有 title
和 text
字段。 在我们的 Post
模型中我们并没有说(和发布日期
恰恰相反)这些字段不是必须的,所以 Django,默认情况下,期望他们会被设置。
尝试不带 title
和 text
内容保存表单。猜猜会发生什么!
Django 会确保验证我们表单里的所有字段都是正确的。这不是很棒?
编辑表单
现在我们知道如何添加一个新的表单。 但是如果我们想编辑一个现有的呢? 这和我们刚才做的非常相似。 让我们快速创建一些重要的东西(如果你有不理解的地方,你应该问问你的教练或者看看前面的章节,因为我们已经覆盖了所有的这些步骤)。
在代码编辑器打开 blog/templates/blog/post_detail.html
并添加下面的代码:
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}"><span class="glyphicon glyphicon-pencil"></span></a>
所以模板看起来像这样:
{% extends 'blog/base.html' %}
{% block content %}
<div class="post">
{% if post.published_date %}
<div class="date">
{{ post.published_date }}
</div>
{% endif %}
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}"><span class="glyphicon glyphicon-pencil"></span></a>
<h2>{{ post.title }}</h2>
<p>{{ post.text|linebreaksbr }}</p>
</div>
{% endblock %}
在编辑器中打开 blog/urls.py
,并添加如下代码:
path('post/<int:pk>/edit/', views.post_edit, name='post_edit'),
我们将复用模板 blog/templates/blog/post_edit.html
,所以最后还缺少 view。
让我们在编辑器中打开 blog/views.py
,并在文件的最后加入:
def post_edit(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == "POST":
form = PostForm(request.POST, instance=post)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.published_date = timezone.now()
post.save()
return redirect('post_detail', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_edit.html', {'form': form})
这看起来几乎完全和我们的 post_new
视图一样,对吗? 但是不完全是。 一方面:我们从 urls
里传递了一个额外的 pk
参数;接着,我们用 get_object_or_404(Post, pk=pk)
得到了想要编辑的 Post
模型;然后当我们创建了一个表单,我们将这篇文章作为一个 instance
来传入表单,保存表单的时候也是一样的……
form = PostForm(request.POST, instance=post)
…还有当我们用这个 post 初始化一个表单进行编辑时:
form = PostForm(instance=post)
好,让我们来试试它是否可以工作!让我们先去 post_detail
页面。在右上角应该有一个编辑按钮:
当你点击它的时候,你会看到包含我们博客文章的表单。
随意修改标题和内容,然后保存更改!
祝贺你!你的应用程序正在变得越来越完整!
如果你需要更多关于 Django 表单的信息,你应该阅读文档:
docs.djangoproject.com/en/2.2/topi...
安全
能够通过点击一条链接创建新的文章确实不错。 但是现在,任何访问你网站的人都能够发布一条新博客日志,这可能不是你想要的。让我们来做些工作,让这个发布按钮只显示给你,而对其他任何人都不显示。
在代码编辑器中打开 blog/templates/blog/base.html
,找到 page-header
div
和你早些时候放在那里的锚点标记。看起来应该像这样:
<a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
我们将添加一个 {% if %}
标记到这里,使得链接只展示给登录到 admin 的用户。现在,将只展示给你!修改 <a>
标记如下:
{% if user.is_authenticated %}
<a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
{% endif %}
只有请求网页的用户处于登录状态,{% if %}
标记才会使链接发送到服务器。这不能完全保护新创建的文章,但是它是一个好的开端。在拓展的课程中,我们会覆盖到更多关于安全性的内容。
记得我们刚刚添加到详情页面的编辑标志吗?我们也想在那里添加同样的修改,使得其他人无法编辑已有的文章。
在编辑器中打开 blog/templates/blog/post_detail.html
并找到下面一行:
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}"><span class="glyphicon glyphicon-pencil"></span></a>
修改为这样:
{% if user.is_authenticated %}
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}"><span class="glyphicon glyphicon-pencil"></span></a>
{% endif %}
由于你现在可能正处于登录状态,刷新页面之后,并不能看到任何的不同。在一个不同的浏览器或者无痕式窗口 (在Windows Edge 称为“无痕”),你将看到链接不再出现,图标也消失了!
还有一件事:部署时间!
让我们看看所有的工作在 PythonAnyWhere 是否正常运行。到了又一次部署的时间!
- 首先,提交新的代码,推送到 GitHub:
$ git status
$ git add --all .
$ git status
$ git commit -m "Added views to create/edit blog post inside the site."
$ git push
$ cd ~/<your-pythonanywhere-domain>.pythonanywhere.com
$ git pull
[...]
(记得用你实际的 PythonAnywhere 域名替换 <your-pythonanywhere-domain>
,去掉尖括号)
- 最后,快速跳转到 "Web" page(“ Web ” 页) (从控制台的右上角的菜单按钮) 然后点击 Reload(重新加载)。 刷新https://subdomain.pythonanywhere.com 博客,查看变化。
这样就可以了!祝贺你!:blush:
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。