Enhancing Your Blog with Advanced Features翻译加实践

在上一章节,创建了一个基本的blog应用.现在,将应用转入具有高级特性的全功能blog,例如email分享post,添加评论,文章标签,接收相似文章.在这一章节,将会学习到如下内容:

  • 使用Django发送邮件
  • 视图中创建并处理表单
  • 使用models创建表单
  • 整合第三方应用
  • 创建混合querysets

使用Email分享posts

首先,我们将允许用户使用Email分享posts.花一点点时间想一下如何使用上一章节学习的视图,URLS,模板创建这个功能.现在,检查允许用户发送邮件需要哪些东西.你将做如下的操作:

  • 创建一个from允许用户填写用户名,email,email接收者,评论.
  • 创建一个view处理数据和发送邮件
  • 为上面的view添加一个url匹配
  • 创建一个template显示from表单

在Django中创建froms表单

让我们来创建分享表单.Django内置表单框架允许你轻松创建表单.表单框架允许你定义表单字段,指定如何显示,标识如何验证数据.Django表单框架灵活的渲染表单和处理数据.
Django使用两个类来创建表单:

  • from: 创建基本表单
  • ModelForm: 创建数据绑定表单

首先,我们在blog应用目录创建from.py文件:

1
2
3
4
5
6
7
8
9
from django import forms


class EmailPostForm(forms.Form):
name = forms.CharField(max_length=25)
email = forms.EmailField()
to = forms.EmailField()
comments = forms.CharField(required=False,
widget=forms.Textarea)

这是我们第一个表单.分析一下代码内容.我们继承Form类创建一个表单.使用了不同的字段类型,Django将相应的验证这些字段.

表单可以存在于Django工程的任何地方,最好在每个应用的目录下存放在form.py文件中

name字段是字符串字段.这个字段渲染为一个HTML元素

1
<input type="text">

每个字段都有一个默认的微件决定字段在HTML中如何渲染.comments字段我们使用了一个Textarea的微件在HTML显示为

1
<textarea>

元素.
字段验证同样信赖字段类型.例如,email和to字段使用EmailField类型.两个字段都需要验证email地址,否则将提示forms.ValidationError错误.其它参数也将被带入验证:我们为name定义了最大长度25字符,comments字段使用required=False表示不是必须的.所有这些都存在字段验证.这里只使用了Django表单字段的一部分,所有表单字段可查阅:https://docs.djangoproject.com/en/2.0/ref/forms/fields/

在视图中处理表单

你需要创建一个新的视图处理当提交成功时表单发送email.编辑应用下的views.py文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 导入表单
from .forms import EmailPostForm


# 处理表单
def post_share(request, post_id):
# 根据post_id取得post对象
post = get_object_or_404(Post, id=post_id, status='published')
# 如果提交操作且经过验证
if request.method == "POST":
# 表单是提交操作
form = EmailPostForm(request.POST)
if form.is_valid():
cd = form.changed_data
# 发送邮件
else:
# 表单是显示操作
form = EmailPostForm
return render(request,
'blog/post/share.html',
{'form': form,
'post': post})

视图处理流程如下:
1 创建post_share视图,带入request和post_id参数
2 使用get_object_or_404()快捷方法接收post对象,根据后面的参数post_id及status=published.
3 使用同一视图显示初始表单及处理表单提交数据.区别是提交还是显示基于request的方法,提交是POST.如果是GET方法,显示表单;如果是POST方法,表单是提交,然后需要view做处理.request.method == ‘POST’是区分两种方式的关键.

下面是处理显示及处理表单数据:
1 当表单是初始状态或接收GET请求,我们创建一个新表单实例在模板中来显示空白表单.
2 当用户输入数据点击提交后接收为POST,然后我们使用提交的数据创建包含request.POST的表单实例:
3 在这个过程中,我们验证提交的数据使用form.is_vaild()方法.这个方法验证表单数据正确返回True.错误返回False.可以使用form.errors查看验证错误列表.
4 如果表单不正确,再次渲染表单模板并显示验证错误.
5 如果表单正确,我们通过form.cleaned_data接收验证过的数据.这个属性是一个表单字段和表单字段值的一个字典.

如果表单数据不正确,cleaned_data将只包含通过验证的数据.

现在,让我学习如何在Django中发送邮件来把事情融合.

使用django发送邮件

