main #1

Merged
xAndy merged 12 commits from main into live 2025-05-15 23:25:56 +02:00
21 changed files with 680 additions and 32 deletions

View File

@ -78,3 +78,19 @@ class FallbackQuota(types.FloatPreference):
section = helper
name = "fallback_quota"
default = 0.7
@global_preferences_registry.register
class EventStartAt(types.DatePreference):
section = helper
name = "event_start_at"
default = datetime.date(2024, 5, 24)
help_text = "The start date and time of the event. Date navigation will only show days between start and end time."
@global_preferences_registry.register
class EventEndAt(types.DatePreference):
section = helper
name = "event_end_at"
default = datetime.date(2025, 5, 28)
help_text = "The end date and time of the event. Date navigation will only show days between start and end time."

View File

@ -97,6 +97,40 @@ class Helper(models.Model):
token.send()
return token
def has_overlapping_shift(self, shift):
new_shift_end = shift.start_at + shift.duration
return (
ShiftRegistration.objects.annotate(
shift_end=ExpressionWrapper(
F("shift__start_at") + F("shift__duration"),
output_field=models.DateTimeField(),
)
)
.filter(
helper=self,
shift__deleted=False,
state__in=[
ShiftRegistration.RegState.REGISTERED,
ShiftRegistration.RegState.CHECKED_IN,
],
)
.filter(
# Case 1: Start time falls between new shift's start and end
Q(
shift__start_at__gte=shift.start_at,
shift__start_at__lt=new_shift_end,
)
|
# Case 2: End time falls between new shift's start and end
Q(shift_end__gt=shift.start_at, shift_end__lte=new_shift_end)
|
# Case 3: Completely encompasses the new shift
Q(shift__start_at__lte=shift.start_at, shift_end__gte=new_shift_end)
)
.first()
)
# current or next shift
def important_shift(self):
ret = (

View File

@ -13,7 +13,11 @@
<div class="notification">Diese Schicht wurde gelöscht.</div>
{% endif %}
{% if not can_register and not is_registered %}
{% if has_overlap %}
<div class="notification is-warning">Du hast bereits eine überlappende Schicht zu dieser Zeit: <a href="{% url 'shift' overlapping_shift.id %}">{{ overlapping_shift.room.name }} ({{ overlapping_shift.start_at }})</a></div>
{% else %}
<div class="notification">Diese Schicht ist bereits besetzt.</div>
{% endif %}
{% endif %}
<div class="content">
<p>

View File

@ -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))

View File

@ -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(

View File

@ -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",)

View File

@ -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",
),
),
]

View File

@ -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
),
),
]

View File

@ -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}"

View File

@ -9,12 +9,42 @@
<div class="content">
<a href="{% url 'pages:view' 'team_faq' %}">Häufig gestellte Fragen zu Teamschichten</a>
</div>
<div class="box">
<h4 class="subtitle">Schicht übernehmen</h4>
<form method="POST">
{% csrf_token %}
<div class="field has-addons">
<div class="control">
{{ trade_form.assignment_id }}
</div>
<div class="control">
{{ trade_form.pin }}
</div>
<div class="control">
<button type="submit" name="take_shift" class="button is-info">Übernehmen/Entfernen</button>
</div>
</div>
{% if form.errors %}
<div class="field">
{% for field in trade_form %}
{% for error in field.errors %}
<p class="help is-danger">{{ error }}</p>
{% endfor %}
{% endfor %}
</div>
{% endif %}
</form>
</div>
{% if assignments %}
{% if is_draw %}
<pre class="mb-5 select_all">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
<table class="table">
<thead>
<tr>
<th>Tausch-ID</th>
<th>Wann</th>
<th>Wie lange</th>
<th>Wo</th>
@ -38,13 +69,19 @@ Diese Schichtzuteilung wurde maschinell erstellt und ist auch ohne Unterschrift
{% for assignment in assignments %}
{% with assignment.shift as shift %}
<tr{% if shift.registration_count == shift.required_helpers|default:shift.room.required_helpers or assignment.was_full %} class="has-text-grey" style="text-decoration: line-through;"{% endif %}>
<td>{{ assignment.id }} {% if assignment.traded_to %}*{% endif %}</td>
<td>{{ shift.start_at }}</td>
<td>{{ shift.duration|duration }}</td>
<td>{{ shift.room.name }} </td>
<td>{{ shift.registration_count }}/{{ shift.required_helpers|default:shift.room.required_helpers }}</td>
<td>
{% 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 %}
</td>
<td>
@ -61,7 +98,7 @@ Diese Schichtzuteilung wurde maschinell erstellt und ist auch ohne Unterschrift
{% if user.is_authenticated %}
<form method="POST">
{% csrf_token %}
<button class="button is-success" type="submit">Schichten zulosen</button>
<button class="button is-success" type="submit" name="draw_shifts">Schichten zulosen</button>
</form>
{% else %}
Noch keine Schichten zugewiesen, bitte wende dich an den Infopoint.

View File

@ -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))

View File

@ -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":
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)

View File

@ -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,)

View File

@ -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),
),
]

View File

@ -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)

View File

@ -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()

View File

@ -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",
]

View File

@ -18,7 +18,18 @@
<tr>
<td>{{ shift.room.name }}</td>
<td>{{ shift.start_at }}</td>
<td>{% for fa in shift.fallbackassignment_set.all %}{% if not fa.was_full %}{{ fa.team_member.name }}{% if not forloop.last %}, {% endif %}{% endif %}{% endfor %}</td>
<td>
{% 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 %}
</td>
</tr>
{% empty %}
<tr>

View File

@ -59,7 +59,11 @@
{% for fallback in shift.event.fallbackassignment_set.all %}
<div class="column is-one-quarter">
<div class="box{% if fallback.was_full %} has-text-grey" style="text-decoration: line-through;{% endif %}">
{% if fallback.traded_to %}
{{ fallback.traded_to.name }}
{% else %}
{{ fallback.team_member.name }}
{% endif %}
</div>
</div>
{% endfor %}

View File

@ -14,6 +14,7 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
@ -28,3 +29,10 @@ urlpatterns = [
path("messages/", include("shiftregister.messaging.urls")),
path("admin/", admin.site.urls),
]
if settings.DEBUG:
import debug_toolbar
urlpatterns += [
path("__debug__/", include(debug_toolbar.urls)),
]

26
texts.md Normal file
View File

@ -0,0 +1,26 @@
# SMS-Texte
## Schichten
```
Die Schichten für das Kontaktfestival sind online: https://helfen.kntkt.de
Ignoriere diese SMS, um keine weiteren von uns zu erhalten.
```
## Teamschichten
```
Ab sofort könnt ihr euch bei (Andi oder) mir melden, um den Link zu euren Teamschichten zu erhalten. Je früher, desto schneller habt ihr die Möglichkeit, die Schichten an eure Freundis weiterzuverteilen :D
```
## Abbauschichterinnerung
```
Deine Schicht beginnt um $UHRZEIT, bitte melde dich auf dem Gelände bei $TEAMMITGLIED https://helfen.kntkt.de/shift/$SHIFT_ID
```
## Helfifesteinladung
```
Danke für deine Hilfe auf dem Festival. Zur Belohnung schmeißen wir eine Grillparty mit allen Helfis: $fb
```