Skip to content

Commit a491df9

Browse files
authored
[feature] Added target_url_suffix param to notify signal #492
Closes #492
1 parent 382f613 commit a491df9

5 files changed

Lines changed: 143 additions & 53 deletions

File tree

docs/developer/notification-types.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,24 @@ type:
5757
software like Aldus PageMaker including versions of *Lorem Ipsum*.""",
5858
)
5959
60+
The URL of a ``generic_message`` notification points to the target object
61+
by default. To append a querystring or fragment to this URL, pass
62+
``target_url_suffix`` to ``notify.send``:
63+
64+
.. code-block:: python
65+
66+
notify.send(
67+
type="generic_message",
68+
level="error",
69+
message="An unexpected error happened!",
70+
sender=User.objects.first(),
71+
target=User.objects.last(),
72+
target_url_suffix="?status=pending",
73+
)
74+
75+
The value of ``target_url_suffix`` must be a string starting with ``?``,
76+
``&`` or ``#``.
77+
6078
Properties of Notification Types
6179
--------------------------------
6280

docs/developer/sending-notifications.rst

Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -64,58 +64,65 @@ The complete syntax for ``notify`` is:
6464
6565
The ``notify`` signal supports the following parameters:
6666

67-
================= ========================================================
68-
**Parameter** **Description**
69-
``actor`` An object of any type that represents the actor
70-
performing the action that triggered the notification.
71-
72-
**Note:** Use ``sender`` instead of ``actor`` if you
73-
intend to use keyword arguments.
74-
``recipient`` The recipient of the notification. This can be a
75-
``Group``, a list or queryset of ``User`` objects, or a
76-
single ``User`` object.
77-
78-
Defaults to ``None``, meaning you need to provide this
79-
argument.
80-
``action_object`` An object related to the action that triggered the
81-
notification (optional).
82-
83-
Defaults to ``None``.
84-
``target`` The target object of the notification (optional).
85-
86-
Defaults to ``None``.
87-
``type`` Set values of other parameters based on registered
88-
:doc:`notification types <./notification-types>`
89-
90-
Defaults to ``None`` meaning you need to provide other
91-
arguments.
92-
``email_subject`` Sets subject of email notification to be sent.
93-
94-
Defaults to the notification message.
95-
``url`` Adds a URL in the email text, e.g.:
96-
97-
``For more information see <url>.``
98-
99-
Defaults to ``None``, meaning the above message would
100-
not be added to the email text.
101-
``verb`` A string describing the action that triggered the
102-
notification.
103-
104-
Defaults to ``None``, meaning you need to provide this
105-
argument.
106-
``level`` The level of the notification, one of 'success', 'info',
107-
'warning' or 'error'.
108-
109-
Defaults to 'info'.
110-
``description`` Additional information to be included in the
111-
notification (optional).
112-
113-
Defaults to ``''``.
114-
``timestamp`` A timestamp (``datetime`` object) for the notification
115-
(optional).
116-
117-
Defaults to the current time.
118-
================= ========================================================
67+
===================== ====================================================
68+
**Parameter** **Description**
69+
``actor`` An object of any type that represents the actor
70+
performing the action that triggered the
71+
notification.
72+
73+
**Note:** Use ``sender`` instead of ``actor`` if you
74+
intend to use keyword arguments.
75+
``recipient`` The recipient of the notification. This can be a
76+
``Group``, a list or queryset of ``User`` objects,
77+
or a single ``User`` object.
78+
79+
Defaults to ``None``, meaning you need to provide
80+
this argument.
81+
``action_object`` An object related to the action that triggered the
82+
notification (optional).
83+
84+
Defaults to ``None``.
85+
``target`` The target object of the notification (optional).
86+
87+
Defaults to ``None``.
88+
``target_url_suffix`` Appends a querystring or fragment to the generated
89+
target URL. The value must be a string starting with
90+
``?``, ``&`` or ``#``. See the :ref:`generic_message
91+
example <notifications_generic_message_type>`.
92+
93+
Defaults to ``None``.
94+
``type`` Set values of other parameters based on registered
95+
:doc:`notification types <./notification-types>`
96+
97+
Defaults to ``None`` meaning you need to provide
98+
other arguments.
99+
``email_subject`` Sets subject of email notification to be sent.
100+
101+
Defaults to the notification message.
102+
``url`` Adds a URL in the email text, e.g.:
103+
104+
``For more information see <url>.``
105+
106+
Defaults to ``None``, meaning the above message
107+
would not be added to the email text.
108+
``verb`` A string describing the action that triggered the
109+
notification.
110+
111+
Defaults to ``None``, meaning you need to provide
112+
this argument.
113+
``level`` The level of the notification, one of 'success',
114+
'info', 'warning' or 'error'.
115+
116+
Defaults to 'info'.
117+
``description`` Additional information to be included in the
118+
notification (optional).
119+
120+
Defaults to ``''``.
121+
``timestamp`` A timestamp (``datetime`` object) for the
122+
notification (optional).
123+
124+
Defaults to the current time.
125+
===================== ====================================================
119126

120127
Passing Extra Data to Notifications
121128
-----------------------------------

openwisp_notifications/base/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,9 @@ def action_url(self):
238238

239239
@property
240240
def target_url(self):
241-
return self._get_related_object_url(field="target")
241+
url = self._get_related_object_url(field="target")
242+
suffix = (self.data or {}).get("target_url_suffix")
243+
return f"{url}{suffix}" if url and url != "#" and suffix else url
242244

243245
@cached_property
244246
def message(self):

openwisp_notifications/handlers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ def notify_handler(**kwargs):
155155
optional_objs = [
156156
(kwargs.pop(opt, None), opt) for opt in ("target", "action_object")
157157
]
158+
target_url_suffix = kwargs.get("target_url_suffix")
159+
if target_url_suffix is not None and (
160+
not isinstance(target_url_suffix, str)
161+
or not target_url_suffix.startswith(("?", "&", "#"))
162+
):
163+
raise ValueError(
164+
_("target_url_suffix must be a string starting with '?', '&' or '#'.")
165+
)
158166

159167
notification_list = []
160168
for recipient in recipients:

openwisp_notifications/tests/test_notifications.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,61 @@ def test_create_with_extra_data(self):
164164
self.assertIn(f"Error: {error}", n.message)
165165
self.assertEqual(n.email_subject, f"Error subject: {error}")
166166

167+
def test_generic_message_target_url_suffix(self):
168+
target = self._create_operator()
169+
target_url = _get_absolute_url(
170+
reverse(f"admin:{self.users_app_label}_user_change", args=[target.pk])
171+
)
172+
with self.subTest("Append querystring suffix"):
173+
notify.send(
174+
type="generic_message",
175+
target=target,
176+
sender=self.admin,
177+
target_url_suffix="?status=pending",
178+
)
179+
notification = notification_queryset.first()
180+
self.assertEqual(
181+
notification.target_url,
182+
f"{target_url}?status=pending",
183+
)
184+
185+
with self.subTest("Append fragment suffix"):
186+
notification.delete()
187+
notify.send(
188+
type="generic_message",
189+
target=target,
190+
sender=self.admin,
191+
target_url_suffix="#pending",
192+
)
193+
notification = notification_queryset.first()
194+
self.assertEqual(
195+
notification.target_url,
196+
f"{target_url}#pending",
197+
)
198+
199+
with self.subTest("Do not append suffix to fallback target URL"):
200+
notification.delete()
201+
notify.send(
202+
type="generic_message",
203+
sender=self.admin,
204+
target_url_suffix="?status=pending",
205+
)
206+
notification = notification_queryset.first()
207+
self.assertEqual(notification.target_url, "#")
208+
209+
def test_generic_message_invalid_target_url_suffix(self):
210+
target = self._create_operator()
211+
invalid_suffixes = ["status=pending", "https://example.com"]
212+
for suffix in invalid_suffixes:
213+
with self.subTest(suffix=suffix):
214+
with self.assertRaises(ValueError):
215+
notify.send(
216+
type="generic_message",
217+
target=target,
218+
sender=self.admin,
219+
target_url_suffix=suffix,
220+
)
221+
167222
def test_batch_email_helpers(self):
168223
with self.subTest("get_user_batched_notifications_cache_key()"):
169224
cache_key = Notification.get_user_batched_notifications_cache_key(

0 commit comments

Comments
 (0)