Django发送邮件是相当直接的.首先,你需要有一个本地SMTP服务器或在settings.py中定义一个外界的SMTP服务器:

  • EMAIL_HOST: smtp服务器地址,默认是localhost
  • EMAIL_PORT: smtp端口,默认25
  • EMAIL_HOST_USER: 用户名
  • EMAIL_HOST_PASSWORD: 密码
  • EMAIL_USE_TLS: 是否使用TLS安全连接
  • EMAIL_USE_SSL: 是否使用SSL安全连接

如果你不能使用SMTP服务器,你可以告诉Django输出email到终端,添加如下配置:

1
2
# For Email
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

这样配置后,Django将输入email到终端.这在没有SMTP服务器的情况下测试非常有用.
如果你想发送邮件,但没有本地SMTP服务器,你可以使用SMTP服务商.下面是使用GMAIL的配置:

1
2
3
4
5
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'your_account@gmail.com'
EMAIL_HOST_PASSWORD = 'your_password'
EMAIL_PORT = 587
EMAIL_USE_TLS = True

运行python manage.py shell打开终端,然后发送email:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> from django.core.mail import send_mail
>>> send_mail('Django mail', 'This e-mail was sent with Django.', 'talenhao@gmail.com', ['talenhao@gmail.com'], fail_silently=False)
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Django mail
From: talenhao@gmail.com
To: talenhao@gmail.com
Date: Sun, 20 Jan 2019 14:11:55 -0000
Message-ID: <154799351565.16947.13299734647581468097@tianfei-opensuse>

This e-mail was sent with Django.
-------------------------------------------------------------------------------
1

send_mail()功能带入标题,信息,发送者,一个接收者列表作为必选参数.可选参数fail_silently=False,我们告知它如果不能正确发送触发一个错误.
如果发送email由GMAIL提供,必需允许低安全应用访问https://myaccount.google.com/lesssecureapps.
现在,我们添加发邮件功能到视图中.
修改post_share视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 发送邮件
from django.core.mail import send_mail


# 处理表单
def post_share(request, post_id):
# 根据post_id取得post对象
post = get_object_or_404(Post, id=post_id, status='published')
sent = False
# 如果提交操作且经过验证
if request.method == "POST":
# 表单是提交操作
form = EmailPostForm(request.POST)
if form.is_valid():
cd = form.changed_data
# 发送邮件
# 创建文章
post_url = request.build_absolute_uri(post.get_absolute_url())
subject = '{} ({}) recommends you reading "{}"'.format(cd['name'],
cd['email'],
post.title)
message = 'Read "{}" at {}\n\n{}\'s comments: {}'.format(post.title,
post_url,
cd['name'],
cd['comments'])
send_mail(subject, message, 'talenhao@test.domain', [cd['to']])
sent = True
else:
# 表单是显示操作
form = EmailPostForm
return render(request,
'blog/post/share.html',
{'form': form,
'post': post,
'sent': sent})

我们宣告一个sent变量,邮件发送成功后设置为True.如果发送成功,我们将稍后在模板中使用这个变量来显示成功消息.因为必需在邮件正文中包含post的链接,我们使用Post模型中定义的get_absolute_url()方法.我们使用这个地址带入request.build_absolute_url()方法来创建包含HTTP结构及主机名的完整的url.
然后我们使用cleaned_data创建邮件标题,信息;最后发送这个邮件到to字段的地址.
现在视图已经完整,但还是需要添加url匹配规则.打开urls.py修改:

1
path('<int:post_id>/share/', views.post_share, name='post_share'),

在模板中渲染表单

创建表单,编写视图,添加URL匹配后,我们还缺少模板.创建一个新文件blog/templates/blog/post/share.html,添加下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{% extends 'blog/base.html' %}

{% block title %}
Share {{ post.title }}
{% endblock %}

{% block content %}
{% if sent %}
<p>
"{{ post.title }}" was successfully sent to {{ form.cleaned_data.to }}
</p>
{% else %}

<h1>Share "{{ post.title }}" by email</h1>
<form action="." method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Send Email">
</form>
{% endblock %}

如果已经发送,这个模板将显示一个成功消息.我们创建了一个html元素,POST方法提交.
我们使用as_p()方法告诉Django渲染表单字段在HTML元素

.我们也可以使用as_ul或as_table.如果我们希望渲染每个字段,我们可以迭代每个字段:

1
2
3
4
5
6
{% for field in form %}
<div>
{{ field.errors }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}
1
{% csrf_token %}

模板标记隐含字段包含一个自动生成的token防止 cross-site request forgery(CSRF)攻击.那些包含恶意网站或不希望的执行程序在你的站点上.在这里可以找到更多说明https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
处理标记生成一个隐含字段如下:

