Compare commits

...

11 Commits

Author SHA1 Message Date
xAndy d034e39cb8 add setting to supress room change messages
continuous-integration/drone/push Build is passing Details
2025-05-18 01:29:10 +02:00
xAndy 04d85811fe change default required helpers to 1, even in import, so locations can be changed 2025-05-18 01:29:10 +02:00
Luca ce8316a69c fix(fallback): fix order of yielded tuple
continuous-integration/drone/push Build is passing Details
2025-05-18 01:18:39 +02:00
Luca d89d480170 feat: improve fallback shift assignment algorithm
continuous-integration/drone/push Build is passing Details
2025-05-18 01:03:19 +02:00
xAndy f2c00ec8d9 avoid auto registration via link previews
continuous-integration/drone/push Build is passing Details
2025-05-17 23:44:13 +02:00
Luca 38cdbec9fa feat(fallback): assign restricted shifts from their own bucket
continuous-integration/drone/push Build is passing Details
2025-05-17 20:13:00 +02:00
xAndy d7d26c56b0 allow shift registrations end to start
continuous-integration/drone/push Build is passing Details
2025-05-17 20:01:02 +02:00
Luca c5bb532749 feat: implement restricted shifts
continuous-integration/drone/push Build is passing Details
2025-05-17 19:38:22 +02:00
Luca 701caae254 fix: hide 'AStA' tag if 'enable_asta' is False 2025-05-17 19:38:22 +02:00
Luca 797c5f5fc8 docs: add another SMS text 2025-05-17 19:38:22 +02:00
Luca 1509cb6312 chore: add migration for room default_helpers=1 2025-05-17 19:38:22 +02:00
21 changed files with 384 additions and 100 deletions

View File

@ -20,7 +20,7 @@ class ShiftInline(admin.TabularInline):
@admin.register(Room)
class RoomAdmin(admin.ModelAdmin):
list_display = ("name", "description_length", "shift_count")
list_display = ("name", "description_length", "shift_count", "required_helpers")
inlines = [ShiftInline]
def description_length(self, object):

View File

@ -94,3 +94,11 @@ class EventEndAt(types.DatePreference):
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."
@global_preferences_registry.register
class SupressRoomChangeNotifications(types.BooleanPreference):
section = helper
name = "supress_room_change_notifications"
default = False
help_text = "If true, notifications about room changes will be suppressed."

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2025-05-16 22:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("app", "0013_room_send_reminders"),
]
operations = [
migrations.AlterField(
model_name="room",
name="required_helpers",
field=models.IntegerField(default=1),
),
]

View File

@ -28,7 +28,43 @@ class Room(models.Model):
return self.shift_set.filter(deleted=False)
class ShiftManager(models.Manager):
def with_reg_count(self):
return (
self.get_queryset()
.annotate(
reg_count=Count(
"shiftregistration",
distinct=True,
filter=Q(
shiftregistration__state__in=(
ShiftRegistration.RegState.REGISTERED,
ShiftRegistration.RegState.CHECKED_IN,
)
),
)
)
.select_related("room")
)
class DisplayShiftManager(ShiftManager):
def get_queryset(self):
return (
super()
.get_queryset()
.select_related("event__calendar")
.filter(
Q(event__isnull=True) | Q(event__calendar__restricted=False),
deleted=False,
)
)
class Shift(models.Model):
all_objects = ShiftManager()
objects = DisplayShiftManager()
room = models.ForeignKey(Room, on_delete=models.RESTRICT)
start_at = models.DateTimeField(db_index=True)
duration = models.DurationField()
@ -38,20 +74,6 @@ class Shift(models.Model):
description = models.TextField(blank=True, default="")
deleted = models.BooleanField(default=False, db_index=True)
def with_reg_count():
return Shift.objects.annotate(
reg_count=Count(
"shiftregistration",
distinct=True,
filter=Q(
shiftregistration__state__in=(
ShiftRegistration.RegState.REGISTERED,
ShiftRegistration.RegState.CHECKED_IN,
)
),
)
).select_related("room")
def __str__(self):
return f"{self.room.name}: {self.start_at}"
@ -123,10 +145,10 @@ class Helper(models.Model):
)
|
# 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)
Q(shift_end__gt=shift.start_at, shift_end__lt=new_shift_end)
|
# Case 3: Completely encompasses the new shift
Q(shift__start_at__lte=shift.start_at, shift_end__gte=new_shift_end)
Q(shift__start_at__lt=shift.start_at, shift_end__gt=new_shift_end)
)
.first()
)

