PythonDjangoBootstrap4

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

旧ブログ移行記事です。

概要

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 }}等が必要です。