From ec5d1da4e302fa14112933339d320bc70dd2502b Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 16 May 2025 23:41:00 +0200 Subject: [PATCH 1/4] chore: add requirements-dev.txt --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..66f2167 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +django-debug-toolbar==5.2.0 -- 2.40.1 From b51e8a9a146de85e7e9dca7e6868e1feb7023a60 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 16 May 2025 23:43:17 +0200 Subject: [PATCH 2/4] docs(README.md): add installation of dev dependencies --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eaeff36..d9dd15d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ * make sure python >= 3.12 is installed * `python -m venv env` * `. env/bin/activate` -* `pip install -r requirements.txt` +* `pip install -r requirements.txt -r requirements-dev.txt` * `cp .env.example .env` * `sed -i '/^ENVIRONMENT=/c ENVIRONMENT=development' .env` * `./manage.py migrate` -- 2.40.1 From 48f82d76db3dd4583aadf3cde0e7b77f1c403076 Mon Sep 17 00:00:00 2001 From: xAndy Date: Sat, 17 May 2025 01:05:54 +0200 Subject: [PATCH 3/4] fix room name/location generation on import --- shiftregister/importer/importer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shiftregister/importer/importer.py b/shiftregister/importer/importer.py index 94d663a..301121b 100644 --- a/shiftregister/importer/importer.py +++ b/shiftregister/importer/importer.py @@ -52,11 +52,14 @@ def import_calendar(calendar): room2, required_helpers = tuple(summary.rsplit(maxsplit=1)) room2 = room2.strip() required_helpers = int(required_helpers) - room = f"{room} ({room2})" + if room != room2: + room = f"{room2} ({room})" except ValueError: required_helpers = int(summary) except ValueError: required_helpers = 0 + if room != summary: + room = f"{summary} ({room})" else: try: room, required_helpers = tuple(summary.rsplit(maxsplit=1)) -- 2.40.1 From 040c9d94f9fbb820f022b7cd04cda22ede8e5548 Mon Sep 17 00:00:00 2001 From: xAndy Date: Sat, 17 May 2025 03:01:46 +0200 Subject: [PATCH 4/4] add filters for bulk sending --- shiftregister/team/forms.py | 55 +++++++++++- shiftregister/team/tests.py | 161 +++++++++++++++++++++++++++++++++++- shiftregister/team/views.py | 22 ++--- 3 files changed, 218 insertions(+), 20 deletions(-) diff --git a/shiftregister/team/forms.py b/shiftregister/team/forms.py index 19eebfd..b869aa4 100644 --- a/shiftregister/team/forms.py +++ b/shiftregister/team/forms.py @@ -1,4 +1,7 @@ from django import forms +from django.db.models import Case, Count, ExpressionWrapper, F, When +from django.db.models.fields import DateTimeField, IntegerField +from django.utils import timezone from .models import Helper, ShiftRegistration @@ -17,12 +20,60 @@ class HelperShift(forms.Form): helper = NameField(label="Helfi", queryset=Helper.objects.order_by("name")) +HELPER_FILTERS = { + "all": {"label": "Alle Helfis", "query": lambda base_query: base_query}, + "checked_in": { + "label": "Helfis mit mindestens einem Check-in", + "query": lambda base_query: base_query.annotate( + shift_count=Count( + Case( + When( + shiftregistration__state__in=[ + ShiftRegistration.RegState.CHECKED_IN, + ], + then=1, + ), + output_field=IntegerField(), + ) + ) + ) + .filter(shift_count__gte=1) + .distinct(), + }, + "current": { + "label": "Aktuell aktive Helfis", + "query": lambda base_query: base_query.annotate( + shift_end=ExpressionWrapper( + F("shiftregistration__shift__start_at") + + F("shiftregistration__shift__duration"), + output_field=DateTimeField(), + ) + ) + .filter( + shiftregistration__shift__start_at__lte=timezone.now(), + shiftregistration__state=ShiftRegistration.RegState.CHECKED_IN, + shift_end__gte=timezone.now(), + ) + .distinct(), + }, + "no_shifts": { + "label": "Helfis ohne Schichtanmeldungen", + "query": lambda base_query: base_query.annotate( + reg_count=Count("shiftregistration") + ).filter(reg_count=0), + }, +} + + class BulkMessage(forms.Form): message = forms.CharField( label="Nachricht", widget=forms.Textarea(attrs={"class": "textarea"}) ) - checked_in_only = forms.BooleanField( - label="Nur an Helfis mit mindestens einem Check-in senden", required=False + helper_filter = forms.ChoiceField( + label="Empfänger auswählen", + choices=[(k, v["label"]) for k, v in HELPER_FILTERS.items()], + initial="all", + widget=forms.Select(attrs={"class": "select"}), ) diff --git a/shiftregister/team/tests.py b/shiftregister/team/tests.py index 7ce503c..95c285b 100644 --- a/shiftregister/team/tests.py +++ b/shiftregister/team/tests.py @@ -1,3 +1,160 @@ -from django.test import TestCase +from datetime import timedelta -# Create your tests here. +from django.contrib.auth.models import User +from django.test import Client, TestCase +from django.urls import reverse +from django.utils import timezone + +from shiftregister.app.models import ( + Helper, + LoginToken, + Message, + Room, + Shift, + ShiftRegistration, +) + +from .forms import HELPER_FILTERS + + +class BulkMessageFilterTests(TestCase): + def setUp(self): + # Create a test user for login + self.user = User.objects.create_user(username="testuser", password="testpass") + self.client = Client() + self.client.login(username="testuser", password="testpass") + + # Create test room + self.room = Room.objects.create( + name="Test Room", required_helpers=1, meeting_location="Test Location" + ) + + # Create test helpers + self.helper1 = Helper.objects.create( + phone="+491234567890", name="Helper 1", number_validated=True + ) + self.helper2 = Helper.objects.create( + phone="+491234567891", name="Helper 2", number_validated=True + ) + self.helper3 = Helper.objects.create( + phone="+491234567892", name="Helper 3", number_validated=True + ) + self.helper4 = Helper.objects.create( + phone="+491234567893", + name="Helper 4", + number_validated=False, # This one should never be included + ) + + # Create login tokens for each helper so message text replacement works + LoginToken.objects.create(helper=self.helper1) + LoginToken.objects.create(helper=self.helper2) + LoginToken.objects.create(helper=self.helper3) + LoginToken.objects.create(helper=self.helper4) + + # Create shifts + now = timezone.now() + self.current_shift = Shift.objects.create( + room=self.room, + start_at=now - timedelta(hours=1), + duration=timedelta(hours=3), + required_helpers=2, + ) + self.past_shift = Shift.objects.create( + room=self.room, + start_at=now - timedelta(hours=5), + duration=timedelta(hours=2), + required_helpers=2, + ) + + # Create registrations + # Helper1: Has a checked-in past shift + ShiftRegistration.objects.create( + shift=self.past_shift, + helper=self.helper1, + state=ShiftRegistration.RegState.CHECKED_IN, + ) + + # Helper2: Currently active in a shift + ShiftRegistration.objects.create( + shift=self.current_shift, + helper=self.helper2, + state=ShiftRegistration.RegState.CHECKED_IN, + ) + + # Helper3: Has no shifts + # Helper4: Not validated, should never be included + + def test_all_helpers_filter(self): + """Test that 'all' filter returns all validated helpers""" + base_query = Helper.objects.filter(number_validated=True) + helpers = HELPER_FILTERS["all"]["query"](base_query) + + self.assertEqual(helpers.count(), 3) + self.assertIn(self.helper1, helpers) + self.assertIn(self.helper2, helpers) + self.assertIn(self.helper3, helpers) + self.assertNotIn(self.helper4, helpers) + + def test_checked_in_helpers_filter(self): + """Test that 'checked_in' filter returns only helpers with at least one check-in""" + base_query = Helper.objects.filter(number_validated=True) + helpers = HELPER_FILTERS["checked_in"]["query"](base_query) + + self.assertEqual(helpers.count(), 2) + self.assertIn(self.helper1, helpers) + self.assertIn(self.helper2, helpers) + self.assertNotIn(self.helper3, helpers) + self.assertNotIn(self.helper4, helpers) + + def test_current_helpers_filter(self): + """Test that 'current' filter returns only helpers currently in a shift""" + base_query = Helper.objects.filter(number_validated=True) + helpers = HELPER_FILTERS["current"]["query"](base_query) + + self.assertEqual(helpers.count(), 1) + self.assertNotIn(self.helper1, helpers) # Past shift + self.assertIn(self.helper2, helpers) # Current shift + self.assertNotIn(self.helper3, helpers) # No shifts + self.assertNotIn(self.helper4, helpers) # Not validated + + def test_no_shifts_helpers_filter(self): + """Test that 'no_shifts' filter returns only helpers without any shift registrations""" + base_query = Helper.objects.filter(number_validated=True) + helpers = HELPER_FILTERS["no_shifts"]["query"](base_query) + + self.assertEqual(helpers.count(), 1) + self.assertNotIn(self.helper1, helpers) # Has past shift + self.assertNotIn(self.helper2, helpers) # Has current shift + self.assertIn(self.helper3, helpers) # No shifts + self.assertNotIn(self.helper4, helpers) # Not validated + + def test_bulk_message_view_with_filters(self): + """Test that the bulk message view correctly applies filters""" + test_message = "Test message" + + for filter_key in HELPER_FILTERS.keys(): + response = self.client.post( + reverse("team:bulk_message"), + {"message": test_message, "helper_filter": filter_key}, + ) + + self.assertEqual(response.status_code, 200) + + # Count messages created + if filter_key == "all": + expected_count = 3 + elif filter_key == "checked_in": + expected_count = 2 + elif filter_key == "current": + expected_count = 1 + else: # no_shifts + expected_count = 1 + + self.assertEqual( + Message.objects.filter(text__startswith=test_message).count(), + expected_count, + f"Expected {expected_count} messages for filter '{filter_key}'", + ) + + # Clean up for next iteration + Message.objects.all().delete() diff --git a/shiftregister/team/views.py b/shiftregister/team/views.py index 7856bdd..9ba48a3 100644 --- a/shiftregister/team/views.py +++ b/shiftregister/team/views.py @@ -12,7 +12,7 @@ from django.utils import timezone from django.views.generic import DetailView, ListView from django.views.generic.edit import FormMixin -from .forms import BulkMessage, HelperMessage, HelperShift +from .forms import HELPER_FILTERS, BulkMessage, HelperMessage, HelperShift from .models import ( Helper, IncomingMessage, @@ -135,21 +135,11 @@ def bulk_message(request): if request.method == "POST": form = BulkMessage(request.POST) if form.is_valid(): - helpers = Helper.objects.filter(number_validated=True) - if form.cleaned_data["checked_in_only"]: - helpers = Helper.objects.annotate( - shift_count=Count( - Case( - When( - shiftregistration__state__in=[ - ShiftRegistration.RegState.CHECKED_IN, - ], - then=1, - ), - output_field=models.IntegerField(), - ) - ) - ).filter(number_validated=True, shift_count__gte=1) + base_query = Helper.objects.filter(number_validated=True) + helper_filter = form.cleaned_data["helper_filter"] + + # Get the query function from HELPER_FILTERS and apply it + helpers = HELPER_FILTERS[helper_filter]["query"](base_query) try: outbox = [] -- 2.40.1