使用高级特性来优化你的博客
在上一章中,你创建了一个基础的博客应用。现在你将要改造它成为一个功能更加齐全的博客,利用一些高级的特性例如通过email来分享帖子,添加评论,给帖子打上tag,检索出相似的帖子。在本章中,你将会学习以下几点:
- 使用Django发送email
- 在views中创建并操作表单
- 通过models创建表单
- 构建复杂的QuerySets
通过email分享帖子
首先,我们会允许用户通过发送邮件来分享他们的帖子。首先让我们花费一小会时间来想下你该如何使用views,urls和templates来创建这个功能根据你在上一章中学到的知识。现在,核对一下你需要哪几点才能允许你的用户通过邮箱来发送帖子。你需要做到以下几点:
- 创建一个表单给用户来填写他们的姓名,email,收件方以及评论,评论不是必选的。
- 在views.py文件中创建一个view来操作发布的数据和发送email
- 在博客应用的urls.py中为新的view添加一个URL pattern
- 创建一个模板来展示这个表单
使用Django创建表单
让我们开始创建一个用来分享帖子的表单。Django有一个内置的表单框架允许你通过简单的方式来创建表单。这个表单框架允许你定义你的表单字段,指定他们必须展示的方式,以及指定他们如何验证输入的数据。Django表单框架还提供一个灵活的方式来渲染表单以及操作数据。
Django应用了两个基础类来创建表单:
- Form: 允许你创建一个标准表单
- ModelForm: 允许你创建一个表单可用于创建或者更新model的实例
首先,创建一个forms.py文件在你博客应用的目录下,输入以下代码:
from django import forms
class EmailPostForm(forms.Form):
name = forms.CharField(max_length=25)
to = forms.EmailField()
comments = forms.CharField(required=False, widget=forms.Textarea)
这是你的第一个Django表单。看下代码:我们已经创建了一个继承基础Form类的表单。我们使用不同的字段类型以使Django来验证字段。
表单可以存在你的Django项目的任何地方,但按照惯例将它们放在每一个应用下面的forms.py文件中
name字段是一个CharField
。这种类型的字段等同于<input type=“text”>
HTML元素。每个字段都有默认的控件来确定它在HTML中的展示。通过改变控件的属性可以重写默认的控件。在comment字段中,我们使用Textarea控件来使它展示成一个<textarea></textarea>
HTML元素来代替默认的<input>
元素。
字段的验证也依赖于字段的类型。举个例子,email和to字段是EmailField
,它们需要一个有效的地址,否则字段验证不通过将会返回forms.ValidationError
异常导致表单提交失败。其他的参数将进入表单验证:我们指定name字段最多只能输入25个字符,通过设置required=False
表明comments字段不是必填项。目前我们在表单中使用的这些字段类型只是Django支持的表单字段的一部分。要查看更多可利用的表单字段,你可以访问:https://docs.djangoproject.com/en/1.8/ref/forms/fields/
现在,你需要学习如何通过Django来发送email。
使用Django发送email
通过Django发送email非常简单。首先,你需要有一个本地的SMTP服务或者获取到一个外部SMTP服务的配置,接下来在你的项目中的setting.py文件中设置如下内容:
EMAIL_HOST: SMTP服务地址。默认本地。
EMAIL_POSR: SMATP服务端口,默认25。
EMAIL_HOST_USER: SMTP服务的用户名。
EMAIL_HOST_PASSWORD: SMTP服务的密码。
EMAIL_USE_TLS: 是否使用TLS加密协议。
EMAIL_USE_SSL: 是否使用SSL加密协议。
如果你没有本地SMTP服务,你可以使用你的email提供的SMTP服务。下面提供了一个简单的例子展示如何通过使用Google账户的Gmail服务来发送email:
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'your_account@gmail.com'
EMAIL_HOST_PASSWORD = 'your_password'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
用django自带的mail服务基本都是上面那样设置,不过我的没搞好,一直报错,于是自己写了个脚本 send_mail.py
:
#coding:utf8
import smtplib
from email.mime.text import MIMEText
# 发送邮件函数
class SendMail():
def __init__(self):
# 设置服务器名称、用户名、密码以及邮件后缀
self.mail_host = "smtp.126.com"
self.mail_user = "iphone3000@126.com"
self.mail_pass = "xxxxxxxxxxx"
self.mail_postfix = "126.com"
def send_mail(self, to_list, sub, context):
me = self.mail_user + "<"+self.mail_user+"@"+self.mail_postfix+">"
msg = MIMEText(context,_charset="utf-8")
msg['Subject'] = sub
msg['From'] = me
msg['To'] = "".join(to_list)
try:
send_smtp = smtplib.SMTP()
send_smtp.connect(self.mail_host)
send_smtp.login(self.mail_user, self.mail_pass)
send_smtp.sendmail(me, to_list, msg.as_string())
send_smtp.close()
return True
except Exception as e:
print(e)
return False
在views中操作表单
你必须创建一个新的view当表单成功提交后进行操作和发送email。编辑博客应用下的views.py文件,添加以下代码:
...
from .send_mail import SendMail
...
def post_share(request, post_id):
post = get_object_or_404(Post, id=post_id, status='published')
sendmail = SendMail()
sent = False
if request.method == 'POST':
form = EmailPostForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
post_url = request.build_absolute_uri(
post.get_absolute_url())
subject = '{} recommends you reading "{}"'.format(cd['name'], post.title)
message = 'Read "{}" at {}\n\n comments: {}'.format(post.title, post_url, cd['comments'])
sendmail.send_mail([cd['to']],subject,message)
sent = True
else:
form = EmailPostForm()
return render(request, 'blog/post/share.html', {'post': post,'form': form,'sent': sent})
该view完成了以下工作:
- 我们定义了post_share视图,参数为request对象和post_id。
- 我们使用
get_object_or_404
方法通过ID获取对应的帖子,并且确保取回的帖子一定是已发布的状态。 - 我们使用同一个view来展示初始表单以及处理提交后的数据。
- 我们会区别被提交的表单和不基于这次请求方法的表单。我们通过使用
POST
来提交表单。我们假设我们会遇到GET
请求,这时就需要展示一个空的表单,而我们遇到POST
请求,我们就需要处理表单提交上来的数据。因此,我们使用request.method == 'POST'
来区分这两种场景。
下面是展示和操作表单的进程:
- 当view加载后遇到一个GET请求,我们会创建一个新的表单实例在模板中展示一个空的表单:
form = EmailPostForm()
- 当用户填写了表单并通过POST方式提交,我们会 创建一个表单实例来使用提交的数据,这些数据被包含在request.POST中:
if request.method == 'POST':
# Form was submitted
form = EmailPostForm(request.POST)
- 在以上步骤之后,我们通过表单的
is_valid()
方法来验证提交的数据。这个方法会验证表单引进的数据,如果所有的字段都是有效数据,将会返回True。一旦有任何一个字段包含无效的数据,is_valid()将会返回False。你可以通过访问form.errors
查看所有验证错误的列表。
- 如果表单数据验证没有通过,我们会再次使用提交的数据在模板中渲染表单。我们会在模板中显示验证错误提示。
- 如果表单数据验证通过,我们通过访问
form.cleaned_data
# 获取验证过的数据。这个属性是一个表单字段和值的字典。
如果表单中的数据没有通过验证,cleaned_data只会包含验证过的数据
请注意,我们声明了一个sent变量并且赋予它True当帖子被成功发送。当表单成功提交的时候,我们会使用这个变量用来在template中显示一条成功提示。因为我们需要在email中包含帖子的超链接,所以我们通过使用post.get_absolute_url()
方法来取到帖子的绝对路径。我们将这个绝对路径作为request.build_absolute_uri()
的输入值来构建一个完整的HTTP链接。我们通过使用验证过的表单数据来构建email的主题和消息内容并最终发送email给表单中的to字段中包含的所有email地址。
现在你的view已经完成了,别忘记为它去添加一个新的URL pattern。打开你的博客应用下的urls.py文件添加post_share的URL pattern如下所示:
urlpatterns = [
# ...
url(r'^(?P<post_id>\d+)/share/$', views.post_share, name='post_share'),
]
在templates中渲染表单
在通过创建表单,编写view以及添加URL patter后,我们就只剩下为这个view添加tempalte了。在blog/templates/blog/post/
目录下创建一个新的文件并命名为share.html。在html文件中添加如下代码:
{% extends "blog/base.html" %}
{% block title %}Share a post{% endblock %}
{% block content %}
{% if sent %}
<h1>E-mail successfully sent</h1>
<p>
"{{ post.title }}" was successfully sent to {{ cd.to }}.
</p>
{% else %}
<h1>Share "{{ post.title }}" by e-mail</h1>
<form action="." method="post">
{{ form.as_p }}
{% csrf_token %}
<input type="submit" value="Send e-mail">
</form>
{% endif %}
{% endblock %}
这个tempalte专门用来显示一个表单或email成功发送后展示一条成功提示信息。就像你所看见的,我们创建的HTML表单元素里面声明了提交方式采用的是POST方法;
{% csrf_token %}
tempalte标签引进了一个隐藏的字段包含一个自动生成的标记来避开Cross-Site request forgery(CSRF)攻击。这些攻击由一个恶意的站点或程序组成并执行一个不需要的操作给你站点中的用户。你可以找到更多的信息通过访问https://en.wikipedia.org/wiki/Cross-site_request_forgery 。上述的标签生成的隐藏字段就像下面一样:
input type='hidden' name='csrfmiddlewaretoken' value='26JjKo2lcEtYkGoV9z4XmJIEHLXN5LDR' />
默认情况下,Django在所有的POST请求中都会检查CSRF标签。请记住要在所有使用POST方法提交的表单中包含csrf_token标签。(译者注:当然你也可以关闭这个检查,注释掉app_list中的csrf应用即可)
现在访问试试:
点击一篇帖子,点击Share this post你会看到一个包含通过email分享这个帖子的表单。看上去如下所示:
为这个表单提供的CSS样式被包含在示例代码中的 static/css/blog.css文件中。当你点击Send e-mail按钮,这个表单会提交并验证。如果所有的字段都通过了验证,你会得到一条成功信息如下所示: send_succed
如果你输入了错误的数据,你会看到表单被再次渲染,并展示出验证错误信息;
检查邮箱会看到刚刚发送的邮件
网友评论