Django by Example·第二章|Enhancing Your Blog with Advanced Features@笔记
这本书的结构确实很不错,如果能够坚持看下去,那么Django框架的各种用法也就掌握的七七八八了。之前写过一篇这本书的第一章,看完第一章就算是入门了,但是还是继续写下去,看完这本书。
部分内容引用自原书,如果大家对这本书感兴趣
请支持原版Django by Example·Antonio Melé
目录
第一章:建立一个博客系统
前言
第一章其实就是使用Django创建了一个博客系统,实现了一个博客系统最基本的功能。那么第二章内容是以第一章为基础的,您将把您的应用程序变成一个功能齐全的博客,具有高级功能,如通过电子邮件共享帖子、添加评论、标记帖子以及按相似度检索帖子。如果您对第一章的内容很熟悉,那么您可以直接看第二章的内容。第二章Antonio Melé主要讲了以下内容:
- Sending e-mails with Django(使用Django发送电子邮件)
- Creating forms and handling them in views(创建表单并在视图中处理它们)
- Creating forms from models(通过模型创建表单)
- Integrating third-party applications(集成第三方应用程序)
- Building complex QuerySets(构建复杂的查询集)
** 正式开始之前
*** 用邮件的方式分享帖子
首先,我们将允许用户通过电子邮件共享帖子。花一点时间思考如何使用视图、URL和模板,使用上一章中所学的内容创建此功能。现在,检查允许用户通过电子邮件发送帖子所需的内容。您需要:
- 创建一个表单,供用户填写姓名和电子邮件、电子邮件收件人和可选备注。
- 在views.py文件中创建一个视图,用于处理发布的数据并发送电子邮件。
- 在blog应用下的urls.py中为新创建的视图添加一个URL模式。
- 在blog应用的template下创建一个模板文件(HTML),用于展示相关的表单。
1 使用Django发送电子邮件
1.1 使用Django创建Form表单
让我们从构建共享帖子的表单开始。Django内置了一个表单框架,允许您以简单的方式创建表单。表单框架允许您定义表单的字段,指定它们的显示方式,并指示它们如何验证输入数据。Django表单框架还提供了一种灵活的方式来呈现表单和处理数据。
Django提供了两个基类来构建表单:
- Form: 允许您创建标准表单。
- ModelForm: 允许您使用已创建的表单去创建或更新模型实例。
首先,在blog应用程序的目录中创建一个forms.py文件,并添加以下代码:
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)
这是您的第一个Django表单。看看代码:我们通过继承基类Form创建了一个表单。我们为Django使用不同的字段类型来分别对应不同的前端HTML元素。
*** 表单可以位于Django项目中的任何位置,但惯例是将它们放置在每个应用程序的forms.py文件中。
以下是上述代码中涉及到的相关字段的解释:
- name字段是CharField。这种类型的字段呈现为<input type=“text”>HTML元素。每个字段类型都有一个默认参数widget,用于确定字段在HTML中的显示方式。在comments字段中,我们将widget参数赋值为forms.Textarea,这样它将作为< textarea>HTML元素显示,而不是默认的<input>元素。
- 字段验证还取决于字段类型。例如,电子邮件和收件人字段是EmailField。这两个字段都需要有效的电子邮件地址,否则将抛出forms.ValidationError异常。表单验证还考虑了其他参数:我们为name字段定义了25个字符的最大长度,并将comments字段设置为可选的(required=False)。所有这些也被考虑到现场验证。此表单中使用的字段类型只是Django表单字段的一部分。有关所有可用表单字段的列表,您可以访问Django官网Form表单字段介绍.
1.2 在视图函数中处理form表单
您必须创建一个新视图来处理表单,并在表单成功提交后发送电子邮件。编辑blog应用程序的views.py文件,并向其中添加以下代码:
from .forms import EmailPostForm
def post_share(request, post_id):
# Retrieve post by id
post = get_object_or_404(Post, id=post_id, status='published')
# 提交表单数据
if request.method == 'POST':
# Form was submitted
form = EmailPostForm(request.POST)
if form.is_valid():
# Form fields passed validation
cd = form.cleaned_data
# ... send email
# 创建一个空白表单
else:
form = EmailPostForm()
return render(request, 'blog/post/share.html', {'post': post, 'form': form})
对该视图函数的一些解释:
- 我们定义了post_share视图,该视图将请求对象request和post_id作为参数。
- 我们使用get_object_or_404()方法按ID检索帖子,并确保检索到的是已发布的帖子。
- 我们使用相同的视图来显示初始表单和处理提交的数据。我们根据请求方法区分表单是否已提交。我们将使用POST提交表格。我们假设,如果收到get请求,则必须显示一个空表单;如果收到POST请求,则表单已提交并需要处理。因此,我们使用request.method=='POST’来区分这两种情况。
以下是显示和处理表单的过程:
-
当视图最初加载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.clean_data的有效数据。此属性是表单字段及其值的字典。
如果表单数据未验证,cleaned_data将仅包含有效字段。
现在,您需要学习如何使用Django发送电子邮件,以将所有内容整合在一起。
1.3 使用Django发送电子邮件
用Django发送电子邮件非常简单。首先,您需要有一个本地SMTP服务器,或者通过在项目的settings.py文件中添加以下设置来定义外部SMTP服务器的配置:
- EMAIL_HOST:SMTP服务器主机。默认localhost。
- EMAIL_PORT:SMTP端口默认值25。
- EMAIL_HOST_USER:SMTP服务器的用户名。
- EMAIL_HOST_PASSWORD:SMTP服务器的密码。
- EMAIL_USE_TLS:是否使用TLS安全连接。
- EMAIL_USE_SSL:是否使用隐式TLS安全连接。
如果没有本地SMTP服务器,则可能可以使用电子邮件提供商的SMTP服务器。以下示例配置适用于使用腾讯QQ邮箱服务器发送电子邮件:
EMAIL_HOST = "smtp.qq.com" # SMTP服务器主机,这个不用改
EMAIL_PORT = 587 # 端口,千万要写587端口,否则容易出错
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST_USER = "********@qq.com" # 邮箱地址
EMAIL_HOST_PASSWORD = "************" # 授权码
EMAIL_USE_TLS= True
EMAIL_FROM = "***********@qq.com"
运行命令python manage.py shell打开python shell并发送邮件:
>>> from django.core.mail import send_mail
>>> send_mail(
'Django mail', # 主题
'This e-mail was sent with Django.', # 消息
'your_account@gmail.com', # 发件人
['your_account@gmail.com'], # 收件人列表
fail_ silently=False
)
send_mail()将主题、消息、发件人和收件人列表作为必需的参数。通过设置可选参数fail_silently=False,我们告诉它如果无法正确发送电子邮件,将引发异常。如果您看到的输出为1,则表明您的电子邮件已成功发送。
现在,我们将把这个方法应用到我们的视图中。编辑博客应用程序views.py文件中的post_share视图,在其中添加以下代码:
from django.core.mail import send_mail
def post_share(request, post_id):
# 获取帖子
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.cleaned_data
# 获取帖子的url
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, '发件人邮箱', [cd['to']])
sent = True
# 加载表单
else:
form = EmailPostForm()
return render(
request,
'blog/post/share.html',
{'post': post, 'form': form, 'sent': sent}
)
注意,我们声明了一个sent变量,并在发送邮件成功后将其设置为True。我们稍后将在模板中将使用该变量,以在表单成功提交时显示成功消息。由于我们必须在电子邮件中包含到文章的链接,因此我们使用其get_absolute_url()方法检索文章的绝对路径。我们使用此路径作为request.build_absolute_uri()的输入,以构建包含HTTP模式和主机名的完整URL。我们使用经过验证的表单的数据构建电子邮件的主题和邮件正文,最后将电子邮件发送到表单的收件人字段中包含的电子邮件地址。
现在通过邮件分享帖子的视图已经完成,请记住为它添加一个新的URL模式。打开blog应用程序的urls.py文件并为post_share 视图添加URL模式:
urlpatterns = [
# ...
url(r'^(?P<post_id>\d+)/share/$', views.post_share, name='post_share'),
]
1.4 创建模板文件用以渲染数据
在创建表单、编程视图并添加URL模式后,我们只缺少此视图的模板。在blog/templates/blog/post/目录中创建一个新文件,并将其命名为share.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 %}
这是发送表单时显示表单或成功消息的模板。如您所见,我们创建了HTML表单元素,指示必须通过POST方法提交:
<form action="." method="post">
然后我们开始处理从后端传回的form表单实例。我们告诉Django使用as_p方法在HTML<p>标签中呈现其字段。我们还可以使用as_ul将表单呈现为HTML无序列表,或者使用as_table将表单呈现成HTML表。我们还可以用下面的方法迭代form表单实例:
{% for field in form %}
<div>
{{ field.errors }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}
{%csrf_token%}模板标记引入了带有django中间件自动生成token,以避免跨站点请求伪造(csrf)攻击。这些攻击包括恶意网站或程序对您网站上的用户执行不必要的操作。有关此的详细信息,请访问https://en.wikipedia.org/wiki/Cross-site_request_forgery。
前面的标记生成了的隐藏字段,如下所示:
<input type='hidden' name='csrfmiddlewaretoken' value='26JjKo2lcEtYkGoV9z4XmJIEHLXN5LDR' />
默认情况下,Django在所有POST请求中检查CSRF令牌。记住在通过POST提交的所有表单中都包含csrf_token标记。
编辑您的博客/post/detail.html模板,并在{{post.body|linebreaks}}变量后添加用来分享帖子的链接:
<p>
<a href="{% url "blog:post_share" post.id %}">分享</a>
</p>
请记住,我们使用Django提供的{%URL%}模板标记动态构建URL。我们使用名为blog的命名空间和名为post_share的URL,并传递postID作为参数来构建绝对URL。
现在,使用命令python manage.py runserver启动开发服务器,然后打开http://127.0.0.1:8000/blog/在浏览器中。单击任何文章标题以查看详细信息页面。在贴体下,您应该看到刚才添加的链接,如下图所示:
单击“共享”,您将看到包含通过电子邮件共享此帖子的表单的页面。它必须如下所示:
表单的CSS样式包含在static/CSS/blog中的示例代码中。单击“发送电子邮件”按钮时,将提交并验证表单。如果所有字段都包含有效数据,您将收到一条成功消息,如以下内容:
如果您输入了无效数据,您将看到表单再次呈现,包括所有验证错误:
2 创建一个评论系统
现在我们将为博客构建一个评论系统,用户可以在其中对帖子进行评论。要构建评论系统,您需要:
- 创建模型以保存评论
- 创建一个表单用于提交评论以及验证输入的数据
- 添加一个视图处理从表单提交的评论
- 编辑帖子详细信息模板以显示评论列表以及添加评论的表单
2.1 创建模型
首先,让我们构建一个模型用来保存评论。打开blog应用程序的models.py文件并添加以下代码:
class Comment(models.Model):
post = models.ForeignKey(Post, 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模型。它包含一个ForeignKey,用于将评论与对应的帖子相关联。这种多对一的关系是在Comment模型(多的一方)中定义的,因为每个评论都将在一篇文章上发表,并且每个文章可能有多条评论。related_name属性允许我们将用于从相关对象返回到此对象的关系的属性命名。在定义了这个属性之后,我们可以使用comment.post检索一个评论对象(comment)的帖子(post),并使用post.comments.all()检索一个帖子(post)的所有注释。如果您没有定义related_name属性,Django将使用模型的底层名称,后跟_set(即comment_set)将相关对象的管理器命名回此对象。
您可以通过这个链接学到更多关于一对多关系的知识一对多关系。
我们已经包含了名为active的布尔字段,我们将使用它来手动屏蔽不适当的评论。默认情况下,我们使用created字段按时间顺序对注释进行排序。
刚刚创建的新Comment模型尚未同步到数据库中。运行以下命令生成迁移文件:
python manage.py makemigrations blog
然后执行迁移,Django将在数据库中创建对应的数据库表:
python manage.py migrate
我们刚刚创建的迁移已经应用,现在数据库中存在一个blog_comment表。
现在,我们可以将新模型添加到管理站点,以便通过一个简单的界面管理评论。打开blog应用程序的admin.py文件,添加以下代码:
from .models import Post, Comment
# 创建评论管理模型
class CommentAdmin(admin.ModelAdmin):
list_display = ('name', 'email', 'post', 'created', 'active')
list_filter = ('active', 'created', 'updated')
search_fields = ('name', 'email', 'body')
# 注册模型
admin.site.register(Comment, CommentAdmin)
使用命令python manage.py runserver启动开发服务器并打开http://127.0.0.1:8000/admin/在浏览器中。您应该可以在Blog部分看到新添加的评论模型,如下图所示:
我们的模型现在已注册到管理站点,我们可以使用一个简单的界面来管理Comment实例。
2.2 创建Comment模型对应的表单
我们仍然需要构建一个表单,让用户可以对博客发表评论。记住Django有两个基类来构建表单:Form和ModelForm。您之前使用了Form,让您的用户通过电子邮件共享帖子。在本例中,您需要使用ModelForm(相对Form使用更简单),因为您必须从Comment模型动态构建表单。编辑blog应用程序的forms.py并添加以下代码:
from .models import Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ('name', 'email', 'body')
要通过模型创建表单,我们只需要在表单的Meta类中指明要使用哪个模型来构建表单。Django会对模型进行内省,并为我们动态构建表单。每个模型字段类型都有一个相应的默认表单字段类型。表单验证考虑了我们定义模型字段的方式。默认情况下,Django为模型中包含的每个字段构建一个表单字段。但是,您可以使用字段列表明确告诉框架要在表单中包含哪些字段,或者使用字段排除列表定义要排除哪些字段。对于我们的CommentForm,我们将只使用表单的名称、电子邮件和正文字段,因为这些是我们的用户能够填写的唯一字段。
2.3 在视图中处理ModelForm
因为帖子和评论是一对多关系,所以我们将使用post_detail视图来实例化评论的表单。编辑blog应用下的models.py文件,为Comment模型和CommentForm表单添加导入,并修改post_detail视图,使其看起来如下所示:
from .models import Post, Comment
from .forms import EmailPostForm, CommentForm
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)
# 获取当前帖子的评论
comments = post.comments.filter(active=True)
# 提交评论
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,
'comment_form': comment_form
}
)
让我们看一下我们在视图中添加的内容。我们使用post_detail视图来显示文章及其评论。我们添加一个QuerySet来检索此帖子的所有为被屏蔽的评论:
comments = post.comments.filter(active=True)
我们还使用相同的视图让用户添加新的评论。因此,如果视图被GET请求调用,我们将使用comment_form=CommentForm()构建一个表单实例。如果请求是通过POST完成的,我们将使用提交的数据实例化表单,并使用is_valid()方法验证它。如果表单无效,我们将呈现带有验证错误的模板。如果表格有效,我们将采取以下措施:
-
我们通过调用表单的save()方法来创建一个新的Comment对象:
new_comment = comment_form.save(commit=False)
通过save()方法可以创建表单对象链接到的模型的实例,并将其保存到数据库中。如果使用commit=False调用它,则创建模型实例,但不将其保存到数据库中。当您想要在最终保存之前修改对象时,这非常方便,这是我们接下来要做的。save()方法适用于ModelForm,但不适用于Form实例,因为它们未链接到任何模型。
-
我们将当前帖子和刚刚创建的评论对象关联起来:
new_comment.post = post
-
最后,我们使用以下代码将新注释保存到数据库中:
new_comment.save()
我们的视图现在可以显示和处理新的注释了。
2.4 为帖子详情页模板添加显示和提交评论的区域
我们已经创建了管理帖子评论的功能(即视图函数)。现在我们需要调整post_detail.html模板以执行以下操作:
- 显示帖子的评论总数
- 显示评论列表
- 显示用户添加新评论的表单
首先,我们将添加评论总数。打开blog_detail.html模板并在内容块中附加以下代码:
{% with comments.count as total_comments %}
<h2>
{{ total_comments }} comment{{ total_comments|pluralize }}
</h2>
{% endwith %}
我们使用的是Django的模板语言,可能不同后端语言对应框架的模板语言都是不一样的。需要注意的是,Django模板语言不使用括号来调用方法,你可以使用类似变量名.count
来得到一个容器包含的元素数量。{%with%}标记允许我们将一个值分配给一个新变量,该变量将在{%endwith%}}标记之前可用。
{%with%}模板标记有助于避免多次访问数据库或访问昂贵的方法。
我们使用复数模板过滤器根据total_comments值显示单词注释的复数后缀。模板过滤器将其应用的变量的值作为输入,并返回计算值。我们将在第3章“扩展博客应用程序”中讨论模板过滤器。
如果值不同于1,则复数模板过滤器显示“s”。前面的文本将呈现为0条注释、1条注释或N条注释。Django包含大量的模板标签和过滤器,可以帮助您以您想要的方式显示信息。
现在,让我们列出评论列表。在前面的代码后将以下行附加到模板:
{% for comment in comments %}
<div class="comment">
<p class="info">
Comment {{ forloop.counter }} by {{ comment.name }}{{ comment.created }}
</p>
{{ comment.body|linebreaks }}
</div>
{% empty %}
<p>There are no comments yet.</p>
{% endfor %}
我们使用{%for%}模板标记循环遍历注释。如果评论列表为空,我们将显示一条默认消息,告诉我们的用户还没有对此文章的评论。我们使用{{forloop.counter}}变量枚举注释,该变量包含每个迭代中的循环计数器。然后,我们显示发布评论的用户的姓名、日期和评论正文。
最后,您需要呈现表单或在成功提交表单时显示成功消息。在上述代码的正下方添加以下行:
{% if new_comment %}
<h2>Your comment has been added.</h2>
{% else %}
<h2>Add a new comment</h2>
<form action="." method="post">
{{ comment_form.as_p }}
{% csrf_token %}
<p><input type="submit" value="Add comment"></p>
</form>
{% endif %}
代码非常简单:如果new_comment对象存在,我们将显示一条成功消息,因为注释已成功创建。否则,我们为每个字段呈现带有段落<p>元素的表单,并包含POST请求所需的CSRF令牌。
打开http://127.0.0.1:8000/blog/在浏览器中,单击文章标题以查看其详细信息页面。您将看到以下内容:
使用表单添加一些评论。它们应该按时间顺序显示在您的帖子下面,如下所示:
打开http://127.0.0.1:8000/admin/blog/comment/在浏览器中。您将看到带有您创建的评论列表的管理页面。单击其中一个按钮进行编辑,取消选中“活动”复选框,然后单击“保存”按钮。您将再次重定向到评论列表,“活动”列将显示该评论的非活动图标。它应该像下面截图中的第一条评论:
如果返回到文章详细信息视图,您将注意到删除的评论不再显示;它也不计入评论总数。由于活动字段,您可以停用不适当的评论,并避免在帖子中显示它们。
3 使用第三方插件 为帖子自动添加标签
在实现注释系统之后,我们将新增一个功能来为我们的帖子添加标签。我们将通过在项目中集成第三方Django标记应用程序来实现这一点。django-taggit是一个可重用的应用程序,它主要为您提供一个Tag模型和一个管理器,可以轻松地向任何模型添加标记。您可以在https://github.com/alex/django-taggit.学到更多关于此插件的使用方法。
首先,您需要通过pip安装django-taggit,运行以下命令:
pip install django-taggit==0.17.1
然后打开mysite项目的settings.py文件,将taggit添加到INSTALLED_APPS列表中,如下所示:
INSTALLED_APPS = (
# ...
'blog',
'taggit',
)
打开blog应用程序的models.py文件,使用以下代码将django-taggit提供的TagableManager()管理器添加到Post模型中:
from taggit.managers import TaggableManager
class Post(models.Model):
# ...
tags = TaggableManager()
TaggableManager()管理器将允许您添加、检索和删除Post对象中的标签。
按顺序运行以下命令为模型生成迁移文件并执行迁移:
python manage.py makemigrations blog
python manage.py migrate
您的数据库现在可以使用django-taggit模型了。使用命令python manage.py shell
打开终端,学习如何使用标记管理器。首先,我们检索一篇帖子(ID为1的帖子):
>>> from blog.models import Post
>>> post = Post.objects.get(id=1)
然后向其添加一些标签,并检索其标签以检查是否已成功添加:
>>> post.tags.add('music', 'jazz', 'django')
>>> post.tags.all()
[<Tag: jazz>, <Tag: django>, <Tag: music>]
Finally, remove a tag and check the list of tags again:
最后,删除标记并再次检查标记列表:
>>> post.tags.remove('django')
>>> post.tags.all()
[<Tag: jazz>, <Tag: music>]
这很容易,对吧?运行命令python manage.py runserver
以再次启动开发服务器并打开http://127.0.0.1:8000/admin/taggit/tag/在浏览器中。您将看到带有taggit应用程序的Tag对象列表的管理页面:
进入到http://127.0.0.1:8000/admin/blog/post/然后单击一篇文章进行编辑。您将看到文章现在包含一个新的标签字段,如下所示,您可以在其中轻松编辑标签:
现在,我们将编辑博客帖子模板文件以显示标签。打开blog/post/list.html模板,并在文章标题下方添加以下html代码:
<p class="tags">Tags: {{ post.tags.all|join:", " }}</p>
连接模板过滤器用作Python字符串join()方法,将元素与给定字符串连接起来。打开http://127.0.0.1:8000/blog/在浏览器中。您应该看到每个帖子标题下的标签列表:
现在,我们将编辑post_list视图,添加标签过滤功能,可以让用户筛选出特定标签的所有帖子。打开blog应用程序的views.py文件,导入标记模型表单django-taggit,添加以下代码:
from taggit.models import Tag
def post_list(request, tag_slug=None):
object_list = Post.published.all()
tag = None
if tag_slug:
tag = get_object_or_404(Tag, slug=tag_slug)
object_list = object_list.filter(tags__in=[tag])
# ...
该视图现在的工作方式如下:
- 视图采用一个可选的tag_slug参数,该参数具有None默认值。此参数将出现在URL中。
- 在视图中,我们构建初始QuerySet,检索所有已发布的帖子,如果有给定的标记段,我们使用get_object_or_404()快捷方式获取带有给定段的tag对象。
- 然后,我们根据包含给定标记的帖子过滤帖子列表。由于这是一个多对多的关系,我们必须根据给定列表中包含的标记进行过滤,在我们的例子中,该列表只包含一个元素。
请记住,Queryset是惰性查询的。只有当我们在呈现模板时循环帖子列表时,才会评估检索帖子的QuerySet。
最后,修改视图底部的render()函数,将标记变量传递给模板。视图最终应该如下所示:
def post_list(request, tag_slug=None):
object_list = Post.published.all()
tag = None
if tag_slug:
tag = get_object_or_404(Tag, slug=tag_slug)
object_list = object_list.filter(tags__in=[tag])
paginator = Paginator(object_list, 3) # 3 posts in each page
page = request.GET.get('page')
try:
posts = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer deliver the first page
posts = paginator.page(1)
except EmptyPage:
# If page is out of range deliver last page of results
posts = paginator.page(paginator.num_pages)
return render(
request,
'blog/post/list.html',
{
'page': page,
'posts': posts,
'tag': tag
}
)
打开blog应用程序的urls.py文件,注释掉基于类的PostListView URL模式,并取消注释post_list视图,如下所示:
url(r'^$', views.post_list, name='post_list'),
# url(r'^$', views.PostListView.as_view(), name='post_list'),
添加以下附加URL模式以按标签筛选帖子:
url(r'^tag/(?P<tag_slug>[-\w]+)/$', views.post_list, name='post_list_by_tag'),
正如您所看到的,这两种模式都指向相同的视图,但我们对它们的命名不同。第一个模式将在没有任何可选参数的情况下调用post_list视图,而第二个模式将使用tag_slug参数调用视图。
由于我们使用的是post_list视图,因此编辑blog/post/list.html模板并修改分页以使用posts对象,如下所示:
{% include "pagination.html" with page=posts %}
在{%for%}循环上方添加以下行:
{% if tag %}
<h2>Posts tagged with "{{ tag.name }}"</h2>
{% endif %}
如果用户正在访问博客,他将看到所有帖子的列表。如果他通过带有特定标签的帖子进行过滤,他将看到这些信息。现在,将标记的显示方式更改为:
<p class="tags">
Tags:
{% for tag in post.tags.all %}
<a href="{% url "blog:post_list_by_tag" tag.slug %}">
{{ tag.name }}
</a>
{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
现在,我们循环浏览一个帖子的所有标签,显示一个指向URL的自定义链接,以根据该标签过滤帖子。我们使用{%URL“blog:post_list_by_tag”tag.slug%}构建URL,使用URL名称和标记slug作为参数。我们用逗号分隔标签。
打开http://127.0.0.1:8000/blog/在浏览器中,单击任何标记链接。您将看到该标签下的帖子列表,如下所示:
4 按相似性检索帖子
现在我们已经为博客文章添加了标签,我们可以用它们做很多有趣的事情。使用标签,我们可以很好地分类我们的博客文章。关于类似主题的帖子将有几个共同的标签。我们将构建一个功能,通过它们共享的标签数量来显示类似的帖子。这样,当用户阅读帖子时,我们可以建议他们阅读其他相关帖子。
为了检索与特定帖子类似的帖子,我们需要:
- 检索当前帖子的所有标签。
- 获取所有带有这些标签的帖子。
- 从该列表中排除当前帖子,以避免推荐同一帖子。
- 根据与当前帖子共享的标签数量排序结果。
- 如果两个或多个帖子具有相同数量的标签,则推荐最近的帖子。
- 将查询限制为我们希望推荐的帖子数量。
这些步骤被转换为一个复杂的QuerySet,我们将在post_detail视图中包含它。打开blog应用程序的views.py文件,并在其顶部添加以下导入:
from django.db.models import Count
这是Django ORM的Count聚合函数。此函数将允许我们执行汇总计数。然后在post_detail视图中的render()函数之前添加以下行:
# List of similar posts
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]
上述代码含义如下:
- 我们检索当前帖子的标签的ID列表。values_list()返回带有给定字段值的QuerySet。我们将其传递为flat=True,以获得类似[1,2,3,…]的展开列表。
- 我们得到所有包含这些标签的帖子,不包括当前帖子。
- 我们使用Count聚合函数生成计算字段samettags,包含与查询的所有标记共享的标记数。
- 我们根据共享标签的数量(后代顺序)和发布对结果进行排序,以首先显示具有相同数量共享标签的帖子的最近帖子。我们对结果进行切片,以仅检索前四个帖子。
将similar_posts对象添加到render()函数的上下文字典中,如下所示:
return render(
request,
'blog/post/detail.html',
{
'post': post,
'comments': comments,
'comment_form': comment_form,
'similar_posts': similar_posts
}
)
现在,编辑blog/post/detail.html模板,并在post注释列表之前添加以下代码:
<h2>Similar posts</h2>
{% for post in similar_posts %}
<p>
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</p>
{% empty %}
There are no similar posts yet.
{% endfor %}
还建议您将标签列表添加到帖子详细信息模板中,方法与我们在帖子列表模板中所做的相同。现在,您的文章详细信息页面应该如下所示:
您成功地向用户推荐了类似的帖子。django-taggit还包括一个类似的objects()管理器,您可以使用它通过共享标记检索对象。您可以在http://django-taggit.readthedocs.org/en/latest/api.html。
总结
在本章中,您学习了如何使用Django表单和模型表单。您创建了一个通过电子邮件共享网站内容的系统,并为您的博客创建了评论系统。您在博客文章中添加了标记,集成了一个可重用的应用程序,并构建了复杂的QuerySet以按相似性检索对象。
在下一章中,您将学习如何创建自定义模板标记和过滤器。您还将为您的博客文章构建自定义站点地图和提要,并将高级搜索引擎集成到应用程序中。