Djangoで週間・月間カレンダー

Python Django Bootstrap4

概要

Djangoでカレンダーを作るシリーズの1つです。Googleカレンダーのように、週間カレンダー、月間カレンダーが一緒に表示され、スケジュール登録もできる総合的なカレンダーを作成します。

次のような見た目です。それなりに、良さそうですね。 Googleカレンダーっぽい

urls.py

/mycalendarと、/mycalendar/2018/5/5 のようなURLで表示します。 year、month、dayといった名前は固定です。

    path('mycalendar/', views.MyCalendar.as_view(), name='mycalendar'),
    path(
        'mycalendar/<int:year>/<int:month>/<int:day>/', views.MyCalendar.as_view(), name='mycalendar'
    ),

forms.py

作成フォームをカッコよく表示したいので、BS4対応したフォームを定義します。

from django import forms
from .models import Schedule


class BS4ScheduleForm(forms.ModelForm):
    """Bootstrapに対応するためのModelForm"""

    class Meta:
        model = Schedule
        fields = ('summary', 'description', 'start_time', 'end_time')
        widgets = {
            'summary': forms.TextInput(attrs={
                'class': 'form-control',
            }),
            'description': forms.Textarea(attrs={
                'class': 'form-control',
            }),
            'start_time': forms.TextInput(attrs={
                'class': 'form-control',
            }),
            'end_time': forms.TextInput(attrs={
                'class': 'form-control',
            }),
        }

    def clean_end_time(self):
        start_time = self.cleaned_data['start_time']
        end_time = self.cleaned_data['end_time']
        if end_time <= start_time:
            raise forms.ValidationError(
                '終了時間は、開始時間よりも後にしてください'
            )
        return end_time

views.py

MonthCalendarMixinとWeekWithScheduleMixin、そしてCreateViewを使います。 独立したMixinとして定義してきたので、他のMixinや組み込みのクラスビューと一緒に使えます。

import datetime
from django.shortcuts import redirect
from django.views import generic
from . import mixins
from .forms import BS4ScheduleForm
from .models import Schedule
...
...

class MyCalendar(mixins.MonthCalendarMixin, mixins.WeekWithScheduleMixin, generic.CreateView):
    """月間カレンダー、週間カレンダー、スケジュール登録画面のある欲張りビュー"""
    template_name = 'app/mycalendar.html'
    model = Schedule
    date_field = 'date'
    form_class = BS4ScheduleForm

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

    def form_valid(self, form):
        month = self.kwargs.get('month')
        year = self.kwargs.get('year')
        day = self.kwargs.get('day')
        if month and year and day:
            date = datetime.date(year=int(year), month=int(month), day=int(day))
        else:
            date = datetime.date.today()
        schedule = form.save(commit=False)
        schedule.date = date
        schedule.save()
        return redirect('app:mycalendar', year=date.year, month=date.month, day=date.day)

それぞれのMixinで定義している、カレンダー情報取得用のメソッドを呼び出します。

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

URLに年月日が情報としてあるので、それを使ってスケジュールを保存します。

    def form_valid(self, form):
        month = self.kwargs.get('month')
        year = self.kwargs.get('year')
        day = self.kwargs.get('day')
        if month and year and day:
            date = datetime.date(year=int(year), month=int(month), day=int(day))
        else:
            date = datetime.date.today()
        schedule = form.save(commit=False)
        schedule.date = date
        schedule.save()
        return redirect('app:mycalendar', year=date.year, month=date.month, day=date.day)

mycalendar.html

{% extends 'app/base.html' %}
{% block content %}
    <div class="row">
        <div class="col-md-3">
            {% include 'app/includes/month.html' %}
            <hr>
            <form action="" method="POST">
                {{ form.non_field_errors }}
                {% for field in form %}
                    <div class="form-group row">
                        <label for="{{ field.id_for_label }}"
                               class="col-sm-4 col-form-label">{{ field.label_tag }}</label>
                        <div class="col-sm-8">
                            {{ field }}
                            {{ field.errors }}
                        </div>
                    </div>
                {% endfor %}
                {% csrf_token %}
                <button type="submit" class="btn btn-primary btn-block">送信</button>
            </form>
        </div>
        <div class="col-md-9">
            {% include 'app/includes/week.html' %}
        </div>
    </div>
{% endblock %}


{% block extrajs %}
    <link rel="stylesheet" type="text/css"
          href="https://cdnjs.cloudflare.com/ajax/libs/timedropper/1.0/timedropper.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/timedropper/1.0/timedropper.min.js"></script>
    <script>
        $(function () {
            // timedropper
            $("#id_start_time").timeDropper({
                format: "H:mm",
                setCurrentTime: false,
            });
            $("#id_end_time").timeDropper({
                format: "H:mm",
                setCurrentTime: false,
            });
        });
    </script>
{% endblock %}

