Skip to content

Commit

Permalink
Refactoring the AbstractNotification model and its tests
Browse files Browse the repository at this point in the history
  • Loading branch information
AlvaroLQueiroz committed Apr 28, 2024
1 parent ec236d5 commit 78faf1a
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 53 deletions.
7 changes: 7 additions & 0 deletions 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
Expand Down Expand Up @@ -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)
7 changes: 4 additions & 3 deletions 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",
]
)
60 changes: 42 additions & 18 deletions notifications/models/base.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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")
Expand All @@ -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``.
Expand Down
104 changes: 72 additions & 32 deletions notifications/tests/test_models.py
Expand Up @@ -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
Expand Down Expand Up @@ -48,59 +50,97 @@ 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()
assert notification.id == notification.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 "<a href=" in url
assert str(notification.actor.id) in url
assert str(getattr(notification, field).id) in url

with patch("notifications.models.base.reverse") as mock:
mock.side_effect = NoReverseMatch
url = notification.actor_object_url()
assert notification.actor.id == url
url = getattr(notification, method)()
assert getattr(notification, field).id == url


@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.parametrize(
Expand Down

0 comments on commit 78faf1a

Please sign in to comment.