diff --git a/docs/developer/notification-types.rst b/docs/developer/notification-types.rst index dff37182..da9a18ec 100644 --- a/docs/developer/notification-types.rst +++ b/docs/developer/notification-types.rst @@ -57,6 +57,24 @@ type: software like Aldus PageMaker including versions of *Lorem Ipsum*.""", ) +The URL of a ``generic_message`` notification points to the target object +by default. To append a querystring or fragment to this URL, pass +``target_url_suffix`` to ``notify.send``: + +.. code-block:: python + + notify.send( + type="generic_message", + level="error", + message="An unexpected error happened!", + sender=User.objects.first(), + target=User.objects.last(), + target_url_suffix="?status=pending", + ) + +The value of ``target_url_suffix`` must be a string starting with ``?``, +``&`` or ``#``. + Properties of Notification Types -------------------------------- diff --git a/docs/developer/sending-notifications.rst b/docs/developer/sending-notifications.rst index 7ead7792..dfe1b45e 100644 --- a/docs/developer/sending-notifications.rst +++ b/docs/developer/sending-notifications.rst @@ -64,58 +64,65 @@ The complete syntax for ``notify`` is: The ``notify`` signal supports the following parameters: -================= ======================================================== -**Parameter** **Description** -``actor`` An object of any type that represents the actor - performing the action that triggered the notification. - - **Note:** Use ``sender`` instead of ``actor`` if you - intend to use keyword arguments. -``recipient`` The recipient of the notification. This can be a - ``Group``, a list or queryset of ``User`` objects, or a - single ``User`` object. - - Defaults to ``None``, meaning you need to provide this - argument. -``action_object`` An object related to the action that triggered the - notification (optional). - - Defaults to ``None``. -``target`` The target object of the notification (optional). - - Defaults to ``None``. -``type`` Set values of other parameters based on registered - :doc:`notification types <./notification-types>` - - Defaults to ``None`` meaning you need to provide other - arguments. -``email_subject`` Sets subject of email notification to be sent. - - Defaults to the notification message. -``url`` Adds a URL in the email text, e.g.: - - ``For more information see .`` - - Defaults to ``None``, meaning the above message would - not be added to the email text. -``verb`` A string describing the action that triggered the - notification. - - Defaults to ``None``, meaning you need to provide this - argument. -``level`` The level of the notification, one of 'success', 'info', - 'warning' or 'error'. - - Defaults to 'info'. -``description`` Additional information to be included in the - notification (optional). - - Defaults to ``''``. -``timestamp`` A timestamp (``datetime`` object) for the notification - (optional). - - Defaults to the current time. -================= ======================================================== +===================== ==================================================== +**Parameter** **Description** +``actor`` An object of any type that represents the actor + performing the action that triggered the + notification. + + **Note:** Use ``sender`` instead of ``actor`` if you + intend to use keyword arguments. +``recipient`` The recipient of the notification. This can be a + ``Group``, a list or queryset of ``User`` objects, + or a single ``User`` object. + + Defaults to ``None``, meaning you need to provide + this argument. +``action_object`` An object related to the action that triggered the + notification (optional). + + Defaults to ``None``. +``target`` The target object of the notification (optional). + + Defaults to ``None``. +``target_url_suffix`` Appends a querystring or fragment to the generated + target URL. The value must be a string starting with + ``?``, ``&`` or ``#``. See the :ref:`generic_message + example `. + + Defaults to ``None``. +``type`` Set values of other parameters based on registered + :doc:`notification types <./notification-types>` + + Defaults to ``None`` meaning you need to provide + other arguments. +``email_subject`` Sets subject of email notification to be sent. + + Defaults to the notification message. +``url`` Adds a URL in the email text, e.g.: + + ``For more information see .`` + + Defaults to ``None``, meaning the above message + would not be added to the email text. +``verb`` A string describing the action that triggered the + notification. + + Defaults to ``None``, meaning you need to provide + this argument. +``level`` The level of the notification, one of 'success', + 'info', 'warning' or 'error'. + + Defaults to 'info'. +``description`` Additional information to be included in the + notification (optional). + + Defaults to ``''``. +``timestamp`` A timestamp (``datetime`` object) for the + notification (optional). + + Defaults to the current time. +===================== ==================================================== Passing Extra Data to Notifications ----------------------------------- diff --git a/openwisp_notifications/base/models.py b/openwisp_notifications/base/models.py index 72278c80..93571869 100644 --- a/openwisp_notifications/base/models.py +++ b/openwisp_notifications/base/models.py @@ -238,7 +238,9 @@ def action_url(self): @property def target_url(self): - return self._get_related_object_url(field="target") + url = self._get_related_object_url(field="target") + suffix = (self.data or {}).get("target_url_suffix") + return f"{url}{suffix}" if url and url != "#" and suffix else url @cached_property def message(self): diff --git a/openwisp_notifications/handlers.py b/openwisp_notifications/handlers.py index 13187a85..f880e243 100644 --- a/openwisp_notifications/handlers.py +++ b/openwisp_notifications/handlers.py @@ -155,6 +155,14 @@ def notify_handler(**kwargs): optional_objs = [ (kwargs.pop(opt, None), opt) for opt in ("target", "action_object") ] + target_url_suffix = kwargs.get("target_url_suffix") + if target_url_suffix is not None and ( + not isinstance(target_url_suffix, str) + or not target_url_suffix.startswith(("?", "&", "#")) + ): + raise ValueError( + _("target_url_suffix must be a string starting with '?', '&' or '#'.") + ) notification_list = [] for recipient in recipients: diff --git a/openwisp_notifications/tests/test_notifications.py b/openwisp_notifications/tests/test_notifications.py index aa12fbf4..e01d0414 100644 --- a/openwisp_notifications/tests/test_notifications.py +++ b/openwisp_notifications/tests/test_notifications.py @@ -164,6 +164,61 @@ def test_create_with_extra_data(self): self.assertIn(f"Error: {error}", n.message) self.assertEqual(n.email_subject, f"Error subject: {error}") + def test_generic_message_target_url_suffix(self): + target = self._create_operator() + target_url = _get_absolute_url( + reverse(f"admin:{self.users_app_label}_user_change", args=[target.pk]) + ) + with self.subTest("Append querystring suffix"): + notify.send( + type="generic_message", + target=target, + sender=self.admin, + target_url_suffix="?status=pending", + ) + notification = notification_queryset.first() + self.assertEqual( + notification.target_url, + f"{target_url}?status=pending", + ) + + with self.subTest("Append fragment suffix"): + notification.delete() + notify.send( + type="generic_message", + target=target, + sender=self.admin, + target_url_suffix="#pending", + ) + notification = notification_queryset.first() + self.assertEqual( + notification.target_url, + f"{target_url}#pending", + ) + + with self.subTest("Do not append suffix to fallback target URL"): + notification.delete() + notify.send( + type="generic_message", + sender=self.admin, + target_url_suffix="?status=pending", + ) + notification = notification_queryset.first() + self.assertEqual(notification.target_url, "#") + + def test_generic_message_invalid_target_url_suffix(self): + target = self._create_operator() + invalid_suffixes = ["status=pending", "https://example.com"] + for suffix in invalid_suffixes: + with self.subTest(suffix=suffix): + with self.assertRaises(ValueError): + notify.send( + type="generic_message", + target=target, + sender=self.admin, + target_url_suffix=suffix, + ) + def test_batch_email_helpers(self): with self.subTest("get_user_batched_notifications_cache_key()"): cache_key = Notification.get_user_batched_notifications_cache_key(