左側(col-md-3)に、月間カレンダーとスケジュール登録フォームを起き、右側(col-md-9)に週間カレンダーwithスケジュールです。

カレンダー部分は{% include %}しています。

時間の選択は、timedropperというjQueryのプラグインを利用しています。

{% block extrajs %}
    <link rel="stylesheet" type="text/css"
          href="https://cdnjs.cloudflare.com/ajax/libs/timedropper/1.0/timedropper.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/timedropper/1.0/timedropper.min.js"></script>
    <script>
        $(function () {
            // timedropper
            $("#id_start_time").timeDropper({
                format: "H:mm",
                setCurrentTime: false,
            });
            $("#id_end_time").timeDropper({
                format: "H:mm",
                setCurrentTime: false,
            });
        });
    </script>
{% endblock %}

開始時間、終了時間をクリックで以下のように表示されます。

良い感じに時間を設定できる

includes/month.html

月間カレンダーや週間カレンダー部分はmycalendar.htmlに書くとちょっと見にくくなります。なので、includeで読み込む形にしました。使いまわしたりもできるでしょう。

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

<table class="table" style="table-layout: fixed;">
    <thead>
    <tr>
        {% for w in week_names %}
            <th>{{ w }}</th>
        {% endfor %}
    </tr>
    </thead
    <tbody>
    {% for week in month_days %}
        <tr>
            {% for day in week %}
                {% if now == day %}
                    <td class="table-success">
                {% else %}
                    <td>
                {% endif %}

                <a href="{% url 'app:mycalendar' day.year day.month day.day %}">{{ day.day }}</a>
                </td>
            {% endfor %}
        </tr>
    {% endfor %}
    </tbody>
</table>

以前のmonth.htmlとほとんど同じなのですが、テーブルの各幅を均等にするためにstyle="table-layout: fixed;"とし、月跨ぎを6/1のように表示させると幅をとるので、単純に31のように表示するようにしました。また、各日付をリンクにしています。

includes/week.html

こちらも前のweek.htmlとほぼ同じですが、テーブル幅を均等にし、曜日は月間カレンダーで表示しているのでわかるだろうということで、曜日表示をなくしています。

<a href="{% url 'app:mycalendar' week_previous.year week_previous.month  week_previous.day %}">前週</a>
{{ week_first | date:"Y年m月d日" }}〜{{ week_last | date:"Y年m月d日" }}
<a href="{% url 'app:mycalendar' week_next.year week_next.month  week_next.day %}">次週</a>

<table class="table" style="table-layout: fixed;">
    <tbody>
    <tr>
        {% for day in week_days %}
            {% if now == day %}
                <td class="table-success">
                    {% else %}
                <td>
            {% endif %}
        {% if week_first.month != day.month %}
            {{ day | date:"m/d" }}
        {% else %}
            {{ day.day }}
        {% endif %}
        </td>
        {% endfor %}
    </tr>
    <tr>
        {% for schedules in week_day_schedules.values %}
            <td>
                {% for s in schedules %}
                    {{ s.start_time }} - {{ s.end_time }}<br>
                    {{ s.summary }}<br>
                    {{ s.description | linebreaks }}
                {% endfor %}
            </td>
        {% endfor %}
    </tr>
    </tbody>
</table>

Relation Posts

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

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

Python Django Bootstrap4 シリーズ・まとめ

Comment

記事にコメントする

名無し

なにが伝えたいのかまとまったのでコメントします。

githubからファイルをダウンロードさせていただいたのですが、このファイルはわたしの勘違いでなければ、欲張りセットのファイルになっているのでしょうか。

なのに、登録フォームなどはでずに月刊カレンダーが出ます。なにをどのようにしたら欲張りセットのような表記になりますか?
何度もすみません。回答お願いします

返信する

なりと

Githubにあげたソースコードは、欲張りセットだけでなく他のものも含まれています。

`/mycalendar` にアクセスすると欲張りセットなページに移動できるので、そちらを試していただけますか。

名無し

わかりました。試してみます。

もし欲張りセットのみを表示させたかったらどのようにしたらいいでしょうか。なんどもごめんなさい。

なりと

その場合は`sampleapp`の`urls.py`を次のように書き換えてください。
```python
from django.urls import path
from . import views

app_name = 'sampleapp'

urlpatterns = [
path('', views.MyCalendar.as_view(), name='mycalendar'),
path(
'mycalendar/<int:year>/<int:month>/<int:day>/', views.MyCalendar.as_view(), name='mycalendar'
),
]
```

名無し

丁寧な解説ありがとうございます。本当に助かりました。
またなにかありましたら連絡します。これからも頑張ってください