diff --git a/tapir/shifts/forms.py b/tapir/shifts/forms.py index 59d0c673e..a01abdddd 100644 --- a/tapir/shifts/forms.py +++ b/tapir/shifts/forms.py @@ -765,6 +765,7 @@ class Meta: "weekdays", "shift_template_group", "staffing_status", + "watched_capabilities", ] weekdays = forms.MultipleChoiceField( @@ -788,21 +789,35 @@ class Meta: label=_("ABCD Week"), ) staffing_status = forms.MultipleChoiceField( - required=True, + required=False, choices=get_staffingstatus_choices, label=_("Shift changes you would like to be informed about"), widget=CheckboxSelectMultiple(), disabled=False, ) + watched_capabilities = forms.MultipleChoiceField( + required=False, + choices=SHIFT_USER_CAPABILITY_CHOICES.items(), + label=_("Notify me when these capabilities become available or unavailable"), + widget=CheckboxSelectMultiple(), + disabled=False, + help_text=_( + "Get notified when someone with specific skills registers or unregisters" + ), + ) WEEKDAYS_ERROR = _( "If weekdays or %(shift_template_group)s are selected, " "%(shift_templates)s may not be selected, and vice versa." ) - AT_LEAST_ONE_ERROR = _( + AT_LEAST_ONE_PATTERN_ERROR = _( "At least one of the fields (%(shift_templates)s, weekdays, or %(shift_template_group)s) must be selected." ) + AT_LEAST_ONE_TARGET_ERROR = _( + "At least one of the fields staffing_status or required capabilities must be selected." + ) + def _format_field_names(self): return { "shift_template_group": self.fields["shift_template_group"].label, @@ -813,6 +828,8 @@ def clean(self): cleaned_data = super().clean() shift_templates = cleaned_data.get("shift_templates") weekdays = cleaned_data.get("weekdays") + staffing_status = cleaned_data.get("staffing_status") + watched_capabilities = cleaned_data.get("watched_capabilities") cleaned_data["weekdays"] = list(map(int, weekdays)) shift_template_group = cleaned_data.get("shift_template_group") @@ -823,6 +840,10 @@ def clean(self): if not (shift_templates or weekdays or shift_template_group): raise forms.ValidationError( - self.AT_LEAST_ONE_ERROR % self._format_field_names() + self.AT_LEAST_ONE_PATTERN_ERROR % self._format_field_names() + ) + if not (staffing_status or watched_capabilities): + raise forms.ValidationError( + self.AT_LEAST_ONE_TARGET_ERROR % self._format_field_names() ) return cleaned_data diff --git a/tapir/shifts/management/commands/send_shift_watch_mail.py b/tapir/shifts/management/commands/send_shift_watch_mail.py index 7802c0adc..ee2c6d3de 100644 --- a/tapir/shifts/management/commands/send_shift_watch_mail.py +++ b/tapir/shifts/management/commands/send_shift_watch_mail.py @@ -14,6 +14,52 @@ from tapir.shifts.services.shift_watch_creation_service import ShiftWatchCreator +def check_staffing_status( + shift_watch_data: ShiftWatch, + valid_attendances_count: int, + notification_reasons: list[str], +) -> None: + """Check for staffing status changes and add notifications if needed.""" + if len(shift_watch_data.staffing_status) == 0: + return + + # Determine staffing status + current_status = ShiftWatchCreator.get_staffing_status_if_changed( + number_of_available_slots=shift_watch_data.shift.slots.count(), + valid_attendances=valid_attendances_count, + required_attendances=shift_watch_data.shift.num_required_attendances, + last_status=shift_watch_data.last_staffing_status, + ) + if current_status: + notification_reasons.append(current_status.label) + shift_watch_data.last_staffing_status = current_status + + # General attendance change notifications + if not notification_reasons: + if valid_attendances_count > len(shift_watch_data.last_valid_slot_ids): + notification_reasons.append(StaffingStatusChoices.ATTENDANCE_PLUS.label) + elif valid_attendances_count < len(shift_watch_data.last_valid_slot_ids): + notification_reasons.append(StaffingStatusChoices.ATTENDANCE_MINUS.label) + + +def check_watched_capabilities( + shift_watch_data: ShiftWatch, + this_valid_slot_ids: list, + notification_reasons: list[str], +) -> None: + """Check for watched capability changes and add notifications if needed.""" + if len(shift_watch_data.watched_capabilities) == 0: + return + + capability_notifications = ShiftWatchCreator.get_capability_status_changes( + this_valid_slot_ids=this_valid_slot_ids, + last_valid_slot_ids=shift_watch_data.last_valid_slot_ids, + watched_capabilities=shift_watch_data.watched_capabilities, + ) + if capability_notifications: + notification_reasons.extend(capability_notifications) + + class Command(BaseCommand): help = "Sent to a member when there is a relevant change in shift staffing and the member wants to know about it." @@ -31,36 +77,14 @@ def send_shift_watch_mail_per_user_and_shift(self, shift_watch_data: ShiftWatch) ) valid_attendances_count = len(this_valid_slot_ids) - required_attendances_count = shift_watch_data.shift.num_required_attendances - number_of_available_slots = shift_watch_data.shift.slots.count() - - # Determine staffing status - current_status = ShiftWatchCreator.get_staffing_status_if_changed( - number_of_available_slots=number_of_available_slots, - valid_attendances=valid_attendances_count, - required_attendances=required_attendances_count, - last_status=shift_watch_data.last_staffing_status, - ) - if current_status: - notification_reasons.append(current_status.label) - shift_watch_data.last_staffing_status = current_status - - # Check watched capabilities - capability_notifications = ShiftWatchCreator.get_capability_status_changes( - this_valid_slot_ids=this_valid_slot_ids, - last_valid_slot_ids=shift_watch_data.last_valid_slot_ids, - watched_capabilities=shift_watch_data.watched_capabilities, + + check_staffing_status( + shift_watch_data, valid_attendances_count, notification_reasons ) - notification_reasons.extend(capability_notifications) - # General attendance change notifications - if not notification_reasons: - if valid_attendances_count > len(shift_watch_data.last_valid_slot_ids): - notification_reasons.append(StaffingStatusChoices.ATTENDANCE_PLUS.label) - elif valid_attendances_count < len(shift_watch_data.last_valid_slot_ids): - notification_reasons.append( - StaffingStatusChoices.ATTENDANCE_MINUS.label - ) + check_watched_capabilities( + shift_watch_data, this_valid_slot_ids, notification_reasons + ) for reason in notification_reasons: self.send_shift_watch_mail(shift_watch=shift_watch_data, reason=reason) diff --git a/tapir/shifts/management/commands/send_understaffed_shift_reminder_mail.py b/tapir/shifts/management/commands/send_understaffed_shift_reminder_mail.py index 5a044e0da..51d95ea28 100644 --- a/tapir/shifts/management/commands/send_understaffed_shift_reminder_mail.py +++ b/tapir/shifts/management/commands/send_understaffed_shift_reminder_mail.py @@ -41,5 +41,5 @@ def handle(self, *args, **options): ) if current_status == StaffingStatusChoices.UNDERSTAFFED: SendShiftWatchCommand.send_shift_watch_mail( - shift_watch_data, staffing_status=StaffingStatusChoices.UNDERSTAFFED + shift_watch_data, reason=StaffingStatusChoices.UNDERSTAFFED ) diff --git a/tapir/shifts/migrations/0076_recurringshiftwatch_watched_capabilities.py b/tapir/shifts/migrations/0076_recurringshiftwatch_watched_capabilities.py new file mode 100644 index 000000000..18b30616a --- /dev/null +++ b/tapir/shifts/migrations/0076_recurringshiftwatch_watched_capabilities.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.15 on 2026-06-14 18:50 + +import django.contrib.postgres.fields +import tapir.shifts.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("shifts", "0075_shiftwatch_watched_capabilities"), + ] + + operations = [ + migrations.AddField( + model_name="recurringshiftwatch", + name="watched_capabilities", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=tapir.shifts.models.get_shift_capability_choices, + max_length=128, + ), + blank=True, + default=list, + size=None, + ), + ), + ] diff --git a/tapir/shifts/models.py b/tapir/shifts/models.py index 14e78de3f..cb6270953 100644 --- a/tapir/shifts/models.py +++ b/tapir/shifts/models.py @@ -1262,12 +1262,17 @@ class ShiftWatch(models.Model): def __str__(self): shift_name = self.shift.get_display_name() shift_url = self.shift.get_absolute_url() + + staffing_statuses = ", ".join(status for status in self.staffing_status) + watched_caps = ", ".join(cap for cap in self.watched_capabilities) + return format_html( - '{} is watching {} for changes of {}', + '{} is watching {} for changes of {} (capabilities: {})', self.user.username, shift_url, shift_name, - ", ".join(status for status in self.staffing_status), + staffing_statuses, + watched_caps, ) class Meta: @@ -1307,3 +1312,12 @@ class to generate recurring ShiftWatches from. blank=False, default=get_staffingstatus_defaults, ) + + watched_capabilities = ArrayField( + models.CharField( + max_length=128, choices=get_shift_capability_choices, blank=False + ), + default=list, + blank=True, + null=False, + ) diff --git a/tapir/shifts/services/shift_watch_creation_service.py b/tapir/shifts/services/shift_watch_creation_service.py index b5f92c486..8caed0f4b 100644 --- a/tapir/shifts/services/shift_watch_creation_service.py +++ b/tapir/shifts/services/shift_watch_creation_service.py @@ -13,7 +13,7 @@ class ShiftWatchCreator: @classmethod def get_staffing_status_for_shift( - cls, shift: Shift, last_status: str = None + cls, shift: Shift, last_status: str | None = None ) -> str | None: """ Compute the staffing status for a Shift instance by extracting the required @@ -58,7 +58,7 @@ def calculate_staffing_status( number_of_available_slots: int, valid_attendances: int, required_attendances: int, - last_status: str = None, + last_status: str | None = None, ): """Determine the staffing status based on attendance counts. Returns None if status has not changed.""" if valid_attendances < required_attendances: @@ -81,7 +81,7 @@ def get_staffing_status_if_changed( number_of_available_slots: int, valid_attendances: int, required_attendances: int, - last_status: str = None, + last_status: str | None = None, ) -> None | StaffingStatusChoices: """ Determine if the staffing status has changed. Return **None** if the staffing status has not changed. @@ -135,6 +135,7 @@ def create_shift_watches_for_recurring(cls, recurring: RecurringShiftWatch): user=recurring.user, shift=shift, staffing_status=recurring.staffing_status, + watched_capabilities=recurring.watched_capabilities, last_staffing_status=ShiftWatchCreator.get_initial_staffing_status_for_shift( shift=shift ), diff --git a/tapir/shifts/templates/shifts/shiftwatch_overview.html b/tapir/shifts/templates/shifts/shiftwatch_overview.html index 6cfdf5315..4acead792 100644 --- a/tapir/shifts/templates/shifts/shiftwatch_overview.html +++ b/tapir/shifts/templates/shifts/shiftwatch_overview.html @@ -62,6 +62,7 @@
{% translate "Recurring Shift Watches" %}
{% translate "Observed changes:" %} diff --git a/tapir/shifts/tests/test_CreateWatchRecurringShiftsView.py b/tapir/shifts/tests/test_CreateWatchRecurringShiftsView.py index 31b3bf300..e51445661 100644 --- a/tapir/shifts/tests/test_CreateWatchRecurringShiftsView.py +++ b/tapir/shifts/tests/test_CreateWatchRecurringShiftsView.py @@ -2,7 +2,12 @@ from tapir.accounts.models import TapirUser from tapir.accounts.tests.factories.factories import TapirUserFactory -from tapir.shifts.models import RecurringShiftWatch, StaffingStatusChoices +from tapir.shifts.forms import RecurringShiftWatchForm +from tapir.shifts.models import ( + RecurringShiftWatch, + ShiftUserCapability, + StaffingStatusChoices, +) from tapir.shifts.tests.factories import ShiftTemplateFactory from tapir.utils.tests_utils import TapirFactoryTestBase @@ -50,7 +55,7 @@ def test_createRecurringShiftWatch_weekdayAndShifTemplateGroup_entryCreated( def test_createRecurringShiftWatch_ShiftTemplate_entryCreated(self): form_data = { **self.default_form_data, - "shift_templates": [self.template1.id, self.template2.id], # Use actual IDs + "shift_templates": [self.template1.id, self.template2.id], } response = self.client.post( @@ -131,3 +136,115 @@ def test_createRecurringShiftWatch_memberOfficeAttemptsToCreateForOthers_entryCr self.assertEqual(302, response.status_code) self.assertEqual(RecurringShiftWatch.objects.count(), 1) + + def test_createRecurringShiftWatch_withWatchedCapabilities_entrySaved(self): + form_data = { + **self.default_form_data, + "weekdays": [1, 2], + "shift_template_group": ["A"], + "watched_capabilities": [ + ShiftUserCapability.CASHIER, + ShiftUserCapability.BREAD_DELIVERY, + ], + } + + response = self.client.post( + reverse(self.VIEW_NAME, args=[self.tapir_user.pk]), data=form_data + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual(RecurringShiftWatch.objects.count(), 1) + created_watch = RecurringShiftWatch.objects.first() + + self.assertEqual( + set(created_watch.watched_capabilities), + {ShiftUserCapability.CASHIER, ShiftUserCapability.BREAD_DELIVERY}, + ) + + def test_createRecurringShiftWatch_noCapabilitiesSelected_entryCreated(self): + form_data = { + **self.default_form_data, + "weekdays": [1, 2], + "shift_template_group": ["A"], + "watched_capabilities": [], + } + + response = self.client.post( + reverse(self.VIEW_NAME, args=[self.tapir_user.pk]), data=form_data + ) + + self.assertEqual(response.status_code, 302) + created_watch = RecurringShiftWatch.objects.first() + self.assertEqual(created_watch.watched_capabilities, []) + + def test_createRecurringShiftWatch_invalidCapability_validationError(self): + form_data = { + **self.default_form_data, + "weekdays": [1, 2], + "shift_template_group": ["A"], + "watched_capabilities": ["invalid_capability"], + } + + response = self.client.post( + reverse(self.VIEW_NAME, args=[self.tapir_user.pk]), data=form_data + ) + + form = response.context["form"] + self.assertFalse(form.is_valid()) + self.assertIn("watched_capabilities", form.errors) + + def test_createRecurringShiftWatch_withCapabilitiesAndStaffingStatus_entrySaved( + self, + ): + form_data = { + **self.default_form_data, + "weekdays": [1, 2], + "shift_template_group": ["A"], + "staffing_status": [ + StaffingStatusChoices.UNDERSTAFFED, + StaffingStatusChoices.FULL, + ], + "watched_capabilities": [ + ShiftUserCapability.CASHIER, + ShiftUserCapability.BREAD_DELIVERY, + ], + } + + response = self.client.post( + reverse(self.VIEW_NAME, args=[self.tapir_user.pk]), data=form_data + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual(RecurringShiftWatch.objects.count(), 1) + created_watch = RecurringShiftWatch.objects.first() + + self.assertEqual( + set(created_watch.staffing_status), + {StaffingStatusChoices.UNDERSTAFFED, StaffingStatusChoices.FULL}, + ) + self.assertEqual( + set(created_watch.watched_capabilities), + {ShiftUserCapability.CASHIER, ShiftUserCapability.BREAD_DELIVERY}, + ) + + def test_createRecurringShiftWatch_neitherStaffingStatusNorCapabilities_validationError( + self, + ): + form_data = { + "shift_templates": [], + "weekdays": [1, 2], + "shift_template_group": [], + "staffing_status": [], + "watched_capabilities": [], + } + + response = self.client.post( + reverse(self.VIEW_NAME, args=[self.tapir_user.pk]), data=form_data + ) + + form = response.context["form"] + self.assertFalse(form.is_valid()) + self.assertIn( + RecurringShiftWatchForm.AT_LEAST_ONE_TARGET_ERROR, + form.non_field_errors(), + ) diff --git a/tapir/shifts/tests/test_shiftwatch_creation_service.py b/tapir/shifts/tests/test_shiftwatch_creation_service.py index a1de24280..92570b18d 100644 --- a/tapir/shifts/tests/test_shiftwatch_creation_service.py +++ b/tapir/shifts/tests/test_shiftwatch_creation_service.py @@ -56,6 +56,7 @@ def test_createShiftWatchesForRecurring_existingShiftWatch_skipsExisting(self): user=self.user, weekdays=[self.base_shift.start_time.weekday()], staffing_status=[StaffingStatusChoices.ALL_CLEAR], + watched_capabilities=[], ) ShiftWatchFactory(user=self.user, shift=self.base_shift) @@ -77,6 +78,7 @@ def test_createShiftWatchForShift_shiftWithoutTemplate_getsAccepted(self): user=self.user, weekdays=[shift.start_time.weekday()], staffing_status=[StaffingStatusChoices.UNDERSTAFFED], + watched_capabilities=[], ) ShiftWatchCreator.create_shift_watches_for_shift_based_on_recurring(shift) @@ -91,6 +93,7 @@ def test_createShiftWatchesForRecurring_RecurringWithoutCriteria_createsNoShiftw user=self.user, weekdays=[], staffing_status=[StaffingStatusChoices.ALL_CLEAR], + watched_capabilities=[], ) # Create two shifts which should not be existing after diff --git a/tapir/shifts/tests/test_shiftwatch_notification.py b/tapir/shifts/tests/test_shiftwatch_notification.py index 795fb8866..d3c141a40 100644 --- a/tapir/shifts/tests/test_shiftwatch_notification.py +++ b/tapir/shifts/tests/test_shiftwatch_notification.py @@ -10,6 +10,7 @@ RecurringShiftWatch, ShiftAttendance, ShiftSlot, + ShiftUserCapability, StaffingStatusChoices, get_staffingstatus_choices, ) @@ -30,25 +31,33 @@ def create_shift_with_attendance(num_attendances): slot = ShiftSlot.objects.create(shift=shift, name="cheese-making") user = TapirUserFactory.create() ShiftAttendance.objects.create(user=user, slot=slot) - slots.append(slot.pk) + slots.append(slot) return shift, slots def create_shift_watch( - user, shift, slots, last_staffing_status=None, staffing_status=None + user, + shift, + last_valid_slot_ids, + last_staffing_status=None, + staffing_status=None, + watched_capabilities=None, ): if last_staffing_status is None: last_staffing_status = ShiftWatchCreator.get_initial_staffing_status_for_shift( shift=shift ) if staffing_status is None: - staffing_status = [event.value for event in get_staffingstatus_choices()] + staffing_status = [] + if watched_capabilities is None: + watched_capabilities = [] return ShiftWatchFactory( user=user, shift=shift, - last_valid_slot_ids=slots, + last_valid_slot_ids=[slot.pk for slot in last_valid_slot_ids], staffing_status=staffing_status, last_staffing_status=last_staffing_status, + watched_capabilities=watched_capabilities, ) @@ -62,15 +71,16 @@ def setUp(self): self.NUM_REQUIRED_ATTENDANCE ) - def unregister_first_slot(self): - first_slot = self.slots[0] - first_shift_attendance = ShiftAttendance.objects.filter(slot=first_slot).first() + def unregister_slot(self, slot: ShiftSlot | None = None): + if slot is None: + slot = self.slots[0] + first_shift_attendance = ShiftAttendance.objects.filter(slot=slot).first() first_shift_attendance.state = ShiftAttendance.State.LOOKING_FOR_STAND_IN first_shift_attendance.save() - def assert_email_sent(self, expected_status_choice): + def assert_email_sent(self, expected_status_choice: str): self.assertEqual(len(mail.outbox), 1) - self.assertIn(str(expected_status_choice.label), mail.outbox[0].body) + self.assertIn(str(expected_status_choice), mail.outbox[0].body) self.assertEmailOfClass_GotSentTo( ShiftWatchEmailBuilder, self.USER_EMAIL_ADDRESS, mail.outbox[0] ) @@ -79,22 +89,25 @@ def test_handle_watchedShiftIsUnderstaffed_correctNotificationIsSent(self): self.shift_watch = create_shift_watch( user=self.user, shift=self.shift_ok_first, - slots=self.slots, + last_valid_slot_ids=self.slots, staffing_status=[StaffingStatusChoices.UNDERSTAFFED], + watched_capabilities=[], ) Command().handle() self.assertEqual(0, len(mail.outbox)) - self.unregister_first_slot() + self.unregister_slot() Command().handle() - self.assert_email_sent(StaffingStatusChoices.UNDERSTAFFED) + self.assertEqual(1, len(mail.outbox)) + self.assert_email_sent(StaffingStatusChoices.UNDERSTAFFED.label) def test_handle_watchedShiftIsAlright_noNotificationIsSent(self): self.shift_watch = create_shift_watch( user=self.user, shift=self.shift_ok_first, - slots=self.slots, + last_valid_slot_ids=self.slots, staffing_status=list(get_staffingstatus_choices()), + watched_capabilities=[], ) Command().handle() self.assertEqual(0, len(mail.outbox)) @@ -110,10 +123,12 @@ def test_handle_initialWatchUnderstaffedShift_noInitialMailIsSent(self): create_shift_watch( user=user, shift=shift_understaffed, - slots=slots, + last_valid_slot_ids=slots, last_staffing_status=ShiftWatchCreator.get_initial_staffing_status_for_shift( shift=shift_understaffed ), + staffing_status=list(get_staffingstatus_choices()), + watched_capabilities=[], ) Command().handle() @@ -126,24 +141,25 @@ def test_handle_initialWatchUnderstaffedShift_noInitialMailIsSent(self): Command().handle() - self.assert_email_sent(StaffingStatusChoices.ALL_CLEAR) + self.assert_email_sent(StaffingStatusChoices.ALL_CLEAR.label) def test_handle_triggeredMultipleTimes_onlyOneMailIsSent(self): self.shift_watch = create_shift_watch( user=self.user, shift=self.shift_ok_first, - slots=self.slots, + last_valid_slot_ids=self.slots, staffing_status=[StaffingStatusChoices.UNDERSTAFFED], + watched_capabilities=[], ) - self.unregister_first_slot() + self.unregister_slot() self.assertEqual(len(mail.outbox), 0) for _ in range(3): Command().handle() - self.assert_email_sent(StaffingStatusChoices.UNDERSTAFFED) + self.assert_email_sent(StaffingStatusChoices.UNDERSTAFFED.label) def test_handle_watchedShiftIsCurrentlyRunning_correctNotificationIsSent(self): self.shift_ok_first.start_time = timezone.now() - datetime.timedelta(hours=4) @@ -153,14 +169,15 @@ def test_handle_watchedShiftIsCurrentlyRunning_correctNotificationIsSent(self): self.shift_watch = create_shift_watch( user=self.user, shift=self.shift_ok_first, - slots=self.slots, + last_valid_slot_ids=self.slots, staffing_status=[StaffingStatusChoices.UNDERSTAFFED], + watched_capabilities=[], ) - self.unregister_first_slot() + self.unregister_slot() Command().handle() - self.assert_email_sent(StaffingStatusChoices.UNDERSTAFFED) + self.assert_email_sent(StaffingStatusChoices.UNDERSTAFFED.label) def test_handle_shiftInThePast_noNotification(self): @@ -173,10 +190,12 @@ def test_handle_shiftInThePast_noNotification(self): self.shift_watch = create_shift_watch( user=self.user, shift=self.shift_ok_first, - slots=self.slots, + last_valid_slot_ids=self.slots, + staffing_status=list(get_staffingstatus_choices()), + watched_capabilities=[], ) - self.unregister_first_slot() + self.unregister_slot() Command().handle() @@ -189,6 +208,7 @@ def test_handle_recurring_noInitialMailIsSent(self): user=self.user, weekdays=[self.shift_ok_first.start_time.weekday()], staffing_status=[event.value for event in get_staffingstatus_choices()], + watched_capabilities=[ShiftUserCapability.SHIFT_COORDINATOR], ) ShiftWatchCreator.create_shift_watches_for_recurring(recurring=recurring) @@ -196,3 +216,84 @@ def test_handle_recurring_noInitialMailIsSent(self): Command().handle() self.assertEqual(len(mail.outbox), 0) + + def test_handle_noStaffingStatusSelected_noMailSent(self): + # Only for watched capabilities + + self.shift_watch = create_shift_watch( + user=self.user, + shift=self.shift_ok_first, + last_valid_slot_ids=self.slots, + staffing_status=[], + watched_capabilities=[ShiftUserCapability.SHIFT_COORDINATOR], + ) + + Command().handle() + self.assertEqual(0, len(mail.outbox)) + + slot_to_unregister = self.slots[0] + + # assert that slot to unregister has no required capability, so it should not trigger notification + self.assertNotEqual( + slot_to_unregister.required_capabilities, + [ShiftUserCapability.SHIFT_COORDINATOR], + ) + self.unregister_slot(slot=slot_to_unregister) + Command().handle() + + self.assertEqual(0, len(mail.outbox)) + + def test_handle_watchedCapability_MailSent(self): + slot_to_unregister = self.slots[0] + slot_to_unregister.required_capabilities = [ + ShiftUserCapability.SHIFT_COORDINATOR + ] + slot_to_unregister.save() + self.shift_watch = create_shift_watch( + user=self.user, + shift=self.shift_ok_first, + last_valid_slot_ids=self.slots, + staffing_status=[], + watched_capabilities=[ShiftUserCapability.SHIFT_COORDINATOR], + ) + + Command().handle() + self.assertEqual(0, len(mail.outbox)) + + self.assertEqual( + slot_to_unregister.required_capabilities, + [ShiftUserCapability.SHIFT_COORDINATOR], + ) + self.unregister_slot(slot=slot_to_unregister) + Command().handle() + self.assertEqual(1, len(mail.outbox)) + + def test_handle_watchDifferentCapability_noMailSent(self): + # watch for Shift-Coordinator, but shift has Cashier-capability + slot_to_unregister = self.slots[0] + slot_to_unregister.required_capabilities = [ShiftUserCapability.CASHIER] + self.slots[1].required_capabilities = [ShiftUserCapability.SHIFT_COORDINATOR] + slot_to_unregister.save() + + self.shift_watch = create_shift_watch( + user=self.user, + shift=self.shift_ok_first, + last_valid_slot_ids=self.slots, + staffing_status=[], + watched_capabilities=[ShiftUserCapability.SHIFT_COORDINATOR], + ) + + Command().handle() + self.assertEqual(0, len(mail.outbox)) + + self.assertEqual( + slot_to_unregister.required_capabilities, + [ShiftUserCapability.CASHIER], + ) + self.assertNotEqual( + slot_to_unregister.required_capabilities, + [ShiftUserCapability.SHIFT_COORDINATOR], + ) + self.unregister_slot(slot=slot_to_unregister) + Command().handle() + self.assertEqual(0, len(mail.outbox)) diff --git a/tapir/translations/locale/de/LC_MESSAGES/django.po b/tapir/translations/locale/de/LC_MESSAGES/django.po index 2f2a47232..59d29e9bd 100644 --- a/tapir/translations/locale/de/LC_MESSAGES/django.po +++ b/tapir/translations/locale/de/LC_MESSAGES/django.po @@ -2803,7 +2803,7 @@ msgstr "Hat Qualifikation" msgid "Does not have qualification" msgstr "Hat die Qualifikation nicht" -#: coop/views/shareowner.py:659 shifts/forms.py:788 +#: coop/views/shareowner.py:659 shifts/forms.py:789 msgid "ABCD Week" msgstr "ABCD-Woche" @@ -3322,36 +3322,40 @@ msgstr "" msgid "I understand that this will delete the shift exemption and create a membership pause" msgstr "" -#: shifts/forms.py:725 shifts/forms.py:793 shifts/views/views.py:419 +#: shifts/forms.py:725 shifts/forms.py:794 shifts/views/views.py:419 #: shifts/views/views.py:420 msgid "Shift changes you would like to be informed about" msgstr "Schicht-Änderungen, bei denen du informiert werden möchtest" -#: shifts/forms.py:732 +#: shifts/forms.py:732 shifts/forms.py:801 msgid "Notify me when these capabilities become available or unavailable" msgstr "" -#: shifts/forms.py:736 +#: shifts/forms.py:736 shifts/forms.py:805 msgid "Get notified when someone with specific skills registers or unregisters" msgstr "" -#: shifts/forms.py:782 shifts/templates/shifts/shift_template_detail.html:7 +#: shifts/forms.py:783 shifts/templates/shifts/shift_template_detail.html:7 #: shifts/templates/shifts/shift_template_detail.html:15 #: shifts/templates/shifts/user_shifts_overview_tag.html:31 #: shifts/templates/shifts/user_shifts_overview_tag.html:43 msgid "ABCD Shift" msgstr "ABCD-Schicht" -#: shifts/forms.py:799 +#: shifts/forms.py:810 #, python-format msgid "If weekdays or %(shift_template_group)s are selected, %(shift_templates)s may not be selected, and vice versa." msgstr "" -#: shifts/forms.py:803 +#: shifts/forms.py:814 #, python-format msgid "At least one of the fields (%(shift_templates)s, weekdays, or %(shift_template_group)s) must be selected." msgstr "" +#: shifts/forms.py:818 +msgid "At least one of the fields staffing_status or required capabilities must be selected." +msgstr "" + #: shifts/models.py:39 msgid "Teamleader" msgstr "Teamleiter*in" @@ -4397,7 +4401,7 @@ msgid "Unwatch" msgstr "Nicht mehr beobachten" #: shifts/templates/shifts/shift_detail.html:49 -#: shifts/templates/shifts/shiftwatch_overview.html:123 +#: shifts/templates/shifts/shiftwatch_overview.html:124 msgid "You will get mail-notifications when shifts you follow change — tailored to the types of updates you choose (e.g., needs help, is full, cancellations)." msgstr "Du erhältst E-Mail-Benachrichtigungen, wenn sich Schichten, denen du folgst, ändern – angepasst an die von dir ausgewählten Arten von Aktualisierungen (z. B. Hilfe benötigt, voll, Stornierungen)." @@ -4824,32 +4828,32 @@ msgstr "Du beobachtest keine Schichten" msgid "Observed changes:" msgstr "Beobachtete Änderungen" -#: shifts/templates/shifts/shiftwatch_overview.html:75 +#: shifts/templates/shifts/shiftwatch_overview.html:76 msgid "Shift Watches" msgstr "Schicht-Beobachtungen" -#: shifts/templates/shifts/shiftwatch_overview.html:81 +#: shifts/templates/shifts/shiftwatch_overview.html:82 msgid "Total" msgstr "Insgesamt" -#: shifts/templates/shifts/shiftwatch_overview.html:104 +#: shifts/templates/shifts/shiftwatch_overview.html:105 #: shifts/templates/shifts/user_shifts_overview_tag.html:64 msgid "Show more" msgstr "Mehr anzeigen" -#: shifts/templates/shifts/shiftwatch_overview.html:126 +#: shifts/templates/shifts/shiftwatch_overview.html:127 msgid "You can watch single shifts for changes by selecting a shift " msgstr "" -#: shifts/templates/shifts/shiftwatch_overview.html:127 +#: shifts/templates/shifts/shiftwatch_overview.html:128 msgid "here" msgstr "" -#: shifts/templates/shifts/shiftwatch_overview.html:128 +#: shifts/templates/shifts/shiftwatch_overview.html:129 msgid " and clicking on the Watch-button" msgstr "" -#: shifts/templates/shifts/shiftwatch_overview.html:134 +#: shifts/templates/shifts/shiftwatch_overview.html:135 msgid "Delete Selected" msgstr "Lösche das Ausgewählte"