Djangoで、月別アーカイブ欄を作る

Python Django

概要

Djangoで、ブログや日記によくある月別アーカイブ欄を作成していきます。

私の日記でいうと、次の赤線で囲んだ部分ですね。 月別アーカイブ

Githubにソースコードを置いているので、欲しい方はダウンロードなどしてください。

モデルの作成

モデルは何でも良いのですが、日付のフィールドを持たせることを忘れないようにしましょう。

from django.db import models
from django.utils import timezone


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    text = models.TextField('本文')
    created_at = models.DateTimeField('作成日', default=timezone.now)

    def __str__(self):
        return self.title

テンプレートタグの作成

月別アーカイブ欄ですが、Djangoで、全てのページにカテゴリ一覧を表示するでやったように、テンプレートタグを使って作成していきます。

アプリケーション内にtemplatetagsディレクトリを作り、app_tags.pyを作ります(ファイル名は好きにつけてください)。中身は次のようにしておきます。

from django import template
from django.utils import timezone
from app.models import Post

register = template.Library()


@register.inclusion_tag('app/includes/month_links.html')
def render_month_links():
    return {
        'dates': Post.objects.filter(created_at__lte=timezone.now()).dates('created_at', 'month', order='DESC'),
    }

Djangoで、全てのページにカテゴリ一覧を表示するで紹介した、register.inclusion_tagデコレータをつけています。タグの内容を、テンプレートタグを使って描画できるものでしたね。

では、Post.objects.dates('created_at', 'month', order='DESC')というコードを説明しましょう。この処理は、次のような結果を返します。

<QuerySet [
    datetime.date(2100, 1, 1), 
    datetime.date(2019, 8, 1) , 
    datetime.date(2019, 7, 1), 
    datetime.date(2019, 2, 1), 
    datetime.date(2018, 12, 1)
]>

記事が存在する月をリスト(Queryset)で取得できます。中身は、datetime.date型ですね。この例ならば、2100年1月、2019年8月、2019年7月、2019年2月、2018年12月に記事があったことになります。

今回は未来の日付のPostを表示したくないので、.filter(created_at__lte=timezone.now())として現在時間までの記事だけに絞り込んでいる、という訳です。未来記事も表示したければ、filterは外してください。

こういった処理は、Djangoで、カスタムマネージャーを使うで紹介したように、マネージャークラス側で現在時間までの記事だけを返す処理を定義しておくと捗ります。

from django.db import models
from django.utils import timezone


class PostQuerySet(models.QuerySet):

    def published(self):
        return self.filter(created_at__lte=timezone.now())


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    text = models.TextField('本文')
    created_at = models.DateTimeField('作成日', default=timezone.now)

    objects = PostQuerySet.as_manager()

    def __str__(self):
        return self.title

すると、テンプレートタグ内でのfilter()といった部分を綺麗に書き換えることができます。

@register.inclusion_tag('app/includes/month_links.html')
def render_month_links():
    return {
        'dates': Post.objects.published().dates('created_at', 'month', order='DESC'),
    }

テンプレートファイルを作る

上のテンプレートタグで呼び出される、月別アーカイブ部分となるテンプレートファイルを作りましょう。app/includes/month_links.htmlです。

{% regroup dates by year as dates_by_year %}
<ul>
    {% for month in dates_by_year %}
        <li>{{ month.grouper }}年
            <ul>
                {% for d in month.list %}
                    <li>{{ d|date:'F' }}</li>
                {% endfor %}
            </ul>
        </li>
    {% endfor %}
</ul>

regroupという、少し複雑な組み込みテンプレートタグを利用しています。これも実際の動作を見たほうがわかりやすいでしょう。

datesオブジェクトの中身は、上で説明したように次のような内容です。

<QuerySet [
    datetime.date(2019, 8, 1) , 
    datetime.date(2019, 7, 1), 
    datetime.date(2019, 2, 1), 
    datetime.date(2018, 12, 1)
]>

各要素はdatetime.dateオブジェクトなので、yearmonthdayといった属性を持っています。今回は、それのyearで更にグループ化しているのです。結果としては次のような内容になります。

