4.6 多样化显示
从功能上看,已经实现了对文章的常规操作。为了让网站功能更丰富,本节将实现另外三个功能,包括:
- 某用户发布的文章总数统计;
- 最新发布的文章列表;
- 评论最多的文章列表。
本来利用前面已经学习的知识是可以实现上述三个功能的,但为了让读者进一步学习Django,下面介绍一种新的方式实现上述功能。
本节要学习的新的方式就是“自定义模板标签”。
除有默认的模板标签外,Django为了应对更多复杂的操作,还允许开发者自定义模板标签。Django中一共有三种自定义模板标签类型,分别是simple_tag、inclusion_tag和assignment_tag。下面在实例中依次讲解这三种类型的自定义标签的使用方法,顺便完成前面提到的三个功能,如下图所示。

4.6.1 统计文章总数
在./article中创建一个目录templatetags,然后在里面创建两个文件__init__.py和article_tags.py,如下图所示。

目录templatetags的名称是Django默认的,必须用这个名称,不能更改。article_tags.py的名称则是笔者自己定义的,习惯上使用类似的这样的命名方式,在后面应用的时候也比较方便。在templatetags目录中,可以编写多个类似article_tags.py的文件,在模板中只要生命使用哪个文件中的标签即可。
下面的工作主要是在article_tags.py文件中定义模板标签。
先来编写实现统计文章总数的 模板标签,代码如下。
1 from django import template #① 2 register = template.Library() #② 3 from article.models import ArticlePost #③ 4 5 @register.simple_tag #④ 6 def total_articles(): 7 return ArticlePost.objects.count() #⑤
语句①引入django.template库,它里面包含着诸多与模板有关的类和方法等,语句②中的Library()就是其中一员。语句②创建了一个实例对象register,这个对象包含了simple_tag方法(及另外两种),它将用于自定义标签中。注意这里的变量名称要使用register,它是template.Library的一个实例。
语句③将有关数据模型类引入到本文件,请注意观察引入方式。
语句④通过装饰器,表明其下面的代码是自定义的simple_tag类型的标签。
语句⑤为返回文章对象数量的查询结果。特备要注意,这里不是视图函数,所以不要习惯性地在参数中写request了,也没有render()。
这样我们就定义好了一个标签。下面将这个标签应用到./templates/article/list/article_titles.html中,编辑此文件,在文件顶部写上{% load article_tags %}。
1 {% extends "base.html" %} 2 {% load article_tags %} #⑥
⑥就是我们要引入自定义标签的声明,article_tags应该与./article/templatetags目录中的标签所在的文件名称一致,这样Django就知道到该文件中查找所定义的标签(函数)。
在此文件中应用所定义的标签,代码如下。
1 <div class="row text-center vertical-middle-sm"> 2 <h1>阅读,丰富头脑,善化行为</h1> 3 <p>这里已经有{% total_articles %}篇文章供你阅读</p> #⑦ 4 </div>
语句⑦中的{% total_articles %}就是刚刚定义的标签,total_articles与article_tags.py文件中的函数名称一致。在这个位置,就可以显示本网站中一共有多少篇文章。
重启Django服务,不要忘记确认Redis服务已经运行,访问http://127.0.0.1:8000/article/list-article-titles/,显示效果如下图所示。

这里仅显示了总的文章数量,能不能显示某个作者一共发布了多少篇文章?
当然可以,还是使用自定义标签的方法。继续在./article/templatetags/article_tags.py文件中编写simple_tag类型的标签函数,代码如下。
1 @register.simple_tag 2 def author_total_articles(user): #⑧ 3 return user.article.count() #⑨
从语句⑧可以看出来,参数user应该是一个用户对象,而不是字符串。语句⑨之所以能够以user.article.count()方式得到该用户的文章总数,是因为在./article/models.py文件中定义ArticlePost数据模型类时,其中属性author = models.ForeignKey(User,on_delete=models.CASCADE,related_name="article"),又一次体现了related_name的作用。
标签定义好之后,编辑模板文件./templates/article/list/author_articles.html,依然要在顶部引入{% load article_tags %},位置跟上面的模板位置一样。
然后在适当位置加入自定义的标签,代码如下。
1 <div> 2 <p>{{ user.username }}</p> 3 <p>共发表文章{% author_total_articles user %}篇</p> #⑩ 4 {% if userinfo %}
语句⑩就是这次增加的显示该作者发表文章数目的标签。重启Django服务,访问某作者文章列表页面,查看是否实现了效果,如下图所示。

