Djangoでbookingアプリを作ってみる

オープンソースでちらほら転がってるブッキングアプリですが、本気で使おうと思うと課金しないと機能が使えなかったり、思いどうりの動作をするものというと、中々なくて、我慢して使うか開発してもらうかといった感じで、じゃぁ作ってみようかなと思って始めた次第です。2025年5月16日時点ではまだ完全には完成していませんが、とりあえず一通り遊べる程度まで形になったので自分の備忘録を含めてここに公開します。

仕様

私が欲しかったブッキングシステムの仕様を以下に列記します。

動作

  • 概要
    • 本アプリは講師が作成した教室に生徒は先着1名のみ受講予約が可能なブッキングシステムです
    • 認証・アカウント管理はDjangoの標準機能を利用しています
    • 画面構成は
      • 標準搭載の管理機能により講師が教室を作成します
      • Accountアプリで生徒の新規登録などの処理をします
      • Appアプリでカレンダー表示を行い、生徒はその画面で教室を予約します
  • 講師
    • 講師は標準の管理画面で教室の作成・変更・削除ができます
    • 講師同氏は互いの教室を見ることはできません(ただし利用者視点で他の講師の教室を見ることは可能)
    • 講師は生徒の一覧を見ることはできません
    • 講師は教室に申し込んだ生徒のアカウントを見ることができます
  • 生徒
    • 生徒は自分のアカウントの作成が可能
    • 生徒はアカウント作成時にログインID・パスワード・メールアドレスを登録する必要がある
    • 生徒はメールで通知される完了のURLにアクセスすることでアカウントをActive化させることができます
    • 生徒は自身の登録内容(メールアドレス・登録した教室)の情報を変更できます
    • 生徒はいくつでも教室の予約が可能でいつでも予約解除が可能

画面

  • 利用者
    • アカウント登録画面
      • 登録確認画面
      • 登録完了画面
    • アクティブ化完了画面
    • プロフィールの確認画面
      • メールの変更画面
      • パスワードの変更画面
    • 予約カレンダー
    • 教室の予約画面
  • 講師
    • 自身の登録教室の一覧表示
    • 教室の登録画面
    • 教室の削除確認画

ER図

