Djangoで、一括作成・更新機能付カレンダー

2019-01-13 / PythonDjangoBootstrap4

概要

Djangoで、カレンダーを作るシリーズの1つです。 一括作成・更新機能付きの月間カレンダーを作成していきます。

次のようなものが作れます。
一括で作成・更新ができる

urls.py

2つ追加します。

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

mixins.py

import calendar
from collections import deque
import datetime
import itertools
from django import forms
...
...
class MonthWithFormsMixin(MonthCalendarMixin):
    """スケジュール付きの、月間カレンダーを提供するMixin"""

    def get_month_forms(self, start, end, days):
        """それぞれの日と紐づくフォームを作成する"""
        lookup = {
            # '例えば、date__range: (1日, 31日)'を動的に作る
            '{}__range'.format(self.date_field): (start, end)
        }
        # 例えば、Schedule.objects.filter(date__range=(1日, 31日)) になる
        queryset = self.model.objects.filter(**lookup)
        days_count = sum(len(week) for week in days)
        FormClass = forms.modelformset_factory(self.model, self.form_class, extra=days_count)
        if self.request.method == 'POST':
            formset = self.month_formset = FormClass(self.request.POST, queryset=queryset)
        else:
            formset = self.month_formset = FormClass(queryset=queryset)

        # {1日のdatetime: 1日に関連するフォーム, 2日のdatetime: 2日のフォーム...}のような辞書を作る
        day_forms = {day: [] for week in days for day in week}

        # 各日に、新規作成用フォームを1つずつ配置
        for empty_form, (date, empty_list) in zip(formset.extra_forms, day_forms.items()):
            empty_form.initial = {self.date_field: date}
            empty_list.append(empty_form)

        # スケジュールがある各日に、そのスケジュールの更新用フォームを配置
        for bound_form in formset.initial_forms:
            instance = bound_form.instance
            date = getattr(instance, self.date_field)
            day_forms[date].append(bound_form)

        # day_forms辞書を、周毎に分割する。[{1日: 1日のフォーム...}, {8日: 8日のフォーム...}, ...]
        # 7個ずつ取り出して分割しています。
        return [{key: day_forms[key] for key in itertools.islice(day_forms, i, i+7)} for i in range(0, days_count, 7)]

    def get_month_calendar(self):
        calendar_context = super().get_month_calendar()
        month_days = calendar_context['month_days']
        month_first = month_days[0][0]
        month_last = month_days[-1][-1]
        calendar_context['month_day_forms'] = self.get_month_forms(
            month_first,
            month_last,
            month_days
        )
        calendar_context['month_formset'] = self.month_formset
        return calendar_context

MonthWithScheduleMixinでは、month_day_schedulesという次のリストを返していました。

[
{1日: [1日の全てのスケジュール], 2日: [2日の全てのスケジュール].....},  # カレンダー1周目
{8日: [8日の全てのスケジュール], 9日: [9日の全てのスケジュール]....},  # カレンダー2周目
...
{29日: [29日の全てのスケジュール]...},  # カレンダー最終の周
]

これに対しMonthWithFormsMixinでは、month_day_formsという次のようなリストを返します。

[
{1日: [1日にあるスケジュールの更新フォーム, 1日の新規作成用フォーム], 2日: [2日にあるスケジュールの更新フォーム, 2日の新規作成用フォーム].....},  # カレンダー1周目
{8日: [8日にあるスケジュールの更新フォーム, 8日の新規作成用フォーム], ....},  # カレンダー2周目
...
]

それに加えて、month_formsetというフォームセットのオブジェクトも返します。フォームセットについては、Djangoで、フォームセットを使うシリーズも良ければご覧ください。

forms.py

今回はシンプルに、スケジュールの題名部分summaryだけ表示するようにしました。また各フォームはsummary以外にdateも送信しなくてはなりませんが、日付の入力欄自体は表示させる訳にはいきません。

なので、forms.HiddenInputとして、各フォームのdateの値はmixin内でempty_form.initial = {self.date_field: date}として設定しています。