View File

@ -2,23 +2,28 @@ from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.shortcuts import reverse
from django.template import Context, Template
from dynamic_preferences.registries import global_preferences_registry
from shiftregister.core.signals import populate_nav
from .models import Message, Shift
global_preferences = global_preferences_registry.manager()
@receiver(pre_save, dispatch_uid="notify_shift_changed")
def notify_shift_changed(sender, **kwargs):
if issubclass(sender, Shift):
instance = kwargs["instance"]
try:
prev = Shift.objects.get(pk=instance.id)
prev = Shift.all_objects.get(pk=instance.id)
except Shift.DoesNotExist:
return
room_unchanged = (prev.room == instance.room) or global_preferences[
"helper__supress_room_change_notifications"
]
if (
prev.room == instance.room
room_unchanged
and prev.start_at == instance.start_at
and prev.duration == instance.duration
and prev.deleted == instance.deleted

View File

@ -0,0 +1,11 @@
{% extends "helper_base.html" %}
{% block title %}Registrierung{% endblock %}
{% block content %}
<p>Du solltest automatisch eingeloggt werden. Falls nicht, klicke bitte hier:</p>
<a class="button is-link" href="{% url 'login_confirm' token %}">Login</a>
<script>
window.location.href = "{% url 'login_confirm' token %}";
</script>
{% endblock %}

View File

@ -13,11 +13,13 @@
<div class="notification">Diese Schicht wurde gelöscht.</div>
{% endif %}
{% if not can_register and not is_registered %}
{% if has_overlap %}
{% if shift.restricted %}
<div class="notification is-warning">Diese Schicht kann nur von Teamis besetzt werden, daher kannst du dich nicht anmelden.</div>
{% elif 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 %}
{% else %}
<div class="notification">Diese Schicht ist bereits besetzt.</div>
{% endif %}
{% endif %}
{% endif %}
<div class="content">
<p>

View File

@ -2,8 +2,9 @@ from datetime import timedelta
from django.test import TestCase
from django.utils import timezone
from dynamic_preferences.registries import global_preferences_registry
from .models import Helper, Room, Shift, ShiftRegistration
from .models import Helper, Message, Room, Shift, ShiftRegistration
class ShiftOverlapTests(TestCase):
@ -43,11 +44,32 @@ class ShiftOverlapTests(TestCase):
)
self.assertIsNone(self.helper.has_overlapping_shift(shift_before))
def test_back_to_back_shifts(self):
"""Test that a shift starting exactly when another ends is allowed"""
shift_after = Shift.objects.create(
room=self.room,
start_at=self.base_shift.start_at + self.base_shift.duration,
duration=timedelta(hours=1),
required_helpers=1,
)
self.assertIsNone(self.helper.has_overlapping_shift(shift_after))
# Also test the reverse case - registering for a shift that ends exactly when another begins
self.base_registration.delete()
ShiftRegistration.objects.create(
shift=shift_after,
helper=self.helper,
state=ShiftRegistration.RegState.REGISTERED,
)
self.assertIsNone(self.helper.has_overlapping_shift(self.base_shift))
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),
start_at=self.base_shift.start_at
+ self.base_shift.duration
+ timedelta(hours=1),
duration=timedelta(hours=1),
required_helpers=1,
)
@ -141,3 +163,79 @@ class ShiftOverlapTests(TestCase):
required_helpers=1,
)
self.assertIsNone(self.helper.has_overlapping_shift(shift_same_time))
class ShiftSignalTests(TestCase):
def setUp(self):
# Create rooms
self.room1 = Room.objects.create(
name="Test Room 1", required_helpers=1, meeting_location="Test Location 1"
)
self.room2 = Room.objects.create(
name="Test Room 2", required_helpers=1, meeting_location="Test Location 2"
)
# Create a helper
self.helper = Helper.objects.create(
phone="+491234567890", name="Test Helper", number_validated=True
)
# Create a shift
self.shift = Shift.objects.create(
room=self.room1,
start_at=timezone.now() + timedelta(hours=1),
duration=timedelta(hours=2),
required_helpers=1,
)
# Register the helper for the shift
self.registration = ShiftRegistration.objects.create(
shift=self.shift,
helper=self.helper,
state=ShiftRegistration.RegState.REGISTERED,
)
# Get global preferences manager
self.global_preferences = global_preferences_registry.manager()
def test_room_change_notification_default(self):
"""Test that room changes create notifications by default"""
# Ensure the preference is False (default)
self.global_preferences["helper__supress_room_change_notifications"] = False
# Change the room
self.shift.room = self.room2
self.shift.save()
# Check that a notification was created
self.assertEqual(Message.objects.count(), 1)
message = Message.objects.first()
self.assertEqual(message.to, self.helper)
self.assertTrue("geändert" in message.text)
def test_room_change_notification_suppressed(self):
"""Test that room changes don't create notifications when suppressed"""
# Enable notification suppression
self.global_preferences["helper__supress_room_change_notifications"] = True
# Change the room
self.shift.room = self.room2
self.shift.save()
# Check that no notification was created
self.assertEqual(Message.objects.count(), 0)
def test_other_changes_still_notify_when_suppressed(self):
"""Test that other changes still create notifications when room notifications are suppressed"""
# Enable notification suppression
self.global_preferences["helper__supress_room_change_notifications"] = True
# Change something other than the room
self.shift.start_at = timezone.now() + timedelta(hours=2)
self.shift.save()
# Check that a notification was created
self.assertEqual(Message.objects.count(), 1)
message = Message.objects.first()
self.assertEqual(message.to, self.helper)
self.assertTrue("geändert" in message.text)