图中所标记出来的就是刚刚增加的自定义标签的显示结果。
从上面的例子中可以看出,simple_tag类型的标签返回的是字符串,这里仅演示了simple_tag的基本引用。另外,在调试时要注意,当自定义标签后,需要将Django服务重新启动。
4.6.2 最新发布文章
汇总最新发布的文章这个功能也可以通过自定义标签实现,这次使用inclusion_tag来实现。
还是在./article/templatetags/article_tags.py中增加自定义标签的函数,代码如下。
1 @register.inclusion_tag('article/list/latest_articles.html') #① 2 def latest_articles(n=5): #② 3 latest_articles = ArticlePost.objects.order_by("-created")[:n] #③ 4 return {"latest_article":latest_articles} #④
语句①仍然使用装饰器来声明自定义的标签类型,只不过这次增加了参数,用('article/list/latest_articles.html')确定所渲染的模板文件。这是因为对于inclusion_tag类型的标签,返回语句④样式的字典类型的数据,此数据被应用到语句①中所指定的模板文件中。
语句②定义了函数名称和参数,在具体使用此标签时会向这里传入参数。
语句③中的order_by()用于实现按照文章发布的时间倒序("-created")查询所有文章对象,并且根据参数(n)的值,获取排在前面的若干个(n个)。这里得到的值是一个以文章对象为元素组成的列表。
这个函数写好之后,就在./template/article/list中创建语句①所指定的模板文件latest_articles.html,其代码如下。
1 <ul> 2 {% for article in latest_articles %} #⑤ 3 <li> 4 <a href="{{ article.get_url_path }}">{{ article.title }}</a> #⑥ 5 </li> 6 {% endfor % } 7 </ul>
语句⑤使用自定义标签函数latest_articles()所返回的字典数据的键(Key)latest_articles,它的值是一个由文章对象组成的列表。
语句⑥中的{{ article.get_url_path }}来源与./article/models.py文件中ArticlePost类里面的get_url_path()方法。
上述工作完成后,算是写好了一个inclusion_tag类似的自定义标签。下面要在./templates/article/list/article_detail.html模板文件中使用这个标签,首先在此文件顶部引入标签{% load article_tags %}。
已经有了“最受欢迎文章”栏目,现在要在其下面显示“最新文章”栏目,于是增加如下代码。
1 <div class="col-md-3"> 2 <p class="text-center"><h3>最受欢迎文章</h3></p> 3 <ol> 4 {% for article_rank in most_viewed %} 5 <li> 6 <a href="{{ article_rank.get_url_path }}">{{ article_rank.title }}</a> 7 </li> 8 {% endfor %} 9 </ol> 10 <hr> 11 <p class="text-center"><h3>最新文章</h3></p> 12 {% latest_articles 4 %} #⑦ 13 </div>
语句⑦中使用自定义标签latest_articles,后面的数字是向这个自定义标签函数传入的参数值,即显示最新的4篇文章。
重新启动Django服务,然后访问某篇文章,应该出现如下图所示的效果。

从上面的过程不难看出,inclusion_tag类型的标签实际上是返回一个模板(装饰器函数中指定的模板文件)。当调用该标签名称时,显示标签装饰器函数中所指定的模板文件的显示方式。
4.6.3 评论最多的文章
下面要找出评论最多的文章,并且将标题列出来,还要得到一系列的文章对象。虽然不使用自定义标签也能够实现,但这里为了学习另外一种类型(assignment_tag)的自定义标签,我们还是承接前面的做法,在./article/templatetags/article_tags.py中编写自定义标签的有关函数,代码如下。
1 from django.db.models import Count 2 3 @register.assignment_tag #① 4 def most_commented_articles(n=3): #② 5 return ArticlePost.objects.annotate(total_comments=Count('comments')).order_by("-total_comments")[:n] #③
语句①依然使用装饰器来声明下面的函数是自定义的assignment_tag类型的标签函数。
语句②中以参数的形式说明显示评论最多的文章数量,这里默认显示评论醉倒的前3篇。
语句③是一条复杂的语句,为了能够清晰地理解,我们可以先打开一个终端,在交互模式中分步研究(面对一个复杂的东西,分解、隔离是一种常用的方法)。
还记得怎样进入到Django的交互模式吗?进入项目目录,执行python manage.py shell,即可进入交互模式。

