diff --git a/ami/agent_admin/forms.py b/ami/agent_admin/forms.py index 59b41e279..31282bed3 100644 --- a/ami/agent_admin/forms.py +++ b/ami/agent_admin/forms.py @@ -53,6 +53,10 @@ class NotificationForm(AMIDsfrBaseForm): content_body = forms.CharField( widget=forms.Textarea(attrs={"rows": 4}), ) + content_private_body = forms.CharField( + widget=forms.Textarea(attrs={"rows": 4}), + required=False, + ) content_icon = forms.CharField( required=False, ) diff --git a/ami/agent_admin/tests/manage/test_send_notification.py b/ami/agent_admin/tests/manage/test_send_notification.py index b3fc98c52..587c84ba2 100644 --- a/ami/agent_admin/tests/manage/test_send_notification.py +++ b/ami/agent_admin/tests/manage/test_send_notification.py @@ -16,6 +16,7 @@ def test_send_notification(app, notifications_agent: Agent) -> None: assert response.forms["send-notification"]["recipient_fc_hash"].value == "" assert response.forms["send-notification"]["content_title"].value == "" assert response.forms["send-notification"]["content_body"].value == "" + assert response.forms["send-notification"]["content_private_body"].value == "" assert response.forms["send-notification"]["content_icon"].value == "" assert response.forms["send-notification"]["item_type"].value == "" assert response.forms["send-notification"]["item_id"].value == "" diff --git a/ami/followup/schemas.py b/ami/followup/schemas.py index 7a8cedc19..1366b052d 100644 --- a/ami/followup/schemas.py +++ b/ami/followup/schemas.py @@ -38,6 +38,9 @@ def from_notifications(cls, notifications: list[Notification]) -> Self | None: first_notification = notifications[0] last_notification = notifications[-1] external_urls = [n.item_external_url for n in notifications if n.item_external_url] + description = last_notification.content_body + if last_notification.content_private_body: + description += f"\n\n{last_notification.content_private_body}" try: status_id = ItemGenericStatus(last_notification.item_generic_status) except ValueError: @@ -49,7 +52,7 @@ def from_notifications(cls, notifications: list[Notification]) -> Self | None: milestone_start_date=last_notification.item_milestone_start_date, milestone_end_date=last_notification.item_milestone_end_date, title=last_notification.content_title, - description=last_notification.content_body, + description=description, external_url=external_urls[-1] if external_urls else None, created_at=first_notification.send_date, updated_at=last_notification.send_date, diff --git a/ami/followup/tests/test_notification_data.py b/ami/followup/tests/test_notification_data.py index 224a32563..84f5c5f2f 100644 --- a/ami/followup/tests/test_notification_data.py +++ b/ami/followup/tests/test_notification_data.py @@ -197,9 +197,33 @@ def test_get_notifications_data(user: User) -> None: partner_id="dinum-ami", ) + notification8 = Notification.objects.create( + user_id=user.id, + content_body="other notification", + content_private_body="some private body content", + content_title="Other Notification title", + item_generic_status="closed", + item_status_label="Validé", + item_type="Other", + item_id="52", + partner_id="dinum-ami", + ) + result = get_notifications_data(current_user=user) assert result == [ + FollowUpInventoryItem( + external_id="dinum-ami:Other:52", + status_id=ItemGenericStatus.CLOSED, + status_label="Validé", + milestone_start_date=None, + milestone_end_date=None, + title="Other Notification title", + description="other notification\n\nsome private body content", + external_url=None, + created_at=notification8.send_date, + updated_at=notification8.send_date, + ), FollowUpInventoryItem( external_id="dinum-ami:Other:42", status_id=ItemGenericStatus.CLOSED, diff --git a/ami/notification/migrations/0010_add_content_private_body.py b/ami/notification/migrations/0010_add_content_private_body.py new file mode 100644 index 000000000..4fbb47780 --- /dev/null +++ b/ami/notification/migrations/0010_add_content_private_body.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.5 on 2026-05-19 13:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("notification", "0009_partner_id"), + ] + + operations = [ + migrations.AddField( + model_name="notification", + name="content_private_body", + field=models.CharField(blank=True, null=True), + ), + migrations.AddField( + model_name="schedulednotification", + name="content_private_body", + field=models.CharField(blank=True, null=True), + ), + ] diff --git a/ami/notification/models.py b/ami/notification/models.py index cd50bea68..a3cfe4101 100644 --- a/ami/notification/models.py +++ b/ami/notification/models.py @@ -22,6 +22,7 @@ class Notification(models.Model): content_title = models.CharField() content_body = models.CharField() + content_private_body = models.CharField(blank=True, null=True) content_icon = models.CharField(blank=True, null=True) item_type = models.CharField(blank=True, null=True) @@ -62,6 +63,7 @@ class ScheduledNotification(models.Model): content_title = models.CharField() content_body = models.CharField() + content_private_body = models.CharField(blank=True, null=True) content_icon = models.CharField() internal_url = models.CharField(blank=True, null=True) @@ -79,6 +81,7 @@ def build_notification(self) -> Notification: user=self.user, content_title=self.content_title, content_body=self.content_body, + content_private_body=self.content_private_body, content_icon=self.content_icon, internal_url=self.internal_url, partner_id="dinum-ami", diff --git a/ami/notification/serializers.py b/ami/notification/serializers.py index 141a4816f..8804c3531 100644 --- a/ami/notification/serializers.py +++ b/ami/notification/serializers.py @@ -19,6 +19,7 @@ class NotificationSerializer(serializers.ModelSerializer): class Meta: fields = [ "content_body", + "content_private_body", "content_icon", "content_title", "created_at", @@ -40,6 +41,7 @@ class Meta: class ScheduledNotificationCreateSerializer(serializers.Serializer): content_title = serializers.CharField(allow_blank=False) content_body = serializers.CharField(allow_blank=False) + content_private_body = serializers.CharField(allow_blank=True, default=None) content_icon = serializers.CharField(allow_blank=False) reference = serializers.CharField(allow_blank=False) internal_url = serializers.CharField(allow_blank=False, default=None) @@ -67,6 +69,11 @@ class PartnerNotificationCreateSerializer(serializers.Serializer): allow_blank=False, help_text="Contenu de la notification", ) + content_private_body = serializers.CharField( + allow_blank=True, + default=None, + help_text="Contenu privé de la notification qui ne sera pas 'push'é", + ) content_icon = serializers.CharField( default=None, help_text="Nom technique de l'icône à associer à la notification dans l'application AMI, " diff --git a/ami/notification/tests/api/test_notification.py b/ami/notification/tests/api/test_notification.py index 41dad8a83..84314eaec 100644 --- a/ami/notification/tests/api/test_notification.py +++ b/ami/notification/tests/api/test_notification.py @@ -36,6 +36,7 @@ def test_get_notifications( other_notification = Notification.objects.create( user=notification.user, content_body="Other notification", + content_private_body="some private body content", content_title="Notification title", ) @@ -56,6 +57,7 @@ def test_get_notifications( "user_id": str(other_notification.user.id), "content_title": "Notification title", "content_body": "Other notification", + "content_private_body": "some private body content", "content_icon": None, "item_type": None, "item_id": None, @@ -73,6 +75,7 @@ def test_get_notifications( "user_id": str(notification.user.id), "content_title": "Notification title", "content_body": "Hello notification", + "content_private_body": None, "content_icon": None, "item_type": None, "item_id": None, @@ -141,6 +144,7 @@ def test_read_notification( "user_id": str(notification.user.id), "content_title": "Notification title", "content_body": "Hello notification", + "content_private_body": None, "content_icon": None, "item_type": None, "item_id": None, diff --git a/ami/notification/tests/api/test_partner_notifications.py b/ami/notification/tests/api/test_partner_notifications.py index 35c1d135f..f59ee93bf 100644 --- a/ami/notification/tests/api/test_partner_notifications.py +++ b/ami/notification/tests/api/test_partner_notifications.py @@ -4,6 +4,7 @@ from unittest.mock import Mock import pytest +import webpush as webpush_lib from asgiref.sync import sync_to_async from channels.testing.websocket import WebsocketCommunicator from pytest_httpx import HTTPXMock @@ -23,13 +24,26 @@ async def test_create_webpush_notification( partner_auth: dict[str, str], httpx_mock: HTTPXMock, websocket: WebsocketCommunicator, + monkeypatch: pytest.MonkeyPatch, ) -> None: # Make sure we don't even try sending a notification to a push server. httpx_mock.add_response(url=webpush_registration.subscription["endpoint"]) + + # Capture the plaintext message passed to the webpush provider before encryption. + captured_push_messages: list[str] = [] + original_webpush_get = webpush_lib.WebPush.get + + def capturing_webpush_get(self, message, **kwargs): + captured_push_messages.append(message) + return original_webpush_get(self, message=message, **kwargs) + + monkeypatch.setattr(webpush_lib.WebPush, "get", capturing_webpush_get) + notification_data = { "recipient_fc_hash": webpush_registration.user.fc_hash, "content_title": "Brouillon de nouvelle demande de démarche d'OTV", "content_body": "Merci d'avoir initié votre demande", + "content_private_body": "Ceci est privé et ne devrait jamais être `push`é", "content_icon": "foo", "item_type": "OTV", "item_id": "A-5-JGBJ5VMOY", @@ -54,6 +68,7 @@ async def test_create_webpush_notification( ) assert notification2.user.id == webpush_registration.user.id assert notification2.content_body == "Merci d'avoir initié votre demande" + assert notification2.content_private_body == "Ceci est privé et ne devrait jamais être `push`é" assert notification2.content_title == "Brouillon de nouvelle demande de démarche d'OTV" assert notification2.content_icon == "foo" assert notification2.item_type == "OTV" @@ -85,7 +100,10 @@ async def test_create_webpush_notification( "id": str(notification2.id), "event": "created", } - assert httpx_mock.get_request() + request = httpx_mock.get_request() + assert request is not None + assert len(captured_push_messages) == 1 + assert notification_data["content_private_body"] not in captured_push_messages[0] @pytest.mark.django_db diff --git a/ami/notification/tests/api/test_scheduled_notification.py b/ami/notification/tests/api/test_scheduled_notification.py index c0a424468..409d92bed 100644 --- a/ami/notification/tests/api/test_scheduled_notification.py +++ b/ami/notification/tests/api/test_scheduled_notification.py @@ -17,6 +17,7 @@ def test_create_scheduled_notification(app, user: User) -> None: scheduled_notification_data = { "content_title": "title", "content_body": "body", + "content_private_body": "private content body", "content_icon": "icon", "reference": "reference", "internal_url": "internal-url", @@ -31,6 +32,7 @@ def test_create_scheduled_notification(app, user: User) -> None: assert scheduled_notification.user.id == user.id assert scheduled_notification.content_title == "title" assert scheduled_notification.content_body == "body" + assert scheduled_notification.content_private_body == "private content body" assert scheduled_notification.content_icon == "icon" assert scheduled_notification.reference == "reference" assert scheduled_notification.internal_url == "internal-url" @@ -47,6 +49,7 @@ def test_create_scheduled_notification_known_reference(app, user: User) -> None: user_id=user.id, content_title="title", content_body="body", + content_private_body="private body", content_icon="icon", reference="reference", internal_url="internal-url", @@ -57,6 +60,7 @@ def test_create_scheduled_notification_known_reference(app, user: User) -> None: scheduled_notification_data = { "content_title": "title-updated", "content_body": "body-updated", + "content_private_body": "private-body-updated", "content_icon": "icon-updated", "reference": "reference", "internal_url": "internal-url-updated", @@ -71,6 +75,7 @@ def test_create_scheduled_notification_known_reference(app, user: User) -> None: assert scheduled_notification.user.id == user.id assert scheduled_notification.content_title == "title-updated" assert scheduled_notification.content_body == "body-updated" + assert scheduled_notification.content_private_body == "private-body-updated" assert scheduled_notification.content_icon == "icon-updated" assert scheduled_notification.reference == "reference" assert scheduled_notification.scheduled_at == scheduled_notification_date @@ -85,6 +90,7 @@ def test_create_scheduled_notification_known_reference(app, user: User) -> None: scheduled_notification_data = { "content_title": "title-updated-again", "content_body": "body-updated-again", + "content_private_body": "private-body-updated-again", "content_icon": "icon-updated-again", "reference": "reference", "scheduled_at": scheduled_notification_date2.isoformat(), @@ -98,6 +104,7 @@ def test_create_scheduled_notification_known_reference(app, user: User) -> None: assert scheduled_notification.user.id == user.id assert scheduled_notification.content_title == "title-updated" assert scheduled_notification.content_body == "body-updated" + assert scheduled_notification.content_private_body == "private-body-updated" assert scheduled_notification.content_icon == "icon-updated" assert scheduled_notification.reference == "reference" assert scheduled_notification.internal_url == "internal-url-updated" @@ -111,6 +118,7 @@ def test_create_scheduled_notification_known_reference(app, user: User) -> None: user_id=other_user.id, content_title="title", content_body="body", + content_private_body="private body", content_icon="icon", reference="other-reference", scheduled_at=now(), @@ -119,6 +127,7 @@ def test_create_scheduled_notification_known_reference(app, user: User) -> None: scheduled_notification_data = { "content_title": "title", "content_body": "body", + "content_private_body": "private body", "content_icon": "icon", "reference": "other-reference", "scheduled_at": scheduled_notification_date.isoformat(), @@ -132,6 +141,7 @@ def test_create_scheduled_notification_known_reference(app, user: User) -> None: assert scheduled_notification.user.id == user.id assert scheduled_notification.content_title == "title" assert scheduled_notification.content_body == "body" + assert scheduled_notification.content_private_body == "private body" assert scheduled_notification.content_icon == "icon" assert scheduled_notification.reference == "other-reference" assert scheduled_notification.internal_url is None diff --git a/ami/notification/tests/test_publish-scheduled-notifications_command.py b/ami/notification/tests/test_publish-scheduled-notifications_command.py index 2d844b844..a5af2bd8c 100644 --- a/ami/notification/tests/test_publish-scheduled-notifications_command.py +++ b/ami/notification/tests/test_publish-scheduled-notifications_command.py @@ -30,6 +30,7 @@ async def test_command_publish_scheduled_notifications( user_id=user.id, content_title="title 1", content_body="body 1", + content_private_body="private body 1", content_icon="icon 1", reference="reference 1", internal_url="internal-url-1", @@ -40,6 +41,7 @@ async def test_command_publish_scheduled_notifications( user_id=user.id, content_title="title 2", content_body="body 2", + content_private_body="private body 2", content_icon="icon 2", reference="reference 2", internal_url="internal-url-2", @@ -49,6 +51,7 @@ async def test_command_publish_scheduled_notifications( user_id=user.id, content_title="title 3", content_body="body 3", + content_private_body="private body 3", content_icon="icon 3", reference="reference 3", internal_url="internal-url-3", @@ -72,6 +75,7 @@ async def test_command_publish_scheduled_notifications( assert notification.user_id == user.id # type: ignore[attr-defined] assert notification.content_title == "title 3" assert notification.content_body == "body 3" + assert notification.content_private_body == "private body 3" assert notification.content_icon == "icon 3" assert notification.item_type is None assert notification.item_id is None diff --git a/ami/replication/models.py b/ami/replication/models.py index 63ff38bc6..661a8fde1 100644 --- a/ami/replication/models.py +++ b/ami/replication/models.py @@ -38,6 +38,7 @@ class AnonymizedNotification(AnonymizedModel): id = models.UUIDField(editable=False) content_title = models.CharField() content_body = models.CharField() + # No `content_private_body` here. content_icon = models.CharField(blank=True, null=True) item_type = models.CharField(blank=True, null=True)