View File

@ -5,6 +5,7 @@ from . import views
urlpatterns = [
path("", views.index, name="index"),
path("l/<slug:token>", views.login, name="token_login"),
path("login_confirm/<slug:token>", views.login_confirm, name="login_confirm"),
path("logout", views.logout, name="token_logout"),
path("register", views.register, name="register"),
path("asta", views.asta, name="asta"),

View File

@ -8,6 +8,7 @@ from django.core.cache import cache
from django.db import transaction
from django.db.models import Count, ExpressionWrapper, F, Q
from django.db.models.fields import DateTimeField
from django.db.models.functions import Coalesce
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from dynamic_preferences.registries import global_preferences_registry
@ -30,7 +31,7 @@ def index(request):
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
start_at__gte=event_start_at, start_at__lte=event_end_at
)
.datetimes("start_at", "day")
.all()
@ -65,13 +66,12 @@ def index(request):
# dont show shifts starting in <60 minutes?
# currently only sorts by date
free_shifts = (
Shift.with_reg_count()
Shift.objects.with_reg_count()
.filter(
help_wanted,
start_at__gte=day + timedelta(hours=6),
start_at__lte=day + timedelta(hours=30),
start_at__gt=timezone.now(),
deleted=False,
)
.order_by("start_at")
for day in days
@ -98,7 +98,19 @@ def login(request, token):
"Wir konnten dich nicht in unserer Datenbank finden. Bitte registriere dich neu, auch wenn du letztes Jahr bereits geholfen hast.",
)
return redirect("register")
return render(request, "login.html", {"token": token})
def login_confirm(request, token):
try:
tk = LoginToken.objects.get(pk=token)
except LoginToken.DoesNotExist:
messages.add_message(
request,
messages.WARNING,
"Wir konnten dich nicht in unserer Datenbank finden. Bitte registriere dich neu, auch wenn du letztes Jahr bereits geholfen hast.",
)
return redirect("register")
if not tk.helper.number_validated:
tk.helper.number_validated = True
tk.helper.save()
@ -191,7 +203,12 @@ def register(request):
@event_state
def shift(request, shiftid):
shift = get_object_or_404(Shift.with_reg_count(), pk=shiftid)
shift = get_object_or_404(
Shift.all_objects.with_reg_count()
.select_related("event__calendar")
.annotate(restricted=Coalesce("event__calendar__restricted", False)),
pk=shiftid,
)
helper = request.helper
context = {
"enable_asta": global_preferences["helper__enable_asta"],
@ -201,15 +218,14 @@ def shift(request, shiftid):
"shift": shift,
"shift_form": EmptyForm,
}
# this currently ignores date/time
request.session["last_seen_shift"] = shiftid
if (
context["can_register"] = (
shift.required_helpers > shift.registration_count()
or shift.required_helpers == 0
and shift.room.required_helpers > shift.registration_count()
):
context["can_register"] = True
) and not shift.restricted
# this currently ignores date/time
request.session["last_seen_shift"] = shiftid
if helper:
context["helper"] = helper

