Djangoで登録/更新/検索フォームにカレンダコントロールを設置

はじめに

Djangoで実装した登録、更新、検索フォームに以下のようなカレンダコントロールを設置する方法をまとめます。

f:id:hiroki-sawano:20200717034524p:plain

django-bootstrap-datepicker-plus (現最新バージョン3.0.5)を使用します。

github.com

前提

  • 登録、更新フォームは django.forms.ModelForm で実装していること
  • 検索フォームは django-filters.FilterSet で実装していること

サンプルアプリの実装

まずは説明に用いるサンプルアプリ(カレンダコントロールなし)を実装していきます。
実行環境は以下の通りです。本エントリの主題ではないため使用する各ライブラリの詳細な説明は省略します。

  • django==3.0.8 ※ Django 2でも良いです。
  • django-bootstrap4==2.2.0 ※ Bootstrap3でも良いです。
  • django-crispy-forms==1.9.2 ※ フォームの描画に使います。
  • django-filter==2.3.0 ※ 検索機能の実装に使います。
INSTALLED_APPS = [
    # ...
    'crispy_forms',
    'django_filters',
    'bootstrap4',
]
CRISPY_TEMPLATE_PACK = 'bootstrap4'

アプリの作成

アプリ app を作成します。

$ python manage.py startapp app

settings.pyINSTALLED_APPSapp を追加します。

INSTALLED_APPS = [
    # ...
    'app',
]

モデル

MyModel モデルに日付項目 date_field と日時項目 datetime_field を用意します。

from django.db import models


class MyModel(models.Model):
    date_field = models.DateField()
    datetime_field = models.DateTimeField()

DBにテーブルを作成します。

$ python manage.py makemigrations
$ python manage.py migrate

ビューとURL

クラスベースの汎用ビューを使用します。
CreateViewUpdateViewDjango標準のビューですが、 FilterViewdjango_filters のビューです。

from django_filters.views import FilterView
from django.urls import path, reverse_lazy
from django.views.generic.edit import CreateView, UpdateView

from app.forms import MyModelForm
from app.filters import MyModelFilter
from app.models import MyModel

app_name = 'app'
urlpatterns = [
    path('my-model/new',
         CreateView.as_view(
             form_class=MyModelForm,
             template_name='app/create_update.html',
             success_url=reverse_lazy('app:search'),
         ),
         name='create'),
    path('my-model/<int:pk>',
         UpdateView.as_view(
             model=MyModel,
             form_class=MyModelForm,
             template_name='app/create_update.html',
             success_url=reverse_lazy('app:search'),
         ),
         name='update'),
    path('my-model/search',
         FilterView.as_view(
             filterset_class=MyModelFilter,
             template_name='app/search.html',
         ),
         name='search'),
]

/app のURLでアクセスできるように my_project/urls.pyapp.urls を登録します。

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('app/', include('app.urls')),
]

モデルフォーム

登録ビュー( app:create )と更新ビュー( app:update )の form_class に指定した MyModelForm の実装です。
このクラスに後ほどカレンダコントロールを組み込みますが、まずは単純に MyModel の全フィールドを指定するだけです。

from django import forms

from app.models import MyModel

class MyModelForm(forms.ModelForm):
    class Meta:
        model = MyModel
        fields = ['date_field', 'datetime_field']

フィルタ

検索ビュー( app:search )の filterset_class に指定した MyModelFilter の実装です。
MyModel の全フィールドを対象に完全一致検索のフィルタを実装しています。 こちらもモデルフォームと同様に後ほど修正対象となります。

import django_filters

from app.models import MyModel


class MyModelFilter(django_filters.FilterSet):
    class Meta:
        model = MyModel
        fields = ['date_field', 'datetime_field']

テンプレート

ベーステンプレート

各テンプレートで共通する箇所を app/templates/app/base.html に実装します。
django-bootstrap4 でBootstrap4を読み込み、各テンプレートで実装する extrahead ブロックと content ブロックを用意しています。

<html>
  <head>
    {% load bootstrap4 %}
    {% bootstrap_css %}
    {% bootstrap_javascript jquery='full' %}

    {% block extrahead %}{% endblock %}
  </head>
  <body>
    <div class="container">
      {% block content %}{% endblock %}
    </div>
  </body>
</html>

登録/更新画面のテンプレート

登録画面と更新画面は全く同じ実装になるため、いずれも app/templates/app/create_update.html を使用します。

base.html を継承し、 content ブロックにフォームを実装します。
CreateViewUpdateView がコンテキストとして返す formdjango-crispy-formscrispyフィルタで描画します。

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

{% block content %}
<form method="post" class="form">
  {% csrf_token %}
  {{ form|crispy }}
  <button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock %}

登録画面の動作確認

/app/my-model/newMyModel のフォームが表示されます。
Submitボタンの押下で新規データが登録、検索画面にリダイレクトされることを確認します。

