知識の枝

"All is well"

Djangoの日付フォームをカレンダーで入力

約384日前 2021年4月28日18:14
デジタル
Django

改訂履歴


2021/4/28 投稿

1. 背景


Djangoを使った開発中に覚えたことを備忘録として残します。

今回はDateTimeフィールドのフォームを入力する際にカレンダーで選択できる機能を実装する方法を解説します。


2. ゴール


カレンダーから日付を選んで入力する。


3. はじめに


完成形のイメージはこんな感じです。



どこかで見たことあるカレンダーアイコンです。
そうです。Djangoの管理ページで使うことができるカレンダー入力機能です。

この機能を管理ページ以外でも使えるようにします。


4. カレンダー入力


基本的にはこちらのサイトを参考にさせて頂きました。ありがとうございます。
Django で Admin 画面の日付入力 Widget を利用するには - アルトエリア++

導入にあたりいくつかトラブルもありましたので、併せて解説したいと思います。

まず前提として日付入力したいフィールドの説明です。
今回はタスク管理アプリの「いつまでにやるか?(=納期)」の日付フィールドを対象にします。
models.py
class Task(models.Model):
"""Task info"""
task = models.CharField(verbose_name='やること', max_length=255, blank=True, null=True)
set_date = models.DateField('いつまでに', blank=True, null=True) #このDateFieldが対象です。

def __str__(self):
return self.task


「set_date」フィールドの入力時にカレンダーを使用しましょう。

まず入力フォームの設定をいじります。
forms.py
from django import forms
from django.contrib.admin.widgets import AdminDateWidget #インポート
from .models import Task


class AddTaskForm(forms.ModelForm):
"""タスク追加フォーム"""
class Meta:
model = Task
fields = '__all__'
widgets = {
'set_date': AdminDateWidget(), #インポートしたウィジェットを使う指示
}
特徴的なのは最後の行「widgets」の部分です。

まずカレンダーのウィジェットを使う為に「AdminDateWidget」をインポートします。
from django.contrib.admin.widgets import AdminDateWidget


フォームモデルの中で日付入力欄にウィジェットを適用させます。
 widgets = {
'set_date': AdminDateWidget(),
}
「set_date」という日付入力フィールドに対して「AdminDateWidget」を適用しています。


次にhtmlテンプレートの<head>内に下記を追加します。
<head>
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/base.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/forms.css' %}"/>
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
{{ add_task_form.media }} #テンプレートに渡しているフォームの変数名を「.」の前に書く
<script type="text/javascript" src="{% static 'admin/js/core.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/admin/RelatedObjectLookups.js' %}"></script>
</head>


上記の中央あたりに{{ add_task_fomr.media }}という記述があります。

この「add_task_form」の部分には、htmlテンプレートに渡しているフォームの変数名を記入して下さい。
変数名はviews.pyで定義したものです。
views.py
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['add_task_form'] = AddTaskForm #ここで定義した変数名
return context
「get_context_data」メソッドの部分だけ抜き出して記載しています。


以上の手順を踏むだけで日付入力フォームの右側にカレンダーが表示されるようになります。


5. モーダルと組み合わせる場合


モーダルとは画面にポップアップ表示される画面のことです。
Bootstrapを導入していると簡単に使用できます。

こんな感じ。



実はモーダル内に通常通りフォームを作るだけではカレンダー入力はできません。

カレンダー入力ボタンはあるのですが、カレンダー自体がモーダル画面の裏側に隠れてしまいます。
隠れてしまうので日付をクリックすることができません。


これに関しては下記記事が参考になりました。

bootstrap datepicker モーダルの下に表示されてしまう

今回使用しているカレンダーは上記とは異なりますが、原因は同じなので同様の対策を行います。

「z-index」というパラメータがどうやら原因のようです。

