naritoブログ

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

Djangoで、週間カレンダーをつくる

プログラミング関連 Bootstrap4 Django Python calendarモジュール 約135日前
2017年7月29日22:11
以前に、月のカレンダーを作成しました。

Djangoで、月間カレンダーをつくる
https://torina.top/detail/380/

あれは便利でしたが、週ごとにスケジュールを確認したいこともあります。
今回は、週間カレンダーをDjangoで作成していきます。

まず開くと、このような画面です。週や月の移動ができます。


スケジュールがあると、このように表示されます。


どの週も月曜日から始まるようにしています。


models.py


月間カレンダーと同様に、メモと日付をフィールドとして持つシンプルなモデルです。
from django.db import models
from django.utils import timezone


class Schedule(models.Model):
    """スケジュール."""

    memo = models.TextField('メモ')
    date = models.DateField('日付')
    created_at = models.DateTimeField('作成日', default=timezone.now)

    def __str__(self):
        return self.memo



urls.py


/と、/2017/7/1 のようなURLでアクセスができるようにしています。
from django.conf.urls import url
from . import views

app_name = 'app'

urlpatterns = [
    url(r'^$', views.WeekView.as_view(), name='home'),
    url(r'^(?P<year>[0-9]+)/(?P<month>[0-9]+)/(?P<week>[0-9]+)/$',
        views.WeekView.as_view(),
        name='week'
    ),
]



views.py


年、月、何周目かを取得し、それをformatweek_tableへ渡し、htmlを作成します。
from django.utils.safestring import mark_safe
from django.views import generic
from .calendarlib import WeekCalendar


class WeekView(generic.TemplateView):
    """週間カレンダーのView."""

    template_name = 'app/week.html'

    def get_context_data(self, *args, **kwargs):
        week = int(self.kwargs.get('week', 1))
        year = self.kwargs.get('year')
        month = self.kwargs.get('month')

        calendar = WeekCalendar()
        html = calendar.formatweek_table(week, year, month)

        context = super().get_context_data(*args, **kwargs)
        context['calendar'] = mark_safe(html)
        return context



calendarlib.py


カレンダーの作成は、このモジュールです。
"""カレンダーに関するモジュール."""
from calendar import monthrange, LocaleHTMLCalendar
import datetime
from django.shortcuts import resolve_url
from .models import Schedule


def add_months(date, num):
    """datetimeオブジェクトに月を加算・減算する.

    引数:
        date: datetime.datetimeオブジェクト。増減の基準となる日付を渡す
        num: 増減の数字。1月後は1、1月前は-1
    返り値:
        n月後、n月前のdatetimeオブジェクト

    """
    month = date.month - 1 + num
    year = int(date.year + month / 12)
    month = month % 12 + 1
    day = min(date.day, monthrange(year, month)[1])
    date = datetime.datetime(year=year, month=month, day=day)
    return date


