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

2018-10-07 / PythonDjangoBootstrap4

概要

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にするとスクロールバーがつく

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

この記事の関連記事

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

2018-10-07 / PythonDjangoBootstrap4シリーズ・まとめ

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

コメント欄

記事にコメントする

名無しし

こんにちは。 質問で登録フォームとスケジュール付き月刊カレンダーを表示させることはできますか?

コメントに返信する

なりと

可能です。

イメージとしては、Djangoで、週間・月間カレンダーから、週間カレンダー部分を消し、月間カレンダー部分をスケジュール付き月間カレンダーに変更するような感じです。

名無しし

その時のsampleapp/urls.pyは欲張りセットのやつで大丈夫ですか?

なりと

はい、大丈夫です。

名無し

試したところNameError: name 'MonthCalendarMixin' is not defined のエラーが出てしまいました

名無し

あのエラーは治りました。

TemplateDoesNotExist at /
templeats/sampleapp/widgets/month_with_schedule.html
Request Method: GET
Request URL:    http://localhost:8000/
Django Version: 2.1.2
Exception Type: TemplateDoesNotExist
Exception Value:    
templeats/sampleapp/widgets/month_with_schedule.html
Exception Location: C:\Users\iniad\Miniconda3\envs\cs2018_web\lib\site-packages\django\template\backends\django.py in reraise, line 84
Python Executable:  C:\Users\iniad\Miniconda3\envs\cs2018_web\python.exe

が出てしまいました

名無し

何度も申し訳ございません。

NoReverseMatch at /
Reverse for 'month_with_schedule' not found. 'month_with_schedule' is not a valid view function or pattern name.
Request Method: GET
Request URL:    http://localhost:8000/
Django Version: 2.1.2
Exception Type: NoReverseMatch
Exception Value:    
Reverse for 'month_with_schedule' not found. 'month_with_schedule' is not a valid view function or pattern name.
Exception Location: C:\Users\iniad\Miniconda3\envs\cs2018_web\lib\site-packages\django\urls\resolvers.py in _reverse_with_prefix, line 622
Python Executable:  C:\Users\iniad\Miniconda3\envs\cs2018_web\python.exe

が出ました。

なりと

現在のソースコードの内容を教えていただけますか。プロジェクト毎送っていただければ確実です。

名無し

申し訳ございません。ありがとうございます。直したところだけ送ります まずmycalendar.htmlでweek部分を消しました

{% extends 'app/base.html' %}
{% block content %}
    <div class="row">
        <div class="col-md-3">
            {% include 'app/includes/month_with_schedule' %}
            <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>
{% 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 %}
名無し

urls.pyは

from django.urls import path
from . import views

app_name = 'app'

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

になってます。いま新たなエラーで

File "C:\Users\iniad\Documents\cs2018_web\django-simple-calendar\app\urls.py", line 7, in <module>
    ppath('mycalendar/', views.MyCalendar.as_view(), name='mycalendar'),

とanaconda側で言われました

名無し

viwesは

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


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

    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

    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の表記がおかしかったのでもう一度送ります

{% extends 'app/base.html' %}
{% block content %}
    <div class="row">
        <div class="col-md-3">
            {% include 'app/includes/month_with_schedule' %}
            <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>
{% 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 %}

コメントに返信する

名無し

あとはweek関連は全部消しました。 長文失礼いたしました。お時間がありましたら返信お願いたします

コメントに返信する

なりと

urls.pyを次のようにします。これで、127.0.0.1:8000 でアクセスできます。

from django.urls import path
from . import views

app_name = 'app'

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

views.pyを次のようにします。

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


class MyCalendar(mixins.MonthWithScheduleMixin, 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)
        month_calendar_context = self.get_month_calendar()
        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)

mycalendar.htmlを次のようにします。

{% extends 'app/base.html' %}
{% block content %}
    <div class="row">
        <div class="col-md-3">
            <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">
            <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>
        </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 %}

