Django搭建个人博客:文章标签功能

5910阅读 · 26评论 · 2019/02/12发布  

“标签”是作者从文章中提取的核心词汇,其他用户可以通过标签快速了解文章的关注点。每一篇文章的标签可能都不一样,并且还可能拥有多个标签,这是与栏目功能不同的。

好在标签功能也有优秀的三方库:Django-taggit,省得自己动手设计了。快速开发就是这样,能“借用”就不要自己重复劳动。

安装及设置

首先在虚拟环境中安装Django-taggit:

pip install django-taggit

安装成功后,修改项目设置以添加库:

my_blog/settings.py

...
INSTALLED_APPS = [
    ...
    'taggit',
]
...

修改文章模型

标签是文章Model的属性,因此需要修改文章模型。

需要注意的是标签引用的不是内置字段,而是库中的TaggableManager,它是处理多对多关系的管理器:

article/models.py

...

# Django-taggit
from taggit.managers import TaggableManager

...
class ArticlePost(models.Model):
    ...

    # 文章标签
    tags = TaggableManager(blank=True)

    ...

然后记得数据迁移

带标签文章的发表

修改文章的表单类,让其能够提交标签字段:

article/forms.py

...
class ArticlePostForm(forms.ModelForm):
    class Meta:
        ...
        fields = ('title', 'body', 'tags')

然后修改发表文章的视图,保存POST中的标签:

article/views.py

...
def article_create(request):
    # 已有代码
    if request.method == "POST":
        article_post_form = ArticlePostForm(data=request.POST)
        if article_post_form.is_valid():
            new_article = article_post_form.save(commit=False)
            ...
            new_article.save()

            # 新增代码,保存 tags 的多对多关系
            article_post_form.save_m2m()

            ...

需要注意的是,如果提交的表单使用了commit=False选项,则必须调用save_m2m()才能正确的保存标签,就像普通的多对多关系一样。

最后就是在发表文章的模板中添加标签的表单项了:

templates/article/create.html

...
<!-- 提交文章的表单 -->
<form method="post" action=".">
    ...

    <!-- 文章标签 -->
    <div class="form-group">
        <label for="tags">标签</label>
        <input type="text" 
               class="form-control col-3" 
               id="tags" 
               name="tags"
        >
    </div>

    ...
</form>
...

运行服务器,就可以在发表页面看到效果了:

多个标签最好用英文逗号进行分隔。中文逗号有的版本会报错,干脆就不要去使用了。

列表中显示标签

虽然保存标签的功能已经实现了,还得把它显示出来才行。

显示标签最常用的位置是在文章列表中,方便用户筛选感兴趣的文章。

修改文章列表的模板,将标签显示出来:

templates/article/list.html

...
<!-- 栏目 -->
...

<!-- 标签 -->
<span>
    {% for tag in article.tags.all %}
        <a href="#"
           class="badge badge-secondary" 
        >
            {{ tag }}
        </a>
    {% endfor %}
</span>

...

链接中的class中是Bootstrap定义的徽章样式

插入位置紧靠在栏目按钮的后面。当然你想放到其他位置也是完全可以的。

刷新列表页面看看效果:

标签过滤

有时候用户想搜索带有某一个标签的所有文章,现在就来做这个功能。

与搜索功能一样,只需要调取数据时用filter()方法过滤结果就可以了。

修改<a>标签中的href,使其带有tag参数返回到View中:

templates/article/list.html

...

<!-- 标签 -->
<span>
    {% for tag in article.tags.all %}
        <a href="{% url 'article:article_list' %}?tag={{ tag }}"
           class="badge badge-secondary" 
        >
            {{ tag }}
        </a>
    {% endfor %}
</span>

...

然后在View中取得tag的值,并进行搜索。

下面的代码将article_list()函数完整写出来了(包括上一章末尾没讲的栏目查询),方便读者比对。

article/views.py

...
def article_list(request):
    # 从 url 中提取查询参数
    search = request.GET.get('search')
    order = request.GET.get('order')
    column = request.GET.get('column')
    tag = request.GET.get('tag')

    # 初始化查询集
    article_list = ArticlePost.objects.all()

    # 搜索查询集
    if search:
        article_list = article_list.filter(
            Q(title__icontains=search) |
            Q(body__icontains=search)
        )
    else:
        search = ''

    # 栏目查询集
    if column is not None and column.isdigit():
        article_list = article_list.filter(column=column)

    # 标签查询集
    if tag and tag != 'None':
        article_list = article_list.filter(tags__name__in=[tag])

    # 查询集排序
    if order == 'total_views':
        article_list = article_list.order_by('-total_views')

    paginator = Paginator(article_list, 3)
    page = request.GET.get('page')
    articles = paginator.get_page(page)

    # 需要传递给模板(templates)的对象
    context = {
        'articles': articles,
        'order': order,
        'search': search,
        'column': column,
        'tag': tag,
    }

    return render(request, 'article/list.html', context)