class WeekCalendar(LocaleHTMLCalendar):
    """週間での表示可能なカレンダー.

    formatweekメソッドで、週ごとにカレンダーを表示できます

    """

    def formatweek_table(self, week_index, year=None, month=None):
        """週間カレンダーを作成する."""
        if year is None and month is None:
            now = datetime.datetime.now()
        elif year and month:
            now = datetime.datetime(year=int(year), month=int(month), day=1)

        v = []
        a = v.append
        a('<table class="table week-table">')
        weeks = self.monthdays2calendar(now.year, now.month)
        now_week = weeks[week_index-1]

        # 前週・次週の部分
        a('<tr>')
        a('<th colspan="7">')

        # 前月へのリンクを作る
        pre_date = add_months(now, -1)
        url = resolve_url(
            'app:week', year=pre_date.year,
            month=pre_date.month, week=1
        )
        a("<a href={0}>前月</a> ".format(url))

        # 1週目じゃなければ、前週のリンクを作る
        if week_index != 1:
            url = resolve_url(
                'app:week', year=now.year,
                month=now.month, week=week_index-1
            )
            a("<a href={0}>前週</a> ".format(url))

        # 最後の週じゃなければ、次週へのリンクを作る
        if week_index != len(weeks):
            url = resolve_url(
                'app:week', year=now.year,
                month=now.month, week=week_index+1
            )
            a("<a href={0}>次週</a> ".format(url))

        # 次月へのリンクを作る
        next_date = add_months(now, 1)
        url = resolve_url(
            'app:week', year=next_date.year,
            month=next_date.month, week=1
        )
        a("<a href={0}>次月</a> ".format(url))

        a('</th>')
        a('</tr>')

        a(self.formatweekheader())

        # 〜日 のヘッダー部分
        a('<tr>')
        for day, index in now_week:
            a('<th>')
            if day != 0:
                day_title = '{0}月{1}日'.format(now.month, day,)
                a(day_title)
            a('</th>')
        a('</tr>')

        # メインのスケジュール部分
        for day, index in now_week:
            a('<td>')
            if day != 0:
                date = datetime.datetime(
                    year=now.year, month=now.month, day=day
                )
                schedules = Schedule.objects.filter(date=date)

                for schedule in schedules:
                    a('★')
                    a(schedule.memo)
                    a('<br>')
            a('</td>')

        a('</table>')
        return ''.join(v)




月間カレンダーでも同様のものを作成しましたが、月の増減をする関数です。
次月・前月のリンクを作るのに使用しています。
def add_months(date, num):
    """datetimeオブジェクトに月を加算・減算する.

    引数:
        date: datetime.datetimeオブジェクト。増減の基準となる日付を渡す
        num: 増減の数字。1月後は1、1月前は-1
    返り値:
        n月後、n月前のdatetimeオブジェクト

    """
    month = date.month - 1 + num
    year = int(date.year + month / 12)
    month = month % 12 + 1
    day = min(date.day, monthrange(year, month)[1])
    date = datetime.datetime(year=year, month=month, day=day)
    return date


HTMLCalendarには既にformatweekメソッドがあるので、違う名前にしました。
    def formatweek_table(self, week_index, year=None, month=None):
        """週間カレンダーを作成する."""


これが肝の部分です。
        weeks = self.monthdays2calendar(now.year, now.month)
        now_week = weeks[week_index-1]


monthdays2calendarは、以下のようなリストを返します。
[[(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6)],
 [(8, 0), (9, 1), (10, 2), (11, 3), (12, 4), (13, 5), (14, 6)],
 [(15, 0), (16, 1), (17, 2), (18, 3), (19, 4), (20, 5), (21, 6)],
 [(22, 0), (23, 1), (24, 2), (25, 3), (26, 4), (27, 5), (28, 6)],
 [(29, 0), (30, 1), (31, 2), (0, 3), (0, 4), (0, 5), (0, 6)]]


第1週目を表すのは、0番目のリストになり、中身は(日付, インデックス)のタプルとなります。
デフォルトは月曜日からはじまるので、この場合は
1日(月) 2日(火) 3日(水) といった週を表しています。
[(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6)]


以下の場合はは29日(月) 30日(火) 31日(水) となります。
日付部分が0になっているのは、存在しない日付ということです。
 [(29, 0), (30, 1), (31, 2), (0, 3), (0, 4), (0, 5), (0, 6)


base.html


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://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
  </head>
  <body>
    {% block content %}{% endblock %}
 
    <!-- jQuery first, then Tether, then Bootstrap JS. -->
    <script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
  </body>
</html>



week.html


table-layout: fixed;で各列の大きさを統一し、背景線などの設定を<style>でしています。
{% extends 'app/base.html' %}
{% block content %}
<style>
    .week-table {
        width: 100%;
        max-width: 100%;
        margin-bottom: 1rem;
        table-layout: fixed;
    }

    .week-table td {
        border: 1px solid #eceeef;
        vertical-align: top;
    }
    
    .week-table th {
        border: 1px solid #eceeef;
        padding: .75rem;
        vertical-align: top;
    }

</style>

<div class="container">
    {{ calendar }}
</div>
{% endblock %}