Django 学习小组:博客开发实战第四周——标签云与文章归档

本教程内容已过时,更新版教程请访问: Django 博客开发入门教程

通过前四周的时间我们开发了一个简单的个人 Blog,相关教程:

第一周Django 学习小组:博客开发实战第一周教程 —— 编写博客的 Model 和首页面
第二周Django 学习小组:博客开发实战第二周教程 —— 博客详情页面和分类页面
第三周Django 学习小组:博客开发实战第三周教程——文章列表分页和代码语法高亮
第五周Django 学习小组:博客开发实战第五周——基于类的通用视图详解(一)
第六周Django 学习小组:博客开发实战第六周教程 —— 实现评论功能

本周我们将实现 blog 的标签云和文章按时间自动归档功能。

提示:在阅读教程的过程中,如有任何问题请访问我们项目的 GithHub 或评论留言以获取帮助,本教程的相关代码已全部上传在 GitHub 的 blog-tutorial 分支 上,请点击链接获取。。如果你对我们的教程或者项目有任何改进建议,请您随时告知我们。更多交流请加入我们的邮件列表 django_study@groups.163.com 和关注我们在 GithHub 上的项目。

本文首发于编程派微信公众号:编程派(微信号:codingpy)是一个专注Python编程的公众号,每天更新有关Python的国外教程和优质书籍等精选干货,欢迎关注。

标签云与文章归档在 Blog 中也是比较常见的功能,标签云显示每篇文章的标签,文章归档显示某个时间段内的发表的文章,就像这样:

标签云
文章归档

下面我们来为我们的 Blog 添加类似的功能,最终会为我们的个人 blog 实现类似于下面这样的效果:

整体效果展示

标签云

标签有点类似于分类,只是分类由于是多对一的关系(我们规定一篇文章只有一个分类,而一个分类下可以有多篇文章),因此在我们的 model 中使用的是 ForeignKeyField 。我们规定一篇文章可以打多个标签,并且一个标签下可能会有多篇文章,是多对多的关系,因此需要使用到 ManyToManyField,其它的实现则和 Category(分类)十分相似。首先修改我们的 model 文件,为标签(tag)新建一个数据库 model,并在文章(Article)中指定它们多对多的关系:

blog/models.py

class Article(models.Model):
    """
    文章model中添加tag关系
    """
    ...
    category = models.ForeignKey('Category', verbose_name='分类', null=True, on_delete=models.SET_NULL)
    tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True)
    ...

class Tag(models.Model):
    """
    tag(标签)对应的数据库model
    """
    name = models.CharField('标签名', max_length=20)
    created_time = models.DateTimeField('创建时间', auto_now_add=True)
    last_modified_time = models.DateTimeField('修改时间', auto_now=True)

    def __str__(self):
        return self.name

类似于 CategoryView,点击某个标签可以获取该标签下的全部文章,对应的视图函数:

blog/views.py

class TagView(ListView):
    template_name = "blog/index.html"
    context_object_name = "article_list"

    def get_queryset(self):
        """
        根据指定的标签获取该标签下的全部文章
        """
        article_list = Article.objects.filter(tags=self.kwargs['tag_id'], status='p')
        for article in article_list:
            article.body = markdown2.markdown(article.body, extras=['fenced-code-blocks'], )
        return article_list

    def get_context_data(self, **kwargs):
        kwargs['tag_list'] = Tag.objects.all().order_by('name')
        return super(TagView, self).get_context_data(**kwargs)

模板文件稍微小变了一下,添加了显示标签的区域(由于模板文件代码比较多,具体请参见 GitHub 的 blog-tutorial 分支 上 blog/templates/blog/index.html 下的模板文件)。

同时 IndexView 里也别忘了把 tag 加到 context 中,以便在模板中渲染显示:

blog/views.py

class IndexView(ListView):
    ...
    def get_context_data(self, **kwargs):
        kwargs['category_list'] = Category.objects.all().order_by('name')
        kwargs['date_archive'] = Article.objects.archive()
        # tag_list 加入 context 里:
        kwargs['tag_list'] = Tag.objects.all().order_by('name')
        return super(IndexView, self).get_context_data(**kwargs)

配置好 url :

blog/urls.py

url(r'^tag/(?P<tag_id>\d+)$', views.TagView.as_view(), name='tag'),

文章归档

文章归档我们实现下面的需求:

在首页会显示已发表文章对应的年份列表,点击相应年份会展开该年年份下对应的月份列表,像这样:

blog 文章归档演示

实现思路大概如下:Django 的 ORM 为我们提供一个 datetimes 函数 ( datetimes 函数用法 ),可以选出数据库中某个 model 对应的全部已去重的时间,并且可以任意指定精度。例如,我们想选出全部文章对应的发表时间,精确到月份:

date_list = Article.objects.datetimes('created_time', 'month', order='DESC')
# created_time 是 Article model 中文章发表时间,对应的是 DatetimeField( datetimes 函数也只能用于DatetimeField ),month 即精确到月,精确到年指定为 year,天则指定为 day 即可。DESC 表示降序排列,默认是升序排列。

# 例如有如下的一系列发表时间:
2009-01-02
2009-01-05
2009-02-02
2010-05-04
2011-06-04
2011-06-07
# 则得到的结果将是精确到月份去重后的结果:
2009-01
2009-02
2010-05
2011-06
# 这正是我们期望的结果

以这个函数为基础,接下来我们使用 Django 的一点高级技巧(自定义 Manager)来实现完整的功能。