...

注意Django-taggit中标签过滤的写法:filter(tags__name__in=[tag]),意思是在tags字段中过滤nametag的数据条目。赋值的字符串tag用方括号包起来。

之所以这样写是因为Django-taggit还支持多标签的联合查询,比如:

Model.objects.filter(tags__name__in=["tag1", "tag2"])

为了实现带参数的交叉查询,还要将翻页等位置的href修改一下:

templates/article/list.html

...

<!-- 所有类似地方加上 tag 参数,如排序、翻页等 -->

<a href="{% url 'article:article_list' %}?search={{ search }}&column={{ column }}&tag={{ tag }}">
    最新
</a>

...

<a href="{% url 'article:article_list' %}?order=total_views&search={{ search }}&column={{ column }}&tag={{ tag }}">
    最热
</a>

...

<a href="?page=1&order={{ order }}&search={{ search }}&column={{ column }}&tag={{ tag }}" class="btn btn-success">
    &laquo; 1
</a>

<!-- 上面3条是举例,其他类似地方也请补充进去 -->
...

标签过滤功能就完成了。

Django-taggit更多的用法请阅读官方文档:Django-taggit

总结

本章学习了使用Django-taggit来完成标签功能。

在学习阶段,你可以不借助他人的轮子,自己实现功能:瞎折腾对掌握基础有很大帮助。

实际开发时,又分为两种情况:

  • 浅层需求某项通用功能,开发完成后改动不大:此类功能建议尽量使用轮子,加快开发效率。人生苦短,能节约的时间,一秒钟都不要浪费。
  • 需要大量定制化的功能,开发完成后需要频繁改动:此类功能因为经常对底层代码进行改动,与其在别人的代码上修修补补,还不如自己从头写了。自己的代码不仅熟悉,而且都是为定制化而生的。

到底如何选择,就根据你的喜欢进行斟酌了。





本文作者: 杜赛
发布时间: 2019年02月12日 - 21:30
最后更新: 2019年02月12日 - 21:30
知识共享许可协议   转载请保留原文链接及作者

鉴于沟通的低效,博主暂时关闭了评论功能。如需技术交流,欢迎加入 QQ 水友群提问、或通过页脚任意一种方式联系博主。


avatar
ax3353 么么哒! 9

请问博主,文章标签如何在update.html中支持修改啊,除了去admin后台修改外还有其他办法么,我研究了一天也没办法修改,添加标签没问题,但不能修改,如果方便的话,请赐教,感谢

1年前


avatar
杜赛 [博主] ax3353 么么哒! 6

taggit 提供了两个接口可以更新标签:

add(*tags)

# 例子
>>> Model.tags.all()
[]
>>> Model.tags.add("tag1", "tag2", "tag3")

这个接口添加新的标签到现有的数据中。

另一种方法:

set(*tags, clear=False)

# 例子
>>> Model.tags.set("newTag", clear=False)
  • clear=True:先会清空旧标签,然后添加新标签
  • clear=False:删除未声明的旧标签,添加新标签

剩下的就是在视图函数里调用合适的接口了。

官方文档有更详细的说明。

1年前


avatar
ax3353 杜赛 [博主] 么么哒! 7

真的很感谢博主能够抽出时间来解答我的问题,让我这种初学者有了更多的动力,感谢

1年前


avatar
杜赛 [博主] ax3353 么么哒! 8

不客气。有问题随时交流。

1年前


avatar
kengsley1993 ax3353 么么哒! 5

```python

article/view.py

...

article.tags.clear()
for tag in request.POST['tags'].split(','):
        if tag.strip():
              article.tags.add(tag.strip())

...

tags = ', '.join(t.name for t in article.tags.all())
context = { 'article': article, 'article_post_form': article_post_form, 'columns': columns, 'tags':tags }

```

6个月前


avatar
samshui kengsley1993 么么哒! 3

想知道下面的join的用意是什么呢

3个月前


avatar
Jiangyciauh-Dawn 么么哒! 8
本回复已被 Jiangyciauh-Dawn 删除

1年前


avatar
ac1864 么么哒! 7

1年前


avatar
南霁姑娘 么么哒! 7

博主,问个弱弱的问题,为什么要返回title、body、tags,而column不用返回

10个月前


avatar
杜赛 [博主] 南霁姑娘 么么哒! 5

返回不返回是看你需不需要用到,或者单纯为了方便。另外column有返回啊。

10个月前


avatar
南霁姑娘 杜赛 [博主] 么么哒! 5

我是说这里