1
2
<input type='hidden' name='csrfmiddlewaretoken'
value='26JjKo2lcEtYkGoV9z4XmJIEHLXN5LDR' />

修改blog/post/detail.html模板添加分享链接:

1
2
3
4
5
6
{{ post.body|linebreaks }}
<p>
<a href="{% url "blog:post_share" post.id %}">
Share this post
</a>
</p>

在这里我们使用Django提供的url动态创建链接.使用命令空间blog和url名称post_share,然后传递post ID作为参数来创建绝对路径的URL.
现在可以启动开发服务器,查看页面运行情况了.



如果输入字段类型不对,会提示错误.

创建一个评论系统

现在,我们将创建一个评论系统,读者能够评论文章.为了实现这个功能,我们将做如下事情:

1 创建一个model存储comments
2 创建一个form验证并提交comments
3 创建一个view处理from并存储commnets到model数据库
4 编辑post detail模板显示comments列表和添加新comment表单

首先,创建一个model存储评论.打开models.py文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Comment(models.Model):
post = models.ForeignKey(Post,
on_delete=models.CASCADE,
related_name='comments')
name = models.CharField(max_length=80)
email = models.EmailField()
body = models.TextField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)

class Meta:
ordering = ('created',)

def __str__(self):
return 'Comment by {} on {}'.format(self.name, self.post)

我们的Comment model,包含外键ForeignKey辅助访问一个post的comment.这里定义了一个多对一关系,因为每个comment只能评论在一个post上,每个post可以有多个comments.related_name属性允许我们为从关联对象回到关系所使用的属性命名.定义后,我们可以使用comment.post接收comment对象的post,接收post的所有comments使用post.comments.all().如果你不定义related_name属性.,Django将使用model的小写加_set作为关联对象post回指的管理器.
查看更多多对一关系https://docs.djangoproject.com/en/2.0/topics/db/examples/many_to_one/

我们创建了一个active布尔字段为了手工关闭不适合的评论.我们使用created创建时间做为排默认排序.
创建完Commnet model后并没有同步到数据库.使用Django迁移命令.

1
2
3
4
5
6
7
8
9
10
11
(django2byExample) haotianfei@tianfei-opensuse:~/PycharmProjects/Django2byExample> python manage.py makemigrations blog
Migrations for 'blog':
blog/migrations/0002_auto_20190121_1444.py
- Create model Comment
- Alter field slug on post
- Add field post to comment
(django2byExample) haotianfei@tianfei-opensuse:~/PycharmProjects/Django2byExample> python manage.py migrate blog
Operations to perform:
Apply all migrations: blog
Running migrations:
Applying blog.0002_auto_20190121_1444... OK

现在,为了通过一个简单的接口管理comments我们添加新model到管理站点.修改admin.py

1
2
3
4
5
6
7
8
from blog.models import Comment


@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ('name', 'email', 'post', 'created', 'updated', 'active')
list_filter = ('active', 'created', 'upcated')
search_fields = ('name', 'email', 'body')

运行开发服务器,执行python manage.py runserver命令,使用浏览器打开http://127.0.0.1:8000/admin,你将会看到新的model已经在Blog中了.
现在model已经注册到管理站点,我们可以使用一个简单的接口管理Comment实例了

从models创建froms

我们需要创建一个能让用户评论文章的表单.Django有两个基础类可以创建表单,Form和ModelForm.我们使用Form在上面创建用户email分享.在这一部分,因为要从Comment模型创建一个动态表单使用ModelForm.编辑forms.py,添加下面的代码:

1
2
3
4
5
6
7
from .models import Comment


class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ('name', 'email', 'body')

从model创建form,只需要在Meta类中指出从哪个model创建.Django自己会跟model交互然后动态的创建.每个model字段类型都对应有默认的表单字段.定义model类型是为了解释表单验证.Django为model中每一个字段创建表单字段.但可以使用fields列表明确的告知框架你想包含在表单中的字段,或使用exclude定义哪些字段你想要排除.在这个CommentForm表单,我们将使用name,email,body字段,因为我们用户只需要填入这些.

在视图中处理ModelForm

我们将在post的详细页面视图中初始化一个表单并处理,这样可以保持简洁.编辑views.py文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# ModelForm
from .forms import CommentForm
from .models import Comment


