Coverage for baibu/notifications/models.py: 100%
39 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
1import uuid
3from django.conf import settings
4from django.db import models
5from django.utils.translation import gettext_lazy as _
8class Notification(models.Model):
9 """A message for one user, shown in their in-app inbox."""
11 id = models.UUIDField(primary_key=True, default=uuid.uuid7, editable=False)
12 user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="notifications")
13 # What it is about, e.g. "submission_accepted". Registered kinds (see
14 # kinds.py) are translated when shown, using ``params``; other kinds show
15 # the stored title and body.
16 kind = models.CharField(max_length=64)
17 params = models.JSONField(default=dict, blank=True)
18 title = models.CharField(max_length=200, blank=True)
19 body = models.TextField(blank=True)
20 # A path inside the platform to open, e.g. /contribute/.
21 link = models.CharField(max_length=500, blank=True)
22 created_at = models.DateTimeField(auto_now_add=True)
23 read_at = models.DateTimeField(null=True, blank=True)
25 class Meta:
26 ordering = ["-created_at"]
27 indexes = [models.Index(fields=["user", "read_at"], name="notification_user_unread")]
28 verbose_name = _("Notification")
29 verbose_name_plural = _("Notifications")
31 def __str__(self) -> str:
32 return str(self.display_title)
34 def _render(self, text) -> str:
35 try:
36 return str(text) % self.params if self.params else str(text)
37 except (KeyError, TypeError, ValueError):
38 return str(text)
40 @property
41 def display_title(self) -> str:
42 from .kinds import KINDS
44 kind = KINDS.get(self.kind)
45 return self._render(kind.title) if kind else self.title
47 @property
48 def display_body(self) -> str:
49 from .kinds import KINDS
51 kind = KINDS.get(self.kind)
52 return self._render(kind.body) if kind else self.body
54 @property
55 def is_read(self) -> bool:
56 return self.read_at is not None