class ArticlePostForm(forms.ModelForm):
    class Meta:
        ...
        fields = ('title', 'body', 'tags')

10个月前


avatar
杜赛 [博主] 南霁姑娘 么么哒! 6

要不要表单类处理实际上是比较灵活的。教程这里已经手动处理好column的赋值,所以就没有用表单类了

10个月前


avatar
1581779395@qq.com 么么哒! 4

博主你好!标签这里有两个问题:

1、是不是没有实现多标签添加

2、编辑文章是不是没有编辑标签

是不是自己完善?

8个月前


avatar
杜赛 [博主] 1581779395@qq.com 么么哒! 4

  • 多个标签之间用英文逗号分隔
  • 编辑标签在后面的文章有讨论(具体位置忘了),你也可以尝试自己查阅库的文档并实现。

8个月前


avatar
511248513 么么哒! 4

您好,我想请教下想获取所有文章的所有标签显示并对标签进行去重,需要怎么获取,类似于做一个标签栏的模块,这个包可以直接用么?

8个月前


avatar
杜赛 [博主] 511248513 么么哒! 4

taggit 没有提供直接的标签云功能。它的 api 请参考官方文档

可以尝试将django-taggitdjango-taggit-templatetags结合使用。按照第二个链接中文档的说明,使用几行模板很容易实现。

8个月前


avatar
511248513 杜赛 [博主] 么么哒! 4

感谢 ,另外想问下博主这些库你是从哪查到的laugh比如:需要啥如何查询?

8个月前


avatar
杜赛 [博主] 511248513 么么哒! 4

8个月前


avatar
weifan01 么么哒! 4
本回复已被 weifan01 删除

7个月前


avatar
andy0andy 么么哒! 3

大佬,为什么我测试删除文章是GET方式,刚写的时候能正常工作,现在是哪个环节出错了吗

article/detail.html

 · <a href="#" onclick="confirm_delete()">删除文章</a>
                {#            增加删除安全性#}
                            <form action="{% url 'article:article_safe_delete' article.id %}" method="POST" style="display:none;" id="safe_delete">
                                {% csrf_token %}
                                <button type="submit">发送</button>
                            </form>
                            <script>
{#                            删除文章弹窗#}
                                function confirm_delete() {
                                    layer.open({
                                        title:'确认删除',
                                        content:'确认删除该文章?',
                                        yes:function (index,layero) {
                                            location.href = "{% url 'article:article_safe_delete' article.id %}"
                                        }
                                    })
                                }

                            </script>

article/views    

def article_safe_delete(request,id):
        if request.method == 'POST':
            article = ArticlePost.objects.get(id=id)
            article.delete()
            return redirect(reverse('article:article_list'))
        else:
            print(request.method)
            return HttpResponse("可能crsf!!!")

5个月前


avatar
北元 么么哒! 3

请问博主,刚才测试了一下发现栏目和标签里的内容搜索不到

标题和正文里的内容能搜索到

也没弹出什么错误提示,搞不清楚为什么,是还需要设置哪里么?

还望指点一二,谢谢!

菜鸟一枚

4个月前


avatar
杜赛 [博主] 北元 么么哒! 3

标签功能是用的 taggit 吧?这是要用库提供的 api 的,不能当成文本来搜索。具体看官方文档

4个月前


avatar
北元 杜赛 [博主] 么么哒! 3

谢谢,搞清楚了,是通过点击标签对应的关键字来搜索,不是通过搜索框来搜索

4个月前


avatar
冰原燎炎 么么哒! 2

老师您好,我想问一下,在这章里你的article_list()视图函数是有栏目查询的,我参考着最热文章那一章自己改了一下模板里栏目的内容,代码如下:

{#  栏目#}
 {% if article.column %}
     <a href="{% url 'article:article_list' %}?column={{ column }}">
           <button type="button"
                   class="btn btn-sm mb-2
                   {% if article.column.title == 'Python' %}
                       btn-success
                   {% elif article.column.title == '名篇名作' %}
                       btn-danger
                   {% elif article.column.title == '个人日记' %}
                       btn-warning
                   {% endif %}
                                "
            >{{ article.column }}</button>
     </a>
{% endif %}

用a标签包裹了原来的button,href里填了url和column。但是测试的时候发现column的筛选没起作用,翻到后面几页之后点击栏目会跳转回第一页,改成?column={{ article.colume }}也不行。

麻烦老师有时间的时候帮我看看是少改了什么东西吗?

4个月前


avatar
杜赛 [博主] 冰原燎炎 么么哒! 2

  1. 检查 url 中的参数是否传入了视图
  2. 检查视图中的 filter 是否起到了效果

4个月前


avatar
yaozeliang 么么哒! 2

2个月前