erDiagram Event { int id PK varchar title int teacher FK int student FK datetime start_time datetime end_time } Auth_User { int id PK varchar username varchar password varchar email bool is_active varchar first_name varchar last_name } Event ||..|{ Auth_User : teacher Event |o..o{ Auth_User : student

利用者の登録

利用者は自ら登録用のWEB画面に必要な情報を入力してアカウントの作成を行います。WEBへの登録が完了すると確認のため登録頂いたメールアドレス宛にアカウントをActiveにするための認証TokenをURLにしたものが添付されたメールを送信し利用者がURLにアクセスするとActive化されサイトにログインができるようになる。

sequenceDiagram 利用者 ->> +Accounts : アカウント作成 Accounts ->> +Auth.User : アカウント作成 Auth.User ->> -Accounts : 成功(非Active) Accounts ->> -利用者 : メール送信(Active化URL) 利用者 ->> +Accounts : Active化URLクリック Accounts ->> +Auth.User : Active化 Auth.User ->> -Accounts : 成功(Active) Accounts ->> -利用者 : 成功(Active)

利用者プロフィール画面の操作

メールアドレス変更

sequenceDiagram 利用者 ->> +Accounts : メールアドレス入力 Accounts ->> +Auth_User : メールアドレスの値を修正 Auth_User ->> -Accounts : 完了 Accounts ->> -利用者 : プロフィール画面に戻る

パスワード変更

sequenceDiagram 利用者 ->> +Accounts : パスワード入力 利用者 ->> Accounts : パスワード確認用入力 Accounts ->> +Auth_User : パスワードの値を修正 Auth_User ->> -Accounts : 完了 Accounts ->> -利用者 : プロフィール画面に戻る

予約カレンダーから教室予約

sequenceDiagram 利用者 ->> +App : カレンダーページリクエスト App ->> +Accounts : 認証確認 Accounts ->> 利用者 : ログイン画面表示(認証墨の際スキップ) Accounts ->> -App : 認証OK App ->> -利用者 : カレンダーと予定を表示 利用者 ->> +App : 予約選択 App ->> -利用者 : 予約詳細画面(予約可なら予約ボタン表示) 利用者 ->> +App : 予約画面押下 App ->> App : 予約完了 App ->> -利用者 : 予約詳細画面(自身が予約済なら解除ボタン)

Djangoの設定

初期設定はこちらで用意した環境に、以降の通りアプリを作る事にします。

アプリの構成

コンテナ側のパスで設定を行った対象を記載します。

/code/
/testprj/ ... 先のLinkのプロジェクトのパス
- settings.py
- urls.py
/testapp/ ... 先のLinkのアプリのパス
/accounts/ ... 認証アプリのパス
- forms.py
- urls.py
- tokens.py
- views.py
/templates/
/accounts/
- user_delete_confirm.html
- register_invalid.html
- delete_invalid.html
- register_email.html
- user_form.html
- user_detail.html
- register_done.html
- delete_done.html
- password_reset.html
/app/ ... カレンダー&予約のアプリのパス
- admin.py
- models.py
- urls.py
- views.py
/templates/
/app/
- event_detail.html
- event_update.html
- calender.html

URLパス

                
URLPATH用途
http://localhost:8010/admin/管理画面
app/calendar/カレンダー表示
app/calendar/YYYY/MM/年月指定でカレンダー表示
app/event/detail/数字教室の詳細表示
app/event/update/数字教室の予約状況変更
accounts/profile/利用者情報表示
accounts/profile/edit/利用者情報更新ページ表示
accounts/profiles/password-reset/利用者パスワード再設定ページ表示
accounts/register/利用者登録ページ表示
accounts/register/done/利用者登録完了ページ表示
accounts/register/complete/利用者登録Active完了ページ表示
accounts/delete/利用者削除ページ表示
accounts/delete/done/利用者削除完了ページ表示
accounts/delete/confirm/<uid>/<token>/利用者削除DeActivate完了ページ表示

プロジェクトの設定

  • /code/testprj/settings.py
ALLOWED_HOSTS = ['*']

INSTALLED_APPS = [
    'app.apps.AppConfig',
    'accounts.apps.AccountsConfig',
<snip>

TIME_ZONE = 'Asia/Tokyo'
USE_I18N = True
USE_TZ = True

<snip>

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

ALLOWED_HOSTSでアクセス制限を外して、INSTALLED_APPSに今回作成したappとaccountアプリの追加、TIME_ZONEの設定と、今回メールサーバの用意をしませんので、メール送信内容をログ出力させることとします。

  • /code/testprj/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.auth import views as auth_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('app/', include('app.urls',)),
    path('testapp/', include('testapp.urls')),
    path('silk/', include('silk.urls', namespace='silk')),
    path('accounts/', include('accounts.urls',)),
    path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
    path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
]

accountアプリ

アプリの作成

$ docker exec -it django /bin/ash
/code # source venv/bin/activate
(venv) /code # cd /code
(venv) /code # django-admin startapp accounts

  • /code/account/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class ProfileForm(forms.Form):
    first_name = forms.CharField(max_length=30, label='姓')
    last_name = forms.CharField(max_length=30, label='名')
    description = forms.CharField(label='自己紹介', widget=forms.Textarea(), required=False)
    image = forms.ImageField(required=False, )

class UserRegisterForm(UserCreationForm):
    email = forms.EmailField(label="メールアドレス")

    class Meta:
        model = User
        fields = ("username", "email") # "email"フィールドを追加


class UserLimitedUpdateForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['email']  # メールアドレスのみ編集可能


class PasswordResetForm(forms.Form):
    new_password = forms.CharField(widget=forms.PasswordInput, label="新しいパスワード")
    confirm_password = forms.CharField(widget=forms.PasswordInput, label="新しいパスワード(確認用)")

    def clean(self):
        cleaned_data = super().clean()
        new_password = cleaned_data.get("new_password")
        confirm_password = cleaned_data.get("confirm_password")

        if new_password != confirm_password:
            raise forms.ValidationError("パスワードが一致しません。")

  • /code/accounts/urls.py
from django.urls import path, include
from .views import *

app_name = 'accounts'
urlpatterns = [
        path('profile/', UserDetailView.as_view(), name='user-detail'),
    path('profile/edit/', UserUpdateView.as_view(), name='user-edit'),
    path('profile/password-reset/', PasswordResetView.as_view(), name='password-reset'),
    path('register/', UserRegisterView.as_view(), name='register'),
    path('register/done/', UserRegisterDoneView.as_view(), name='register-done'),
    path('register/complete/<uidb64>/<token>/', UserRegisterCompleteView.as_view(), name='register-complete'),
    path('delete/', UserDeleteView.as_view(), name='user-delete'),
    path('delete/done/', UserDeleteDoneView.as_view(), name='user-delete-done'),
    path('delete/confirm/<uidb64>/<token>/', UserDeleteConfirmView.as_view(), name='user-delete-confirm'),
]

  • /code/accounts/
from django.contrib.auth.tokens import PasswordResetTokenGenerator
import six

class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
    def _make_hash_value(self, user, timestamp):
        return (
            six.text_type(user.pk) + six.text_type(timestamp) +
            six.text_type(user.is_active)
        )

account_activation_token = AccountActivationTokenGenerator()

class AccountDeletionTokenGenerator(PasswordResetTokenGenerator):
    def _make_hash_value(self, user, timestamp):
        return (
            six.text_type(user.pk) + six.text_type(timestamp) +
            six.text_type(user.is_active)
        )

account_deletion_token = AccountDeletionTokenGenerator()

  • /code/accounts/views.py
from django.shortcuts import render, redirect
from django.urls import reverse_lazy
from django.views import generic

from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User, Group
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from .tokens import account_activation_token, account_deletion_token
from django.contrib import messages

from . import models
from .models import *
from .forms import *

from app.models import Event

# Create your views here.

class UserDetailView(LoginRequiredMixin, generic.DetailView):
    model = User
    template_name = 'accounts/user_detail.html'
    slug_field = 'username'
    slug_url_kwarg = 'username'
    context_object_name = 'user_detail'

    def get_object(self, queryset=None):
        return self.request.user

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['registered_events'] = Event.objects.filter(student=self.object)
        return context


class UserUpdateView(LoginRequiredMixin, generic.UpdateView):
    model = User
    template_name = 'accounts/user_form.html'
    success_url = reverse_lazy('accounts:user-detail')
    form_class = UserLimitedUpdateForm

    def get_object(self, queryset=None):
        return self.request.user

    def form_valid(self, form):
        messages.success(self.request, 'アカウント情報を更新しました。')
        return super().form_valid(form)


class UserRegisterView(generic.CreateView):
    form_class = UserRegisterForm
    template_name = 'accounts/user_form.html'
    success_url = reverse_lazy('accounts:register-done')

    def form_valid(self, form):
        user = form.save(commit=False)
        user.is_active = False  # メール認証が完了するまで非アクティブ
        user.save()

        # add Group
        group = Group.objects.get(name='event_student')
        user.groups.add(group)
        current_site = self.request.get_host()

        subject = 'アカウント登録確認'
        message = render_to_string('accounts/register_email.html', {
            'user': user,
            'domain': current_site,
            'uid': urlsafe_base64_encode(force_bytes(user.pk)),
            'token': account_activation_token.make_token(user),
        })
        send_mail(subject, message, 'admin@showdrop.asia', [user.email]) # 送信元メールアドレスを設定
        return super().form_valid(form)


class UserRegisterDoneView(generic.TemplateView):
    template_name = 'accounts/register_done.html'


class UserRegisterCompleteView(generic.View):
    def get(self, request, uidb64, token):

        try:
            uid = force_str(urlsafe_base64_decode(uidb64))
            user = User.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None

        if user is not None and account_activation_token.check_token(user, token):
            user.is_active = True
            user.save()
            messages.success(request, 'アカウント認証が完了しました。ログインしてください。')
            return redirect('accounts:user-detail')
        else:
            return render(request, 'accounts/register_invalid.html')


class UserDeleteView(LoginRequiredMixin, generic.FormView):
    template_name = 'accounts/user_delete_confirm.html'
    form_class = UserChangeForm # パスワード確認などに利用
    success_url = reverse_lazy('accounts:user-delete-done')

    def form_valid(self, form):
        user = self.request.user
        if form.check_password(form.cleaned_data['password']): # UserChangeFormにpasswordフィールドが存在する前提
            current_site = self.request.get_host()
            subject = 'アカウント削除確認'
            message = render_to_string('accounts/delete_email.html', {
                'user': user,
                'domain': current_site,
                'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                'token': account_deletion_token.make_token(user),
            })
            send_mail(subject, message, 'your_email@example.com', [user.email]) # 送信元メールアドレスを設定
            messages.info(self.request, 'アカウント削除確認メールを送信しました。メール内のリンクから削除を完了してください。')
            return redirect(self.success_url)
        else:
            form.add_error('password', 'パスワードが一致しません。')
            return self.form_invalid(form)


class UserDeleteDoneView(generic.TemplateView):
    template_name = 'accounts/delete_done.html'


class UserDeleteConfirmView(generic.View):
    def get(self, request, uidb64, token):
        try:
            uid = force_str(urlsafe_base64_decode(uidb64))
            user = User.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None

        if user is not None and account_deletion_token.check_token(user, token):
            user.delete()
            messages.success(request, 'アカウントを削除しました。')
            return redirect('login')
        else:
            return render(request, 'accounts/delete_invalid.html')


class PasswordResetView(LoginRequiredMixin, generic.FormView):
    template_name = 'accounts/password_reset.html'
    success_url = reverse_lazy('accounts:user-detail')
    form_class = PasswordResetForm

    def form_valid(self, form):
        user = self.request.user
        user.set_password(form.cleaned_data['new_password'])
        user.save()
        messages.success(self.request, 'パスワードを更新しました。')
        return super().form_valid(form)

  • /code/templates/accounts/delete_done.html
<!DOCTYPE html>
<html>
<head>
<title>削除完了</title>
</head>
<body>
<h1>削除完了</h1>
<p>
    アカウント削除確認メールを送信しました。
    メール内のリンクをクリックして削除を完了してください。
</p>
</body>
</html>

  • /code/templates/accounts/delete_invalid.html
<!DOCTYPE html>
<html>
<head>
<title>削除失敗</title>
</head>
<body>
<h1>削除失敗</h1>
<p>
    アカウント削除に失敗しました。
    リンクが無効であるか、すでに削除済みです。
</p>
<a href="{% url 'login' %}">ログインページへ</a>
</body>
</html>

  • /code/templates/accounts/password_reset.html
{% extends "admin/base.html" %}

{% block content %}
    <h2>パスワード変更</h2>

    <form method="post">
        {% csrf_token %}
        <div>
            <label for="id_new_password">新しいパスワード:</label>
            <input type="password" name="new_password" id="id_new_password" required>
        </div>
        <div>
            <label for="id_confirm_password">新しいパスワード(確認用):</label>
            <input type="password" name="confirm_password" id="id_confirm_password" required>
        </div>

        <button type="submit">変更する</button>
    </form>

    {% if messages %}
        <ul>
            {% for message in messages %}
                <li>{{ message }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <p><a href="{% url 'accounts:user-detail' %}">プロフィールに戻る</a></p>
{% endblock %}

  • /code/templates/accounts/register_done.html
<!DOCTYPE html>
<html>
<head>
<title>登録完了</title>
</head>
<body>
<h1>登録完了</h1>
<p>
    アカウント登録確認メールを送信しました。
    メール内のリンクをクリックして登録を完了してください。
</p>
</body>
</html>

  • /code/templates/accounts/register_email.html
{% autoescape off %}
{{ user }} 様

いつもご利用いただきありがとうございます。
以下のリンクをクリックして、アカウントを有効化してください。

http://{{ domain }}{% url 'accounts:register-complete' uidb64=uid token=token %}

もしこのメールに心当たりがない場合は、お手数ですが破棄してください。

{% endautoescape %}

  • /code/templates/accounts/register_invalid.html
<!DOCTYPE html>
<html>
<head>
<title>登録失敗</title>
</head>
<body>
<h1>登録失敗</h1>
<p>
    アカウント登録に失敗しました。
    リンクが無効であるか、すでに有効化済みです。
</p>
<a href="{% url 'login' %}">ログインページへ</a>
</body>
</html>

  • /code/templates/accounts/user_delete_confirm.html
<!DOCTYPE html>
<html>
<head>
<title>アカウント削除確認</title>
</head>
<body>
<h1>アカウント削除確認</h1>
<p>アカウントを削除します。よろしいですか?</p>
<form method="post">
{% csrf_token %} {{ form.as_p }}
<button type="submit">削除</button>
<a href="{% url 'accounts:user-detail' %}">キャンセル</a>
</form>
</body>
</html>

  • /code/templates/accounts/user_detail.html
<!DOCTYPE html>
 </head>
<body>
<h1>アカウント削除確認</h1>
<p>アカウントを削除します。よろしいですか?</p>
<form method="post">
{% csrf_token %} {{ form.as_p }}
<button type="submit">削除</button>
<a href="{% url 'accounts:user-detail' %}">キャンセル</a>
</form>
</body>
</html>

カレンダー&予約アプリ

アプリの作成

$ docker exec -it django /bin/ash
/code # source venv/bin/activate
(venv) /code # cd /code
(venv) /code # django-admin startapp app

  • /code/app/admin.py
from django.contrib import admin
from .models import *
from django.utils.timezone import localtime
import time
from django.contrib.auth import get_user_model
from django.db.models import Q

User = get_user_model()

class EventAdmin(admin.ModelAdmin):
  list_display = ('title', 'get_start_time', 'get_end_time', 'student', 'description')
  search_fields = ('title', 'start_time', 'end_time', 'description')
  readonly_fields = ('teacher', 'student',)

  def get_start_time(self, obj):
    return localtime(obj.start_time).strftime('%Y/%m/%d %H:%M')

  def get_end_time(self, obj):
    return localtime(obj.end_time).strftime('%Y/%m/%d %H:%M')

  def get_queryset(self, request):
    qs = super().get_queryset(request)
    qs = qs.filter(teacher=request.user)  # 自分が講師のEventのみ表示
    return qs

admin.site.register(Event, EventAdmin)

  • /code/app/models.py
from django.db import models
from django.urls import reverse
from django.conf import settings

# Create your models here.

class Event(models.Model):
    id = models.AutoField(unique=True, primary_key=True)
    start_time = models.DateTimeField()
    end_time = models.DateTimeField()
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True, null=True)
    teacher = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='teaching_events')
    student = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='registered_events')

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('event_detail', args=[str(self.id)])

  • /code/app/urls.py