什么是 Manager(管理器)?Manager 可以看成是一个 model 的管理器,很多从数据库中获取 model 数据的方法都定义在这个类里,比如我们经常用的 Article.objects.all()Article.objects.filter(),这里的 objects 就是一个 Manager 的实例,django 为每一个 model 都指定了一个默认的 Manager ,名字叫做 objects。但现在 Manager 中一些默认的方法无法满足我们的需求了,因此我们拓展一下 Manager 的功能,为其添加一个归档(archive)方法,拓展一个类的最佳方式就是继承它:

blog/models.py

class ArticleManage(models.Manager):
    """
    继承自默认的 Manager ,为其添加一个自定义的 archive 方法
    """
    def archive(self):
        date_list = Article.objects.datetimes('created_time', 'month', order='DESC')
        # 获取到降序排列的精确到月份且已去重的文章发表时间列表
        # 并把列表转为一个字典,字典的键为年份,值为该年份下对应的月份列表
        date_dict = defaultdict(list)
        for d in date_list:
            date_dict[d.year].append(d.month)
        # 模板不支持defaultdict,因此我们把它转换成一个二级列表,由于字典转换后无序,因此重新降序排序
        return sorted(date_dict.items(), reverse=True)

自定义了 Manger 后需要在 model 中显示地指定它:

blog/models.py

class Article(models.model):
    ...
    # 仍然使用默认的 objects 作为 manager 的名字
    objects = ArticleManager()
    ...

现在在视图函数中就可以调用了:

blog/views.py

class IndexView(ListView):
    template_name = "blog/index.html"
    context_object_name = "article_list"

    def get_queryset(self):
        article_list = Article.objects.filter(status='p')
        for article in article_list:
            article.body = markdown2.markdown(article.body, extras=['fenced-code-blocks'], )
        return article_list

    def get_context_data(self, **kwargs):
        kwargs['category_list'] = Category.objects.all().order_by('name')
        # 调用 archive 方法,把获取的时间列表插入到 context 上下文中以便在模板中渲染
        kwargs['date_archive'] = Article.objects.archive()
        kwargs['tag_list'] = Tag.objects.all().order_by('name')
        return super(IndexView, self).get_context_data(**kwargs)
    
# 现在我们的时间归档列表格式是这样的:
[(2012,[09,02,01]),(2011,[12,10,06,01]),...]
# 因此在模板中我们可以这样循环以实现我们预初的设计:
{% for year,month_list in date_archive %}
    {{year}} 年
    {% for month in month_list %}
        {{month}}月
# 使用一些 bootstrap 的组件即可实现上图一样的效果了。

完整的模板请参考GitHub 的 blog-tutorial 分支 的 blog/templates/blog/index.html 模板文件。

最后一件事就是实现点击相应的时间后显示该时间下的全部已发表文章列表了,实现思路即通过 url 把对应的年份和月份传给视图函数,视图函数通过年份和月份过滤所需文章,然后再模板渲染即可,实现和 category 与 tag 的方式十分类似:

blog/views.py

class ArchiveView(ListView):
    template_name = "blog/index.html"
    context_object_name = "article_list"

    def get_queryset(self):
        # 接收从url传递的year和month参数,转为int类型
        year = int(self.kwargs['year'])
        month = int(self.kwargs['month'])
        # 按照year和month过滤文章
        article_list = Article.objects.filter(created_time__year=year, created_time__month=month)
        for article in article_list:
            article.body = markdown2.markdown(article.body, extras=['fenced-code-blocks'], )
        return article_list

    def get_context_data(self, **kwargs):
        kwargs['tag_list'] = Tag.objects.all().order_by('name')
        return super(ArchiveView, self).get_context_data(**kwargs)

url:

blog/urls.py

url(r'^archive/(?P<year>\d+)/(?P<month>\d+)$', views.ArchiveView.as_view(), name='archive'),

templates:

blog/index.html

# 详细请参阅 github 上的模板文件完整代码
{% for year,month_list in date_archive %}
    {{year}} 年
    {% for month in month_list %}
        <a href="{% url 'blog:archive' year month %}"><p>{{ month }} 月</p></a>

接下来做什么?

我们的个人 blog 基本已经成型了!首页展示文章列表、标签云、文章归档、分类,文章 markdown 语法标记,代码高亮显示,利用 django 后台,我们可以使用它来写 blog 文章了,你可以先尝试着找一个部署教程把 blog 部署上线。当然我们接下来也会出如何部署的教程,敬请期待。下一周我们将实现评论功能,允许用户对我们发表的文章进行评论。为了学习,我们将不使用第三方 app,而是重新发明轮子。

Django学习小组简介

django学习小组是一个促进 django 新手互相学习、互相帮助的组织。

小组在一边学习 django 的同时将一起完成几个项目,包括:

  • 一个简单的 django 博客,用于发布小组每周的学习和开发文档;
  • django中国社区,为国内的 django 开发者们提供一个长期维护的 django 社区;

上面所说的这个社区类似于 segmentfault 和 stackoverflow ,但更加专注(只专注于 django 开发的问题)。

更多的信息请关注我们的 github 组织,本教程项目的相关源代码也已上传到 GitHub 的 blog-tutorial 分支 上,请点击链接获取。

同时,你也可以加入我们的邮件列表 django_study@groups.163.com ,随时关注我们的动态。我们会将每周的详细开发文档和代码通过邮件列表发出。

如有任何建议,欢迎提 Issue,欢迎 fork,pr,当然也别忘了 star 哦!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容