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

Python - Django
2018年11月19日2:44に更新(約9時間前)
2018年11月7日0:03に作成(約12日前)

旧ブログ移行記事です。

概要

Djangoのクラスビューとしてよく使うものは、TemplateViewListViewDetailViewCreateViewUpdateViewDeleteViewFormView 等があります。

これらはviews.genericパッケージに入っていますが、その中にはdatesという日付に関連したクラスビューのあるモジュールがあります。

公式ドキュメントにもちらっと説明はあるのですが、実際にどのような動作をするか作っていきます。

公式ドキュメントの1ページに収まらないぐらい高機能です。

今回、以下のサンプルモデルで説明していきます。日付に関連したクラスビューなので、日付のフィールドを含むモデルを定義します。DateFieldでもDateTimeFieldでも大丈夫です。

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


class Post(models.Model):
    title = models.CharField('タイトル', max_length=200)
    pub_date = models.DateField('公開日', default=timezone.now)

ArchiveIndexView

ArchiveをIndex表示する、という感じのビューです。

urls.pyです。

path('', views.PostArchive.as_view(), name='post_archive'),

views.pyです。 モデルと、日付なフィールドをdate_fieldで指定します。

class PostArchive(generic.ArchiveIndexView):
    model = Post
    date_field = 'pub_date'

post_archive.htmlです。モデル名_archive.html という名前が自動で探されます。

{% extends 'app/base.html' %}

{% block content %}

<h1>全ての記事</h1>
<hr>

<!-- 記事がある、全ての年が表示される -->
{% for date in date_list %}
  <h2>{{ date }}</h2>
{% endfor %}
<hr>

<!-- 新しい順に、全ての記事を表示 -->
{% for post in latest %}
  <p>{{ post.pub_date }},{{ post.title }}</p>
{% endfor %}
<hr>

{% endblock %}

すると、このような表示になります。 ArchiveIndexViewの見た目

2018年に3記事、2017年に1記事、2016年に1記事のように記事が存在する全ての年がまず表示され、その後に全ての記事を表示していますね。

date_listの中に、記事が存在する全ての年が詰まっています。datetime.date or datetime 型として渡されるので、year等の属性にアクセスできます。テンプレートフィルタのdateを使った日付のフォーマットもしやすいよう、変数名をdtとしました。試しに、年だけ表示するように書き換えましょう。

{% for dt in date_list %}
  <h2>{{ dt.year }}</h2>
{% endfor %}

ArchiveIndexViewの見た目2

latest、又はobject_listとして全ての記事が取得されており、表示できます。 他のビューにも言えることですが、新しいものが上に表示されるよう、並び替えられています。

{% for post in latest %}
  <p>{{ post.pub_date }},{{ post.title }}</p>
{% endfor %}

デフォルトでは、データが一件もないと404になります。それが嫌な場合はallow_empty = Trueとします。

class PostArchive(generic.ArchiveIndexView):
    model = Post
    date_field = 'pub_date'
    allow_empty = True  # データが一件もなくても、エラーにならない

またデフォルトでは、未来の日付として作成されたデータも表示されません。allow_future=Trueとすると未来も表示されます。generic.dates内の他ビューもそうです。 後ほどDetailViewを年月日で指定するDateDetailViewも紹介しますが、allow_future=False(デフォルトのまま)にしておくと、当日になったら自動公開される記事も作成できます。

class PostArchive(generic.ArchiveIndexView):
    model = Post
    date_field = 'pub_date'
    allow_empty = True  # データが一件もなくても、エラーにならない
    allow_future = True  # 未来の日付も表示する

ブログのサイドバーにはよく「年別アーカイブ」みたいな項目がありますが、これを使うと簡単に実現できそうです。また、generic.datesの他のビューとの連携もしやすいように作成されています。

YearArchiveView

Yearと頭についていますが、これはその年に属する全てのデータや全ての月を表示します。まんま、ArchiveIndexViewを年にした感じですね。

YearAchiveViewとの連携がしやすいように、さきほどのpost_archive.htmlを変更しておきましょう。各年の部分がYearAchiveViewへのリンクになります。

{% for dt in date_list %}
  <h2>
    <a href="{% url 'app:post_year_archive' dt.year %}">
      {{ dt.year }}年へ
    </a>
  </h2>
{% endfor %}
<hr>