View File

@ -0,0 +1,56 @@
import math
from random import random
from django.db.models import Case, Count, F, Sum, When
from shiftregister.importer.models import Event
class Bucket:
def __init__(self, selector, sub_buckets=[]):
if not isinstance(sub_buckets, list) or not all(
map(lambda b: isinstance(b, Bucket), sub_buckets)
):
raise ValueError("sub_buckets must be a list of instances of class Bucket")
self.selector = selector
self.sub_buckets = sub_buckets
def __iter__(self):
for bucket in self.sub_buckets:
yield bucket.selector, bucket
def has_sub_buckets(self):
return len(self.sub_buckets) > 0
def distribute(total_slots, total_assignments, pool, qs=None):
if not isinstance(pool, Bucket):
raise ValueError("pool must be an instance of class Bucket")
qs = (qs or Event.objects).filter(pool.selector)
for selector, bucket in pool:
bucket_qs = qs.filter(selector).annotate(
real_required_helpers=Case(
When(required_helpers=0, then=F("room__required_helpers")),
default=F("required_helpers"),
)
)
bucket_slots = bucket_qs.aggregate(sum=Sum("real_required_helpers"))["sum"]
extra_chance, bucket_assignments = math.modf(
bucket_slots / total_slots * total_assignments
)
bucket_assignments = int(bucket_assignments)
if extra_chance > random():
bucket_assignments += 1
if bucket.has_sub_buckets():
yield from distribute(bucket_slots, bucket_assignments, bucket, qs)
else:
yield bucket_qs, bucket_assignments
total_slots -= bucket_slots
total_assignments -= bucket_assignments

View File

