Skip to content

Commit

Permalink
[feature] Added generic message notification type (shown in dialg box)
Browse files Browse the repository at this point in the history
…#254

Implements and closes #254

---------

Co-authored-by: Gagan Deep <[email protected]>
Co-authored-by: Federico Capoano <[email protected]>
  • Loading branch information
3 people authored Jun 13, 2024
1 parent e50e7aa commit 65b59c6
Show file tree
Hide file tree
Showing 11 changed files with 442 additions and 98 deletions.
67 changes: 62 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -527,11 +527,61 @@ value you want but it needs to be unique. For more details read `preventing dupl
Notification Types
------------------

**OpenWISP Notifications** simplifies configuring individual notification by
using notification types. You can think of a notification type as a template
**OpenWISP Notifications** allows defining notification types for
recurring events. Think of a notification type as a template
for notifications.

These properties can be configured for each notification type:
``generic_message``
~~~~~~~~~~~~~~~~~~~

.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/1.1/generic_message.png
:target: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/1.1/generic_message.png
:align: center

This module includes a notification type called ``generic_message``.

This notification type is designed to deliver custom messages in the
user interface for infrequent events or errors that occur during
background operations and cannot be communicated easily to the user
in other ways.

These messages may require longer explanations and are therefore
displayed in a dialog overlay, as shown in the screenshot above.
This notification type does not send emails.

The following code example demonstrates how to send a notification
of this type:

.. code-block:: python
from openwisp_notifications.signals import notify
notify.send(
type='generic_message',
level='error',
message='An unexpected error happened!',
sender=User.objects.first(),
target=User.objects.last(),
description="""Lorem Ipsum is simply dummy text
of the printing and typesetting industry.
### Heading 3
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
when an unknown printer took a galley of type and scrambled it to make a
type specimen book.
It has survived not only **five centuries**, but also the leap into
electronic typesetting, remaining essentially unchanged.
It was popularised in the 1960s with the release of Letraset sheets
containing Lorem Ipsum passages, and more recently with desktop publishing
software like Aldus PageMaker including versions of *Lorem Ipsum*."""
)
Properties of Notification Types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The following properties can be configured for each notification type:

+------------------------+----------------------------------------------------------------+
| **Property** | **Description** |
Expand Down Expand Up @@ -569,8 +619,15 @@ These properties can be configured for each notification type:
+------------------------+----------------------------------------------------------------+


**Note**: A notification type configuration should contain atleast one of ``message`` or ``message_template``
settings. If both of them are present, ``message`` is given preference over ``message_template``.
**Note**: It is recommended that a notification type configuration
for recurring events contains either the ``message`` or
``message_template`` properties. If both are present,
``message`` is given preference over ``message_template``.

If you don't plan on using ``message`` or ``message_template``,
it may be better to use the existing ``generic_message`` type.
However, it's advised to do so only if the event being notified
is infrequent.

**Note**: The callable for ``actor_link``, ``action_object_link`` and ``target_link`` should
have the following signature:
Expand Down
3 changes: 3 additions & 0 deletions openwisp_notifications/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ def data(self):


class NotificationListSerializer(NotificationSerializer):
description = serializers.CharField(source='rendered_description')

class Meta(NotificationSerializer.Meta):
fields = [
'id',
'message',
'description',
'unread',
'target_url',
'email_subject',
Expand Down
92 changes: 63 additions & 29 deletions openwisp_notifications/base/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from contextlib import contextmanager

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
Expand Down Expand Up @@ -29,6 +30,35 @@
logger = logging.getLogger(__name__)


@contextmanager
def notification_render_attributes(obj, **attrs):
"""
This context manager sets temporary attributes on
the notification object to allowing rendering of
notification.
It can only be used to set aliases of the existing attributes.
By default, it will set the following aliases:
- actor_link -> actor_url
- action_link -> action_url
- target_link -> target_url
"""
defaults = {
'actor_link': 'actor_url',
'action_link': 'action_url',
'target_link': 'target_url',
}
defaults.update(attrs)

for target_attr, source_attr in defaults.items():
setattr(obj, target_attr, getattr(obj, source_attr))

yield obj

for attr in defaults.keys():
delattr(obj, attr)


class AbstractNotification(UUIDModel, BaseNotification):
CACHE_KEY_PREFIX = 'ow-notifications-'
type = models.CharField(max_length=30, null=True, choices=NOTIFICATION_CHOICES)
Expand Down Expand Up @@ -103,40 +133,44 @@ def target_url(self):

@cached_property
def message(self):
return self.get_message()
with notification_render_attributes(self):
return self.get_message()

@cached_property
def rendered_description(self):
if not self.description:
return
with notification_render_attributes(self):
data = self.data or {}
desc = self.description.format(notification=self, **data)
return mark_safe(markdown(desc))

@property
def email_message(self):
return self.get_message(email_message=True)
with notification_render_attributes(self, target_link='redirect_view_url'):
return self.get_message()

def get_message(self, email_message=False):
if self.type:
# setting links in notification object for message rendering
self.actor_link = self.actor_url
self.action_link = self.action_url
self.target_link = (
self.target_url if not email_message else self.redirect_view_url
)
try:
config = get_notification_configuration(self.type)
data = self.data or {}
if 'message' in config:
md_text = config['message'].format(notification=self, **data)
else:
md_text = render_to_string(
config['message_template'], context=dict(notification=self)
).strip()
except (AttributeError, KeyError, NotificationRenderException) as exception:
self._invalid_notification(
self.pk,
exception,
'Error encountered in rendering notification message',
)
# clean up
self.actor_link = self.action_link = self.target_link = None
return mark_safe(markdown(md_text))
else:
def get_message(self):
if not self.type:
return self.description
try:
config = get_notification_configuration(self.type)
data = self.data or {}
if 'message' in data:
md_text = data['message'].format(notification=self, **data)
elif 'message' in config:
md_text = config['message'].format(notification=self, **data)
else:
md_text = render_to_string(
config['message_template'], context=dict(notification=self, **data)
).strip()
except (AttributeError, KeyError, NotificationRenderException) as exception:
self._invalid_notification(
self.pk,
exception,
'Error encountered in rendering notification message',
)
return mark_safe(markdown(md_text))

@cached_property
def email_subject(self):
Expand Down
14 changes: 7 additions & 7 deletions openwisp_notifications/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ def notify_handler(**kwargs):
except NotificationRenderException as error:
logger.error(f'Error encountered while creating notification: {error}')
return
level = notification_template.get(
'level', kwargs.pop('level', Notification.LEVELS.info)
level = kwargs.pop(
'level', notification_template.get('level', Notification.LEVELS.info)
)
verb = notification_template.get('verb', kwargs.pop('verb', None))
user_app_name = User._meta.app_label
Expand Down Expand Up @@ -198,22 +198,22 @@ def send_email_notification(sender, instance, created, **kwargs):
# Do not send email if notification is malformed.
return
url = instance.data.get('url', '') if instance.data else None
description = instance.message
body_text = instance.email_message
if url:
target_url = url
elif instance.target:
target_url = instance.redirect_view_url
else:
target_url = None
if target_url:
description += _('\n\nFor more information see %(target_url)s.') % {
body_text += _('\n\nFor more information see %(target_url)s.') % {
'target_url': target_url
}

send_email(
subject,
description,
instance.message,
subject=subject,
body_text=body_text,
body_html=instance.email_message,
recipients=[instance.recipient.email],
extra_context={
'call_to_action_url': target_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@
.ow-notification-level-text {
padding: 0px 6px;
text-transform: uppercase;
font-weight: bold;
}
.ow-notification-elem .icon,
.ow-notification-toast .icon {
Expand Down Expand Up @@ -300,6 +301,66 @@
}
.ow-notification-elem:last-child .ow-notification-inner { border-bottom: none }

/* Generic notification dialog */
.ow-overlay-notification {
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
transition: opacity 0.3s;
}
.ow-dialog-notification {
position: relative;
background-color: white;
padding: 20px;
padding-top: 20px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
width: 90%;
max-width: 500px;
text-align: left;
}
.ow-dialog-notification .ow-notification-date {
padding-right: 15px;
}
.ow-dialog-notification .button {
margin-right: 10px;
}
.ow-dialog-notification-level-wrapper {
display: flex;
justify-content: space-between;
}
.ow-dialog-notification .icon {
min-height: 15;
min-width: 15px;
background-repeat: no-repeat;
}
.ow-dialog-close-x {
cursor: pointer;
font-size: 1.75em;
position: absolute;
display: block;
font-weight: bold;
top: 3px;
right: 10px;
}
.ow-dialog-close-x:hover {
color: #df5d43;
}
.ow-message-title {
color: #333;
margin-bottom: 10px;
}
.ow-message-title a {
color: #df5d43;
}
.ow-message-title a:hover {
text-decoration: underline;
}
.ow-dialog-buttons {
line-height: 3em;
}

@media screen and (max-width: 600px) {
.ow-notification-dropdown {
width: 98%;
Expand Down
Loading

0 comments on commit 65b59c6

Please sign in to comment.