from django.urls import path, include
from . import views
from .views import *

app_name = 'app'
urlpatterns = [
    path('calendar/', views.CalendarView.as_view(), name='calendar'),
    path('calendar/<int:year>/<int:month>/', views.CalendarView.as_view(), name='calendar_month'),
    path('event/detail/<int:pk>/', views.EventDetailView.as_view(), name='event_detail'),
    path('event/update/<int:pk>/', views.EventUpdateView.as_view(), name='event_update'),
]

  • /code/app/views.py
from django.views import generic
from django.urls import reverse_lazy
from .models import Event
from datetime import datetime, date, time, timedelta
import calendar
import jpholiday
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.shortcuts import redirect, get_object_or_404

User = get_user_model()


class CalendarView(LoginRequiredMixin, generic.TemplateView):
    template_name = 'app/calendar.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        # 現在の年月の取得(URL引数または今日)
        year = self.kwargs.get('year', datetime.today().year)
        month = self.kwargs.get('month', datetime.today().month)

        # 先月と今月と来月
        current_date = date(year, month, 1)

        prev_month = current_date - timedelta(days=1)
        prev_year = prev_month.year
        prev_month = prev_month.month

        next_month = current_date + timedelta(days=32) # 32日後を指定することで確実に翌月になる
        next_year = next_month.year
        next_month = next_month.month

        # 月のカレンダー取得
        cal = calendar.Calendar(firstweekday=6)  # 日曜始まり
        month_days = cal.itermonthdates(year, month)

        # 各日付にイベント情報と祝日判定を付加
        calendar_data = []
        for day in month_days:
            day_events = Event.objects.filter(start_time__date=day)
            is_holiday = jpholiday.is_holiday(day)
            calendar_data.append({
                'date': day,
                'in_month': day.month == month,
                'events': day_events,
                'weekday': day.weekday(),
                'is_holiday': is_holiday,
            })

        # 選択された日付(GETパラメータ)
        selected_day_str = self.request.GET.get('day')
        selected_day = None

        if selected_day_str:
            try:
                selected_day = datetime.strptime(selected_day_str, '%Y-%m-%d')
            except ValueError:
                selected_day = None


        # タイムテーブルのイベント取得
        timetable_events = []
        if selected_day:
            selected_date = selected_day.date()
            timetable_events = Event.objects.filter(start_time__date=selected_date)

        # calendar_data を 7日ごとの週に分割
        weeks = [calendar_data[i:i+7] for i in range(0, len(calendar_data), 7)]

        context.update({
            'weeks': weeks,
            'calendar_data': calendar_data,
            'year': year,
            'month': month,
            'selected_day': selected_day,
            'timetable_events': timetable_events,
            'weekdays': ['日', '月', '火', '水', '木', '金', '土'],
            'prev_year': prev_year,
            'prev_month': prev_month,
            'next_year': next_year,
            'next_month': next_month,
        })
        return context


