NARITO BLOG

Djangoでスケジュール付き月間カレンダー

Python, Django, Bootstrap4,

概要

Djangoで、カレンダーを作るシリーズの1つです。 スケジュール付きの、月間カレンダーを作成していきます。

以下のようなものが作れます。 スケジュール付きの月間カレンダー

mixins.py

MonthCalendarMixinをベースに、新しいMixinを定義します。

import itertools  # 追加
...
...
class MonthWithScheduleMixin(MonthCalendarMixin):
    """スケジュール付きの、月間カレンダーを提供するMixin"""

    def get_month_schedules(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)

        # {1日のdatetime: 1日のスケジュール全て, 2日のdatetime: 2日の全て...}のような辞書を作る
        day_schedules = {day: [] for week in days for day in week}
        for schedule in queryset:
            schedule_date = getattr(schedule, self.date_field)
            day_schedules[schedule_date].append(schedule)

        # day_schedules辞書を、周毎に分割する。[{1日: 1日のスケジュール...}, {8日: 8日のスケジュール...}, ...]
        # 7個ずつ取り出して分割しています。
        size = len(day_schedules)
        return [{key: day_schedules[key] for key in itertools.islice(day_schedules, i, i+7)} for i in range(0, size, 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_schedules'] = self.get_month_schedules(
            month_first,
            month_last,
            month_days
        )
        return calendar_context

基本的には月間カレンダーと同じで、get_month_calendar()がメインです。返すものも同様ですが、month_day_schedulesという{日:その日のスケジュール}な辞書...を各周の分だけ含んだリストも返すようになりました。次のようなリストです。

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

スケジュール付き週間カレンダーのweek_day_schedulesが複数詰まったリストと考えるとわかりやすいでしょう。

urls.py

追加します。

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

views.py

今までと同じ流れです。

class MonthWithScheduleCalendar(mixins.MonthWithScheduleMixin, generic.TemplateView):
    """スケジュール付きの月間カレンダーを表示するビュー"""
    template_name = 'app/month_with_schedule.html'
    model = Schedule
    date_field = 'date'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        calendar_context = self.get_month_calendar()
        context.update(calendar_context)
        return context

month_with_schedule.html

スケジュール付き月間カレンダーのテンプレートです。

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

        td > div {
            height: 100px;
            overflow: hidden;
            white-space: nowrap;
        }

    </style>
    <a href="{% url 'app:month_with_schedule' month_previous.year month_previous.month %}">前月</a>
    {{ month_current | date:"Y年m月" }}
    <a href="{% url 'app:month_with_schedule' month_next.year month_next.month %}">次月</a>
    <table class="table">
        <thead>
        <tr>
            {% for w in week_names %}
                <th>{{ w }}</th>
            {% endfor %}
        </tr>
        </thead>
        <tbody>
        {% for week_day_schedles in month_day_schedules %}
            <tr>
                {% for day, schedules in week_day_schedles.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 schedule in schedules %}
                        <p>{{ schedule.summary }}</p>
                    {% endfor %}
                </div>
                </td>
                {% endfor %}
            </tr>
        {% endfor %}
        </tbody>
    </table>
{% endblock %}

month_day_schedulesはスケジュール付き週間カレンダーのweek_day_schedulesが複数詰まったリストなので、次のように取り出します。

        {% for week_day_schedles in month_day_schedules %}
            <tr>
                {% for day, schedules in week_day_schedles.items %}

スケジュールを取り出しているのは以下の部分です。とりあえず、概要だけ表示しています。

            {% for schedule in schedules %}
                <p>{{ schedule.summary }}</p>
            {% endfor %}

また、日付やスケジュール部分をdivで囲っていることに注意してください。 後ほど紹介しますが、divで囲わないと、いくつかのCSSの設定ができなくなります。

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

                    {% for schedule in schedules %}
                        <p>{{ schedule.summary }}</p>
                    {% endfor %}
                </div>

次に<style>内です。table-layout: fixedはテーブルの各幅を均等にします。

    table {
        table-layout: fixed;
    }

そして、td内につくったdivにいくつか指定をしています。heightは、高さの指定ですね。 overflow:hiddenですが、これは説明するよりも実際に外して見せたほうがわかりやすいので、外してみます。

    td > div {
      height: 100px;
      overflow: hidden;
      white-space: nowrap;
    }

外すと、隠れていた内容がはみでましたね。 今回はheight:100pxとして、100pxに収まりきらない内容は隠しました。その指定がoverflow:hiddenです。 overflow:hiddenがない場合ははみ出る

white-space: nowrap;を消すと、幅に収まらない場合は折り返されて表示されます。 white-space:nowrapを消すと折り返される

もしはみでた部分も表示したいならば、overflow:auto;にすると不恰好ですがスクロールバーがつけれます。 overflow:autoにするとスクロールバーがつく

今回のスケジュール部分の表示の仕方はあくまで一例です。色々試してみてください。