Skip to content

Commit

Permalink
feat(notification): enregistrer les retours sur les notifs mails (#891)
Browse files Browse the repository at this point in the history
## Description

🎸 Ajouter l'uuid de la `notification` dans l'url du message de notif.
🎸 Intercepter ce paramètre dans les requêtes entrantes pour mettre à
jour la `notification`

## Type de changement

🎢 Nouvelle fonctionnalité (changement non cassant qui ajoute une
fonctionnalité).
🚧 technique
  • Loading branch information
vincentporte authored Jan 28, 2025
1 parent 4ae46d0 commit ec67205
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 238 deletions.
3 changes: 2 additions & 1 deletion config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"lacommunaute.utils.middleware.ParkingPageMiddleware",
"lacommunaute.openid_connect.middleware.ProConnectLoginMiddleware",
"lacommunaute.notification.middleware.NotificationMiddleware",
]

THIRD_PARTIES_MIDDLEWARE = [
Expand Down Expand Up @@ -268,7 +269,7 @@

AWS_S3_ACCESS_KEY_ID = os.getenv("CELLAR_ADDON_KEY_ID")
AWS_S3_SECRET_ACCESS_KEY = os.getenv("CELLAR_ADDON_KEY_SECRET")
AWS_S3_ENDPOINT_URL = f"{os.getenv('CELLAR_ADDON_PROTOCOL','https')}://{os.getenv('CELLAR_ADDON_HOST')}"
AWS_S3_ENDPOINT_URL = f"{os.getenv('CELLAR_ADDON_PROTOCOL', 'https')}://{os.getenv('CELLAR_ADDON_HOST')}"
AWS_STORAGE_BUCKET_NAME = os.getenv("S3_STORAGE_BUCKET_NAME")
AWS_STORAGE_BUCKET_NAME_PUBLIC = os.getenv("S3_STORAGE_BUCKET_NAME_PUBLIC")
AWS_S3_STORAGE_BUCKET_REGION = os.getenv("S3_STORAGE_BUCKET_REGION")
Expand Down
26 changes: 17 additions & 9 deletions lacommunaute/notification/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,32 @@ class EmailSentTrackAdmin(admin.ModelAdmin):
list_filter = ("kind",)


class SentNotificationListFilter(admin.SimpleListFilter):
title = _("sent")
parameter_name = "sent"

class BaseNotificationListFilter(admin.SimpleListFilter):
def lookups(self, request, model_admin):
return [
("sent", _("sent")),
("not sent", _("not sent")),
("yes", _(self.title)),
("no", _("not " + self.title)),
]

def queryset(self, request, queryset):
if self.value() is not None:
return queryset.filter(sent_at__isnull=(self.value() != "sent"))
field_name = self.parameter_name + "_at"
return queryset.filter(**{f"{field_name}__isnull": (self.value() != "yes")})


class SentNotificationListFilter(BaseNotificationListFilter):
title = _("sent")
parameter_name = "sent"


class VisitedNotificationListFilter(BaseNotificationListFilter):
title = _("visited")
parameter_name = "visited"


@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
list_display = ("recipient", "kind", "delay", "created", "sent_at", "post_id")
list_filter = ("kind", "delay", SentNotificationListFilter)
list_display = ("recipient", "kind", "delay", "created", "sent_at", "visited_at", "post_id")
list_filter = ("kind", "delay", SentNotificationListFilter, VisitedNotificationListFilter)
raw_id_fields = ("post",)
search_fields = ("recipient", "post__id", "post__subject")
21 changes: 21 additions & 0 deletions lacommunaute/notification/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import uuid

from django.utils import timezone
from django.utils.deprecation import MiddlewareMixin

from lacommunaute.notification.models import Notification


class NotificationMiddleware(MiddlewareMixin):
def process_request(self, request):
if "notif" not in request.GET:
return

notif_uuid = request.GET.get("notif", "")

try:
uuid.UUID(notif_uuid, version=4)
except ValueError:
pass
else:
Notification.objects.filter(uuid=notif_uuid).update(visited_at=timezone.now())
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 5.1.5 on 2025-01-23 13:56

import uuid

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("notification", "0011_alter_emailsenttrack_kind_alter_notification_kind"),
]

