我们博客的文章(Post)模型除了通过 ForeignKey 关联了 Category(分类)外,还通过 ManyToMany 关联了 Tag(标签)。在我们的 Demo 的侧边栏可以看到一个标签云效果的全部标签列表。现在我们来给博客实现这个效果,让 Django 从数据库中获取全部标签的数据列表,然后在模板中显示它们,并且点击相应的标签,就可以显示该标签下的全部文章列表。
获取标签列表
很明显的能够发现,标签和之前我们开发的分类功能是十分类似的,唯一的不同是一篇文章(Post)只能指定一个分类,但是却可以指定多个标签。回顾一下我们获取博客侧边栏的分类列表时是怎么做的呢?我们自定义了一个模板标签函数 get_categories
。具体的代码是:
blog/templatetags/blog_tags.py
@register.simple_tag
def get_categories():
# 记得在顶部引入 count 函数
return Category.objects.annotate(num_posts=Count('post')).filter(num_posts__gt=0)
然后我们在模板中使用这个模板标签获取到文章数大于 0 的分类列表,并渲染显示它。
templates/base.html
<div class="widget widget-category">
<h3 class="widget-title">分类</h3>
{% get_categories as category_list %}
<ul>
{% for category in category_list %}
<li>
<a href="{% url 'blog:category' category.pk %}">{{ category.name }}
<span class="post-count">({{ category.num_posts }})</span>
</a>
</li>
{% empty %}
暂无分类!
{% endfor %}
</ul>
</div>
事实上,标签云的实现方法和分类列表完全一样。我们定义一个 get_tags
模板标签,获取到文章数大于 0 的标签列表,然后在模板中渲染显示它。代码如下,你可以看到代码和分类功能的代码几乎是一样的,只是把 Category(分类)换成了 Tag(标签)。
blog/templatetags/blog_tags.py
from ..models import Post, Category, Tag
@register.simple_tag
def get_tags():
# 记得在顶部引入 Tag model
return Tag.objects.annotate(num_posts=Count('post')).filter(num_posts__gt=0)
然后在模板中循环显示这些标签:
templates/base.html
<div class="widget widget-tag-cloud">
<h3 class="widget-title">标签云</h3>
{% get_tags as tag_list %}
<ul>
{% for tag in tag_list %}
<li>
<a href="#">{{ tag.name }}</a>
</li>
{% empty %}
暂无标签!
{% endfor %}
</ul>
</div>
关于自定义模板标签以及使用方法,请参考 页面侧边栏:使用自定义模板标签。
OK 了!在 Django 后台添加一些标签,并且为发表的文章指定这些标签,就可以看到博客的侧边栏显示出这些标签了。
显示某个标签下的文章列表
同样的,显示某个标签下的文章列表和我们之前做的点击分类后显示该分类下的文章列表是一样的。回顾一下显示分类下的文章列表时的做法,经典的 Django 三部曲。首先是定义视图函数,然后编写模板文件,最后将视图函数和 URL 模式绑定。标签和分类是完全一样的步骤,因此稍微修改一下分类相关的代码就可以用于标签了。
标签视图函数
blog/views.py
class TagView(ListView):
model = Post
template_name = 'blog/index.html'
context_object_name = 'post_list'
def get_queryset(self):
tag = get_object_or_404(Tag, pk=self.kwargs.get('pk'))
return super(TagView, self).get_queryset().filter(tags=tag)
和 CategoryView
一样,我们使用了类视图。代码几乎和 CategoryView
是一样的,因此这里不再详细说明,具体请参考 CategoryView
部分的代码和说明 基于类的通用视图:ListView 和 DetailView。
模板
由于显示的是文章列表,因此我们直接复用了用于显示文章列表的 index.html 模板。
绑定 URL
同样的,URL 模式和分类也是完全类似的,这里不再多做解释:
blog/urls.py
app_name = 'blog'
urlpatterns = [
# 其它 URL 模式...
url(r'^category/(?P<pk>[0-9]+)/$', views.CategoryView.as_view(), name='category'),
url(r'^tag/(?P<pk>[0-9]+)/$', views.TagView.as_view(), name='tag'),
]
设置标签跳转链接
设置一下标签的超链接,这样点击标签后就可以跳转到该标签下的文章列表页面了。这里用到了 {% url %} 模板标签,其用法和分类的超链接一模一样,这里就不再过多解释,请参考上边给出的一些文章。
<a href="{% url 'blog:tag' tag.pk %}">{{ tag.name }}</a>
在文章详情页显示标签
上边获取的是全部标签的列表,当在文章详情页面时,我们希望显示的这篇文章所属的标签,具体该怎么做呢?这里我只说明几个关键的点,然后给出一个大致的实现思路。既然你已经通过教程学习到了这里,相信你对 Django 已经有了一定了解了,根据提示并稍加思考,相信你一定可以很好地完成这个功能。
首先来回顾一下文章(Post) 和标签(Tag)的模型:
blog/models.py
class Post(models.Model):
title = models.CharField(max_length=70)
body = models.TextField()
category = models.ForeignKey('Category')
tags = models.ManyToManyField(Tag, blank=True)
# 其它属性...
def __str__(self):
return self.title
class Tag(models.Model):
name = models.CharField(max_length=100)
可以看到 Post
下有一个 tags
属性,这个属性通过多对多的关系关联着 Tag
。回顾一下我们是如何获取某篇文章 post 对应的分类的?我们直接通过访问 category
属性来获得分类,即通过 post.categoty
来获取 post 的分类。那要获得某篇文章 post 对应的标签,可不可以通过 post.tags
来获取呢?思路是正确的,不过和获取分类稍微有点不同。由于 Post
和 Categoty
是一对多的关系(ForeignKey),所以 post.categoty
是唯一的。但是 Post
和 Tag
是多对多的关系(ManyToManyField),那么 post.tags
就有可能有多个值。所以 Django 没有让 post.tags
返回全部标签,而是返回了一个模型管理器(类似于 objects),然后我们可以调用这个模型管理器的 all
方法,来获取这篇 post 下的全部标签列表了。
因此大体思路就清晰了,我们可以在文章的详情页模板中,通过 post.tags.all()
获取到这篇 post 下的标签列表。但是要注意模板中调用方法需要去掉括号,类似于:
{% for tag in post.tags.all %}
{{ tag.name }}
{% endfor %}
具体的代码编写就当做练习交给你了,如果自己实在无法独立解决,可以参考下方给出的 GitHub 分支中的相关代码。
总结
本章节的代码位于:Step22: tag cloud。
如果遇到问题,请通过下面的方式寻求帮助。
- 简单问题在下方评论区留言。
- 在 Pythonzhcn 社区的新手问答版块 发布帖子。
-- EOF --
我是用 inclusion_tag 实现的,比较简单。
我是直接在base页面中标签云的相关代码上加了extends, 然后在detail页面最后面覆盖的, 也有效
<div class="entry-content clearfix">
{{ article.body|safe }}
<div class="widget widget-tag-cloud">
<ul>
标签:
{% for tag in article.tags.all %}
<li><a href="{% url 'blog:tag' tag.pk %}"># {{ tag.name }}</a></li>
{% endfor %}
</ul>
</div>
</div>
这段在detail里的代码我已经对比了好多遍了,但是在详情页里就是显示不出标签,其他页面的显示,跳转都没问题,就是这段不行。救命啊有谁知道为什么显示不出来呢,有多少种原因呢?
还有我想请问下怎么把models里的类属性字段设置成只读,那个阅读量可以手动改啊很尴尬
你可以检查一下你的模板继承是否存在问题,代码本身并没有错误。
设置只读可以在field中加入 editable=True。
1.谢谢博主百忙之中的回复,我刚刚已经检查了模板继承,并没有错,而且如果模板继承有错,从一开始整个详情页按理来说就不会正常显示了,但除了文章标签显示不出,其他一切正常。
2.还有那个添加只读是这样么:
read = models.PositiveIntegerField(default=0,editable=True)
我刚学很多不懂,可能这个问题很白痴,哈哈,但是这么添好像还是木有用0.0,还是可以改。
3.还有我发现我的Django后台在增加文章时标签并不能多选呢,我退回去看了下发现博主的是按control就行,我的就不行,我用的是Django2.0,跟着有关系么?
诶呀那个标签不能多选我发现哪不对了,哈哈,顺手写成外键了,没写多对多关系,不过另外两个还是没解决0.0
嘻嘻,博主没事儿了!我把文章和标签的关系改成多对多之后,标签就出来了!!这可太神奇了,为什么呢?害的我找了一下午零一上午的bug TAT
还有那个设置为只读好像不怎么起作用=。=
访问通过外键和多对多关联的对象的方法是不同的
sorry,editable=False才是不允许编辑,从字面意思可以读出来
好了好了,谢谢博主
views.py
class TagView(ListView): model = Article template_name = 'tp/list.html' context_object_name = 'post_list' def get_queryset(self): tag = get_object_or_404(Tag,pk=self.kwargs.get('pk')) return super(TagView,self).get_queryset().filter(tag=tag)
tags.py
# 标签云@register.simple_tagdef get_tags(): return Tag.objects.annotate(num_posts=Count('article')).filter(num_posts__gt=0)
list.html
<h3>标签云</h3><div class="widget-sentence-content"> {% get_tags as post_list %} <ul class="plinks ptags"> {% for tag in post_list %} <li><a href="{% url 'tp:tag' tag.pk %}" title="{{ tag.name }}" draggable="false">{{ tag.name }} <span class="badge">{{ tag.num_posts }}</span></a></li> {% empty %} 暂无标签 {% endfor %} </ul></div>
按照博主的方式写的。可点击标签后没有文章显示? 这个是什么问题造成的呢? 麻烦博主帮助解决下,谢谢
你可以使用富文本编辑器输入代码块,这样看起来不会这么乱。
我要怎么登出账号...
作者你好,我问个问题,admin提交文章时,tag为啥无法选择呢,强制是全部选择的,有几个tag就强制选择几个tag?
已解决。。。
麻烦问下你出现这种情况的原因是什么,怎么解决的
我记得是个小问题,具体怎么操作的我忘了,你检查下你的代码吧
class PostAdmin(admin.ModelAdmin):
filter_horizontal = ('tags',)
.....
return super(TagView, self).get_queryset().filter(tags=tag)
这个tags字段是多对多的关系 这样直接用等号匹配 会不会有问题?
请问博主,在博客列表页,比如 首页,附加每篇博文的标签该如何做?给个思路就好,谢谢.
见笑了,跟detail页面一样调用就好.
博主,我已经在文章详情页模板写上了
{% for tag in post.tags.all %}
{{ tag.name }}
{% endfor %}
为什么文章详情页标签还是显示的主页的标签云呢?
极有可能是你的主页内容和详情内容模板搞混。
我的确是在detail.html上改的,但是它显示的好像还是base.html的内容
已经解决了,谢谢博主。提个小建议,教程里面关于前端模板的修改可以写详细一点,我对比github的代码发现自己写的还是有些差别的。
首先感谢博主的分享,博主的教程细致入微,每个细节都点到了,受益匪浅。
其次有个问题要请教:本文中类视图TagView继承自ListView,为什么不选择继承自己实现的IndexView呢?这样不是还能具备分页功能吗。
No module named 'mdx_markdown'
突然进文章详情报这个错误了
还有,我发现一个问题,就是视图函数里面filter后面应该是(tag=tag),这样才行
取决你模型中tag的命名。我在 post 中标签命名为 tags,你可能少了 s,做相应调整即可。
嗯嗯,对
博主,代码是一样的,我打开详情页,页面的标签里面却没有内容,是哪出了问题
通常来说极有可能是你的代码和示例项目的不同。再仔细对比一下,尤其是不起眼的地方。如果依然发现不了问题,请附上相关代码。
找到问题了,谢谢博主
请问这个return super(TagView, self).get_queryset().filter(tags=tag)中的tags=tag这个关键字参数,key可以是任意值么?比如abc=tag
不可以的,必须是你定义在model 中的属性名。
大神,有些疑惑,为什么我本地的其他静态样式无法用呢?
一用就变成光板网页,什么样式都没有
你可能需要 static 标签以及正确放置静态文件路径
大神,您是四川的吗?我也是..我看到第五章: 博客首页视图了,,,写的简单明了.太厉害了.
谢谢您.
嗯,我现在在四川读书,不客气,感谢你的阅读!