これでおそらくお望みの動作になると思います。

名無し

丁寧な回答ありがとうございます。 TemplateSyntaxError at / Invalid block tag on line 95: 'endblock'. Did you forget to register or load this tag? Request Method: GET Request URL: http://127.0.0.1:8000/ Django Version: 2.1.2 Exception Type: TemplateSyntaxError Exception Value:
Invalid block tag on line 95: 'endblock'. Did you forget to register or load this tag? Exception Location: C:\Users\iniad\Miniconda3\envs\cs2018_web\lib\site-packages\django\template\base.py in invalid_block_tag, line 534 Python Executable: C:\Users\iniad\Miniconda3\envs\cs2018_web\python.exe Python Version: 3.7.1 Python Path:
['C:\Users\iniad\Documents\cs2018_web\django-simple-calendar', 'C:\Users\iniad\Miniconda3\envs\cs2018_web\python37.zip', 'C:\Users\iniad\Miniconda3\envs\cs2018_web\DLLs', 'C:\Users\iniad\Miniconda3\envs\cs2018_web\lib', 'C:\Users\iniad\Miniconda3\envs\cs2018_web', 'C:\Users\iniad\Miniconda3\envs\cs2018_web\lib\site-packages'] とエラーが出てしまいました。申し訳ございません。

コメントに返信する

名無し

すみませんできました!! 大変助かりました。ありがとうございます。 これからもブログ参考にさせていただきますし、書籍や有料コンテンツもチェックしてみます。

名無し

こんにちは。 いつもべんきょうさしていただいてます。 親切なサイト、ありがとうございます。 カレンダーアプリの月間スケジュールの表示がうまくいかないのですが エラーが expected string or bytes-like object となっていまいちよくわからないのです。 思い当たる節がありましたら教えていただけませんか?。

いまのコードです。

myapp/mixins.py

import calendar
from collections import deque
import datetime
import itertools


class BaseCalendarMixin:
    #カレンダー関連Mixinの、基底クラス

    # 0は月曜から、1は火曜から。6なら日曜日からになります。
    first_weekday = 0
    # これは、月曜日から書くことを想定します。['Mon', 'Tue'...
    week_names = ['月', '火', '水', '木', '金', '土', '日']

    def setup(self):
        """内部カレンダーの設定処理
        calendar.Calendarクラスの機能を利用するため、インスタンス化します。
        Calendarクラスのmonthdatescalendarメソッドを利用していますが、default
        が月曜日からで、火曜日から表示したい(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)
        return week_names


"""カレンダー機能のMixin ここまで"""



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

    def get_previous_month(self,date):
        if date.month == 1:
            return date.replace(year = date.year-1,month=12)
        else:
            return date.replace(month = date.month-1,day=1)

    def get_next_month(self,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(),
            'month_days': self.get_month_days(current_month),
            'month_current': current_month,
            'month_previous': self.get_previous_month(current_month),
            'month_next': self.get_next_month(current_month),
            'week_names': self.get_week_names(),
        }
        return calendar_data

myapp/views.py

from django.views import generic
from . import mixins
from .models import Schedule


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

    def get_context_data(self,**kwargs):

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

class MonthWithScheduleCalendar(mixins.MonthWithScheduleMixin,generic.TemplateView):
    #スケジュール付き月間カレンダーを表示するビュー

    template_name = 'myapp/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

コメントに返信する

名無し

あの後 サイトのコードをコピペさしていただいたのですが うまく動作しました。原因探してもう一度最初からやり直してみます。 もっと早くに思いつけばよかったんですが 失礼しました。

名無し

今更ではありますが、 MonthWithScheduleMixinの get_month_calenderの month_first, month_lastだと思います。 naritoさんの中では、それぞれmonthdays[0][0], [-1][-1] のなっていますが、これを month_adys[0], [-1] のようにしてあると同様のエラーが出ました。