CSS の z-index プロパティは、位置指定要素とその子孫要素、またはフレックスアイテムの z 順を定義します。より大きな z-index を持つ要素はより小さな要素の上に重なります。

https://developer.mozilla.org/


簡単に言うと「奥行のパラメータ」です。
数字が大きいと手前、小さいと奥です。


今回、カレンダーが隠れてしまったのはモーダル要素のz-indexよりもカレンダーのz-indexが小さかったからだと考えられます。
したがって、カレンダーのz-indexを高くすることで上記問題が解決します。


Bootstrapのモーダルのz-indexは「1050」です。
カレンダーのz-indexを1050より大きく設定」します。


カレンダーの設定ファイルは下記フォルダの中にあります。
~\Lib\site-packages\django\contrib\admin\static\admin\css
「widgets.css」というファイルの中に「calendarboxクラス」があります。

/* CALENDARS & CLOCKS */

.calendarbox, .clockbox {
margin: 5px auto;
font-size: 12px;
width: 19em;
text-align: center;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
overflow: hidden;
position: relative;
}
この「calendarboxクラス」に「z-index: 1051;」を設定したら良さそうです。

しかしDjangoの元ファイルを改変するのは気が引けるので、自前のcssで設定を上書き(追加)したいと思います。

自前のcssファイル
.calendarbox {
z-index: 1051;
}
これでOKです。htmlテンプレートでcssを読み込むよう設定すればモーダルでもカレンダーが隠れなくなります。


6. さいごに


カレンダー入力ができるようになって便利です!
ぜひ設定してみてください。
yoshi 約84日前 2022年2月22日20:43 返信する
カレンダーが表示したくて試行錯誤しています。
こちらの記事をトレースしてやってみておりますがなんとも表示できておりません
repository: https://github.com/duri0214/python/tree/master/mypage

トレースした部分は省略します(githubにはあげてあります)が、テンプレートのところは以下のようになっています。フォームが表示されそうなところを「A」で囲んでみてもなにもでてきません。わたしの見落としがどこにあるかわかったら教えてほしいです。{{ add_task_form.media }}自体がカレンダーなのでしょうか。単に表示するためのタグなりがわかってないだけのような気もしてます
```hoge/templates/hoge/base.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">

<head>
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/base.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/forms.css' %}"/>
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
{{ add_task_form.media }}
<script type="text/javascript" src="{% static 'admin/js/core.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/admin/RelatedObjectLookups.js' %}"></script>
</head>
<body>

A{{ form.as_p }}A
{% block content %}{% endblock %}

</body>
</html>
```
chuna 約77日前 2022年3月1日22:52
こんにちはyoshiさん。
修正が必要だと思われる箇所がいくつかあります。
1つずつ挙げていきますね。

①{{form.as_p}}について
テンプレート内でformを表示する場合は<form>タグで囲ってあげる必要があります。
したがって下記のような書き方になります。
<form method="post">
{{form.as_p}}
</form>
また、送信ボタンは別途必要ですので</form>の前に
<button type="submit">送信</button>を入れてあげましょう。
尚、POST送信する場合はCSRFトークンが必要ですので、{% csrf_token %}を忘れず書きましょう。

②テンプレートに渡しているフォームの変数名が間違っている
yoshiさんはテンプレート上で「form」という変数を表示させようとしています。 => {{form.as_p}}

実際にテンプレートに渡されている変数は「add_task_form」です。
以下views.pyから抜粋
"""
context['add_task_form'] = AddTaskForm
"""
上記を見ると「add_task_form」という変数にAddTaskFormのフォームが入っていますよね。

ですので、フォームをテンプレート上で呼び出す場合は
{{add_task_form.as_p}}という書き方になります。


ここまでをまとめると下記のような書き方になると思います。
<body>
<form method="post">
{{add_task_form.as_p}}
{% csrf_token %}
<button type="submit">送信</button>
</form>
</body>

若干うろ覚えで書いているので、解決しなかったらすみません。