[
    {'grouper': 2019, 'list': [datetime.date(2019, 8, 1), datetime.date(2019, 7, 1), datetime.date(2019, 2, 1)]},
    {'grouper': 2018, 'list': [datetime.date(2018, 12, 1)]}

]

書式は。regroup データの集まり グループ化する属性名 結果できるリストの名前です。

後は、作成したrender_month_linksタグをテンプレートで呼び出すだけです。

<h2>Archives</h2>
{% render_month_links %}

これで月別アーカイブ部分は作成できました。しかし殆どの場合、月別アーカイブはリンクになっていて、クリックでその年や月のデータ一覧が表示されます。こういった機能は、Django、generic.datesモジュールを使うで紹介したgeneric.datesが便利で、一緒に使うと良いでしょう。「概要」にあるGithubのサンプルソースコードはgeneric.datesも使い、月別アーカイブもリンクにしているので、参考にしてみてください。

その月の件数も取得したい

各月に、記事が何件あったかも知りたくなるかもしれません。これはモデル名.objects.datesでは厳しいので、自力で実装することにします。

テンプレートタグを次のようにします。

from django import template
from django.db.models import Count
from django.db.models.functions import TruncMonth
from app.models import Post

register = template.Library()


@register.inclusion_tag('app/includes/month_links.html')
def render_month_links():
    dates = Post.objects.annotate(month=TruncMonth('created_at')).values('month').annotate(count=Count('pk'))
    return {
        'dates': dates,
    }

できあがる結果のQuerySetは次のようになります。

<QuerySet [
    {'month': datetime.datetime(2019, 3, 1, 0, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>), 'count': 1},
    {'month': datetime.datetime(2019, 11, 1, 0, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>), 'count': 1},
    {'month': datetime.datetime(2019, 12, 1, 0, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>), 'count': 2}]>

TruncMonthですが、先程使ったobjects.datesの内部でも使われている処理です。今回はそれを直接使って、datesと似たような処理を自力で作ったことになります。

TruncMonthの公式ドキュメントにデータのカウント処理のサンプルがあったので、それをそのまま使っています。モデル名.objects.datesの内部の処理も、気になる人は見てみてください。

後は、テンプレートを上の処理に合わせるだけです。

{% regroup dates by month.year as dates_by_year %}
<ul>
    {% for group in dates_by_year %}
        <li>{{ group.grouper }}年
            <ul>
                {% for d in group.list %}
                    <li>{{ d.month |date:'F' }} ({{ d.count }})</li>
                {% endfor %}
            </ul>
        </li>
    {% endfor %}
</ul>

結果のQuerySetが少しネストしているので、regroupの引数はmonth.yearになったり、d.monthのようにして日付を取り出していることに注意してください。

Relation Posts

Django、generic.datesモジュールを使う

Djangoのクラスビューには「generic.dates」という日付に関連したクラスビューもあります。今回は、これを紹介していきます。

Python Django

Djangoで、全てのページにカテゴリ一覧を表示する

Webサイトでは、全てのページに配置したい情報が出てきます。例えばサイドバーのカテゴリ一覧とか、ヘッダー内にあるサイト内検索といったコンテンツです。今回は、それらをDjangoで実装していきます。

Python Django

Comment

記事にコメントする

名無し

はじめまして、月別アーカイブスの横に件数をつける方法について質問させてください。

現在下記コードの様にテンプレートフィルタを使い、 DBの"created_at"から、"-対象の月-"をキーにして無理やり件数を取得しています。

def article_count(x):
    counts= Article.objects.filter(created_at__contains="-"+str(x)+"-").count()
    return counts
{% load count_montharchive %}
{% regroup dates by month as dates_by_year %}

<ul>
    {% for month in dates_by_year %}
    {% for d in month.list %}
    <a href="{% url 'contents:article_month_archive' d.year d.month %}">
        <li>{{ d | date:" Y年m月" }} ( {{ d | date:"m" | article_count }} )</li>
    </a>
    {% endfor %}
    {% endfor %}
</ul>

これだと毎回月の数分クエリ(?)が走ってると思い、あまりいいやり方ではないのかなと考えています。 他にNaritoさんが考えるいい方法があれば、教えていただけると幸いです。 よろしくおねがいします。

返信する

なりと

記事に、件数追加について書きました。