class EventDetailView(LoginRequiredMixin, generic.DetailView):
    model = Event
    template_name = 'app/event_detail.html'


class EventStudentGroupRequiredMixin(LoginRequiredMixin, UserPassesTestMixin):
    def test_func(self):
        return self.request.user.groups.filter(name='event_student').exists()


class EventTeacherGroupRequiredMixin(LoginRequiredMixin, UserPassesTestMixin):
    def test_func(self):
        return self.request.user.groups.filter(name='event_teacher').exists()


class EventUpdateView(EventStudentGroupRequiredMixin, generic.UpdateView):
    model = Event
    template_name = 'app/event_update.html'
    success_url = reverse_lazy('app:calendar')
    fields = ['title', 'start_time', 'end_time', 'description',]

    # テンプレートで使う値を設定
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['current_user'] = self.request.user
        context['is_event_student'] = self.request.user.groups.filter(name='event_student').exists()
        context['is_event_teacher'] = self.request.user.groups.filter(name='event_teacher').exists()
        return context

    # フォームからPOSTされてきた値に応じて登録
    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        current_user = request.user

        if 'register_student' in request.POST and request.user.groups.filter(name='event_student').exists():
            # 予約がNULLなら予約できる
            self.object.student = current_user
            self.object.save()
        elif 'unregister_student' in request.POST and request.user.groups.filter(name='event_student').exists():
            # 既に自分が予約している場合は解除できる
            self.object.student = None
            self.object.save()

        return redirect(self.success_url)

  • /code/app/templates/app/calendar.html
{% load static %}
<!DOCTYPE html>
<html>
<head>
    <title>カレンダー</title>
    <style>
        table { border-collapse: collapse; width: 100%; }
        td, th { border: 1px solid #ccc; padding: 8px; text-align: center; height: 80px; font-size: 200%; }
        .today { background: #ffffe0; }
        .not-current { color: #ccc; }
        .holiday { color: red; }
        .sunday { color: red; }
        .saturday { color: blue; }
    </style>
</head>
<body>
    <h1>{{ year }}年 {{ month }}月</h1>
    <a href="{% url 'accounts:user-detail' %}">あなたの情報はこちら</a>
    <div class="prev-next-links">
        <a href="{% url 'app:calendar_month' prev_year prev_month %}"><< 先月</a>
        &nbsp
        &nbsp
        &nbsp
        <a href="{% url 'app:calendar_month' next_year next_month %}">来月 >></a>
    </div>
    <table style='width:100%; table-layout:fixed'>
        <thead>
            <tr>
                {% for weekday in weekdays %}
                    <th {% if weekday == "日" %} class="sunday" {% endif %}
                        {% if weekday == "土" %} class="saturday" {% endif %}
                        >
                            {{ weekday }}
                    </th>
                {% endfor %}
            </tr>
        </thead>
        <tbody>
            {% for week in weeks %}
            <tr>
                {% for day in week %}
                <td {% if not day.in_month %} class="out-of-month" {% endif %}
                    {% if day|date:"D" == "日" %} class="sunday" {% endif %}
                    {% if day|date:"D" == "土" %} class="saturday" {% endif %}
                    {% if day.is_holiday %} class="holiday" {% endif %}
                    >
                    <a href="?day={{ day.date|date:'Y-m-d' }}">
                        {{ day.date.day }}
                    </a>
                    {% for event in day.events %}
                        <div><a href="{% url 'app:event_update' event.id %}">
                        <font size="+1" {% if event.student %}color='#AAAAAA'{% endif %}>
                                {{ event.title|truncatechars:8 }}
                        </font>
                        </a></div>
                    {% endfor %}
                </td>
                {% endfor %}
            </tr>
            {% endfor %}
        </tbody>
    </table>

    {% if timetable_events %}
        <h1>{{ selected_day|date:"Y年m月d日" }} のスケジュール</h1>
        <ul>
            {% for event in timetable_events %}
                <li>
                    <a href="{% url 'app:event_update' event.id %}">
                        <font size="+3" {% if event.student %}color='#AAAAAA'{% endif %}>
                        {{ event.start_time|time:"H:i" }}~{{ event.end_time|time:"H:i" }}: {{ event.title }}
                        </font>
                    </a>
                </li>
            {% empty %}
                <li>予定はありません</li>
            {% endfor %}
        </ul>
    {% else %}
        <h2>予定はありません</h2>
    {% endif %}
</body>
</html>

  • /code/app/templates/app/event_detail.html
<!DOCTYPE html>
<html>
<head>
    <title>{{ object.title }}</title>
</head>

<body>
    <form method="post">
    {% csrf_token %}

    {% if request.user.is_authenticated and is_event_teacher %}
    <p>担当講師: {% if event.teacher %}{{ event.teacher.username }}{% else %}未担当{% endif %}</p>
    {% endif %}

        {% if event.student == current_user %}
            <button type="submit" name="unregister_student" class="btn btn-danger">予約解除</button>
        {% elif not event.student %}
            <button type="submit" name="register_student" class="btn btn-primary">予約する</button>
        {% endif %}

    <table>
    {{ form.as_table }}
    </table>

    <a href="{% url 'app:calendar' %}?day={{ event.start_time|date:'Y-m-d' }}" class="btn btn-secondary">戻る</a>
</form>
</body>
</html>

  • /code/app/templates/app/event_update.html
<!DOCTYPE html>
<html>
<head>
    <title>{{ object.title }}</title>
</head>


<body>
    <form method="post">
    {% csrf_token %}

    {% if request.user.is_authenticated and is_event_teacher %}
    <p>担当講師: {% if event.teacher %}{{ event.teacher.username }}{% else %}未担当{% endif %}</p>
    {% endif %}

        {% if event.student == current_user %}
            <button type="submit" name="unregister_student" class="btn btn-danger">予約解除</button>
        {% elif not event.student %}
            <button type="submit" name="register_student" class="btn btn-primary">予約する</button>
        {% endif %}

    <table>
    {{ form.as_table }}
    </table>

    <a href="{% url 'app:calendar' %}?day={{ event.start_time|date:'Y-m-d' }}" class="btn btn-secondary">戻る</a>
</form>
</body>
</html>

モデルをDBに反映

$ docker exec -it django /bin/ash
/code # source venv/bin/activate
(venv) /code # cd /code
(venv) /code # python manage.py showmigrations
(venv) /code # python manage.py makemigrations
(venv) /code # python manage.py migrate

動作確認

準備

流れ

  • 教師機能の確認
    • 教師用アカウントを複数作成
    • 教師で管理画面にログイン
    • 複数の教師で各々教室を作成
    • 教師で別の教師の教室が見えないことを確認
    • 利用者の予約の後、教師アカウントでログインしなおし利用者の予約状況を確認
  • 利用者機能の確認
    • 利用者アカウントを登録
      • アカウント・メールアドレス・パスワードの登録
      • ログに出力されたメールのToken入りURLを取得しアクセス
      • 利用者アカウントのActivate化
      • 複数アカウントを作成
    • 利用者アカウントでログイン
    • 利用者アカウントとプロフィールを表示
    • 利用者アカウントでメールアドレスを変更
    • 利用者アカウントでパスワードを変更
    • 利用者アカウントでカレンダーを表示
    • 利用者アカウントで教室を予約
    • 別の利用者アカウントで上記カレンダーと予約を確認
    • 自身以外のアカウントの教室で予約されている場合は予約ボタンが出ないことを確認
    • 予約済教室は文字が灰色になっており(済)が付いていることを確認

教師機能の確認

アカウント作成

Djangoの管理画面に管理者でログインし教師用アカウントを作成する。

利用者機能の確認

以降は画面キャプチャ付きでおいおい書いていく予定。。。

コードの解説もおいおい…


投稿日

カテゴリー:

投稿者: