naritoブログ

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

DjangoとBootstrap4でタイムスケジュールを作成する

プログラミング関連 Bootstrap4 Django Python timedropper 約35日前
2017年7月13日7:11
前回、PythonとBootstrap4でタイムスケジュールを作成しました。
https://torina.top/detail/384/
今回はそれをDjangoに組み込んでいきます。

まずはこのような画面


データを追加してみると


ちゃんと反映されます。


終了時間が開始時間以下の場合は、エラーになるようにしました。



Django1.11
Python3.6
です。

models.py


今回使うのは、開始時間と終了時間、そしてメモを持つシンプルなモデルです。
カレンダーなんかと一緒に使う場合は、これに日付を表すフィールドが増えることでしょう。
import datetime\
from django.db import models


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

    memo = models.TextField('メモ')
    start_time = models.TimeField('開始時間', default=datetime.time(0, 0, 0))
    end_time = models.TimeField('終了時間', default=datetime.time(0, 0, 0))

    def __str__(self):
        return self.memo


forms.py


Bootstrap4に対応するための記述と、clean_end_timeメソッドで終了時間が開始時間以下ならメッセージを出すようにしています。
from django import forms
from .models import Schedule


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

    class Meta:
        model = Schedule
        fields = ('memo', 'start_time', 'end_time')
        widgets = {
            'memo': 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


urls.py


from django.conf.urls import url
from app import views 

app_name = 'app'

urlpatterns = [
    url(r'^$', views.TimeSchedule.as_view(), name='index'),
]


views.py


CreateViewでデータの作成を行えるようにしつつ、get_context_dataメソッドの上書きでcontextにスケジュールのhtmlを格納しています。
from django.urls import reverse_lazy
from django.utils.safestring import mark_safe
from django.views import generic
from .forms import ScheduleForm
from .lib import TimeScheduleBS4
from .models import Schedule


class TimeSchedule(generic.CreateView):
    model = Schedule
    form_class = ScheduleForm
    success_url = reverse_lazy('app:index')
    template_name = 'app/index.html'

    def get_context_data(self, *args, **kwargs):
        schedules = Schedule.objects.order_by('start_time')
        time_schedule = TimeScheduleBS4(step=10, minute_height=0.5)
        context = super().get_context_data(*args, **kwargs)
        # テンプレートにhtmlを含んだ文字列を渡すときは、mark_safeをしておけばよい
        context['time_schedule'] = mark_safe(
            time_schedule.format_schedule(schedules)
        )
        return context



lib.py


スケジュール作成用モジュールです。中身は前回
https://torina.top/detail/384/
とかわりないです。
"""スケジュールを作成するためのモジュール."""
import datetime


class TimeScheduleBS4:
    """タイムスケジュールを作成する(Bootstrap4)."""

    def __init__(
        self, minute_height=1, hours=None, schedule_color='bg-info', step=1
    ):
        """初期化.
        引数:
        minute_height: 1分の高さ(px)。1ならば1時間が60px、全体で1440px
        hours: スケジュールに記載する時間の幅。range(6, 13)だと6〜12時まで
        schedule_color: スケジュールがある場合の背景色
        step: 何分毎にdivタグを入れるか。デフォルトは1分毎に1divタグ
              1に近いほどdivタグが多くなりパフォーマンスが落ちるが、細かい時間
              でも色をつけることができる
        """
        # hoursがNoneなら0から23時で
        if hours is None:
            self.hours = [x for x in range(24)]
        else:
            self.hours = hours
        self.step = step
        self.minute_height = minute_height
        self.hour_height = self.minute_height * 60
        self.max_height = self.hour_height * len(self.hours)
        self.schedule_color = schedule_color

    def convert(self, obj):
        """(開始時間、終了時間、スケジュールテキスト)のタプルを返す.
        format_schedueメソッドに渡した各scheduleオブジェクトを
        (開始時間, 終了時間,テキスト)の形に変換するためのメソッド
        return obj.start, obj.end, obj.text
        return obj['start'], obj['end'], obj['title']+obj['text']
        のようにしてください
        """
        message = '{}〜{}<br>{}'.format(
            obj.start_time, obj.end_time, obj.memo
        )
        return obj.start_time, obj.end_time, message

    def format_hour_name(self, hour):
        """左側の列、時間表示部分の作成."""
        div = '<div style="height:{0}px;" class="hour-name">{1}:00</div>'
        return div.format(self.hour_height, hour)

    def format_minute(self, schedule, now):
        """分部分の作成."""
        start, end, text = self.convert(schedule)
        context = {
            'color': self.schedule_color,
            'height': self.minute_height * self.step,
            'just-hour': '',
            'text': text,
        }
        # 1:00、2:00などの0分に枠線を入れるためのcss
        if now.minute == 0:
            context['just-hour'] = 'just-hour'

        # 現在ループの時間が開始時間〜終了時間内なら、色をつける
        if start <= now < end:

            # 既にtooltipを入れているなら背景色だけ
            if self.already_tooltip:
                base_html = (
                    '<div class="{color} {just-hour}" '
                    'style="height:{height}px;"></div>'
                )

            # 最初の予定なら、tooltipをつけてフラグをTrueに
            else:
                self.already_tooltip = True
                base_html = (
                    '<div class="{color} {just-hour}" '
                    'style="height:{height}px;" '
                    'data-html="true" title="{text}" data-placement="top" '
                    'data-trigger="manual"  data-toggle="tooltip">'
                    '</div>'
                )

        else:
            base_html = (
                '<div class="{just-hour}" style="height:{height}px;"></div>'
            )

        return base_html.format_map(context)

    def format_schedule(self, schedules):
        """タイムスケジュールを作成する."""
        v = []
        a = v.append
        a('<div class="row no-gutters">')

        # 左列、時間表示部分の作成
        a('<div class="col" style="height:{0}px;">'.format(self.max_height))
        for hour in self.hours:
            a(self.format_hour_name(hour))
        a('</div>')

        # 右列、スケジュール作成部分
        for schedule in schedules:
            # 予定の最初のdivタグにtooltipを導入するためのフラグ
            self.already_tooltip = False
            a('<div class="col minute-wrapper" style="height:{0}px;">'.format(
                self.max_height
            ))
            for hour in self.hours:
                for minute in range(0, 60, self.step):
                    now = datetime.time(hour=hour, minute=minute)
                    a(self.format_minute(schedule, now))
            a('</div>')

        a('</div>')

        return ''.join(v)


base.html


Bootstrap4を使っています。
他のテンプレートでscriptタグの記述が必要なため、最後に {% block extrajs %}{% endblock %}としています。
<!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>
    {% block extrajs %}{% endblock %}
  </body>
</html>


index.html


{% extends "app/base.html" %}
{% block content %}
<style>
    .minute-wrapper {
        border: 1px solid #eceeef;
    }
 
    .hour-name {
        padding: 5px;
        text-align: right;
        border: 1px solid #eceeef;
    }
 
    .just-hour {
        border-top: 1px solid #eceeef;
    }
</style>
<div class="container">
    <div class="row mt-5">
        <div class="col-sm-8">
            {{ time_schedule }}
        </div>
        <div class="col-sm-4">
            <form action="" method="POST">
                {% for field in form %}
                <div class="form-group">
                    <label for="{{ field.id_for_label }}">{{ field.label }}</label>
                    {{ field }}
                    {{ field.errors }}
                </div>
                {% endfor %}
                {% csrf_token %}
                <button type="submit" class="btn btn-primary">送信</button>
            </form>         
        </div>
    </div><!-- /.row -->
</div><!-- /.container -->
{% endblock %}

{% block extrajs %}
<script>
    $(function() {
        $('[data-toggle="tooltip"]').tooltip('show');
    });
</script>
{% endblock %}


時間の入力は、もう少し便利にしたいと思う方も多いはずです。
jQueryのプラグインなんかに頼るのも良いでしょう。
今回は「timedropper」を使ってみます。


公式:http://felicegattuso.com/projects/timedropper/
Github:https://github.com/felicegattuso/timedropper/

index.html


{% block extrajs %}内に追加されました。
{% extends "app/base.html" %}
{% load static %}
{% block content %}
<style>
    .minute-wrapper {
        border: 1px solid #eceeef;
    }
 
    .hour-name {
        padding: 5px;
        text-align: right;
        border: 1px solid #eceeef;
    }
 
    .just-hour {
        border-top: 1px solid #eceeef;
    }
</style>
<div class="container">
    <div class="row mt-5">
        <div class="col-sm-8">
            {{ time_schedule }}
        </div>
        <div class="col-sm-4">
            <form action="" method="POST">
                {% for field in form %}
                <div class="form-group">
                    <label for="{{ field.id_for_label }}">{{ field.label }}</label>
                    {{ field }}
                    {{ field.errors }}
                </div>
                {% endfor %}
                {% csrf_token %}
                <button type="submit" class="btn btn-primary">送信</button>
            </form>         
        </div>
    </div><!-- /.row -->
</div><!-- /.container -->
{% endblock %}

{% block extrajs %}
<!-- timedropper css js -->
<link rel="stylesheet" type="text/css" href="{% static 'app/timedropper/timedropper.min.css' %}">
<script src="{% static 'app/timedropper/timedropper.min.js' %}"></script>
<script>
    $(function() {
        // Bootstrap4 tooltip
        $('[data-toggle="tooltip"]').tooltip('show');

        // timedropper
        $( "#id_start_time" ).timeDropper({
            format: "H:mm",
            setCurrentTime: false,
        });
        $( "#id_end_time" ).timeDropper({
            format: "H:mm",
            setCurrentTime: false,
        });
    });
</script>
{% endblock %}


たとえば、入力部分をプルダウンにしてみる場合は以下です。これは少しコードが増えます。


forms.py


start_hour、start_minute、end_hour、end_minuteというフィールドをフォームに作り、それらを使ってモデルのstart_time、end_timeを作ります。
import datetime
from django import forms
from .models import Schedule

HOURS = [(x, x) for x in range(0, 24)]
MINUTES = [(x, x) for x in range(0, 60)]

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

    start_hour = forms.ChoiceField(
        label='開始:時間',
        choices=HOURS,
    )
    start_minute = forms.ChoiceField(
        label='開始:分',
        choices=MINUTES,
    )
    end_hour = forms.ChoiceField(
        label='終了:時間',
        choices=HOURS,
    )
    end_minute = forms.ChoiceField(
        label='終了:分',
        choices=MINUTES,
    )

    class Meta:
        model = Schedule
        fields = ('memo',)
        widgets = {
            'memo': forms.Textarea(attrs={
                'class': 'form-control',
            }),
        }

    def clean_end_minute(self):
        start_time = datetime.time(
            hour=int(self.cleaned_data['start_hour']),
            minute=int(self.cleaned_data['start_minute'])
        )
        end_time = datetime.time(
            hour=int(self.cleaned_data['end_hour']),
            minute=int(self.cleaned_data['end_minute'])
        )
        if end_time <= start_time:
            raise forms.ValidationError(
                '終了時間は、開始時間よりも後にしてください'
            )
        return self.cleaned_data['end_minute']


views.py


form_validが増えました。
import datetime
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.safestring import mark_safe
from django.views import generic
from .forms import ScheduleForm
from .lib import TimeScheduleBS4
from .models import Schedule


class TimeSchedule(generic.CreateView):
    model = Schedule
    form_class = ScheduleForm
    success_url = reverse_lazy('app:index')
    template_name = 'app/index.html'

    def get_context_data(self, *args, **kwargs):
        schedules = Schedule.objects.order_by('start_time')
        time_schedule = TimeScheduleBS4(step=10, minute_height=0.5)
        context = super().get_context_data(*args, **kwargs)
        context['time_schedule'] = mark_safe(
            time_schedule.format_schedule(schedules)
        )
        return context

    def form_valid(self, form):
        schedule = form.save(commit=False)
        schedule.start_time = datetime.time(
            int(form.cleaned_data['start_hour']),
            int(form.cleaned_data['start_minute'])
        )
        schedule.end_time = datetime.time(
            int(form.cleaned_data['end_hour']),
            int(form.cleaned_data['end_minute'])
        )     
        self.object = schedule.save()
        return redirect('app:index')


index.html



{% extends "app/base.html" %}
{% block content %}
<style>
    .minute-wrapper {
        border: 1px solid #eceeef;
    }
 
    .hour-name {
        padding: 5px;
        text-align: right;
        border: 1px solid #eceeef;
    }
 
    .just-hour {
        border-top: 1px solid #eceeef;
    }
</style>
<div class="container">
    <div class="row mt-5">
        <div class="col-sm-8">
            {{ time_schedule }}
        </div>
        <div class="col-sm-4">
            <form action="" method="POST">
                {{ form.errors }}
                <div class="form-group">
                    <label for="{{ form.memo.id_for_label }}">{{ form.memo.label }}</label>
                    {{ form.memo }}
                </div>
                <div class="form-group">
                    <label for="{{ form.start_hour.id_for_label }}">時間</label>
                    {{ form.start_hour }}時{{ form.start_minute }}分
                    〜{{ form.end_hour }}時{{ form.end_minute }}
                </div>
                {% csrf_token %}
                <button type="submit" class="btn btn-primary">送信</button>
            </form>         
        </div>
    </div><!-- /.row -->
</div><!-- /.container -->
{% endblock %}

{% block extrajs %}
<script>
    $(function() {
        $('[data-toggle="tooltip"]').tooltip('show');
    });
</script>
{% endblock %}