@ -12,6 +12,8 @@ from django.utils import timezone
from shiftregister.importer.models import *
from .distribution import Bucket, distribute
def generate_id():
return int.from_bytes(secrets.token_bytes(3), byteorder="big")
@ -40,8 +42,38 @@ class TeamMember(models.Model):
return urlsafe_b64encode(self.id.to_bytes(3, byteorder="big")).decode("utf-8")
def assign_random_shifts(self):
if self.fallback_shifts.count() != 0:
return
needs_fallback = Q(deleted=False, calendar__needs_fallback=True)
total_slots = (
Event.objects.filter(needs_fallback)
.annotate(
real_required_helpers=Case(
When(required_helpers=0, then=F("room__required_helpers")),
default=F("required_helpers"),
),
)
.aggregate(sum=Sum("real_required_helpers"))["sum"]
)
number_of_team_members = TeamMember.objects.count()
quota = global_preferences["helper__fallback_quota"]
max_shifts_per_member = total_slots / max(number_of_team_members * quota, 1)
active_team_members = (
TeamMember.objects.filter(~Q(fallback_shifts=None)).count() + 1
)
shifts_per_member = total_slots / active_team_members
extra_chance, total_assignments = math.modf(
min(max_shifts_per_member, shifts_per_member)
)
total_assignments = int(total_assignments)
if extra_chance > random():
total_assignments += 1
current_tz = timezone.get_current_timezone()
# create a datetime combining the last date having fallback shifts
# after 20:00 with the time 20:00 in the current timezone
@ -56,47 +88,38 @@ class TeamMember(models.Model):
is_last_night = Q(start_at__gte=last_night)
is_night_shift = Q(start_at__hour__gte=21) | Q(start_at__hour__lte=10)
if self.fallback_shifts.count() != 0:
return
is_restricted = Q(calendar__restricted=True)
day_shifts = ~is_night_shift & ~is_last_night & needs_fallback
night_shifts = is_night_shift & ~is_last_night & needs_fallback
shit_shifts = is_last_night & needs_fallback
day_shifts = Bucket(~is_night_shift & ~is_last_night)
night_shifts = Bucket(is_night_shift & ~is_last_night)
shit_shifts = Bucket(is_last_night)
self._assign_from_bucket(day_shifts)
self._assign_from_bucket(night_shifts)
self._assign_from_bucket(shit_shifts)
restricted_buckets = [
Bucket(Q(calendar=calendar))
for calendar in Calendar.objects.filter(
needs_fallback=True, restricted=True
)
]
def _assign_from_bucket(self, bucket_selector):
bucket = Event.objects.filter(bucket_selector).annotate(
fallback_count=Count("fallbackassignment"),
real_required_helpers=Case(
When(required_helpers=0, then=F("room__required_helpers")),
default=F("required_helpers"),
for bucket, assignments in distribute(
total_slots,
total_assignments,
Bucket(
needs_fallback,
[
Bucket(~is_restricted, [day_shifts, night_shifts, shit_shifts]),
Bucket(is_restricted, restricted_buckets),
],
),
)
):
self._assign_from_bucket(bucket, assignments)
def _assign_from_bucket(self, bucket, shift_count):
bucket = bucket.annotate(fallback_count=Count("fallbackassignment"))
free_bucket = bucket.filter(fallback_count__lt=F("real_required_helpers"))
total_slot_count = bucket.aggregate(sum=Sum("real_required_helpers"))["sum"]
quota = global_preferences["helper__fallback_quota"]
number_of_team_members = TeamMember.objects.count()
max_shifts_per_member = total_slot_count / max(
number_of_team_members * quota, 1
)
active_team_members = (
TeamMember.objects.filter(~Q(fallback_shifts=None)).count() + 1
)
shifts_per_member = total_slot_count / active_team_members
extra_chance, shift_count = math.modf(
min(max_shifts_per_member, shifts_per_member)
)
shift_count = int(shift_count)
if extra_chance > random():
shift_count += 1
blocked_times = []
for shift in self.fallback_shifts.all():
blocked_times.append(
@ -140,9 +163,7 @@ class TeamMember(models.Model):
while shifts_needed > 0:
# this is a bit more complex and uses subqueries and id lists because we want to reuse the query selector for events.
# maybe there is a good way to transform q-expressions to add prefixes to fields but for now this has to work
candidate_shift_ids = Event.objects.filter(bucket_selector).values(
"shift_ptr_id"
)
candidate_shift_ids = bucket.values("shift_ptr_id")
relevant_assignments = FallbackAssignment.objects.filter(
shift_id__in=candidate_shift_ids
)

View File

@ -10,5 +10,5 @@ def update_calendar(modeladmin, request, queryset):
@admin.register(Calendar)
class CalendarAdmin(admin.ModelAdmin):
list_display = ("name", "url", "needs_fallback", "has_errors")
list_display = ("name", "url", "needs_fallback", "restricted", "has_errors")
actions = (update_calendar,)

View File

@ -89,9 +89,7 @@ def import_calendar(calendar):
rooms[r.name] = r
for room, r in rooms.items():
if r == None:
rooms[room] = Room(
name=room, required_helpers=0
) # required_helpers=0 ensures a shift in a new room is not displayed unless the correct number of required helpers is set or the shift itself specifies it
rooms[room] = Room(name=room)
rooms[room].save()
for e in Event.objects.filter(calendar=calendar, uuid__in=events):

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2025-05-16 22:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("importer", "0004_calendar_name"),
]
operations = [
migrations.AddField(
model_name="calendar",
name="restricted",
field=models.BooleanField(default=False),
),
]

View File

@ -8,6 +8,7 @@ class Calendar(models.Model):
needs_fallback = models.BooleanField(default=False, editable=True)
has_errors = models.BooleanField(default=False, editable=False)
name = models.CharField(max_length=255, null=True, blank=True)
restricted = models.BooleanField(default=False)
def update(self):
# break circular import
@ -18,5 +19,7 @@ class Calendar(models.Model):
class Event(Shift):
objects = models.Manager()
uuid = models.UUIDField(primary_key=True, editable=False)
calendar = models.ForeignKey(Calendar, on_delete=models.CASCADE)

View File

@ -35,7 +35,7 @@ def metrics(request):
),
(
"fallback_shifts_full",
Shift.with_reg_count()
Shift.objects.with_reg_count()
.annotate(
real_required_helpers=Case(
When(
@ -46,7 +46,6 @@ def metrics(request):
fallbackassignment_count=Count("fallbackassignment"),
)
.filter(
deleted=False,
reg_count__gte=F("real_required_helpers"),
fallbackassignment_count__gt=0,
)
@ -96,16 +95,14 @@ def metrics(request):
),
(
"helpers_required",
Shift.objects.filter(deleted=False)
.annotate(
Shift.objects.annotate(
real_required_helpers=Case(
When(
required_helpers=0, then=F("room__required_helpers")
),
default=F("required_helpers"),
)
)
.aggregate(sum=Sum("real_required_helpers"))["sum"]
).aggregate(sum=Sum("real_required_helpers"))["sum"]
or 0,
),
(
@ -124,15 +121,15 @@ def metrics(request):
*(
(
f'shifts{{room="{room.name}"}}',
Shift.objects.filter(deleted=False, room=room).count(),
Shift.objects.filter(room=room).count(),
)
for room in Room.objects.all()
),
*(
(
f'shifts_occupied{{room="{room.name}"}}',
Shift.with_reg_count()
.filter(deleted=False, reg_count__gte=1, room=room)
Shift.objects.with_reg_count()
.filter(reg_count__gte=1, room=room)
.count(),
)
for room in Room.objects.all()
@ -140,7 +137,7 @@ def metrics(request):
*(
(
f'shifts_full{{room="{room.name}"}}',
Shift.with_reg_count()
Shift.objects.with_reg_count()
.annotate(
real_required_helpers=Case(
When(
@ -151,7 +148,7 @@ def metrics(request):
)
)
.filter(
deleted=False,
reg_count__gt=0,
reg_count__gte=F("real_required_helpers"),
room=room,
)

View File

@ -33,16 +33,16 @@ def public_dashboard(request):
)
num_free_shifts = (
Shift.with_reg_count()
.filter(help_wanted, deleted=False, start_at__gte=timezone.now())
Shift.objects.with_reg_count()
.filter(help_wanted, start_at__gte=timezone.now())
.count()
)
if num_free_shifts > 0:
facts.append(("Zu übernehmende Schichten", num_free_shifts))
next_free_shifts = (
Shift.with_reg_count()
.filter(help_wanted, start_at__gt=timezone.now(), deleted=False)
Shift.objects.with_reg_count()
.filter(help_wanted, start_at__gt=timezone.now())
.order_by("start_at")[:4]
)
@ -79,7 +79,7 @@ def team_dashboard(request):
day = today
team_shifts = (
Shift.with_reg_count()
Shift.all_objects.with_reg_count()
.annotate(
end_at=ExpressionWrapper(
F("start_at") + F("duration"), output_field=models.DateTimeField()

View File

@ -21,7 +21,7 @@
<td>
<a href="{% url 'team:helper' reg.helper.pk %}">
{{ reg.helper.name }} ({{ reg.helper.phone|stringformat:"s"|slice:"-3:" }})
{% if not reg.helper.asta_confirmed %}
{% if asta_required and not reg.helper.asta_confirmed %}
<span class="tag is-danger">AStA</span>
{% endif %}
</a>

View File

@ -11,6 +11,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.generic import DetailView, ListView
from django.views.generic.edit import FormMixin
from dynamic_preferences.registries import global_preferences_registry
from .forms import HELPER_FILTERS, BulkMessage, HelperMessage, HelperShift
from .models import (
@ -23,7 +24,7 @@ from .models import (
ShiftRegistration,
)
# Create your views here.
global_preferences = global_preferences_registry.manager()
def index(request):
@ -43,7 +44,7 @@ def shift_overview(request):
context = {}
context["running_shifts"] = (
Shift.with_reg_count()
Shift.all_objects.with_reg_count()
.prefetch_related("event__calendar")
.annotate(
checkin_count=Count(
@ -64,7 +65,7 @@ def shift_overview(request):
)
context["next_shifts"] = (
Shift.with_reg_count()
Shift.all_objects.with_reg_count()
.prefetch_related("event__calendar")
.annotate(checkin_count=checkin_count)
.filter(
@ -79,7 +80,7 @@ def shift_overview(request):
context["next_shifts_per_room"] = filter(
lambda x: x is not None,
(
Shift.with_reg_count()
Shift.all_objects.with_reg_count()
.prefetch_related("event__calendar")
.filter(room=room, start_at__gt=timezone.now(), deleted=False)
.order_by("start_at")
@ -98,7 +99,10 @@ def add_helper_shift(self):
@login_required
def shift_detail(request, pk):
shift = get_object_or_404(
Shift.with_reg_count().prefetch_related("shiftregistration_set__helper"), pk=pk
Shift.all_objects.with_reg_count().prefetch_related(
"shiftregistration_set__helper"
),
pk=pk,
)
form = HelperShift()
if request.method == "POST":
@ -216,7 +220,7 @@ class ShiftList(LoginRequiredMixin, ListView):
title = "Alle Schichten"
def get_queryset(self):
return Shift.with_reg_count()
return Shift.all_objects.with_reg_count()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -235,7 +239,7 @@ class FreeShiftList(ShiftList):
required_helpers=0
) & Q(room__required_helpers__gt=F("reg_count"))
return (
Shift.with_reg_count()
Shift.all_objects.with_reg_count()
.annotate(
end_at=ExpressionWrapper(
F("start_at") + F("duration"),
@ -264,7 +268,7 @@ class RoomShiftList(ShiftList):
required_helpers=0
) & Q(room__required_helpers__gt=F("reg_count"))
return (
Shift.with_reg_count()
Shift.all_objects.with_reg_count()
.filter(
deleted=False,
room=room,
@ -288,6 +292,7 @@ class CheckinList(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["asta_required"] = global_preferences["helper__enable_asta"]
context["page_range"] = context["paginator"].get_elided_page_range(
context["page_obj"].number
)

View File

@ -7,6 +7,11 @@ Die Schichten für das Kontaktfestival sind online: https://helfen.kntkt.de
Ignoriere diese SMS, um keine weiteren von uns zu erhalten.
```
```
Die Schichten für das Kontaktfestival sind online: https://helfen.kntkt.de
Du erhältst diese SMS, da du als Helfi registriert bist.
```
## Teamschichten
```