c = ArticlePost.objects.annotate(total_comments=Count("comments"))中annotate()函数的作用是个QuerySet中的每个对象添加注释。如果仅这么说,估计读者会感到迷茫,可是如果我去搜索,会发现不少文章中都是这样解释的,更甚者直接说annotate()函数的作用就是annotate(注释)。语言是思维的工具,annotate的汉语解释一般就是“注释”,但是在这里我们如何描述其作用呢?看本书慢慢道来。
语句④其实还可以继续分解。

articles就是查询到的所有文章对象序列,然后对这个文章序列进行注释。

现在是不是已经逐步理解了?annotate()函数是要给查询到的文章对象进行标注。用什么标注呢?就是里面的参数Count("comments")。
Count("comments")的作用是给每篇文章的评论计数,这里的comments来自./article/models.py文件的Comment数据模型类中定义的article = models.ForeignKey(ArticlePoston_delete=models.CASCADE,,related_name="comments"),related_name再次实现了通过ArticlePost对象查询到其相关的Comment独享,所以Count("comments")得到的是ArticlePost对象关联的Comment对象的个数,将这个个数标注到前面得到的articles所对应的ArticlePost对象上,这就是语句④的含义。有点拗口,笔者是在没有能力用别的方式通俗且严谨地表述了。
接着对语句④的操作进行探索。


既然语句④得到的结果是由一个个ArticlePost对象组成的列表,并且每个ArticlePost对象都标注上了total_comments=Count("comments"),那么久可以理解为每个对象又有了totaL_COMMENTS属性,其实就是该ArticlePost对象所关联的Comment对象的数量,即评论的数量。因此,通过c[0].total_comments就得到了第一个ArticlePost对象的Comment对象数量(就是得到第一篇文章的评论数目)。
继续保持慢速咀嚼式的阅读。

c.order_by("-total_comments")语句⑤就是上述得到的序列排序,按照total_comments数量从大到小进行排序。读者可能发现结果与前面一致,这纯粹是一种巧合,完全可以去掉符号,像下面一样。

将上面的内容反复思考,并且在交互模式中进行尝试。
编辑./templates/article/list/article_detail.html文件,实现“最多评论文章”栏目的显示。将下面的代码写在“最新文章”栏目的代码块后面。
1 <hr> 2 <p class="text-center"><h3>最多评论文章</h3></p> 3 {% most_commented_articles as most_comments %} #⑤ 4 <ul> 5 {% for comment_article in most_comments %} #⑥ 6 <li> 7 <a href="{{ comment_article.get_url_path }}">{{ comment_article.title }}</a> 8 </li> 9 {% endfor %} 10 </ul>
语句⑤是必须的,即把自定义标签所得到的对象赋值给一个变量,然后在语句⑥中循环此变量。
保存所有修改文件,并且重启Django服务。访问某篇文章,查看效果,如下图所示。

自定义标签能够让我们在前端很方便地实现某些渲染结果。对于前端,还可以做其他自定义的东西。
4.6.4 自定义模板选择器
对于模板选择器,在前面曾经遇到过,比如显示文章前多少个字作为摘要,使用了{{article.body|slice:'70'|linebreaks}},这就是一个模板选择器(filter有时也被翻译为“过滤器”)。模板选择器的基本使用方式是{{ variable|filter }}或者{{ variable|filter:"foo" }}。其中,variable代表传入模板的对象的变量,filter是所传入的值按照一定的规则进行选择对的选择器,foo则是给选择器传入的参数。还可以同时使用多个选择器,样式为{{ variable|filter1|filter2 }}.
下面要做一个选择器,用来实现在模板页面将Markdown语法解析为HTML代码,从而在页面中显示。虽然类似的工作我们在前面已经做过了,但是那时用的纯粹的前端JavaScript插件完成的,而这里要使用自定义的模板选择器。
安装Markdown第三方库,这里我们要在项目中使用的。