class SimpleScheduleForm(forms.ModelForm):
    """シンプルなスケジュール登録用フォーム"""

    class Meta:
        model = Schedule
        fields = ('summary', 'date',)
        widgets = {
            'summary': forms.TextInput(attrs={
                'class': 'form-control',
            }),
            'date': forms.HiddenInput,
        }

views.py

フォームセットが絡んだ処理の上に、そのフォームセットをMixin内で作っています。CreateView等では整合性を取るのが厳しいので、genric.Viewを使ってビューを定義しました。

import datetime
from django.shortcuts import redirect, render
from django.views import generic
from .forms import BS4ScheduleForm, SimpleScheduleForm
from .models import Schedule
from . import mixins
...
...
class MonthWithFormsCalendar(mixins.MonthWithFormsMixin, generic.View):
    """フォーム付きの月間カレンダーを表示するビュー"""
    template_name = 'app/month_with_forms.html'
    model = Schedule
    date_field = 'date'
    form_class = SimpleScheduleForm

    def get(self, request, **kwargs):
        context = self.get_month_calendar()
        return render(request, self.template_name, context)

    def post(self, request, **kwargs):
        context = self.get_month_calendar()
        formset = context['month_formset']
        if formset.is_valid():
            formset.save()
            return redirect('app:month_with_forms')

        return render(request, self.template_name, context)

month_with_forms.html

{% extends 'app/base.html' %}
{% block content %}
    <style>
        table {
            table-layout: fixed;
        }

    </style>

    <a href="{% url 'app:month_with_forms' month_previous.year month_previous.month %}">前月</a>
    {{ month_current | date:"Y年m月" }}
    <a href="{% url 'app:month_with_forms' month_next.year month_next.month %}">次月</a>

    <form action="" method="POST">
        {{ month_formset.management_form }}
        <table class="table">
            <thead>
            <tr>
                {% for w in week_names %}
                    <th>{{ w }}</th>
                {% endfor %}
            </tr>
            </thead>
            <tbody>
            {% for week_day_forms in month_day_forms %}
                <tr>
                    {% for day, forms in week_day_forms.items %}
                        {% if now == day %}
                            <td class="table-success">
                                {% else %}
                            <td>
                        {% endif %}

                    <div>
                        {% if month_current.month != day.month %}
                            {{ day | date:"m/d" }}
                        {% else %}
                            {{ day.day }}
                        {% endif %}

                        {% for form in forms %}
                            {{ form.as_p }}
                        {% endfor %}
                    </div>
                    </td>
                    {% endfor %}
                </tr>
            {% endfor %}
            </tbody>
        </table>
        {% csrf_token %}
        <button type="submit" class="btn btn-primary">送信</button>
    </form>
{% endblock %}

フォーム等を取り出す流れはDjangoでスケジュール付き月間カレンダーと同様です。

フォームセットを使うのでform要素や、{{ month_formset.management_form }}等が必要です。

この記事の関連記事

Djangoでカレンダーを作るシリーズ

2018-10-07 / PythonDjangoBootstrap4シリーズ・まとめ

- Djangoで、月間カレンダーや週間カレンダー、それぞれにスケジュール表示機能がついたもの、スケジュール登録フォームがついたもの等、様々なカレンダーを作成していきます。

Djangoでフォームセットを使うシリーズ

2018-10-17 / PythonDjangoシリーズ・まとめ

- Djangoにはフォームセットという、複数のフォームを一括で扱うための機能があります。いくつか種類があり、様々な機能があるので、それらを紹介していきます。

コメント欄

記事にコメントする

名無し

失礼いたします。こちらのカレンダー機能を子機能として、親機能の中に入れたいのですが、可能でしょうか? 例えば、各社員ごとのスケジュール表のような感じで、社員のmodelを作って、それぞれの社員ごとのスケジュールを登録&見れるようにしたいなと思っております。URLでいうと、親モデル(社員)/<int:pk>/子モデル(スケジュール表)/<int:pk>/というような形を目指しているのですが、その際のviewとurlの書き方がいまいち分かりません。もしお時間があればお手すきの際に方針についてご教授いただけたらと思います。