def post_detail(request, year, month, day, post):
post = get_object_or_404(Post,
slug=post,
status='published',
publish__year=year,
publish__month=month,
publish__day=day)
# 接收所有当前post评论
comments = post.comments.filter(active=True)
comment_form = CommentForm()
new_comment = None
if request.method == "POST":
comment_form = CommentForm(data=request.POST)
if comment_form.is_valid():
new_comment = comment_form.save(commit=False)
new_comment.post = post
new_comment.save()
else:
comment_form = CommentForm()
return render(request,
'blog/post/detail.html',
{'post': post,
'comments': comments,
'new_comment': new_comment,
'comment_form': comment_form})

我们使用post的详细页显示评论.首先使用一个QuerySet接收所有post评论,命名为comments,使用Commnet关系模型关联属性对象的管理器接收对象.
我们同时使用相同的视图来主我们的用户添加新的评论.初始化new_comment变量为None.我们将在创建新的comment时使用这个变量.如果是request.GET请求使用comment_form创建一个初始表单.如果请求是POST,我们将使用提交数据和变量初始表单并使用is_vaild()验证方法.如果表单正确,将产生以下动作:
1 使用表单save()方法创建新Comment对象,附给new_commnet变量.save()方法创建一个表单链接新的模型实例,并存储到数据库.如果调用时使用commit=False,创建实例但不保存到数据库.这可以在保存之间修改对象,我们需要修改数据.save只在ModelForm中有用,Form无效.因为Form没有关联model
2 关联post到刚刚创建的commnet.这样就指定了新的评论到post.
3 最后,调用save()保存新的comment到数据库

现在视图已经可以显示并处理新的评论.

添加评论到文章详细页模板

我们已经创建post管理comment的功能.现在,需要对post/detail.html模板做下面的事情:

  • 显示一篇post的总commnet数量
  • 显示commnets列表
  • 显示一个新commnet表单

首先,添加评论总数.

1
2
3
4
5
6
7
<span>
{% with comments.count as total_comments %}
<h2>
{{ total_comments }} comment{{ total_comments|pluralize }}
</h2>
{% endwith %}
</span>

在模板中使用Django ORM,执行QuerySet comments.count.Django模板语言调用方法不会使用复数.with标签允许附加值到一个新的变量.pluralize模板过滤器根据commnets的值来显示复数后辍.模板过滤器带入变量值做为输入,然后返回一个混合后的值.第三章节将详细介绍过滤器.
如果结果不为1,pluralize过滤器将返回字母s.Django包含完美的模板标记和过滤器帮助显示你想要的信息.
现在,列出评论.代码如下:

1
2
3
4
5
6
7
8
9
10
11
{% for comment in comments %}
<div class="comment">
<p class="info">
Comment {{ forloop.counter }} by {{ comment.name }}
Created {{ comment.created }}
</p>
{{ comment.body|linebreaks }}
</div>
{% empty %}
<p>No Comments.</p>
{% endfor %}

我们使用for模板标签循环commnets.如果评论为空,显示一条消息,提示用户现在还没有评论.列举评论使用forloop.counter变量,包含整数循环计数.然后我们显示评论用户名,日期,评论内容.

最后,需要渲染一个表单或显示成功提交消息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<span>
{% if new_comment %}
<h2>
Your comment has been added.
</h2>
{% else %}
<h2>Add a new commnet</h2>
<form action="." method="post">
{{ comment_form.as_p }}
{% csrf_token %}
<p>
<input type="submit" value="Add comment">
</p>
</form>
{% endif %}
</span>

代码相当简洁直接.如果new_comment对象存在,显示一条成功消息.否则,显示post表单并包含csrf token.

可以通过编辑http://127.0.0.1/admin/blog/comment active字段不显示不合适的评论,从文章评论中屏蔽它们.

添加标签功能

完成实施comment系统后,接下来创建post tag.工程将整合一个第三方Django tag应用。django-tag模块是的利用程序,一个主要提供一个Tag model和轻松标签到任何model一个管理器.可以到https://github.com/alex/django-taggit查看源码。

首先,执行以下命令使用pip安装django-taggit。

1
pip install django-taggit

打开settings.py文件添加taggit到INSTALLED_APPS:

1
2
3
4
5
6
7
8
9
10
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog.apps.BlogConfig',
'taggit',
]

打开models.py文件添加django-taggit提供的TaggableManager管理器到Post model:

1
2
3
4
5
6
7
from taggit.managers import TaggableManager


class Post(models.Model):
tags = TaggableManager()
objects = models.Manager()
...

tags管理器将允许从Post对象中添加,接收,移除tag。
运行下面的命令创建migration。

