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:" %}
{% for status in recurringshiftwatch.staffing_status %}- {{ status }}
{% endfor %}
+ {% for status in recurringshiftwatch.watched_capabilities %}- {{ status }}
{% endfor %}
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"