diff --git a/src/onegov/org/cronjobs.py b/src/onegov/org/cronjobs.py index b06ae75920..dce6ece90d 100644 --- a/src/onegov/org/cronjobs.py +++ b/src/onegov/org/cronjobs.py @@ -542,10 +542,11 @@ def send_monthly_ticket_statistics(request: OrgRequest) -> None: ) -@OrgApp.cronjob(hour=6, minute=5, timezone='Europe/Zurich') +@OrgApp.cronjob(hour='*', minute='*/5', timezone='Europe/Zurich') def send_daily_resource_usage_overview(request: OrgRequest) -> None: today = to_timezone(utcnow(), 'Europe/Zurich') weekday = WEEKDAYS[today.weekday()] + current_time = f'{today.hour:02d}:{today.minute:02d}' # get all recipients which require an e-mail today recipients_q = ( @@ -561,11 +562,13 @@ def send_daily_resource_usage_overview(request: OrgRequest) -> None: # If the key 'daily_reservations' doesn't exist, the recipient was # created before anything else was an option, therefore it must be true + # Legacy recipients without 'daily_reservations_times' default to 06:00. recipients = [ (address, content['resources']) for address, content in recipients_q if content.get('daily_reservations', True) and weekday in content['send_on'] + and current_time in content.get('daily_reservations_times', ['06:00']) ] if not recipients: diff --git a/src/onegov/org/forms/resource_recipient.py b/src/onegov/org/forms/resource_recipient.py index 8c41e78444..f51959a823 100644 --- a/src/onegov/org/forms/resource_recipient.py +++ b/src/onegov/org/forms/resource_recipient.py @@ -1,7 +1,9 @@ from __future__ import annotations +import re + from onegov.form import Form -from onegov.form.fields import MultiCheckboxField +from onegov.form.fields import MultiCheckboxField, TagsField from onegov.org import _ from onegov.reservation import Resource, ResourceCollection from wtforms.fields import EmailField @@ -15,6 +17,8 @@ from onegov.org.request import OrgRequest +_TIME_RE = re.compile(r'^([01]\d|2[0-3]):([0-5]\d)$') + WEEKDAYS = ( ('MO', _('Mo')), ('TU', _('Tu')), @@ -56,8 +60,8 @@ class ResourceRecipientForm(Form): label=_('Daily Reservations'), fieldset=_('Notifications *'), description=_("On each day selected below, a notification with the " - "day's reservations will be sent to the recipient above " - "at 06:00."), + "day's reservations will be sent to the recipient " + "above."), ) customer_messages = BooleanField( @@ -93,6 +97,13 @@ class ResourceRecipientForm(Form): render_kw={'prefix_label': False, 'class_': 'oneline-checkboxes'} ) + daily_reservations_times = TagsField( + label=_('Delivery Times'), + fieldset='Tage und Ressourcen', + description=_('e.g. 07:05'), + depends_on=('daily_reservations', 'y'), + ) + resources = MultiCheckboxField( label=_('Resources'), fieldset='Tage und Ressourcen', @@ -100,6 +111,33 @@ class ResourceRecipientForm(Form): choices=None ) + def ensure_valid_delivery_times(self) -> bool | None: + if not self.daily_reservations.data: + return None + + times = self.daily_reservations_times.data + if not times: + self.daily_reservations_times.errors.append( # type: ignore[attr-defined] + _('Please enter at least one delivery time.') + ) + return False + + for t in times: + m = _TIME_RE.match(t) + if not m: + self.daily_reservations_times.errors.append( # type: ignore[attr-defined] + _('Invalid time "${time}". Use HH:MM format (e.g. 06:00).', + mapping={'time': t}) + ) + return False + if int(m.group(2)) % 5 != 0: + self.daily_reservations_times.errors.append( # type: ignore[attr-defined] + _('Minutes must be a multiple of 5 (e.g. 06:10, 16:35).') + ) + return False + + return None + def ensure_at_least_one_notification(self) -> bool | None: if not ( self.new_reservations.data @@ -113,6 +151,9 @@ def ensure_at_least_one_notification(self) -> bool | None: return None def on_request(self) -> None: + if not self.request.POST and not self.daily_reservations_times.data: + self.daily_reservations_times.data = ['06:00'] # legacy default + default_group = self.request.translate(_('General')) self.resources.choices = [ diff --git a/src/onegov/org/locale/de_CH/LC_MESSAGES/onegov.org.po b/src/onegov/org/locale/de_CH/LC_MESSAGES/onegov.org.po index d40d5c5b06..7fad1dbb38 100644 --- a/src/onegov/org/locale/de_CH/LC_MESSAGES/onegov.org.po +++ b/src/onegov/org/locale/de_CH/LC_MESSAGES/onegov.org.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2026-06-11 11:12+0200\n" +"POT-Creation-Date: 2026-06-19 09:20+0200\n" "PO-Revision-Date: 2022-03-15 10:21+0100\n" "Last-Translator: Marc Sommerhalder \n" "Language-Team: German\n" @@ -1975,11 +1975,10 @@ msgstr "Tägliche Reservationen" msgid "" "On each day selected below, a notification with the day's reservations will " -"be sent to the recipient above at 06:00." +"be sent to the recipient above." msgstr "" "An jedem unten ausgewählten Tag wird eine Benachrichtigung mit den " -"Reservierungen des Tages um 06:00 Uhr an den oben genannten Empfänger " -"gesendet." +"Reservierungen des Tages an den oben genannten Empfänger gesendet." msgid "Customer Messages" msgstr "Kunden Nachrichten" @@ -1992,9 +1991,6 @@ msgstr "" "Reservierung hinzufügt, wird eine eine Benachrichtigung an den oben " "genannten Empfänger gesendet." -msgid "Internal Comments" -msgstr "Internes Kommentarfeld" - msgid "Internal Notes" msgstr "Interne Notizen" @@ -2019,6 +2015,26 @@ msgstr "" msgid "Send on" msgstr "Senden am" +msgid "Delivery Times" +msgstr "Zustellungszeiten" + +msgid "e.g. 07:05" +msgstr "z.B. 07:05" + +#. type: ignore[attr-defined] +msgid "Please enter at least one delivery time." +msgstr "Bitte mindestens eine Zustellungszeit eingeben." + +#. type: ignore[attr-defined] +#, python-format +msgid "Invalid time \"${time}\". Use HH:MM format (e.g. 06:00)." +msgstr "" +"Ungültige Zeit \"${time}\". Bitte im Format HH:MM eingeben (z.B. 06:00)." + +#. type: ignore[attr-defined] +msgid "Minutes must be a multiple of 5 (e.g. 06:10, 16:35)." +msgstr "Minuten müssen ein Vielfaches von 5 sein (z.B. 06:10, 16:35)." + msgid "Please add at least one notification." msgstr "Bitte wählen sie mindestens eine Benachrichtigung." @@ -3735,9 +3751,6 @@ msgstr "Der Eintrag wurde übernommen" msgid "The entry is not valid, please adjust it" msgstr "Der Eintrag ist nicht gültig, bitte korrigieren" -msgid "An entry with this name already exists" -msgstr "Ein Eintrag mit diesem Namen existiert bereits" - msgid "Your directory submission has been adopted" msgstr "Ihr Verzeichniseintrag wurde übernommen" @@ -3907,6 +3920,12 @@ msgstr "" msgid "Delete content" msgstr "Inhalt löschen" +msgid "Internal Comments" +msgstr "Internes Kommentarfeld" + +msgid "Administrative" +msgstr "Administrativ" + msgid "Photo album" msgstr "Fotoalbum" @@ -6523,9 +6542,6 @@ msgstr "Kunden-Login bestätigen" msgid "A link was added to the clipboard" msgstr "Ein Verweis wurde in die Zwischenablage kopiert" -msgid "Administrative" -msgstr "Administrativ" - msgid "Added a new directory" msgstr "Ein neues Verzeichnis wurde hinzugefügt" @@ -8029,6 +8045,15 @@ msgstr "Der Benutzer wurde erfolgreich erstellt" msgid "Please enter your e-mail address in order to continue" msgstr "Bitte geben Sie ihre E-Mail Adresse ein um fortzufahren" +#~ msgid "e.g. 07:00 or 07:00, 14:30" +#~ msgstr "z.B. 07:00 oder 07:00, 14:30" + +#~ msgid "Minutes must be a multiple of 5 (e.g. 06:00, 06:30)." +#~ msgstr "Minuten müssen ein Vielfaches von 5 sein (z.B. 06:00, 06:30)." + +#~ msgid "An entry with this name already exists" +#~ msgstr "Ein Eintrag mit diesem Namen existiert bereits" + #~ msgid "Back" #~ msgstr "Zurück" diff --git a/src/onegov/org/locale/fr_CH/LC_MESSAGES/onegov.org.po b/src/onegov/org/locale/fr_CH/LC_MESSAGES/onegov.org.po index d40e651833..d11a2eb8c8 100644 --- a/src/onegov/org/locale/fr_CH/LC_MESSAGES/onegov.org.po +++ b/src/onegov/org/locale/fr_CH/LC_MESSAGES/onegov.org.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2026-06-11 11:12+0200\n" +"POT-Creation-Date: 2026-06-19 09:20+0200\n" "PO-Revision-Date: 2022-03-15 10:50+0100\n" "Last-Translator: Marc Sommerhalder \n" "Language-Team: French\n" @@ -1979,10 +1979,10 @@ msgstr "Réservations quotidiennes" msgid "" "On each day selected below, a notification with the day's reservations will " -"be sent to the recipient above at 06:00." +"be sent to the recipient above." msgstr "" "Pour chaque jour sélectionné ci-dessous, une notification avec les " -"réservations du jour sera envoyée au destinataire ci-dessus à 6h00.." +"réservations du jour sera envoyée au destinataire ci-dessus." msgid "Customer Messages" msgstr "Messages des clients" @@ -1994,9 +1994,6 @@ msgstr "" "Chaque fois qu'un client ajoute un message au ticket pour une réservation, " "une notification est envoyée au destinataire ci-dessus." -msgid "Internal Comments" -msgstr "Commentaires internes" - msgid "Internal Notes" msgstr "Notes internes" @@ -2020,6 +2017,25 @@ msgstr "" msgid "Send on" msgstr "Envoyer sur" +msgid "Delivery Times" +msgstr "Heures d'envoi" + +msgid "e.g. 07:05" +msgstr "p.ex. 07:05" + +#. type: ignore[attr-defined] +msgid "Please enter at least one delivery time." +msgstr "Veuillez saisir au moins une heure d'envoi." + +#. type: ignore[attr-defined] +#, python-format +msgid "Invalid time \"${time}\". Use HH:MM format (e.g. 06:00)." +msgstr "Heure invalide \"${time}\". Utiliser le format HH:MM (p.ex. 06:00)." + +#. type: ignore[attr-defined] +msgid "Minutes must be a multiple of 5 (e.g. 06:10, 16:35)." +msgstr "Les minutes doivent être un multiple de 5 (p.ex. 06:10, 16:35)." + msgid "Please add at least one notification." msgstr "Veuillez ajouter au moins une notification." @@ -3743,9 +3759,6 @@ msgstr "La proposition a été adoptée" msgid "The entry is not valid, please adjust it" msgstr "L'entrée n'est pas valide, veuillez la rectifier" -msgid "An entry with this name already exists" -msgstr "Une entrée du même nom existe déjà" - msgid "Your directory submission has been adopted" msgstr "Votre proposition de répertoire a été adoptée" @@ -3915,6 +3928,12 @@ msgstr "" msgid "Delete content" msgstr "Supprimer le contenu" +msgid "Internal Comments" +msgstr "Commentaires internes" + +msgid "Administrative" +msgstr "Administratif" + msgid "Photo album" msgstr "Album photo" @@ -6538,9 +6557,6 @@ msgstr "Confirmer la connexion du client" msgid "A link was added to the clipboard" msgstr "Un lien a été ajouté au presse-papiers" -msgid "Administrative" -msgstr "Administratif" - msgid "Added a new directory" msgstr "Ajout d'un nouveau dossier" @@ -8040,6 +8056,15 @@ msgstr "L'utilisateur a bien été créé" msgid "Please enter your e-mail address in order to continue" msgstr "Veuillez saisir votre adresse e-mail pour continuer" +#~ msgid "e.g. 07:00 or 07:00, 14:30" +#~ msgstr "p.ex. 07:00 ou 07:00, 14:30" + +#~ msgid "Minutes must be a multiple of 5 (e.g. 06:00, 06:30)." +#~ msgstr "Les minutes doivent être un multiple de 5 (p.ex. 06:00, 06:30)." + +#~ msgid "An entry with this name already exists" +#~ msgstr "Une entrée du même nom existe déjà" + #~ msgid "Back" #~ msgstr "Retour" diff --git a/src/onegov/org/locale/it_CH/LC_MESSAGES/onegov.org.po b/src/onegov/org/locale/it_CH/LC_MESSAGES/onegov.org.po index b387a4c737..a9520a005c 100644 --- a/src/onegov/org/locale/it_CH/LC_MESSAGES/onegov.org.po +++ b/src/onegov/org/locale/it_CH/LC_MESSAGES/onegov.org.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" -"POT-Creation-Date: 2026-06-11 11:12+0200\n" +"POT-Creation-Date: 2026-06-19 09:20+0200\n" "PO-Revision-Date: 2022-03-15 10:52+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -1979,10 +1979,10 @@ msgstr "Prenotazioni giornaliere" msgid "" "On each day selected below, a notification with the day's reservations will " -"be sent to the recipient above at 06:00." +"be sent to the recipient above." msgstr "" -"Alle ore 6:00 di ogni giorno selezionato qui sotto, al suddetto destinatario " -"verrà inviata una notifica con le prenotazioni del giorno." +"Per ogni giorno selezionato qui sotto, al suddetto destinatario verrà " +"inviata una notifica con le prenotazioni del giorno." msgid "Customer Messages" msgstr "Messaggi dei clienti" @@ -1994,9 +1994,6 @@ msgstr "" "Ogni volta che un cliente aggiunge un messaggio al ticket di una " "prenotazione, viene inviata una notifica al destinatario sopra indicato." -msgid "Internal Comments" -msgstr "Commenti interni" - msgid "Internal Notes" msgstr "Note interne" @@ -2020,6 +2017,25 @@ msgstr "" msgid "Send on" msgstr "Invia il" +msgid "Delivery Times" +msgstr "Orari di invio" + +msgid "e.g. 07:05" +msgstr "es. 07:05" + +#. type: ignore[attr-defined] +msgid "Please enter at least one delivery time." +msgstr "Inserire almeno un orario di invio." + +#. type: ignore[attr-defined] +#, python-format +msgid "Invalid time \"${time}\". Use HH:MM format (e.g. 06:00)." +msgstr "Orario non valido \"${time}\". Usare il formato HH:MM (es. 06:00)." + +#. type: ignore[attr-defined] +msgid "Minutes must be a multiple of 5 (e.g. 06:10, 16:35)." +msgstr "I minuti devono essere un multiplo di 5 (es. 06:10, 16:35)." + msgid "Please add at least one notification." msgstr "Aggiungere almeno una notifica." @@ -3737,9 +3753,6 @@ msgstr "L'iscrizione è stata adottata" msgid "The entry is not valid, please adjust it" msgstr "L'elemento non è valido, modificalo" -msgid "An entry with this name already exists" -msgstr "Esiste già un elemento con questo nome" - msgid "Your directory submission has been adopted" msgstr "L'invio della cartella è stato adottato" @@ -3910,6 +3923,12 @@ msgstr "" msgid "Delete content" msgstr "Cancellare il contenuto" +msgid "Internal Comments" +msgstr "Commenti interni" + +msgid "Administrative" +msgstr "Administrativo" + msgid "Photo album" msgstr "Album fotografico" @@ -6504,9 +6523,6 @@ msgstr "Confermare l'accesso del cliente" msgid "A link was added to the clipboard" msgstr "È stato aggiunto un collegamento agli appunti" -msgid "Administrative" -msgstr "Administrativo" - msgid "Added a new directory" msgstr "Aggiunta una nuova cartella" @@ -7999,6 +8015,15 @@ msgstr "Utente creato correttamente" msgid "Please enter your e-mail address in order to continue" msgstr "Inserisci il tuo indirizzo e-mail per continuare" +#~ msgid "e.g. 07:00 or 07:00, 14:30" +#~ msgstr "es. 07:00 o 07:00, 14:30" + +#~ msgid "Minutes must be a multiple of 5 (e.g. 06:00, 06:30)." +#~ msgstr "I minuti devono essere un multiplo di 5 (es. 06:00, 06:30)." + +#~ msgid "An entry with this name already exists" +#~ msgstr "Esiste già un elemento con questo nome" + #~ msgid "Back" #~ msgstr "Indietro" diff --git a/src/onegov/org/models/recipient.py b/src/onegov/org/models/recipient.py index dc500b3910..e7fb89d506 100644 --- a/src/onegov/org/models/recipient.py +++ b/src/onegov/org/models/recipient.py @@ -13,6 +13,8 @@ class ResourceRecipient(GenericRecipient): __mapper_args__ = {'polymorphic_identity': 'resource'} daily_reservations: dict_property[bool | None] = content_property() + daily_reservations_times: dict_property[list[str] | None] = ( + content_property()) new_reservations: dict_property[bool | None] = content_property() customer_messages: dict_property[bool | None] = content_property() internal_notes: dict_property[bool | None] = content_property() diff --git a/src/onegov/org/views/resource_recipient.py b/src/onegov/org/views/resource_recipient.py index baad7b7366..e4eddd2788 100644 --- a/src/onegov/org/views/resource_recipient.py +++ b/src/onegov/org/views/resource_recipient.py @@ -89,9 +89,11 @@ def handle_new_resource_recipient( medium='email', address=form.address.data, daily_reservations=form.daily_reservations.data, + daily_reservations_times=form.daily_reservations_times.data, new_reservations=form.new_reservations.data, customer_messages=form.customer_messages.data, internal_notes=form.internal_notes.data, + rejected_reservations=form.rejected_reservations.data, send_on=form.send_on.data, resources=form.resources.data, ) @@ -136,6 +138,8 @@ def handle_edit_resource_recipient( ) elif not request.POST: form.process(obj=self) + if not form.daily_reservations_times.data: + form.daily_reservations_times.data = ['06:00'] title = _('Edit Recipient') diff --git a/tests/onegov/org/test_cronjobs.py b/tests/onegov/org/test_cronjobs.py index 2db51403ce..8647b86eaa 100644 --- a/tests/onegov/org/test_cronjobs.py +++ b/tests/onegov/org/test_cronjobs.py @@ -543,6 +543,7 @@ def test_daily_reservation_overview(client: Client[TestOrgApp]) -> None: medium='email', address='gym@example.org', daily_reservations=True, + daily_reservations_times=['06:00'], send_on=['FR'], resources=[ gymnasium.id.hex @@ -553,6 +554,7 @@ def test_daily_reservation_overview(client: Client[TestOrgApp]) -> None: medium='email', address='day@example.org', daily_reservations=True, + daily_reservations_times=['06:00'], send_on=['FR'], resources=[ dailypass.id.hex @@ -563,6 +565,7 @@ def test_daily_reservation_overview(client: Client[TestOrgApp]) -> None: medium='email', address='both@example.org', daily_reservations=True, + daily_reservations_times=['06:00'], send_on=['SA'], resources=[ dailypass.id.hex, @@ -577,22 +580,25 @@ def test_daily_reservation_overview(client: Client[TestOrgApp]) -> None: job.app = client.app url = get_cronjob_url(job) - tz = ensure_timezone('Europe/Zurich') + # Naive UTC (avoids pytz LMT bug); tick=True prevents email filename + # collisions. January Zurich = CET (UTC+1), so 06:00 CET = 05:00 UTC. + fri_0600 = datetime(2017, 1, 6, 5, 0) # Friday 06:00 Zurich + sat_0600 = datetime(2017, 1, 7, 5, 0) # Saturday 06:00 Zurich # do not send an e-mail outside the selected days for day in [2, 3, 4, 5, 8]: - with freeze_time(datetime(2017, 1, day, tzinfo=tz), tick=True): + with freeze_time(datetime(2017, 1, day, 5, 0), tick=True): client.get(url) assert len(os.listdir(client.app.maildir)) == 0 # only send e-mails to the users with the right selection - with freeze_time(datetime(2017, 1, 6, tzinfo=tz), tick=True): + with freeze_time(fri_0600, tick=True): client.get(url) assert len(os.listdir(client.app.maildir)) == 2 - with freeze_time(datetime(2017, 1, 7, tzinfo=tz), tick=True): + with freeze_time(sat_0600, tick=True): client.get(url) assert len(os.listdir(client.app.maildir)) == 3 @@ -601,7 +607,7 @@ def test_daily_reservation_overview(client: Client[TestOrgApp]) -> None: # e-mail will not contain any information info client.flush_email_queue() - with freeze_time(datetime(2017, 1, 6, tzinfo=tz), tick=True): + with freeze_time(fri_0600, tick=True): client.get(url) mails = [client.get_email(i) for i in range(2)] @@ -636,10 +642,10 @@ def test_daily_reservation_overview(client: Client[TestOrgApp]) -> None: transaction.commit() - with freeze_time(datetime(2017, 1, 6, tzinfo=tz), tick=True): + with freeze_time(fri_0600, tick=True): client.get(url) - with freeze_time(datetime(2017, 1, 7, tzinfo=tz), tick=True): + with freeze_time(sat_0600, tick=True): client.get(url) # NOTE: These seem to not always get sent in the same order... @@ -665,7 +671,7 @@ def test_daily_reservation_overview(client: Client[TestOrgApp]) -> None: transaction.commit() - with freeze_time(datetime(2017, 1, 6, tzinfo=tz), tick=True): + with freeze_time(fri_0600, tick=True): client.get(url) # NOTE: These seem to not always get sent in the same order... @@ -683,6 +689,72 @@ def test_daily_reservation_overview(client: Client[TestOrgApp]) -> None: assert 'day-reservation' in text +def test_daily_reservation_overview_delivery_times( + client: Client[TestOrgApp], +) -> None: + """Emails are sent only at configured HH:MM times; multiple times work.""" + resources = ResourceCollection(client.app.libres_context) + room = resources.add('Room', 'Europe/Zurich', type='room') + + recipients = ResourceRecipientCollection(client.app.session()) + # recipient with two delivery times: 06:00 and 14:30 Zurich CET + recipients.add( + name='Multi', + medium='email', + address='multi@example.org', + daily_reservations=True, + daily_reservations_times=['06:00', '14:30'], + send_on=['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'], + resources=[room.id.hex] + ) + # legacy recipient without daily_reservations_times (defaults to 06:00) + recipients.add( + name='Legacy', + medium='email', + address='legacy@example.org', + daily_reservations=True, + send_on=['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'], + resources=[room.id.hex] + ) + + transaction.commit() + + job = get_cronjob_by_name(client.app, 'daily_resource_usage') + assert job is not None + job.app = client.app + url = get_cronjob_url(job) + + # All times are naive UTC. January Zurich = CET (UTC+1), so -1h. + t_0700_utc = datetime(2017, 1, 6, 6, 0) # 07:00 Zurich — wrong time + t_0600_utc = datetime(2017, 1, 6, 5, 0) # 06:00 Zurich + t_1430_utc = datetime(2017, 1, 6, 13, 30) # 14:30 Zurich + t_1431_utc = datetime(2017, 1, 6, 13, 31) # 14:31 Zurich — between buckets + + # wrong time: no emails + with freeze_time(t_0700_utc, tick=True): + client.get(url) + assert len(os.listdir(client.app.maildir)) == 0 + + # 06:00: multi-time recipient AND legacy recipient both fire + with freeze_time(t_0600_utc, tick=True): + client.get(url) + assert len(os.listdir(client.app.maildir)) == 2 + client.flush_email_queue() + + # 14:30: only the multi-time recipient fires + with freeze_time(t_1430_utc, tick=True): + client.get(url) + assert len(os.listdir(client.app.maildir)) == 1 + assert client.get_email(0)['To'] == 'multi@example.org' + client.flush_email_queue() + + # 14:31: nothing fires (cronjob runs every 5 min, 14:31 is not a bucket) + with freeze_time(t_1431_utc, tick=True): + client.get(url) + assert len(os.listdir(client.app.maildir)) == 0 + + + @pytest.mark.parametrize('secret_content_allowed', [False, True]) def test_send_scheduled_newsletters( client: Client[TestOrgApp], diff --git a/tests/onegov/org/test_views_resource_recipients.py b/tests/onegov/org/test_views_resource_recipients.py new file mode 100644 index 0000000000..1953e55bc2 --- /dev/null +++ b/tests/onegov/org/test_views_resource_recipients.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import transaction + +from onegov.org.models import ResourceRecipientCollection +from onegov.reservation import ResourceCollection + + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from tests.onegov.org.conftest import Client + from tests.shared.client import ExtendedResponse + + +def test_resource_recipient_overview(client: Client) -> None: + resources = ResourceCollection(client.app.libres_context) + gymnasium = resources.add('Gymnasium', 'Europe/Zurich', type='room') + dailypass = resources.add('Dailypass', 'Europe/Zurich', type='daypass') + resources.add('Meeting', 'Europe/Zurich', type='room') + + recipients = ResourceRecipientCollection(client.app.session()) + recipients.add( + name='John', + medium='email', + address='john@example.org', + new_reservations=True, + daily_reservations=True, + send_on=['FR', 'SU'], + resources=[gymnasium.id.hex, dailypass.id.hex] + ) + transaction.commit() + client.login_admin() + + page = client.get('/resource-recipients') + assert "John" in page + assert "john@example.org" in page + assert "Erhält Benachrichtigungen für neue Reservationen." in page + assert "für Reservationen des Tages an folgenden Tagen:" in page + assert "Fr , So" in page + assert "Gymnasium" in page + assert "Dailypass" in page + assert "Meeting" not in page + + +def test_resource_recipient_overview_with_notes_notification( + client: Client, +) -> None: + resources = ResourceCollection(client.app.libres_context) + gymnasium = resources.add('Gymnasium', 'Europe/Zurich', type='room') + dailypass = resources.add('Dailypass', 'Europe/Zurich', type='daypass') + resources.add('Meeting', 'Europe/Zurich', type='room') + + recipients = ResourceRecipientCollection(client.app.session()) + recipients.add( + name='John', + medium='email', + address='john@example.org', + internal_notes=True, + resources=[gymnasium.id.hex, dailypass.id.hex] + ) + transaction.commit() + client.login_admin() + + page = client.get('/resource-recipients') + assert "John" in page + assert "john@example.org" in page + assert "Erhält Benachrichtigungen für interne Notizen" in page + + +def test_resource_recipient_delivery_times(client: Client) -> None: + resources = ResourceCollection(client.app.libres_context) + resources.add('Room', 'Europe/Zurich', type='room') + transaction.commit() + client.login_admin() + + # new form pre-fills 06:00 + page: ExtendedResponse = client.get('/resource-recipients/new-recipient') + assert '06:00' in page + + # create with two delivery times + page.form['name'] = 'User A' + page.form['address'] = 'user@example.org' + page.form['daily_reservations'] = True + page.form['daily_reservations_times'] = '09:00,14:30' + page.form.set('resources', True, index=0) + page = page.form.submit().follow() + assert 'User A' in page + + # both times round-trip to the edit form (not corrupted to e.g. 0,9,:,0,0) + edit_page: ExtendedResponse = client.get('/resource-recipients') + edit_page = edit_page.click('Bearbeiten', index=0) + assert '09:00' in edit_page + assert '14:30' in edit_page + + # update to a single time + edit_page.form['daily_reservations_times'] = '08:00' + edit_page = edit_page.form.submit().follow() + edit_page = client.get('/resource-recipients').click('Bearbeiten', index=0) + assert '08:00' in edit_page + + # invalid: single-digit hour + edit_page.form['daily_reservations_times'] = '7:03' + result = edit_page.form.submit() + assert result.status_int == 200 + assert 'HH:MM' in result + + # invalid: mixed valid/invalid in a list + edit_page.form['daily_reservations_times'] = '06:35,7:05' + result = edit_page.form.submit() + assert result.status_int == 200 + assert 'HH:MM' in result + + # invalid: minutes not a multiple of 5 + edit_page.form['daily_reservations_times'] = '06:35,07:03' + result = edit_page.form.submit() + assert result.status_int == 200 + assert 'Minuten müssen ein Vielfaches von 5 sein' in result + + # empty times are rejected + edit_page = client.get('/resource-recipients').click('Bearbeiten', index=0) + edit_page.form['daily_reservations_times'] = '' + result = edit_page.form.submit() + assert result.status_int == 200 + assert 'Bitte mindestens eine Zustellungszeit eingeben' in result + + # delivery times are not validated when daily_reservations is unchecked + edit_page = client.get('/resource-recipients').click('Bearbeiten', index=0) + edit_page.form['daily_reservations'] = False + edit_page.form['daily_reservations_times'] = 'badtime' + edit_page.form['new_reservations'] = True + edit_page = edit_page.form.submit().follow() + assert 'User A' in edit_page + + +def test_resource_recipient_legacy_delivery_time(client: Client) -> None: + resources = ResourceCollection(client.app.libres_context) + room = resources.add('Room', 'Europe/Zurich', type='room') + + recipients = ResourceRecipientCollection(client.app.session()) + recipients.add( + name='Legacy', + medium='email', + address='user@example.org', + daily_reservations=True, + send_on=['MO'], + resources=[room.id.hex] + ) + transaction.commit() + client.login_admin() + + edit_page: ExtendedResponse = client.get( + '/resource-recipients').click('Bearbeiten', index=0) + assert '06:00' in edit_page diff --git a/tests/onegov/town6/test_views_resource_recipients.py b/tests/onegov/town6/test_views_resource_recipients.py deleted file mode 100644 index 80091f2b41..0000000000 --- a/tests/onegov/town6/test_views_resource_recipients.py +++ /dev/null @@ -1,76 +0,0 @@ -from __future__ import annotations - -import transaction - -from onegov.org.models import ResourceRecipientCollection -from onegov.reservation import ResourceCollection - - -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from .conftest import Client - - -def test_resource_recipient_overview(client: Client) -> None: - resources = ResourceCollection(client.app.libres_context) - gymnasium = resources.add('Gymnasium', 'Europe/Zurich', type='room') - dailypass = resources.add('Dailypass', 'Europe/Zurich', type='daypass') - resources.add('Meeting', 'Europe/Zurich', type='room') - - recipients = ResourceRecipientCollection(client.app.session()) - recipients.add( - name='John', - medium='email', - address='john@example.org', - new_reservations=True, - daily_reservations=True, - send_on=['FR', 'SU'], - resources=[ - gymnasium.id.hex, - dailypass.id.hex - ] - ) - - transaction.commit() - client.login_admin() - - page = client.get('/resource-recipients') - assert "John" in page - assert "john@example.org" in page - assert "Erhält Benachrichtigungen für neue Reservationen." in page - assert "für Reservationen des Tages an folgenden Tagen:" in page - assert "Fr , So" in page - assert "Gymnasium" in page - assert "Dailypass" in page - assert "Meeting" not in page - - -def test_resource_recipient_overview_with_notes_notification( - client: Client -) -> None: - - resources = ResourceCollection(client.app.libres_context) - gymnasium = resources.add('Gymnasium', 'Europe/Zurich', type='room') - dailypass = resources.add('Dailypass', 'Europe/Zurich', type='daypass') - resources.add('Meeting', 'Europe/Zurich', type='room') - - recipients = ResourceRecipientCollection(client.app.session()) - recipients.add( - name='John', - medium='email', - address='john@example.org', - internal_notes=True, - resources=[ - gymnasium.id.hex, - dailypass.id.hex - ] - ) - - transaction.commit() - client.login_admin() - - page = client.get('/resource-recipients') - - assert "John" in page - assert "john@example.org" in page - assert "Erhält Benachrichtigungen für interne Notizen" in page