コメントに返信する

なりと

各ユーザー毎にスケジュールとカレンダー機能を持たせるようにしたサンプルコードを作ったので、こちらを参考にしてください。

名無し

サンプルコードあげて頂き、本当にありがとうございます。 viewの書き方やテンプレートの書き方、大変勉強になります。

自分の実装してみようと思っていたものも作れそうです。 今後ともnaritoさんのブログや動画などで勉強させて頂きたいと思います!

名無し

何度も失礼致します。 こちらのプログラムをベースとして作っているのですが、スケジュール登録機能ページにログインしているadminのみしか見れないように設定をしたいのですが、 調べてみたところ、viewに ジェネレータの@login_requierdをいれるだけで成立するという情報があったため、class MonthWithFormsCalender内にある、関数のdef get と def post に @login_requiredをつけたのですが、 'MonthWithFormsCalendar' object has no attribute 'user'というエラーが発生してしまいました。 もしこれの解決策が分かれば、教えて頂けたらと思います。

あと、関係無い話で恐縮ですが、こちらのブログのコメントテキストフォームが、iphoneでの入力が不備がありそうです。(文字の変換が出来ない等)

なりと

クラスベースビューにデコレータをつけたい場合は、関数ビューと少し違います。具体的には、method_decoratorというデコレータをつける必要があります。

例えば、views.pyで次のようにします。

from django.views import generic
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator


class YourView(generic.TemplateView):
    template_name = 'app/hoge.html'

    @method_decorator(login_required)  # ここ。method_decoratorが必要
    def get(self, request, *args, **kwargs):
        return super().get(request, *args, **kwargs)

    @method_decorator(login_required)  # ここ。method_decoratorが必要
    def post(self, request, *args, **kwargs):
        return super().post(request, *args, **kwargs)

getとpostにこのデコレータをつけるならば、getとpostの前に呼ばれるdispatch()の時点でつけたほうが単純かもしれません。

class YourView(generic.TemplateView):
class Top(generic.TemplateView):
    template_name = 'app/top.html'

    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)

更に、もっと楽な書き方もあります。

@method_decorator(login_required, name='dispatch')
class Top(generic.TemplateView):
    template_name = 'app/top.html'

クラスベースビューを使っていて、そのデコレータの機能がMixinとして提供されている場合は、そのMixinクラスを使うほうが一般的です。今回はこの方法が一番おすすめです。

from django.contrib.auth.mixins import LoginRequiredMixin


class Top(LoginRequiredMixin, generic.TemplateView):
    template_name = 'app/top.html'

後は別の方法としてurls.pyの時点で指定する等もあります。

path('', login_required(views.MyView.as_view()), name='myview')
なりと

iPhoneでの入力不備のお知らせありがとうございます。コメントフォームのエディタはそのうち変更する予定なので、それができたら治ると思います。

名無し

返信頂きありがとうございます!ご教示頂いた方法で、実現することが出来ました。 dispachをつけたデコレータ のやり方があるのを知りませんでした。mixinでも提供されているのですね・・・。 本当に勉強になります。 ちなみにですが、なりとさんは、このような実現方法をどのような過程で知ったのでしょうか?また、どのようにDjangoを学習していったのかとても気になりますので、もし良かったらそういったなりとさんのDjangoの学習スタイルや今までの過程など、記事にして頂けると非常に嬉しいというか、とても価値があると思います。 また、素人目線で恐縮ですが、Djangoで分からない事をgoogleで調べてみても情報があまり出て来ないことも多々あり、このように第1次情報源がなりとさんである事がとても多いです。 やはり公式のドキュメントの原文を1から読んで行く必要があるのでしょうか?質問ばかりで申し訳御座いません。

なりと

中々に難しいトピックなので、今度まとめると思います。息抜きに日記アプリを作っているので、そちらに書くと思います。

なりと

日記に、↑のことについてちょっと書いてみました。