diff --git a/shiftregister/app/tests.py b/shiftregister/app/tests.py index 7ce503c..4f50b91 100644 --- a/shiftregister/app/tests.py +++ b/shiftregister/app/tests.py @@ -1,3 +1,143 @@ -from django.test import TestCase +from datetime import timedelta -# Create your tests here. +from django.test import TestCase +from django.utils import timezone + +from .models import Helper, Room, Shift, ShiftRegistration + + +class ShiftOverlapTests(TestCase): + def setUp(self): + # Create a room + self.room = Room.objects.create( + name="Test Room", required_helpers=1, meeting_location="Test Location" + ) + + # Create a helper + self.helper = Helper.objects.create( + phone="+491234567890", name="Test Helper", number_validated=True + ) + + # Create a base shift for testing overlaps + self.base_shift = Shift.objects.create( + room=self.room, + start_at=timezone.now() + timedelta(hours=1), + duration=timedelta(hours=2), + required_helpers=1, + ) + + # Register the helper for the base shift + self.base_registration = ShiftRegistration.objects.create( + shift=self.base_shift, + helper=self.helper, + state=ShiftRegistration.RegState.REGISTERED, + ) + + def test_no_overlap_before(self): + """Test a shift that ends before the base shift starts""" + shift_before = Shift.objects.create( + room=self.room, + start_at=self.base_shift.start_at - timedelta(hours=3), + duration=timedelta(hours=1), + required_helpers=1, + ) + self.assertIsNone(self.helper.has_overlapping_shift(shift_before)) + + def test_no_overlap_after(self): + """Test a shift that starts after the base shift ends""" + shift_after = Shift.objects.create( + room=self.room, + start_at=self.base_shift.start_at + timedelta(hours=4), + duration=timedelta(hours=1), + required_helpers=1, + ) + self.assertIsNone(self.helper.has_overlapping_shift(shift_after)) + + def test_overlap_start(self): + """Test a shift that starts during the base shift""" + shift_overlap_start = Shift.objects.create( + room=self.room, + start_at=self.base_shift.start_at + timedelta(hours=1), + duration=timedelta(hours=2), + required_helpers=1, + ) + self.assertIsNotNone(self.helper.has_overlapping_shift(shift_overlap_start)) + + def test_overlap_end(self): + """Test a shift that ends during the base shift""" + shift_overlap_end = Shift.objects.create( + room=self.room, + start_at=self.base_shift.start_at - timedelta(hours=1), + duration=timedelta(hours=2), + required_helpers=1, + ) + self.assertIsNotNone(self.helper.has_overlapping_shift(shift_overlap_end)) + + def test_overlap_contained(self): + """Test a shift that is completely contained within the base shift""" + shift_contained = Shift.objects.create( + room=self.room, + start_at=self.base_shift.start_at + timedelta(minutes=30), + duration=timedelta(hours=1), + required_helpers=1, + ) + self.assertIsNotNone(self.helper.has_overlapping_shift(shift_contained)) + + def test_overlap_contains(self): + """Test a shift that completely contains the base shift""" + shift_contains = Shift.objects.create( + room=self.room, + start_at=self.base_shift.start_at - timedelta(hours=1), + duration=timedelta(hours=4), + required_helpers=1, + ) + self.assertIsNotNone(self.helper.has_overlapping_shift(shift_contains)) + + def test_exact_same_time(self): + """Test a shift that has exactly the same time as the base shift""" + shift_same_time = Shift.objects.create( + room=self.room, + start_at=self.base_shift.start_at, + duration=self.base_shift.duration, + required_helpers=1, + ) + self.assertIsNotNone(self.helper.has_overlapping_shift(shift_same_time)) + + def test_deleted_shift_no_overlap(self): + """Test that deleted shifts are not considered for overlap""" + self.base_shift.deleted = True + self.base_shift.save() + + shift_same_time = Shift.objects.create( + room=self.room, + start_at=self.base_shift.start_at, + duration=self.base_shift.duration, + required_helpers=1, + ) + self.assertIsNone(self.helper.has_overlapping_shift(shift_same_time)) + + def test_cancelled_registration_no_overlap(self): + """Test that cancelled registrations are not considered for overlap""" + self.base_registration.state = ShiftRegistration.RegState.CANCELED + self.base_registration.save() + + shift_same_time = Shift.objects.create( + room=self.room, + start_at=self.base_shift.start_at, + duration=self.base_shift.duration, + required_helpers=1, + ) + self.assertIsNone(self.helper.has_overlapping_shift(shift_same_time)) + + def test_failed_registration_no_overlap(self): + """Test that failed registrations are not considered for overlap""" + self.base_registration.state = ShiftRegistration.RegState.FAILED + self.base_registration.save() + + shift_same_time = Shift.objects.create( + room=self.room, + start_at=self.base_shift.start_at, + duration=self.base_shift.duration, + required_helpers=1, + ) + self.assertIsNone(self.helper.has_overlapping_shift(shift_same_time)) diff --git a/shiftregister/app/views.py b/shiftregister/app/views.py index 9f268d0..0bca633 100644 --- a/shiftregister/app/views.py +++ b/shiftregister/app/views.py @@ -26,8 +26,16 @@ def index(request): days = cache.get("event_days") if not days: - days = Shift.objects.filter(deleted=False).datetimes("start_at", "day").all() - cache.set("event_days", days) + event_start_at = global_preferences["helper__event_start_at"] + event_end_at = global_preferences["helper__event_end_at"] + days = ( + Shift.objects.filter( + deleted=False, start_at__gte=event_start_at, start_at__lte=event_end_at + ) + .datetimes("start_at", "day") + .all() + ) + cache.set("event_days", days, 60 * 60) context = { "days": days, @@ -211,6 +219,13 @@ def shift(request, shiftid): context["can_register"] = False if reg[0].can_cancel(): context["can_cancel"] = True + elif context["can_register"]: + # Check for overlapping shifts + overlapping_reg = helper.has_overlapping_shift(shift) + if overlapping_reg: + context["can_register"] = False + context["has_overlap"] = True + context["overlapping_shift"] = overlapping_reg.shift if request.method == "POST": if EmptyForm(request.POST).is_valid(): @@ -236,6 +251,14 @@ def shift(request, shiftid): ) return redirect("index") if context["can_register"]: + overlapping_reg = helper.has_overlapping_shift(shift) + if overlapping_reg: + messages.add_message( + request, + messages.ERROR, + "Du hast bereits eine überlappende Schicht zu dieser Zeit.", + ) + return redirect("shift", shiftid=shift.pk) s = ShiftRegistration(helper=helper, shift=shift) s.save() messages.add_message( diff --git a/shiftregister/fallback/admin.py b/shiftregister/fallback/admin.py index bc59f6e..8e90945 100644 --- a/shiftregister/fallback/admin.py +++ b/shiftregister/fallback/admin.py @@ -25,11 +25,12 @@ class FallbackAssignmentInline(admin.TabularInline): model = FallbackAssignment ordering = ("shift__start_at",) readonly_fields = ("shift",) + fk_name = "team_member" @admin.register(TeamMember) class TeamMemberAdmin(admin.ModelAdmin): - fields = ("id", "name", "comment", "url") + fields = ("id", "name", "comment", "url", "pin") readonly_fields = ("id", "url") list_display = ("name", "comment", "shift_count") ordering = ("name",) diff --git a/shiftregister/fallback/migrations/0009_fallbackassignment_traded_to.py b/shiftregister/fallback/migrations/0009_fallbackassignment_traded_to.py new file mode 100644 index 0000000..5bd8b3d --- /dev/null +++ b/shiftregister/fallback/migrations/0009_fallbackassignment_traded_to.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.4 on 2025-05-14 11:39 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("fallback", "0008_alter_teammember_comment"), + ] + + operations = [ + migrations.AddField( + model_name="fallbackassignment", + name="traded_to", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="received_trades", + to="fallback.teammember", + ), + ), + ] diff --git a/shiftregister/fallback/migrations/0010_teammember_pin.py b/shiftregister/fallback/migrations/0010_teammember_pin.py new file mode 100644 index 0000000..8fa900b --- /dev/null +++ b/shiftregister/fallback/migrations/0010_teammember_pin.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.4 on 2025-05-14 11:55 + +from django.db import migrations, models + +import shiftregister.fallback.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("fallback", "0009_fallbackassignment_traded_to"), + ] + + operations = [ + migrations.AddField( + model_name="teammember", + name="pin", + field=models.IntegerField( + default=shiftregister.fallback.models.generate_pin + ), + ), + ] diff --git a/shiftregister/fallback/models.py b/shiftregister/fallback/models.py index e1e050f..fa5b7ce 100644 --- a/shiftregister/fallback/models.py +++ b/shiftregister/fallback/models.py @@ -2,7 +2,7 @@ import math import secrets from base64 import urlsafe_b64encode from datetime import datetime, time -from random import random +from random import randint, random import sentry_sdk from django.db.models import Count, Exists, ExpressionWrapper, Max, OuterRef, Sum @@ -17,22 +17,28 @@ def generate_id(): return int.from_bytes(secrets.token_bytes(3), byteorder="big") +def generate_pin(): + return randint(1000, 9999) + + class TeamMember(models.Model): id = models.IntegerField(default=generate_id, editable=False, primary_key=True) name = models.CharField(max_length=100) comment = models.CharField(max_length=100, blank=True, default="") - fallback_shifts = models.ManyToManyField(Shift, through="FallbackAssignment") + pin = models.IntegerField(default=generate_pin) + fallback_shifts = models.ManyToManyField( + Shift, through="FallbackAssignment", through_fields=("team_member", "shift") + ) def url(self): return "https://helfen.kntkt.de" + reverse( "my_fallback_shifts", - kwargs={ - "team_member_id": urlsafe_b64encode( - self.id.to_bytes(3, byteorder="big") - ).decode("utf-8") - }, + kwargs={"team_member_id": self.url_id()}, ) + def url_id(self): + return urlsafe_b64encode(self.id.to_bytes(3, byteorder="big")).decode("utf-8") + def assign_random_shifts(self): needs_fallback = Q(deleted=False, calendar__needs_fallback=True) @@ -199,6 +205,15 @@ class FallbackAssignment(models.Model): shift = models.ForeignKey(Shift, on_delete=models.CASCADE) team_member = models.ForeignKey(TeamMember, on_delete=models.CASCADE) was_full = models.BooleanField(default=False) + traded_to = models.ForeignKey( + TeamMember, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="received_trades", + ) def __str__(self): + if self.traded_to: + return f"{self.shift} {self.team_member.name} -> {self.traded_to.name}" return f"{self.shift} {self.team_member.name}" diff --git a/shiftregister/fallback/templates/my_fallback_shifts.html b/shiftregister/fallback/templates/my_fallback_shifts.html index d371307..dd3100f 100644 --- a/shiftregister/fallback/templates/my_fallback_shifts.html +++ b/shiftregister/fallback/templates/my_fallback_shifts.html @@ -9,12 +9,42 @@
+ +Schicht übernehmen
+ +Hallo {{ team_member.name }}, hier deine Teamschichten für das Festival: {{ team_member.url }} +Deine PIN um Schichten zu übernehmen ist {{ team_member.pin }} + Deine Schichten werden in den nächsten Tagen weniger werden, wenn wir alle Schichten unter mehr Teammitgliedern verteilen. Du kannst unter dem Link immer nachschauen, welche Schichten du noch hast und welche schon von Helfis belegt sind. Bei Schichten mit mehreren Personen, bei denen nicht alle von Helfis belegt sind, koordiniere dich bitte mit den anderen Teammitgliedern, wer von euch die Schicht übernimmt. @@ -26,6 +56,7 @@ Diese Schichtzuteilung wurde maschinell erstellt und ist auch ohne Unterschrift
Tausch-ID | Wann | Wie lange | Wo | @@ -38,13 +69,19 @@ Diese Schichtzuteilung wurde maschinell erstellt und ist auch ohne Unterschrift {% for assignment in assignments %} {% with assignment.shift as shift %}|||
---|---|---|---|---|---|---|
{{ assignment.id }} {% if assignment.traded_to %}*{% endif %} | {{ shift.start_at }} | {{ shift.duration|duration }} | {{ shift.room.name }} | {{ shift.registration_count }}/{{ shift.required_helpers|default:shift.room.required_helpers }} | -{% for assignment in shift.fallbackassignment_set.all %} - {{ assignment.team_member.name }}{% if not forloop.last %}, {% endif %} +{% for fa in shift.fallbackassignment_set.all %} + {% if fa.traded_to %} + {{ fa.traded_to.name }} + {% else %} + {{ fa.team_member.name }} + {% endif %} + {% if not forloop.last %}, {% endif %} {% endfor %} | @@ -61,7 +98,7 @@ Diese Schichtzuteilung wurde maschinell erstellt und ist auch ohne Unterschrift {% if user.is_authenticated %} {% else %} Noch keine Schichten zugewiesen, bitte wende dich an den Infopoint. diff --git a/shiftregister/fallback/tests.py b/shiftregister/fallback/tests.py index 7ce503c..c242819 100644 --- a/shiftregister/fallback/tests.py +++ b/shiftregister/fallback/tests.py @@ -1,3 +1,171 @@ -from django.test import TestCase +import uuid +from datetime import timedelta -# Create your tests here. +from django.test import Client, TestCase +from django.urls import reverse +from django.utils import timezone + +from shiftregister.fallback.models import FallbackAssignment, TeamMember +from shiftregister.importer.models import Calendar, Event, Room + + +class ShiftTradeTests(TestCase): + def setUp(self): + # Create test data + self.room = Room.objects.create(name="Test Room", required_helpers=2) + + self.event = Event.objects.create( + uuid=uuid.uuid4(), + calendar=Calendar.objects.create(url="https://example.com/calendar"), + start_at=timezone.now(), + duration=timedelta(hours=2), + room=self.room, + deleted=False, + ) + + self.team_member1 = TeamMember.objects.create(name="Team Member 1", pin=1111) + + self.team_member2 = TeamMember.objects.create(name="Team Member 2", pin=2222) + + self.team_member3 = TeamMember.objects.create(name="Team Member 3", pin=3333) + + self.assignment = FallbackAssignment.objects.create( + shift=self.event, team_member=self.team_member1, traded_to=self.team_member3 + ) + + self.client = Client() + + def test_invalid_pin_keeps_assignment_unchanged(self): + """Test that wrong PIN prevents any changes to the assignment""" + response = self.client.post( + reverse( + "my_fallback_shifts", + kwargs={"team_member_id": self.team_member2.url_id()}, + ), + {"take_shift": "true", "assignment_id": self.assignment.id, "pin": "9999"}, + ) + + self.assignment.refresh_from_db() + self.assertEqual(self.assignment.team_member, self.team_member1) + self.assertEqual(self.assignment.traded_to, self.team_member3) + + def test_take_shift_updates_traded_to(self): + """Test that taking a shift properly updates the traded_to field""" + response = self.client.post( + reverse( + "my_fallback_shifts", + kwargs={"team_member_id": self.team_member2.url_id()}, + ), + { + "take_shift": "true", + "assignment_id": self.assignment.id, + "pin": self.team_member2.pin, + }, + follow=True, + ) + + # Assignment should now be traded to team_member2 + self.assignment.refresh_from_db() + self.assertEqual(self.assignment.traded_to, self.team_member2) + self.assertEqual( + self.assignment.team_member, self.team_member1 + ) # Original owner unchanged + + def test_return_shift_clears_traded_to(self): + """Test that returning a shift clears the traded_to field""" + + # Then return it + response = self.client.post( + reverse( + "my_fallback_shifts", + kwargs={"team_member_id": self.team_member3.url_id()}, + ), + { + "take_shift": "true", + "assignment_id": self.assignment.id, + "pin": self.team_member3.pin, + }, + follow=True, + ) + + # Assignment should be returned (traded_to cleared) + self.assignment.refresh_from_db() + self.assertIsNone(self.assignment.traded_to) + self.assertEqual( + self.assignment.team_member, self.team_member1 + ) # Original owner unchanged + + def test_reclaim_shift_clears_traded_to(self): + """Test that original owner can reclaim their shift""" + + # Original owner reclaims it + response = self.client.post( + reverse( + "my_fallback_shifts", + kwargs={"team_member_id": self.team_member1.url_id()}, + ), + { + "take_shift": "true", + "assignment_id": self.assignment.id, + "pin": self.team_member1.pin, + }, + follow=True, + ) + + # Assignment should be reclaimed (traded_to cleared) + self.assignment.refresh_from_db() + self.assertIsNone(self.assignment.traded_to) + self.assertEqual(self.assignment.team_member, self.team_member1) + + def test_take_shift_from_other_trader(self): + """Test that a member can take over a shift that was already traded to someone else""" + # Verify initial state + self.assertEqual(self.assignment.team_member, self.team_member1) + self.assertEqual(self.assignment.traded_to, self.team_member3) + + # team_member2 takes the shift from team_member3 + response = self.client.post( + reverse( + "my_fallback_shifts", + kwargs={"team_member_id": self.team_member2.url_id()}, + ), + { + "take_shift": "true", + "assignment_id": self.assignment.id, + "pin": self.team_member2.pin, + }, + follow=True, + ) + + # Assignment should now be traded to team_member2 + self.assignment.refresh_from_db() + self.assertEqual( + self.assignment.team_member, self.team_member1 + ) # Original owner unchanged + self.assertEqual(self.assignment.traded_to, self.team_member2) # New trader + + def test_form_validation_prevents_changes(self): + """Test that invalid form data doesn't modify any assignments""" + initial_count = FallbackAssignment.objects.count() + initial_state = FallbackAssignment.objects.values_list( + "id", "team_member_id", "traded_to_id" + ) + + response = self.client.post( + reverse( + "my_fallback_shifts", + kwargs={"team_member_id": self.team_member1.url_id()}, + ), + { + "take_shift": "true", + "assignment_id": "not_a_number", + "pin": "also_not_a_number", + }, + ) + + # No assignments should be created or modified + self.assertEqual(FallbackAssignment.objects.count(), initial_count) + current_state = FallbackAssignment.objects.values_list( + "id", "team_member_id", "traded_to_id" + ) + self.assertEqual(list(initial_state), list(current_state)) diff --git a/shiftregister/fallback/views.py b/shiftregister/fallback/views.py index 4ce94eb..1916de5 100644 --- a/shiftregister/fallback/views.py +++ b/shiftregister/fallback/views.py @@ -1,13 +1,23 @@ from base64 import urlsafe_b64decode +from django import forms +from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.db.models import Count +from django.db.models import Count, Q from django.http import HttpResponse -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse -from shiftregister.fallback.models import TeamMember +from shiftregister.fallback.models import FallbackAssignment, TeamMember -# Create your views here. + +class TradeForm(forms.Form): + assignment_id = forms.IntegerField( + widget=forms.NumberInput(attrs={"class": "input", "placeholder": "Schicht-ID"}) + ) + pin = forms.IntegerField( + widget=forms.PasswordInput(attrs={"class": "input", "placeholder": "Deine PIN"}) + ) def my_fallback_shifts(request, team_member_id): @@ -21,17 +31,58 @@ def my_fallback_shifts(request, team_member_id): team_member = get_object_or_404(TeamMember, pk=team_member_id) is_draw = False + trade_form = TradeForm( + request.POST + if request.method == "POST" and "take_shift" in request.POST + else None + ) + if request.method == "POST": - team_member.assign_random_shifts() - is_draw = True + if "draw_shifts" in request.POST: + team_member.assign_random_shifts() + is_draw = True + elif "take_shift" in request.POST and trade_form.is_valid(): + assignment_id = trade_form.cleaned_data["assignment_id"] + pin = trade_form.cleaned_data["pin"] + + if pin != team_member.pin: + messages.error(request, "Ungültige PIN") + else: + try: + assignment = FallbackAssignment.objects.get(pk=assignment_id) + if assignment.team_member == team_member: + assignment.traded_to = None + messages.success(request, f"Schicht erfolgreich zurückgenommen") + elif assignment.traded_to == team_member: + assignment.traded_to = None + messages.success(request, f"Schicht erfolgreich zurückgegeben") + else: + assignment.traded_to = team_member + messages.success(request, f"Schicht erfolgreich übernommen") + assignment.save() + return redirect( + reverse( + "my_fallback_shifts", + kwargs={"team_member_id": team_member.url_id()}, + ) + ) + except FallbackAssignment.DoesNotExist: + messages.error(request, "Ungültige Schicht-ID") + + assignments = ( + FallbackAssignment.objects.filter( + Q(team_member=team_member, traded_to__isnull=True) + | Q(traded_to=team_member) + ) + .prefetch_related("shift", "traded_to", "team_member") + .order_by("shift__start_at") + ) context = { "team_member": team_member, - "assignments": team_member.fallbackassignment_set.order_by( - "shift__start_at" - ).all(), - # "shifts": team_member.fallback_shifts.order_by("start_at").all(), + "assignments": assignments, "is_draw": is_draw, + "trade_form": trade_form, } return render(request, "my_fallback_shifts.html", context) diff --git a/shiftregister/importer/admin.py b/shiftregister/importer/admin.py index 01f1530..669a774 100644 --- a/shiftregister/importer/admin.py +++ b/shiftregister/importer/admin.py @@ -3,6 +3,12 @@ from django.contrib import admin from .models import Calendar +def update_calendar(modeladmin, request, queryset): + for calendar in queryset: + calendar.update() + + @admin.register(Calendar) class CalendarAdmin(admin.ModelAdmin): list_display = ("url", "needs_fallback", "has_errors") + actions = (update_calendar,) diff --git a/shiftregister/importer/migrations/0003_alter_calendar_url.py b/shiftregister/importer/migrations/0003_alter_calendar_url.py new file mode 100644 index 0000000..77f1e44 --- /dev/null +++ b/shiftregister/importer/migrations/0003_alter_calendar_url.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2025-05-12 22:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("importer", "0002_calendar_needs_fallback"), + ] + + operations = [ + migrations.AlterField( + model_name="calendar", + name="url", + field=models.URLField(max_length=1000, primary_key=True, serialize=False), + ), + ] diff --git a/shiftregister/importer/models.py b/shiftregister/importer/models.py index 2d09c90..8134d43 100644 --- a/shiftregister/importer/models.py +++ b/shiftregister/importer/models.py @@ -4,10 +4,17 @@ from shiftregister.app.models import * class Calendar(models.Model): - url = models.URLField(primary_key=True) + url = models.URLField(primary_key=True, max_length=1000) needs_fallback = models.BooleanField(default=False, editable=True) has_errors = models.BooleanField(default=False, editable=False) + def update(self): + # break circular import + from .importer import import_calendar + + self.has_errors = not import_calendar(self) + self.save() + class Event(Shift): uuid = models.UUIDField(primary_key=True, editable=False) diff --git a/shiftregister/importer/tasks.py b/shiftregister/importer/tasks.py index 948f5fb..32eec41 100644 --- a/shiftregister/importer/tasks.py +++ b/shiftregister/importer/tasks.py @@ -7,5 +7,4 @@ from .models import Calendar @shared_task def import_shifts(): for calendar in Calendar.objects.all(): - calendar.has_errors = not import_calendar(calendar) - calendar.save() + calendar.update() diff --git a/shiftregister/settings.py b/shiftregister/settings.py index 79dbe4b..34e8083 100644 --- a/shiftregister/settings.py +++ b/shiftregister/settings.py @@ -10,6 +10,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.0/ref/settings/ """ +import sys from pathlib import Path import environ @@ -31,6 +32,9 @@ environ.Env.read_env(BASE_DIR / ".env") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env("ENVIRONMENT") == "development" +# Check if we're running tests +TESTING = "test" in sys.argv + # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env( "SECRET_KEY", @@ -61,6 +65,9 @@ THIRDPARTY_APPS = [ "phonenumber_field", ] +if DEBUG and not TESTING: + THIRDPARTY_APPS += ["debug_toolbar"] + LOCAL_APPS = [ "shiftregister.app", "shiftregister.core", @@ -87,6 +94,9 @@ MIDDLEWARE = [ "shiftregister.app.middleware.check_helper", ] +if DEBUG and not TESTING: + MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") + ROOT_URLCONF = "shiftregister.urls" LOGIN_URL = "/admin/login/" @@ -260,3 +270,26 @@ LOGGING = { }, }, } + +# Debug Toolbar settings +if DEBUG: + INTERNAL_IPS = ["127.0.0.1"] + DEBUG_TOOLBAR_CONFIG = { + "SHOW_TOOLBAR_CALLBACK": lambda request: True, # Always show toolbar in debug mode + "SHOW_TEMPLATE_CONTEXT": True, + "SQL_WARNING_THRESHOLD": 100, # ms + } + DEBUG_TOOLBAR_PANELS = [ + "debug_toolbar.panels.timer.TimerPanel", + "debug_toolbar.panels.settings.SettingsPanel", + "debug_toolbar.panels.headers.HeadersPanel", + "debug_toolbar.panels.request.RequestPanel", + "debug_toolbar.panels.sql.SQLPanel", + "debug_toolbar.panels.staticfiles.StaticFilesPanel", + "debug_toolbar.panels.templates.TemplatesPanel", + "debug_toolbar.panels.cache.CachePanel", + "debug_toolbar.panels.signals.SignalsPanel", + "debug_toolbar.panels.logging.LoggingPanel", + "debug_toolbar.panels.redirects.RedirectsPanel", + "debug_toolbar.panels.profiling.ProfilingPanel", + ] diff --git a/shiftregister/signage/templates/team_dashboard.html b/shiftregister/signage/templates/team_dashboard.html index 239884b..d4d325b 100644 --- a/shiftregister/signage/templates/team_dashboard.html +++ b/shiftregister/signage/templates/team_dashboard.html @@ -18,7 +18,18 @@ |
{{ shift.room.name }} | {{ shift.start_at }} | -{% for fa in shift.fallbackassignment_set.all %}{% if not fa.was_full %}{{ fa.team_member.name }}{% if not forloop.last %}, {% endif %}{% endif %}{% endfor %} | ++ {% for fa in shift.fallbackassignment_set.all %} + {% if not fa.was_full %} + {% if fa.traded_to %} + {{ fa.traded_to.name }} ({{ fa.id }}) + {% else %} + {{ fa.team_member.name }} ({{ fa.id }}) + {% endif %} + {% if not forloop.last %}, {% endif %} + {% endif %} + {% endfor %} + | |||