naritoブログ

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

Djangoで、月間カレンダー

約24日前 2018年5月4日1:38
プログラミング関連
Bootstrap4 Django Python

概要


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

以下のようなものが作れます。
月間カレンダー

scalendar/views.py


月間カレンダー用のMixinクラスを定義します。何を作って何を返すMixinなのかを次に説明し、実際に使っているところを観ると、コードの中も理解できると思います。
import calendar
from collections import deque
import datetime


class BaseCalendarMixin:
    """カレンダー関連Mixinの、基底クラス"""
    first_weekday = 0  # 0は月曜から、1は火曜から。6なら日曜日からになります。お望みなら、継承したビューで指定してください。
    week_names = ['月', '火', '水', '木', '金', '土', '日']  # これは、月曜日から書くことを想定します。['Mon', 'Tue'...

    def setup(self):
        """カレンダーのセットアップ処理

        calendar.Calendarクラスの機能を利用するため、インスタンス化します。
        Calendarクラスのmonthdatescalendarメソッドを利用していますが、デフォルトが月曜日からで、
        火曜日から表示したい(first_weekday=1)、といったケースに対応するためのセットアップ処理です。

        """
        self._calendar = calendar.Calendar(self.first_weekday)

    def get_week_names(self):
        """first_weekday(最初に表示される曜日)にあわせて、week_namesをシフトする"""
        week_names = deque(self.week_names)
        week_names.rotate(-self.first_weekday)  # リスト内の要素を右に1つずつ移動...なんてときは、dequeを使うと中々面白いです
        return week_names


class MonthCalendarMixin(BaseCalendarMixin):
    """月間カレンダーの機能を提供するMixin"""

    @staticmethod
    def get_previous_month(date):
        """前月を返す"""
        if date.month == 1:
            return date.replace(year=date.year-1, month=12, day=1)

        else:
            return date.replace(month=date.month-1, day=1)

    @staticmethod
    def get_next_month(date):
        """次月を返す"""
        if date.month == 12:
            return date.replace(year=date.year+1, month=1, day=1)

        else:
            return date.replace(month=date.month+1, day=1)

    def get_month_days(self, date):
        """その月の全ての日を返す"""
        return self._calendar.monthdatescalendar(date.year, date.month)

    def get_current_month(self):
        """現在の月を返す"""
        month = self.kwargs.get('month')
        year = self.kwargs.get('year')
        if month and year:
            month = datetime.date(year=int(year), month=int(month), day=1)
        else:
            month = datetime.date.today().replace(day=1)
        return month

    def get_month_calendar(self):
        """月間カレンダー情報の入った辞書を返す"""
        self.setup()
        current_month = self.get_current_month()
        calendar_data = {
            'now': datetime.date.today(),
            'days': self.get_month_days(current_month),
            'current': current_month,
            'previous': self.get_previous_month(current_month),
            'next': self.get_next_month(current_month),
            'week_names': self.get_week_names(),
        }
        return calendar_data



MonthCalendarMixinの説明


一番重要なメソッドはget_month_calendarメソッドで、これが月間カレンダー構築に必要なものが詰まった辞書を返します。
以下は、辞書の各キーと返すものの説明です。

now


現在の日付で、datetime.date型のオブジェクトを返します。これを使うと、その日が今日なら色をつける...なんてことができます。

days


これは、その月の全ての日を返しています。以下のような二次元のリストになります。
[[30, 1, 2, 3, 4, 5, 6],
[7, 8, 9, 10, 11, 12, 13],
[14, 15, 16, 17, 18, 19, 20],
[21, 22, 23, 24, 25, 26, 27]
[28, 29, 30, 31, 1, 2, 3]]

さらに、全ての日はdatetime.date型です。月をまたいている日付がありますが、これらは正しく跨いだ月でのdatetime.date型になっています、安心です。

current


そのカレンダーが表示している月です(datetime.date型)

previous


currentの、前の月です(datetime.date型)

next


currentの、次の月です(datetime.date型)


BaseCalendarMixinで返しているもの


week_namesは、MonthCalendarMixinだけでなく他のMixinでも同じものを返します。

week_names


['月', '火', '水', '金', '土', '日']のような、1週間の曜日をリストで返します。
これをずらしたい場合、ビューにてfirst_weekday = 1 のように上書きします。0は月曜から、1は火曜から。
例えばfirst_weekday=6なら日曜日からになります...つまり、['日', '月', '火'...]のようにです。
daysで取得される日付も、ちゃんと日曜日から取得されるようになるので安心です。
['Mon', 'Tue', 'Wed'..]のように曜日の表示を変えたい場合は、week_names = ['Mon', 'Tue', 'Wed'..]とします。ここは、初めが月曜日になるように!


MonthCalendarMixinが返すのは、これらのオブジェクト(ほとんどはdatetime.date)だけです!これらを元に、好きにカレンダーを作れます。

sampleapp/urls.py


/と、/month/2018/5 のようなURLで、月間カレンダーが表示されます。
Mixin内でself.kwargs['year']やself.kwargs['month']というように年と月を取得するので、yearやmonthといった名前は今の所固定です。
from django.urls import path
from . import views

app_name = 'sampleapp'

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


sampleapp/views.py


ビューでは、scalendarで定義したMixinを継承し、get_context_data内でget_month_calendarメソッドを呼び出せば、後はテンプレートに好きに書くことができます。
from django.views import generic
from scalendar.views import MonthCalendarMixin


class MonthCalendar(MonthCalendarMixin, generic.TemplateView):
    """月間カレンダーを表示するビュー"""
    template_name = 'sampleapp/month.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['month'] = self.get_month_calendar()
        return context


base.html


scalendarにテンプレートはないので、全てsampleapp内に作ります。
よくあるBootstrap4用のテンプレートです。
<!doctype html>
<html lang="ja">
<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
        integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">

  <title>カレンダー</title>
</head>
<body>
  <div class="container-fluid mt-3">
    {% block content %}{% endblock %}
  </div>

  <!-- Optional JavaScript -->
  <!-- jQuery first, then Popper.js, then Bootstrap JS -->
  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
          integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
          crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js"
          integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ"
          crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"
          integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm"
          crossorigin="anonymous"></script>
  {% block extrajs %}{% endblock %}
</body>
</html>


month.html


そして、月間カレンダーを実際に表示しているテンプレートです。
{% extends 'sampleapp/base.html' %}
{% block content %}

<a href="{% url 'sampleapp:month' month.previous.year month.previous.month %}">前月</a>
{{ month.current | date:"Y年m月" }}
<a href="{% url 'sampleapp:month' month.next.year month.next.month %}">次月</a>
<table class="table">
  <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 %}
          {% if month.current.month != day.month %}
            {{ day | date:"m/d" }}
          {% else %}
            {{ day.day }}
          {% endif %}
          </td>
        {% endfor %}
      </tr>
    {% endfor %}
  </tbody>
</table>
{% endblock %}


Djangoのdateフィルターを使って、2018年06月 のように表示させています。{{ month.current.year}年{{ month.current.month}}月と書くよりも単純です。
{{ month.current | date:"Y年m月" }}


前月、次月は、それぞれprevious、nextがありましたので、それが使えます。
<a href="{% url 'sampleapp:month' month.previous.year month.previous.month %}">前月</a>
...
<a href="{% url 'sampleapp:month' month.next.year month.next.month %}">次月</a>


月の全ての日付は、2次元なリストなので、forで2回回す必要があります。
{% if month.now == day %} は、その日が今日なら、という意味です。今日ならtrタグに色をつけます。
{% if month.current.month != day.month %}は、月を跨いだ日付なら分かりやすくするために6/1 のように表示させています。
    {% for week in month.days %}
      <tr>
        {% for day in week %}
          {% if month.now == day %}
            <td class="table-success">
          {% else %}
            <td>
          {% endif %}
          {% if month.current.month != day.month %}
            {{ day | date:"m/d" }}
          {% else %}
            {{ day.day }}
          {% endif %}
          </td>
        {% endfor %}
      </tr>
    {% endfor %}