1
2
3
4
5
6
7
8
9
10
11
(django2byExample) haotianfei@tianfei-opensuse:~/PycharmProjects/Django2byExample> python manage.py makemigrations blog
Migrations for 'blog':
blog/migrations/0003_post_tags.py
- Add field tags to post
(django2byExample) haotianfei@tianfei-opensuse:~/PycharmProjects/Django2byExample> python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions, taggit
Running migrations:
Applying taggit.0001_initial... OK
Applying taggit.0002_auto_20150616_2121... OK
Applying blog.0003_post_tags... OK

现在数据库已经准备好使用django-taggit models。让我们学习如何来使用tags管理器。打开终端python manage.py shell.首先,接收一个post;然后添加tags到这个post;检查是否已经成功添加。最后,移除一个tag然后再次检查。

1
2
3
4
5
6
7
8
9
10
11
>>> from blog.models import Post
>>> post = Post.objects.get(id=2)
>>> post
<Post: This is the second post>
>>> post.tags.add('music', 'jazz', 'django')
>>> post.tags.all()
<QuerySet [<Tag: music>, <Tag: jazz>, <Tag: django>]>
>>> post.tags.remove('django')
>>> post.tags.all()
<QuerySet [<Tag: music>, <Tag: jazz>]>
>>>

是不是很容易?运行runserver命令,打开admin网站查看看tag对象。
然后到post对象上查看tag字段,还可以编辑它。
下面将在编辑post页面显示tags。
打开blog/post/list.html template添加下面的html代码:

1
<p class="tags">Tags: {{ post.tags.all|join:", " }}</p>

接下来,我们将编辑post_list view让用户可以列出所有打了指定tag的posts。编辑view.py,从django-taggit import Tag model,通过修改post_list view使用可选的tag_slug过滤一个tag的所有posts:

1
2
3
4
5
6
7
8
9
# tag,tag标签复用了post_list模板
from taggit.models import Tag
def post_list(request, tag_slug=None):
# 获取所有的post对象
object_list = Post.published.all()
tag = None
if tag_slug:
tag = get_object_or_404(Tag, slug=tag_slug)
object_list = Post.published.filter(tags__in=[tag])

post_list veiw作用流程如下:
1 带入一个可选的参数tag_slug(默认为None).这个参数来自url.
2 在view内部,创建一个初始QuerySet,接收所有发布的posts,但如果有一个tag slug参数,我们将使用get_object_or_404快捷方式带入slug取得Tag对象.
3 然后,我们过滤包含tag的posts列表.这个一个多对多关联,所有使用包含过滤,在这里,只包含一个元素.

QuerySet只在我们解析模板时才循环post列表.
最后,修改view底部的render()函数将tag变量传递到模板.

1
2
3
4
5
return render(request,
'blog/post/listold.html',
{'posts': posts,
'page': page,
'tag': tag})

编辑urls.py,注释掉PostListView,去掉post_list view的注释,添加tag

1
path('tag/<slug:tag_slug>/', views.post_list, name='post_list_by_tag'),

修改post_list.html加入tag判断

1
2
3
4
5
6
{% for post in posts %}
<p>
{% if tag %}
<h3>Posts tagged with {{ tag.name }}</h3>
{% endif %}
</p>

为每个tag标签添加超链接

1
2
3
4
5
6
7
<p class="tags">
Tags:
{% for p_tag in post.tags.all %}
<a href="{% url "blog:post_list_by_tag" p_tag.slug %}">{{ p_tag.name }}</a>
{% endfor %}
{% if not forloop.last %}{% endif %}
</p>

接收相似文章

在post_detail view中计算相关性,在detail.html中列举:

1
2
3
4
5
6
7
8
9
10
11
12
post_tags_ids = post.tags.values_list('id', flat=True)
similar_posts = Post.published.filter(tags__in=post_tags_ids)\
.exclude(id=post.id)
similar_posts = similar_posts.annotate(same_tags=Count('tags'))\
.order_by('-same_tags', '-publish')[:4]
return render(request,
'blog/post/detail.html',
{'post': post,
'comments': comments,
'new_comment': new_comment,
'comment_form': comment_form,
'similar_posts': similar_posts})
1
2
3
4
5
6
7
8
9
10
11
12

<span>
<h2>Similar Posts</h2>
{% for similar_post in similar_posts %}
<p>
<a href="{{ similar_post.get_absolute_url }}">{{ similar_post.title }}</a>
</p>
{% empty %}
<p>There are no similar posts yet.</p>
{% endfor %}

</span>

总结

坚持原创技术分享,您的支持将鼓励我继续创作!