operations = [
migrations.AddField(
model_name="notification",
name="uuid",
field=models.UUIDField(blank=True, null=True, unique=True, verbose_name="uuid"),
),
migrations.AlterField(
model_name="notification",
name="uuid",
field=models.UUIDField(
blank=True,
default=uuid.uuid4,
null=True,
unique=True,
verbose_name="uuid",
),
),
migrations.AddField(
model_name="notification",
name="visited_at",
field=models.DateTimeField(blank=True, null=True, verbose_name="clicked at"),
),
]
3 changes: 3 additions & 0 deletions lacommunaute/notification/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from itertools import groupby
from operator import attrgetter

Expand Down Expand Up @@ -66,6 +67,8 @@ class Notification(DatedModel):
default=NotificationDelay.ASAP,
)
sent_at = models.DateTimeField(verbose_name=_("sent at"), null=True, blank=True)
visited_at = models.DateTimeField(verbose_name=_("clicked at"), null=True, blank=True)
uuid = models.UUIDField(verbose_name=_("uuid"), null=True, blank=True, unique=True, default=uuid.uuid4)

class Meta:
verbose_name = _("Notification")
Expand Down
29 changes: 29 additions & 0 deletions lacommunaute/notification/tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from lacommunaute.notification.factories import NotificationFactory
from lacommunaute.notification.models import Notification


@pytest.mark.parametrize(
"notif_param, expected_visited_at",
[
("", None),
("malformed", None),
("74eb2ba8-04bf-489f-8c1d-fd1b34b0f3e9", None),
(lambda: str(NotificationFactory().uuid), True),
],
)
def test_notif_param(client, db, notif_param, expected_visited_at):
misc_notification = NotificationFactory()
if callable(notif_param):
notif_param = notif_param()

client.get(f"/?notif={notif_param}")

if expected_visited_at:
notification = Notification.objects.get(uuid=notif_param)
notification.refresh_from_db()
assert notification.visited_at is not None

misc_notification.refresh_from_db()
assert misc_notification.visited_at is None
10 changes: 10 additions & 0 deletions lacommunaute/notification/tests/tests_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import pytest
from django.contrib.auth.models import AnonymousUser
from django.db import IntegrityError
from django.db.models import F
from django.test import TestCase

Expand Down Expand Up @@ -89,3 +91,11 @@ def test_mark_topic_posts_read_invalid_arguments(self):

with self.assertRaises(ValueError):
Notification.objects.mark_topic_posts_read(topic, None)


class TestNotificationModel:
def test_uuid_uniqueness(self, db):
notification = NotificationFactory()

with pytest.raises(IntegrityError):
NotificationFactory(uuid=notification.uuid)
4 changes: 2 additions & 2 deletions lacommunaute/notification/tests/tests_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_post_is_topic_head(self, db):
"poster": notification.post.poster_display_name,
"action": "a posé une nouvelle question",
"forum": notification.post.topic.forum.name,
"url": notification.post.topic.get_absolute_url(with_fqdn=True),
"url": f"{notification.post.topic.get_absolute_url(with_fqdn=True)}?notif={notification.uuid}",
}
]

Expand All @@ -70,6 +70,6 @@ def test_post_is_not_topic_head(self, db):
"poster": notification.post.poster_display_name,
"action": f"a répondu à '{notification.post.topic.subject}'",
"forum": notification.post.topic.forum.name,
"url": notification.post.topic.get_absolute_url(with_fqdn=True),
"url": f"{notification.post.topic.get_absolute_url(with_fqdn=True)}?notif={notification.uuid}",
}
]
2 changes: 1 addition & 1 deletion lacommunaute/notification/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def get_serialized_messages(notifications):
"a posé une nouvelle question" if n.post.is_topic_head else f"a répondu à '{n.post.topic.subject}'"
),
"forum": n.post.topic.forum.name,
"url": n.post.topic.get_absolute_url(with_fqdn=True),
"url": f"{n.post.topic.get_absolute_url(with_fqdn=True)}?notif={n.uuid}",
}
for n in notifications
]
9 changes: 9 additions & 0 deletions locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,12 @@ msgstr "envoyée"
msgid "not sent"
msgstr "en attente"

msgid "visited"
msgstr "visité"

msgid "not visited"
msgstr "non visité"

#: lacommunaute/notification/models.py
msgid "Notification"
msgstr "Notification"
Expand All @@ -1024,6 +1030,9 @@ msgstr "délai"
msgid "sent at"
msgstr "envoyée le"

msgid "clicked at"
msgstr "lien visité le"

#: lacommunaute/notification/enums.py
msgid "New messages"
msgstr "Nouveaux messages"
Expand Down
Loading

0 comments on commit ec67205

Please sign in to comment.