urls.pyです。

path('<int:year>/', views.PostYearArchive.as_view(), name='post_year_archive'),

views.pyです。

class PostYearArchive(generic.YearArchiveView):
    model = Post
    date_field = 'pub_date'
    make_object_list = True

post_archive_year.html です。モデル名_archive_year.html がデフォルトです。

{% extends 'app/base.html' %}

{% block content %}

<h1>{{ year.year }}年の全ての記事</h1>
{% if previous_year %}
  <a href="{% url 'app:post_year_archive' previous_year.year %}">
    前年
  </a>
{% endif %}
{% if next_year %}
  <a href="{% url 'app:post_year_archive' next_year.year %}">
    次年
  </a>
{% endif %}
<hr>

{% for dt in date_list %}
  <h2>{{ dt.month }}月</h2>
{% endfor %}
<hr>

{% for post in object_list %}
  <p>{{ post.pub_date }},{{ post.title }}</p>
{% endfor %}
<hr>

{% endblock %}

見た目はこのようになります。 YearArchiveViewの見た目

前年、次年で移動できます。表示内容も自動で絞り込まれますね。 年移動にも対応

yearprevious_yearnext_yearという3つの変数がテンプレートへ渡されます。全てdatetime.date or datetimeオブジェクトです。

{% if previous_year %}
  <a href="{% url 'app:post_year_archive' previous_year.year %}">
    前年
  </a>
{% endif %}
{% if next_year %}
  <a href="{% url 'app:post_year_archive' next_year.year %}">
    次年
  </a>
{% endif %}

date_listや、object_list変数はさっきと同様ですね。

{% for dt in date_list %}
  <h2>{{ dt.month }}月</h2>
{% endfor %}
<hr>

{% for post in object_list %}
  <p>{{ post.pub_date }},{{ post.title }}</p>
{% endfor %}
<hr>

各月だけ取得できればいい、データの一覧が要らない、という場合はmake_object_list属性を上書きしなければ良いです。

class PostYearArchive(generic.YearArchiveView):
    model = Post
    date_field = 'pub_date'

デフォルトでは、前年、次年は、データがある年まで飛ばされます。2011年、2016年にデータがあれば、2011の次は2016年に飛びます。 allow_empty=Trueにすると、これらを必ず1つずつ移動にできます。途中の2012~2015は空で、エラーにはなりません。 他のビューも似たように動作するので、覚えておきましょう。

MonthArchiveView

ある月に属する、全てのデータと全ての日付を取得できます。

MonthAchiveViewとの連携がしやすいように、さきほどのpost_archive_year.htmlを変更しておきましょう。各月の部分がMonthArchiveViewへのリンクになります。

{% for dt in date_list %}
  <h2>
    <a href="{% url 'app:post_month_archive' dt.year dt.month %}">
      {{ dt.month}}月へ
    </a>
  </h2>
{% endfor %}

urls.pyです。

path('<int:year>/<int:month>/',views.PostMonthArchive.as_view(), name='post_month_archive'),

views.pyです。

class PostMonthArchive(generic.MonthArchiveView):
    model = Post
    date_field = 'pub_date'
    make_object_list = True
    month_format='%m'

post_archive_month.html です。モデル名_archive_month.htmlがデフォルトのテンプレートです。

{% extends 'app/base.html' %}

{% block content %}

<h1>{{ month | date:"Y年m月"}}の全ての記事</h1>
{% if previous_month %}
  <a href="{% url 'app:post_month_archive' previous_month.year previous_month.month  %}">
    前月
  </a>
{% endif %}
{% if next_month %}
  <a href="{% url 'app:post_month_archive' next_month.year next_month.month  %}">
    次月
  </a>
{% endif %}
<hr>

{% for dt in date_list %}
  <h2>{{ dt.day }}日</h2>
{% endfor %}
<hr>

{% for post in object_list %}
  <p>{{ post.pub_date }},{{ post.title }}</p>
{% endfor %}
<hr>

{% endblock %}

テンプレートフィルターの'date'フィルターを使ってみました。 それ以外の部分は、概ねYearArchiveと同じですね。

<h1>{{ month | date:"Y年m月"}}の全ての記事</h1>

