diff --git a/notifications/models/base.py b/notifications/models/base.py
index 2d0a3b36..76c96ddd 100644
--- a/notifications/models/base.py
+++ b/notifications/models/base.py
@@ -115,7 +115,7 @@ class Meta:
verbose_name = _("Notification")
verbose_name_plural = _("Notifications")
- def __str__(self):
+ def __str__(self) -> str:
ctx = {
"actor": self.actor,
"verb": self.verb,
@@ -153,37 +153,29 @@ def mark_as_unread(self) -> None:
self.unread = True
self.save()
- def actor_object_url(self) -> str:
+ def _build_url(self, field_name: str) -> str:
+ app_label = getattr(getattr(self, f"{field_name}_content_type"), "app_label")
+ model = getattr(getattr(self, f"{field_name}_content_type"), "model")
+ obj_id = getattr(self, f"{field_name}_object_id")
try:
url = reverse(
- f"admin:{self.actor_content_type.app_label}_{self.actor_content_type.model}_change",
- args=(self.actor_object_id,),
+ f"admin:{app_label}_{model}_change",
+ args=(obj_id,),
)
- return format_html("{id}", url=url, id=self.actor_object_id)
+ return format_html("{id}", url=url, id=obj_id)
except NoReverseMatch:
- return self.actor_object_id
+ return obj_id
- def action_object_url(self) -> str:
- try:
- url = reverse(
- f"admin:{self.action_object_content_type.app_label}_{self.action_object_content_type.model}_change",
- args=(self.action_object_id,),
- )
- return format_html("{id}", url=url, id=self.action_object_object_id)
- except NoReverseMatch:
- return self.action_object_object_id
+ def actor_object_url(self) -> str:
+ return self._build_url("actor")
- def target_object_url(self) -> str:
- try:
- url = reverse(
- f"admin:{self.target_content_type.app_label}_{self.target_content_type.model}_change",
- args=(self.target_object_id,),
- )
- return format_html("{id}", url=url, id=self.target_object_id)
- except NoReverseMatch:
- return self.target_object_id
+ def action_object_url(self) -> Union[str, None]:
+ return self._build_url("action_object")
+
+ def target_object_url(self) -> Union[str, None]:
+ return self._build_url("target")
- def naturalday(self):
+ def naturalday(self) -> Union[str, None]:
"""
Shortcut for the ``humanize``.
Take a parameter humanize_type. This parameter control the which humanize method use.
@@ -192,5 +184,5 @@ def naturalday(self):
return naturalday(self.timestamp)
- def naturaltime(self):
+ def naturaltime(self) -> str:
return naturaltime(self.timestamp)
diff --git a/notifications/tests/factories/notifications.py b/notifications/tests/factories/notifications.py
index 0ff720c4..bc4320b3 100644
--- a/notifications/tests/factories/notifications.py
+++ b/notifications/tests/factories/notifications.py
@@ -1,33 +1,58 @@
import factory
from django.contrib.contenttypes.models import ContentType
+from swapper import load_model
-from notifications.models import Notification
-from notifications.tests.factories.users import Actor, Recipient, Target
+from notifications.tests.factories.users import (
+ ActorFactory,
+ RecipientFactory,
+ TargetFactory,
+)
+
+VERB_LIST_SHORT = ("reached level 60", "joined to site")
+
+VERB_LIST_WITH_TARGET = (
+ "commented on",
+ "started follow",
+ "liked",
+)
-VERB_LIST = (
- "commented",
+VERB_LIST_FULL = (
+ "closed",
+ "opened",
"liked",
- "deleted",
)
+Notification = load_model("notifications", "Notification")
+
-class NotificationFactory(factory.django.DjangoModelFactory):
- recipient = factory.SubFactory(Recipient)
+class NotificationShortFactory(factory.django.DjangoModelFactory):
+ recipient = factory.SubFactory(RecipientFactory)
- actor = factory.SubFactory(Actor)
+ actor = factory.SubFactory(ActorFactory)
actor_object_id = factory.SelfAttribute("actor.id")
actor_content_type = factory.LazyAttribute(lambda obj: ContentType.objects.get_for_model(obj.actor))
- verb = factory.Iterator(VERB_LIST)
+ verb = factory.Iterator(VERB_LIST_SHORT)
description = factory.Faker("catch_phrase")
- target = factory.SubFactory(Target)
+ class Meta:
+ model = Notification
+
+
+class NotificationWithTargetFactory(NotificationShortFactory):
+ verb = factory.Iterator(VERB_LIST_WITH_TARGET)
+
+ target = factory.SubFactory(TargetFactory)
target_object_id = factory.SelfAttribute("target.id")
target_content_type = factory.LazyAttribute(lambda obj: ContentType.objects.get_for_model(obj.target))
- action_object = factory.SubFactory(Target)
+
+class NotificationWithActionObjectFactory(NotificationShortFactory):
+ verb = factory.Iterator(VERB_LIST_WITH_TARGET)
+ action_object = factory.SubFactory(TargetFactory)
action_object_object_id = factory.SelfAttribute("action_object.id")
action_object_content_type = factory.LazyAttribute(lambda obj: ContentType.objects.get_for_model(obj.action_object))
- class Meta:
- model = Notification
+
+class NotificationFullFactory(NotificationWithTargetFactory, NotificationWithActionObjectFactory):
+ verb = factory.Iterator(VERB_LIST_FULL)
diff --git a/notifications/tests/factories/users.py b/notifications/tests/factories/users.py
index 0a82ba36..34129ea6 100644
--- a/notifications/tests/factories/users.py
+++ b/notifications/tests/factories/users.py
@@ -2,7 +2,7 @@
from django.conf import settings
-class Recipient(factory.django.DjangoModelFactory):
+class RecipientFactory(factory.django.DjangoModelFactory):
username = factory.Sequence(lambda n: f"recipient-{n}")
first_name = factory.SelfAttribute("username")
@@ -10,7 +10,7 @@ class Meta:
model = settings.AUTH_USER_MODEL
-class Actor(factory.django.DjangoModelFactory):
+class ActorFactory(factory.django.DjangoModelFactory):
username = factory.Sequence(lambda n: f"actor-{n}")
first_name = factory.SelfAttribute("username")
@@ -18,7 +18,7 @@ class Meta:
model = settings.AUTH_USER_MODEL
-class Target(factory.django.DjangoModelFactory):
+class TargetFactory(factory.django.DjangoModelFactory):
username = factory.Sequence(lambda n: f"target-{n}")
first_name = factory.SelfAttribute("username")
diff --git a/notifications/tests/test_migrations/test_level_migration.py b/notifications/tests/test_migrations/test_level_migration.py
index ef402a06..917a0f37 100644
--- a/notifications/tests/test_migrations/test_level_migration.py
+++ b/notifications/tests/test_migrations/test_level_migration.py
@@ -12,9 +12,9 @@ def test_main_migration0002(migrator):
OldNotification = old_state.apps.get_model("notifications", "Notification") # pylint: disable=invalid-name
OldContentType = old_state.apps.get_model("contenttypes", "ContentType") # pylint: disable=invalid-name
- mark_follower = factory.create(OldUser, FACTORY_CLASS=user_factory.Recipient)
- guido = factory.create(OldUser, FACTORY_CLASS=user_factory.Target)
- mark = factory.create(OldUser, FACTORY_CLASS=user_factory.Actor)
+ mark_follower = factory.create(OldUser, FACTORY_CLASS=user_factory.RecipientFactory)
+ guido = factory.create(OldUser, FACTORY_CLASS=user_factory.TargetFactory)
+ mark = factory.create(OldUser, FACTORY_CLASS=user_factory.ActorFactory)
user_type = OldContentType.objects.get_for_model(mark)
notification_base = {
diff --git a/notifications/tests/test_models.py b/notifications/tests/test_models.py
new file mode 100644
index 00000000..3ce33e96
--- /dev/null
+++ b/notifications/tests/test_models.py
@@ -0,0 +1,118 @@
+from datetime import datetime, timedelta
+from unittest.mock import patch
+
+import pytest
+from django.urls import NoReverseMatch
+from freezegun import freeze_time
+from swapper import load_model
+
+from notifications.tests.factories.notifications import (
+ NotificationFullFactory,
+ NotificationShortFactory,
+ NotificationWithActionObjectFactory,
+ NotificationWithTargetFactory,
+)
+
+Notification = load_model("notifications", "Notification")
+
+
+@pytest.mark.django_db
+def test__str__():
+ notification = NotificationShortFactory()
+
+ notification_str = str(notification)
+ assert str(notification.actor) in notification_str
+ assert str(notification.verb) in notification_str
+ assert str(notification.action_object) not in notification_str
+ assert str(notification.target) not in notification_str
+
+ notification = NotificationWithTargetFactory()
+ notification_str = str(notification)
+ assert str(notification.actor) in notification_str
+ assert str(notification.verb) in notification_str
+ assert str(notification.target) in notification_str
+ assert str(notification.action_object) not in notification_str
+
+ notification = NotificationWithActionObjectFactory()
+ notification_str = str(notification)
+ assert str(notification.actor) in notification_str
+ assert str(notification.verb) in notification_str
+ assert str(notification.action_object) in notification_str
+ assert str(notification.target) not in notification_str
+
+ notification = NotificationFullFactory()
+ notification_str = str(notification)
+ assert str(notification.actor) in notification_str
+ assert str(notification.verb) in notification_str
+ assert str(notification.target) in notification_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
+
+
+def test_slug():
+ notification = NotificationShortFactory()
+ assert notification.id == notification.slug
+
+
+@pytest.mark.parametrize(
+ "before,method",
+ (
+ (True, "mark_as_read"),
+ (False, "mark_as_unread"),
+ ),
+)
+@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)
+ func()
+ assert Notification.objects.filter(unread=before).count() == 0
+ assert Notification.objects.filter(unread=not before).count() == 1
+
+
+@pytest.mark.django_db
+def test_build_url():
+ notification = NotificationShortFactory()
+
+ url = notification.actor_object_url()
+
+ assert "=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
typing = ["typing-extensions (>=4.8)"]
+[[package]]
+name = "freezegun"
+version = "1.4.0"
+description = "Let your Python tests travel through time"
+optional = true
+python-versions = ">=3.7"
+files = [
+ {file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"},
+ {file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"},
+]
+
+[package.dependencies]
+python-dateutil = ">=2.7"
+
[[package]]
name = "identify"
version = "2.5.35"
@@ -1215,9 +1229,9 @@ files = [
[extras]
dev = ["django-debug-toolbar", "pre-commit", "psycopg2-binary"]
lint = ["bandit", "black", "isort", "mypy", "pylint", "pylint-django"]
-test = ["coverage", "django-test-migrations", "factory-boy", "pytest", "pytest-cov", "pytest-django", "pytest-xdist"]
+test = ["coverage", "django-test-migrations", "factory-boy", "freezegun", "pytest", "pytest-cov", "pytest-django", "pytest-xdist"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "c05e64d20b9253c2d60188553b6a88be71d8a40ccd9caff588ae06bfd42931fc"
+content-hash = "7934a50f2a5ec5aa8f522e683839042fa165ec26367ab4767f194c1455f313bd"
diff --git a/pyproject.toml b/pyproject.toml
index 753ed98d..616b207c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -101,6 +101,7 @@ pytest = {version = "^7", optional = true }
pytest-cov = {version = "^4", optional = true }
pytest-django = {version = "^4", optional = true }
pytest-xdist = {version = "^3.3.1", optional = true }
+freezegun = {version = "^1.4.0", optional = true}
[tool.poetry.extras]
dev = [
@@ -121,6 +122,7 @@ test = [
"django-test",
"django-test-migrations",
"factory-boy",
+ "freezegun",
"pytest",
"pytest-cov",
"pytest-django",
@@ -170,6 +172,7 @@ max-line-length = 120
ignored-modules = [
"pytest",
"factory",
+ "freezegun"
]
[tool.black]