diff --git a/notifications/helpers.py b/notifications/helpers.py index 26051e3..d18a6e2 100644 --- a/notifications/helpers.py +++ b/notifications/helpers.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ImproperlyConfigured from django.forms import model_to_dict from notifications.settings import notification_settings @@ -34,3 +35,9 @@ def get_notification_list(request, method_name="all"): if request.GET.get("mark_as_read"): notification.mark_as_read() return notification_list + + +def assert_soft_delete() -> None: + if not notification_settings.SOFT_DELETE: + msg = "To use this feature you need activate SOFT_DELETE in settings.py" + raise ImproperlyConfigured(msg) diff --git a/notifications/models/__init__.py b/notifications/models/__init__.py index 80600c4..f09fc4e 100644 --- a/notifications/models/__init__.py +++ b/notifications/models/__init__.py @@ -1,7 +1,8 @@ -from .base import NotificationLevel +from .base import AbstractNotification, NotificationLevel from .notification import Notification -__all__ = [ +__all__ = ( + "AbstractNotification", "Notification", "NotificationLevel", -] +) diff --git a/notifications/models/base.py b/notifications/models/base.py index 7fa44a6..18462b0 100644 --- a/notifications/models/base.py +++ b/notifications/models/base.py @@ -12,7 +12,9 @@ from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ +from notifications.helpers import assert_soft_delete from notifications.querysets import NotificationQuerySet +from notifications.settings import notification_settings class NotificationLevel(models.IntegerChoices): @@ -52,8 +54,6 @@ class AbstractNotification(models.Model): """ - level = models.IntegerField(_("level"), choices=NotificationLevel.choices, default=NotificationLevel.INFO) - recipient = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, @@ -62,7 +62,6 @@ class AbstractNotification(models.Model): verbose_name=_("recipient"), blank=False, ) - unread = models.BooleanField(_("unread"), default=True, blank=False, db_index=True) actor_content_type = models.ForeignKey( ContentType, @@ -102,10 +101,12 @@ class AbstractNotification(models.Model): action_object.short_description = _("action object") timestamp = models.DateTimeField(_("timestamp"), default=timezone.now, db_index=True) + level = models.IntegerField(_("level"), choices=NotificationLevel.choices, default=NotificationLevel.INFO) - public = models.BooleanField(_("public"), default=True, db_index=True) deleted = models.BooleanField(_("deleted"), default=False, db_index=True) emailed = models.BooleanField(_("emailed"), default=False, db_index=True) + public = models.BooleanField(_("public"), default=True, db_index=True) + unread = models.BooleanField(_("unread"), default=True, blank=False, db_index=True) data = models.JSONField(_("data"), blank=True, null=True) @@ -135,27 +136,42 @@ def __str__(self) -> str: return _("%(actor)s %(verb)s %(action_object)s %(timesince)s ago") % ctx return _("%(actor)s %(verb)s %(timesince)s ago") % ctx - def timesince(self, now: Union[None, datetime.datetime] = None) -> str: - """ - Shortcut for the ``django.utils.timesince.timesince`` function of the - current timestamp. - """ - - return timesince.timesince(self.timestamp, now) - @property def slug(self): return self.id - def mark_as_read(self) -> None: - if self.unread: - self.unread = False + def _mark_as(self, field: str, status: bool) -> None: + if getattr(self, field, None) != status: + setattr(self, field, status) self.save() + def mark_as_active(self) -> None: + assert_soft_delete() + self._mark_as("deleted", False) + + def mark_as_deleted(self) -> None: + if notification_settings.SOFT_DELETE: + self._mark_as("deleted", True) + else: + self.delete() + + def mark_as_sent(self) -> None: + self._mark_as("emailed", True) + + def mark_as_unsent(self) -> None: + self._mark_as("emailed", False) + + def mark_as_public(self) -> None: + self._mark_as("public", True) + + def mark_as_private(self) -> None: + self._mark_as("public", False) + + def mark_as_read(self) -> None: + self._mark_as("unread", False) + def mark_as_unread(self) -> None: - if not self.unread: - self.unread = True - self.save() + self._mark_as("unread", True) def _build_url(self, field_name: str) -> str: app_label = getattr(getattr(self, f"{field_name}_content_type"), "app_label") @@ -179,6 +195,14 @@ def action_object_url(self) -> Union[str, None]: def target_object_url(self) -> Union[str, None]: return self._build_url("target") + def timesince(self, now: Union[None, datetime.datetime] = None) -> str: + """ + Shortcut for the ``django.utils.timesince.timesince`` function of the + current timestamp. + """ + + return timesince.timesince(self.timestamp, now) + def naturalday(self) -> Union[str, None]: """ Shortcut for the ``humanize``. diff --git a/notifications/tests/test_models.py b/notifications/tests/test_models.py index daf8ee6..2d198ab 100644 --- a/notifications/tests/test_models.py +++ b/notifications/tests/test_models.py @@ -2,6 +2,8 @@ from unittest.mock import patch import pytest +from django.core.exceptions import ImproperlyConfigured +from django.test import override_settings from django.urls import NoReverseMatch from freezegun import freeze_time from swapper import load_model @@ -48,22 +50,6 @@ def test__str__(): assert str(notification.action_object) in notification_str -@pytest.mark.parametrize( - "increase,expected_result", - ( - ({"minutes": 10}, "10\xa0minutes"), - ({"days": 2}, "2\xa0days"), - ), -) -@pytest.mark.django_db -def test_timesince(increase, expected_result): - initial_date = datetime(2023, 1, 1, 0, 0, 0) - with freeze_time(initial_date): - notification = NotificationShortFactory() - with freeze_time(initial_date + timedelta(**increase)): - assert notification.timesince() == expected_result - - @pytest.mark.django_db def test_slug(): notification = NotificationShortFactory() @@ -71,36 +57,90 @@ def test_slug(): @pytest.mark.parametrize( - "before,method", + "field,initial_status,method_name,expected", ( - (True, "mark_as_read"), - (False, "mark_as_unread"), + ("emailed", True, "mark_as_sent", True), + ("emailed", False, "mark_as_sent", True), + ("emailed", True, "mark_as_unsent", False), + ("emailed", False, "mark_as_unsent", False), + ("public", True, "mark_as_public", True), + ("public", False, "mark_as_public", True), + ("public", True, "mark_as_private", False), + ("public", False, "mark_as_private", False), + ("unread", True, "mark_as_read", False), + ("unread", False, "mark_as_read", False), + ("unread", True, "mark_as_unread", True), + ("unread", False, "mark_as_unread", True), ), ) @pytest.mark.django_db -def test_mark_as_read_unread(before, method): - notification = NotificationShortFactory(unread=before) - - assert Notification.objects.filter(unread=before).count() == 1 - func = getattr(notification, method) +def test_mark_as_methods(field, initial_status, method_name, expected): + notification = NotificationShortFactory(**{field: initial_status}) + func = getattr(notification, method_name) func() - assert Notification.objects.filter(unread=before).count() == 0 - assert Notification.objects.filter(unread=not before).count() == 1 + assert getattr(notification, field, None) is expected @pytest.mark.django_db -def test_build_url(): - notification = NotificationShortFactory() +def test_mark_as_active(): + notification = NotificationShortFactory(deleted=True) + with pytest.raises(ImproperlyConfigured): + notification.mark_as_active() + + with override_settings(DJANGO_NOTIFICATIONS_CONFIG={"SOFT_DELETE": True}): + notification.mark_as_active() + assert notification.deleted is False + + +@pytest.mark.django_db +def test_mark_as_deleted(): + notification = NotificationShortFactory(deleted=False) + + with override_settings(DJANGO_NOTIFICATIONS_CONFIG={"SOFT_DELETE": True}): + notification.mark_as_deleted() + assert notification.deleted is True + + notification.mark_as_deleted() + assert Notification.objects.count() == 0 - url = notification.actor_object_url() + +@pytest.mark.parametrize( + "method,field", + ( + ("actor_object_url", "actor"), + ("action_object_url", "action_object"), + ("target_object_url", "target"), + ), +) +@pytest.mark.django_db +def test_build_url(method, field): + notification = NotificationFullFactory() + + url = getattr(notification, method)() assert "