f:id:hiroki-sawano:20200717012224g:plain

更新画面の動作確認

/app/my-model/<int:pk> でURLパラメタ pk に指定したPK値のモデルオブジェクトが表示されます。
以下の例では date_field の値を 2020-01-01 から 2020-01-03 に変更しています。

f:id:hiroki-sawano:20200717012806g:plain

検索画面のテンプレート

登録/更新画面のテンプレートとの差異は以下の通りです。

  • <form>methodget を指定
  • フォームを filter.form で参照
  • フォーム以下に検索結果( filter.qs )を表示
{% extends 'app/base.html' %}
{% load crispy_forms_tags %}

{% block content %}
<form method="get" class="form">
  {% csrf_token %}
  {{ filter.form|crispy }}
  <button type="submit" class="btn btn-primary">Search</button>
</form>
{% for obj in filter.qs %}
  date_field: {{ obj.date_field }} datetime_field: {{ obj.datetime_field }}<br/>
{% endfor %}
{% endblock %}

検索画面の動作確認

date_field2020-01-01 であるデータを検索しています。

f:id:hiroki-sawano:20200717014440g:plain

カレンダコントロールの導入

ここまででようやくサンプルアプリの実装が完了したので、いよいよカレンダコントロールを設置していきます。

django-bootstrap-datepicker-plusのインストール

pipでインストールし、 INSTALLED_APPSbootstrap_datepicker_plus を追加します。

$ pip install django-bootstrap-datepicker-plus
INSTALLED_APPS = [
    ...
    'bootstrap_datepicker_plus',
]

登録/更新画面

モデルフォームの修正

次の修正を加えます。

  • bootstrap_datepicker_plus をインポート
  • Meta.widgets の追加
    • 日付項目( DateField )の場合は datetimepicker.DatePickerInput を指定
    • 日時項目( DateTimeField )の場合は datetimepicker.DateTimePickerInput を指定

DatePickerInputDateTimePickerInputformatPythonのdatetimeフォーマットを指定します。
optionsJavaScriptdatepicker インスタンスに渡されるので、Bootstrap Datepicker Options Referenceを指定できます。

import bootstrap_datepicker_plus as datetimepicker
# ...

class MyModelForm(forms.ModelForm):
    class Meta:
        # ...
        widgets = {
            'date_field': datetimepicker.DatePickerInput(
                format='%Y-%m-%d',
                options={
                     'locale': 'ja',
                     'dayViewHeaderFormat': 'YYYY年 MMMM',
                }
            ),
            'datetime_field': datetimepicker.DateTimePickerInput(
                format='%Y-%m-%d %H:%M:%S',
                options={
                    'locale': 'ja',
                    'dayViewHeaderFormat': 'YYYY年 MMMM',
                }
            ),
        }

テンプレートの修正

extrahead ブロックに {{ form.media }} を追加します。
これでカレンダコントロールウィジェットに必要なCSSJavascriptが読み込まれます。

<!-- 省略 -->
{% block extrahead %}
{{ form.media }}
{% endblock %}
<!-- 省略 -->

動作確認

これでフォームにカレンダアイコンのボタンが設置され、カレンダコントロールで日時が入力できるようになりました。
以下は登録画面ですが更新画面も動きは同じです。

f:id:hiroki-sawano:20200717024716g:plain

検索画面

フィルタの修正

次の修正を加えます。 widget の指定はモデルフォームと同じです。

  • bootstrap_datepicker_plus をインポート
  • 日付項目( DateField )の場合は django_filters.DateFilterwidget 引数に datetimepicker.DatePickerInput を指定
  • 日時項目( DateTimeField )の場合は django_filters.DateTimeFilterwidget 引数に datetimepicker.DateTimePickerInput を指定
import bootstrap_datepicker_plus as datetimepicker
# ...

class MyModelFilter(django_filters.FilterSet):
    date_field = django_filters.DateFilter(
        widget=datetimepicker.DatePickerInput(
            format='%Y-%m-%d',
            options={
                'locale': 'ja',
                'dayViewHeaderFormat': 'YYYY年 MMMM',
            }
        ),
    )
    datetime_field = django_filters.DateTimeFilter(
        widget=datetimepicker.DateTimePickerInput(
            format='%Y-%m-%d %H:%M:%S',
            options={
                'locale': 'ja',
                'dayViewHeaderFormat': 'YYYY年 MMMM',
            }
        ),
    )
    class Meta:
        # ...

テンプレートの修正

登録、更新画面と同様にフォームの media を読み込みます。
form.media ではなく、 filter.form.media である点に注意です。

<!-- 省略 -->
{% block extrahead %}
{{ filter.form.media }}
{% endblock %}
<!-- 省略 -->

動作確認

カレンダコントロールで検索条件を入力できるようになりました。

f:id:hiroki-sawano:20200717025709g:plain

さいごに

出来上がったプロジェクトは以下のリポジトリに格納しておきました。

github.com