見た目はこのようになります。 MonthArchiveViewの見た目

DayArchiveView

日毎です。その日のデータを全て表示できます。

DayAchiveViewとの連携がしやすいように、さきほどのpost_archive_month.htmlを変更しておきましょう。各日の部分がDayArchiveViewへのリンクになります。

{% for dt in date_list %}
  <h2>
    <a href="{% url 'app:post_day_archive' dt.year dt.month dt.day %}">
      {{ dt.day }}日へ
    </a>
  </h2>
{% endfor %}

urls.pyです。

path('<int:year>/<int:month>/<int:day>/',views.PostDayArchive.as_view(), name='post_day_archive'),

views.pyです。

class PostDayArchive(generic.DayArchiveView):
    model = Post
    date_field = 'pub_date'
    month_format='%m'

post_archive_day.html です。モデル名_archove_day.htmlです。

{% extends 'app/base.html' %}

{% block content %}

<h1>{{ day | date:"Y年m月d日"}}の全ての記事</h1>
{% if previous_day %}
  <a href="{% url 'app:post_day_archive' previous_day.year previous_day.month previous_day.day  %}">
    前日
  </a>
{% endif %}
{% if next_day %}
  <a href="{% url 'app:post_day_archive' next_day.year next_day.month next_day.day  %}">
    次日
  </a>
{% endif %}
<hr>

{% for post in object_list %}
  <p>{{ post.pub_date }},{{ post.title }}</p>
{% endfor %}
<hr>

{% endblock %}

見た目はこのようになります。 DayArchiveViewの見た目

dateフィルターで、Y月M月D日としています。

<h1>{{ day | date:"Y年m月d日"}}の全ての記事</h1>

前日、次日を作成しています。urlタグの引数も多いので、打ち間違いに注意しましょう。

{% if previous_day %}
  <a href="{% url 'app:post_day_archive' previous_day.year previous_day.month previous_day.day  %}">
    前日
  </a>
{% endif %}
{% if next_day %}
  <a href="{% url 'app:post_day_archive' next_day.year next_day.month next_day.day  %}">
    次日
  </a>
{% endif %}

例によってobject_listとしてその日のデータが取得できます。make_object_list属性の上書きは必要なしです。 また、date_list変数はもう渡されません。(渡そうとしても何も渡せ無さそうですね...時間とか?)

{% for post in object_list %}
  <p>{{ post.pub_date }},{{ post.title }}</p>
{% endfor %}

次月の最初の日、前月の最初の日、といったリンクを作るためのprevious_monthnext_month変数もあります。 ただ、前・次月の1日と強制的に設定するので、リンクを貼るならば、その月の1日にデータがなければエラーになります。その場合は、allow_empty = Trueallow_futureも使いましょう。YearArchiveViewでも話しましたが、allow_empty=Trueにすると必ず1日、1月ずつの移動になることも忘れないようにします。

{% if previous_month %}
  <a href="{% url 'app:post_day_archive' previous_month.year previous_month.month previous_month.day  %}">
    前月
  </a>
{% endif %}
{% if next_month %}
  <a href="{% url 'app:post_day_archive' next_month.year next_month.month next_month.day  %}">
    次月
  </a>
{% endif %}

DateDetailView

これはgeneric.DetailViewに日付情報をくっつけたものです。

DayDetailViewとの連携がしやすいように、さきほどのpost_archive_day.htmlを変更しておきましょう。記事の詳細部分へのリンクを貼ります。

{% for post in object_list %}
  <p>
    <a href="{% url 'app:post_day_detail' day.year day.month day.day  post.pk %}">{{ post.title }}</a>
  </p>
{% endfor %}

urls.pyです。

path('<int:year>/<int:month>/<int:day>/<int:pk>/',views.PostDayDetail.as_view(), name='post_day_detail'),

views.pyです。

class PostDayDetail(generic.DateDetailView):
    model = Post
    date_field = 'pub_date'
    month_format = '%m'

post_detail.html です。テンプレートへ渡されるのも、postモデルインスタンスのみとシンプルです。 get_allow_futureをTrueにしないと未来の日付のデータは表示できないので、それを活用して当日になったら公開される記事として作成できますね。

{% extends 'app/base.html' %}

{% block content %}

<h1>{{ post.title }}</h1>
<p>{{ post.pub_date }}</p>

{% endblock %}

TodayArchiveView

これはDayArchiveViewとほとんど同じです。本日の日付内でデータが取得されるだけです。

post_archive.htmlあたりにでも、今日のデータのようにしてリンクを貼っておきます。

<a href="{% url 'app:post_today_archive' %}">今日のデータ</a>

urls.pyです。

path('today/',views.PostTodayArchive.as_view(), name='post_today_archive'),

views.pyです。

class PostTodayArchive(generic.TodayArchiveView):
    model = Post
    date_field = 'pub_date'

テンプレートは、DayArchiveViewと同じものが使われます(post_archive_day.html)。

WeekArchiveView

今度は週毎です。週は、1〜53までの年間の週です。12月31日なら、53週という感じですね。

post_detail.htmlに、リンクを貼っておきます。 post.pub_dateという日付型のデータから、週の番号を取り出す必要があります。これ自体はdateフィルターのdate:'W'で取り出せるのですが、それをurlタグ内に渡すため、withタグを使ってweek_numberという変数で週番号を格納し、そのweek_numberをurlタグで使っています。

<p>
    {% with week_number=post.pub_date|date:'W' %}
      <a href="{% url 'app:post_week_archive' post.pub_date.year week_number %}">
        {{ post.pub_date | date:"Y年W週へ"}}
      </a>
    {% endwith %}
</p>

urls.pyです。

path('<int:year>/week/<int:week>/',views.PostWeekArchive.as_view(), name='post_week_archive'),

views.py です。make_object_list=Trueで、その週のデータ一覧が取得できます。

class PostWeekArchive(generic.WeekArchiveView):
    model = Post
    date_field = 'pub_date'
    make_object_list = True
    week_format = '%W'

post_archive_week.html です。モデル名_archive_week.html ですね。date_listは持っていないビューです。

{% extends 'app/base.html' %}

{% block content %}

<h1>{{ week | date:"Y年W週"}}の全ての記事</h1>
{% if previous_week %}
  {% with week_number=previous_week|date:'W' %}
    <a href="{% url 'app:post_week_archive' previous_week.year week_number  %}">
      前週
    </a>
  {% endwith %}
{% endif %}

{% if next_week %}
  {% with week_number=next_week|date:'W' %}
    <a href="{% url 'app:post_week_archive' next_week.year week_number  %}">
      次週
    </a>
  {% endwith %}
{% endif %}
<hr>

{% for post in object_list %}
  <p>
    <a href="{% url 'app:post_day_detail' post.pub_date.year post.pub_date.month post.pub_date.day  post.pk %}">{{ post.title }}</a>
  </p>
{% endfor %}
<hr>

{% endblock %}

このように表示されます。 WeekArchiveViewの見た目

まとめ

色々と使いましたが、非常に便利でした。日付でデータを管理したい場合は重宝しそうです。ブログ等をはじめ、社内の業務アプリ等で利用できる場面もありそうです。

テンプレートへ渡されるのは、絞り込まれたモデルインスタンス一覧と、yearmonthday、といった日付型のオブジェクトに統一されているので、他のビューへのリンクも作りやすそうです。例えば...

post_detail.html

{% extends 'app/base.html' %}

{% block content %}

<h1>{{ post.title }}</h1>
<p>
  {% with year=post.pub_date.year month=post.pub_date.month day=post.pub_date.day week_number=post.pub_date|date:'W' %}
    <a href="{% url 'app:post_year_archive' year %}">{{ year }}</a>年
    <a href="{% url 'app:post_month_archive' year month %}">{{ month }}</a>月
    <a href="{% url 'app:post_day_archive' year month  day %}">{{ day }}</a>日(
    <a href="{% url 'app:post_week_archive' year week_number  %}">{{ week_number }}</a>週)
  {% endwith %}
</p>
<p><a href="{% url 'app:post_archive'%}">全てのデータ一覧へ</a></p>
{% endblock %}

これは、以下のようになります。記事の詳細ページから、全てのビューにリンクが晴れました。 全てのdatesビューにリンクを張った

Githubに、今回のプロジェクトを置きました。試したい方はクローンして使ってみてください。

記事にコメントする