naritoブログ

このブログはPython/DjangoとBootstrap4で作成されました
Githubのソースコード

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

約78日前 2018年5月4日12:32
プログラミング関連
Bootstrap4 Django Python

概要


Djangoで、カレンダーを作るシリーズの1つです。
今までのまとめとして、スケジュール付きの週間カレンダー、月間カレンダー、スケジュール登録フォームを表示してみます。

以下のようなものが作れます。
欲張りセット

sampleapp/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'
    ),


sampleapp/views.py


MonthCalendarMixinとWeekWithScheduleMixin、そしてCreateViewを使います。
独立したMixinとして定義してきたので、他のMixinや組み込みのクラスビューと一緒に使えます。
import datetime
from django.shortcuts import redirect
from django.views import generic
from scalendar.forms import BS4ScheduleForm
from scalendar.views import MonthCalendarMixin, WeekCalendarMixin, WeekWithScheduleMixin
...
...

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

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['week'] = self.get_week_calendar()
        context['month'] = self.get_month_calendar()
        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('sampleapp:mycalendar', year=date.year, month=date.month, day=date.day)



それぞれのMixinで定義している、カレンダー情報取得用のメソッドを呼び出します。
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['week'] = self.get_week_calendar()
        context['month'] = self.get_month_calendar()
        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:  # /mycalendar のように年月日がない場合は、当日にデータを保存
            date = datetime.date.today()
        schedule = form.save(commit=False)
        schedule.date = date
        schedule.save()
        return redirect('sampleapp:mycalendar', year=date.year, month=date.month, day=date.day)



scalendar/forms.py


scalendarアプリケーション側で、Bootstrap4対応のフォームを定義しておきました。
もちろん、これを使わず自分で定義しても良いです。
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



mycalendar.html


テンプレートです。
{% extends 'sampleapp/base.html' %}
{% block content %}
<div class="row">
  <div class="col-md-3">
    {% include 'sampleapp/widgets/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 'sampleapp/widgets/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)に週間カレンダーとスケジュールです。
<div class="row">
  <div class="col-md-3">
    {% include 'sampleapp/widgets/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-10">
    {% include 'sampleapp/widgets/week.html' %}
  </div>
</div>


時間の選択は、timedropperというjQueryのプラグインを利用しています。その設定です。
<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>



開始時間、終了時間をクリックで以下のように表示されます。
timedropperで、時間選択を簡単にしている

widgets/month.html


月間カレンダーや週間カレンダー部分はmycalendar.htmlに書くとちょっと見にくくなります。なので、includeで読み込む形にしました。使いまわしたりもできるでしょう。
<a href="{% url 'sampleapp:mycalendar' month.previous.year month.previous.month month.previous.day %}">前月</a>
{{ month.current | date:"Y年m月" }}
<a href="{% url 'sampleapp:mycalendar' month.next.year month.next.month month.next.day %}">次月</a>
<table class="table" style="table-layout: fixed;">
  <thead>
    <tr>
      {% for w in month.week_names %}
        <th>{{ w }}</th>
      {% endfor %}
    </tr>
  </thead>
  <tbody>
    {% for week in month.days %}
      <tr>
        {% for day in week %}
          {% if month.now == day %}
            <td class="table-success">
          {% else %}
            <td>
          {% endif %}
          <a href="{% url 'sampleapp:mycalendar' day.year day.month day.day %}">{{ day.day }}</a>
          </td>
        {% endfor %}
      </tr>
    {% endfor %}
  </tbody>
</table>


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

widgets/week.html


こちらも前のweek.htmlとほぼ同じですが、テーブル幅を均等にし、曜日は月間カレンダーで表示しているのでわかるだろうということで、曜日表示をなくしています。
<a href="{% url 'sampleapp: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 'sampleapp:mycalendar' week.next.year week.next.month  week.next.day %}">次週</a>
<table class="table table-bordered" style="table-layout: fixed;">
  <tbody>
      <tr>
        {% for day in week.days %}
          {% if week.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 day_schedule_list in week.schedule_list %}
          <td>
            {% for s in day_schedule_list %}
                {{ s.start_time }} - {{ s.end_time }}<br>
                {{ s.summary }}<br>
                {{ s.description | linebreaks }}
            {% endfor %}
          </td>
        {% endfor %}
      </tr>
  </tbody>
</table>
生徒 約64日前 2018年5月18日19:06 返信する
いつも楽しく拝見しています。
今回、コードを勉強させて頂こうと思い、カレンダーシリーズの欲張りセットをダウンロードしてdjangoの中に入れました。
開発環境で立ち上げると、http://127.0.0.1:8000/は月間のカレンダーが表示されるのですが、
mycalendar/にアクセスすると、no such table: scalendar_scheduleというエラーメッセージが表示されて欲張りセットの画面が表示されません。
この開発方法を、ご教授頂けませんでしょうか。
誠に恐れ入りますが、何卒宜しくお願い致します。
なりと 約64日前 2018年5月18日21:30
migrateコマンドの実行を試してみてください。
生徒 約64日前 2018年5月18日21:51
誠に恐れ入ります。無事解決しました。
生徒 約64日前 2018年5月18日22:24 返信する
いつも大変お世話になっております。
誠に恐れ入りますが、追加で質問させて下さい。
今回、narito先生は現在の月や週の値を取り出すために、以下のようなコードを使用されています。

month = self.kwargs.get('month')
day = self.kwargs.get('day')

print()で値を表示させるとカレンダーを表示させるための月、日が表示されるので、このコードの役目は理解できるのですが、このコードがどのような原理で動いているのか理解できません。特にkwargsの役目が分かりません。
このコードの原理覚えてぜひ応用したいので、簡単でよいので説明して頂くことは可能でしょうか。
なりと 約64日前 2018年5月18日23:21
/month/2018/5/
のようなURLから、2018や5を取り出すための記述がself.kwargs.get('month')といった記述です。

urls.pyにて/month/<int:year>/<int:month>/という記述をしていると、クラスベースビューではkwargs.get('year')やget('month')で取り出すことができます。
なりと 約64日前 2018年5月18日23:26
添付ファイルダウンロード(1.png)
以前にkwargsについて説明を行ったことがあったので、その際の画像を添付しています。
生徒 約64日前 2018年5月19日0:42
なるほど。
URLから値を取り出しているとは思いませんでした。
とても勉強になりました。
誠に有難うございます!
名無し 約30日前 2018年6月21日14:31 返信する
こちらの記事のサンプルコードですが、Githubには存在していないのでしょうか?
ブログ上の「Githubのソースコード」のリンクをクリックするとBlogのソースコードページに飛んでしまいます。
なりと 約30日前 2018年6月21日15:39
https://github.com/naritotakizawa/django-simple-calendar
にあります。
名無し 約30日前 2018年6月21日14:53 返信する
いつも拝見させていただいております。
ぜひとも、本カレンダー機能に編集機能を追加頂きたいです!
一度登録した後adminサイトを使わないと編集できないのはちょっと実用的でないかなと思いました。
なりと 約30日前 2018年6月21日15:47
このシリーズはカレンダーの、重要な部分のサンプルコードとしての意味合いが強く、編集機能をつけるかは何ともいえないです。
要望が多そうでしたら追加します。