然后在./article/templatetags/article_tags.py文件中编写将Markdown编码转换为HTML代码的选择器函数。
在文件顶部引入如下内容。
1 from django.utils.safestring import mark_safe 2 import markdown
Django对于模板上显示字符串这个事情是非常谨慎的,比如字符串中包含“>”符号,是显示为大于号还是显示为HTML中常见的诸如<p>左边的符号呢?django.utils.safestring的作用就是将字符串编程为“safe strings”,即实实在在的字符,而不是HTML代码,那么这种情况下“>”就是大于号了。mark_safe()方法返回的就是这种“safe strings”。
1 @register.filter(name='markdown') #① 2 def markdown_filter(text): #② 3 return mark_safe(markdown.markdown(text)) #③
从语句②开始定义选择器函数,其参数text就是等待传入的字符串。
语句③返回的是"safe srting",并且经过markdown()方法之后返回的结果,使Django模板能够顺利接收。
语句①的作用是重命名语句②中的选择器函数,即将名字由markdown_filter修改为markdown。我们在语句②中之所以没有使用markdown这个名字,是因为在前面引入的第三方库中有import markdown,要避免语句②的函数名和引入的第三方库的名字冲突,所以语句②使用了markdown_filter的名字,否则语句③中的markdown.markdown(text)会报错。但是,在模板中使用这个选择器时,还是要按照习惯使用markdown这个名字。因此,在函数前面增加语句①,对语句②重命名。
接下来就是在Django模板中使用这个选择器了。这里仅演示一个页面,其他部分读者可以自行修改。打开./templates/article/list/article_detail.html文件,找到如下显示文章内容的代码。
1 <textarea id="append-test" style="display:none;"> 2 {{ article.body }} 3 </textarea>
将这部分代码用下面的代码替换。
1 {{ article.body | markdown }}
完成之后看一下效果。检查Django和Redis数据库服务是否已经运行,确认运行之后,在浏览器中访问某篇文章,会看到与前面一样的显示效果。这就是使用了Markdown选择器的结果。

4.6.5 知识点
1、表单:CSRF
只要提交表单,就要注意CSRF问题,为什么这个问题如此受关注呢?
CSRF(Cross-Site Request Forgery)意为跨站请求伪造,也被称为one click arrack/aession riding,缩写CSRF/XSRF。
对于CSRF,可以简单地理解为“有人用你的身份,并以你的名义发送恶意请求”,表现为:以你的名义发送邮件、消息,盗取你的账号,购买商品,虚拟货币转账等。它会造成个人隐私泄露,乃至财产安全等问题。
幸亏,在经过了深刻的校训之后,开发者已经认识到了CSRF的危害性,在Django中自动对此进行了防范。因此,在提交表单时,必须注意这个问题(如本项目所示)。就POST和 GET两种请求方式而言,GET毫无安全性,因此只用做数据的查询;而一旦涉及数据的添加、修改、删除,一定要采用POST方式。所以,CSRF也不是多么可怕,只要网站涉及符合规范,CSRF攻击便无从谈起。
2、表单:显示
显示表单可以用HTML代码中的<form>和<input>实现,也可以使用{{ form }}变量。


默认有<label>标签,并且名称和字段一样,只不过首字母大写,后面默认增加一个冒号。
<input>中的id也是根据默认原则生成的。在默认状态下手,每个字段都不能为空,所以有required参数。

在form2这个表单类实例中,使用了参数auto_id和label_suffix,对HTML代码中的<label>和<input>中的id名称进行了控制。
在模板页面中,可以通过{{ form.as_p }}或者{{ form.as_ul }}的格式显示,默认为{{ form.as_ul }}的格式显示,默认为{{ form.as_table }}方式。
- {{ form.as_table }}:得到的是用<tr>显示的效果。
- {{ form.as_p }}:得到<p>包裹的HTML代码。
- {{ form.as_ul }}:得到按照<li>显示的效果。
在模板页面中,还可以使用渲染对象的各种报错信息。这方面内容官方文档中有详细描述,请读者自行阅读。
其实,刚才建立的form也是一个对象,可以按照Python中对对象的理解,理解表单的方法和属性。

浙公网安备 33010602011771号