From c207c9d65861917429ae92a07902d069e3a46f9d Mon Sep 17 00:00:00 2001 From: Dhanus Date: Tue, 14 May 2024 17:17:36 +0530 Subject: [PATCH 01/32] [feature] Description of notification shown in a dialog admin site --- openwisp_notifications/api/serializers.py | 1 + .../css/notifications.css | 62 +++++++++++++++++++ .../js/notifications.js | 60 +++++++++++++----- .../templates/admin/base_site.html | 11 ++++ openwisp_notifications/types.py | 16 ++++- 5 files changed, 133 insertions(+), 17 deletions(-) diff --git a/openwisp_notifications/api/serializers.py b/openwisp_notifications/api/serializers.py index 4303d4bf..c5060a42 100644 --- a/openwisp_notifications/api/serializers.py +++ b/openwisp_notifications/api/serializers.py @@ -59,6 +59,7 @@ class Meta(NotificationSerializer.Meta): fields = [ 'id', 'message', + 'description', 'unread', 'target_url', 'email_subject', diff --git a/openwisp_notifications/static/openwisp-notifications/css/notifications.css b/openwisp_notifications/static/openwisp-notifications/css/notifications.css index 90b49de8..52409236 100644 --- a/openwisp_notifications/static/openwisp-notifications/css/notifications.css +++ b/openwisp_notifications/static/openwisp-notifications/css/notifications.css @@ -5,6 +5,68 @@ border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; } +.ow-dialog-overlay { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + transition: opacity 0.3s; +} +.ow-dialog-notifications { + position: relative; + background-color: white; + padding: 20px; + padding-top: 40px; + border-radius: 10px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 500px; + text-align: left; +} +.ow-dialog-close-x { + color: #333; + cursor: pointer; + font-size: 1.25em; + position: absolute; + display: block; + font-weight: bold; + top: 10px; + right: 10px; +} +.ow-dialog-close-x:hover { + color: #e04343; +} +.ow-message-title { + color: #333; + margin-bottom: 10px; +} +.ow-message-title a { + color: #df5d43 !important; + font-weight: bold; +} +.ow-message-description { + margin-bottom: 20px; + line-height: 1.6; + color: #666; +} +.ow-dialog-buttons button { + margin-right: 10px; + background-color: #007bff; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} +.ow-dialog-buttons button:hover { + background-color: #0056b3; +} /* Notification bell */ .ow-notifications { diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 16fa3312..4ecbc62e 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -87,6 +87,21 @@ function initNotificationDropDown($) { }); } +// Used to convert absolute URLs in notification messages to relative paths +function convertMessageWithRelativeURL(htmlString) { + const parser = new DOMParser(), + doc = parser.parseFromString(htmlString, 'text/html'), + links = doc.querySelectorAll('a'); + links.forEach((link) => { + let url = link.getAttribute('href'); + if (url) { + url = new URL(url); + link.setAttribute('href', url.pathname); + } + }); + return doc.body.innerHTML; +} + function notificationWidget($) { let nextPageUrl = getAbsoluteUrl('/api/v1/notifications/notification/'), @@ -208,21 +223,6 @@ function notificationWidget($) { } klass = notificationReadStatus.get(elem.id); - // Used to convert absolute URLs in notification messages to relative paths - function convertMessageWithRelativeURL(htmlString) { - const parser = new DOMParser(), - doc = parser.parseFromString(htmlString, 'text/html'), - links = doc.querySelectorAll('a'); - links.forEach((link) => { - let url = link.getAttribute('href'); - if (url) { - url = new URL(url); - link.setAttribute('href', url.pathname); - } - }); - return doc.body.innerHTML; - } - return `
@@ -324,7 +324,35 @@ function notificationWidget($) { if (elem.hasClass('unread')) { markNotificationRead(elem.get(0)); } - window.location = elem.data('location'); + + var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); + if (notification.description) { + document.querySelector('.ow-message-title').innerHTML = convertMessageWithRelativeURL(notification.message); + document.querySelector('.ow-message-description').textContent = notification.description; + $('.ow-dialog-overlay').removeClass('ow-hide'); + if (notification.target_url) { + var target_url = new URL(notification.target_url); + console.log(target_url.pathname) + $(document).on('click', '.ow-message-target-redirect', function () { + window.location = target_url.pathname + }); + $('.ow-message-target-redirect').removeClass('ow-hide'); + } + } else { + window.location = elem.data('location'); + } + console.log(fetchedPages) + }); + + // Close dialog on click, keypress or esc + $('.ow-dialog-close').on('click keypress', function (e) { + if (e.type === 'keypress' && e.which !== 13 && e.which !== 27) { + return; + } + $('.ow-dialog-overlay').addClass('ow-hide'); + if (!$('.ow-message-target-redirect').hasClass('ow-hide')) { + $('.ow-message-target-redirect').addClass('ow-hide'); + } }); // Handler for marking notification as read on mouseout event diff --git a/openwisp_notifications/templates/admin/base_site.html b/openwisp_notifications/templates/admin/base_site.html index 94150ec8..c4cb32d6 100644 --- a/openwisp_notifications/templates/admin/base_site.html +++ b/openwisp_notifications/templates/admin/base_site.html @@ -34,6 +34,17 @@

{% trans 'No new notification.' %}

+
+
+ × +

+
+
+ + +
+
+
{% endblock %} diff --git a/openwisp_notifications/types.py b/openwisp_notifications/types.py index 88a5f436..a02ca9c3 100644 --- a/openwisp_notifications/types.py +++ b/openwisp_notifications/types.py @@ -17,9 +17,23 @@ 'email_notification': True, 'web_notification': True, }, + 'general_message': { + 'level': 'info', + 'verb': 'message verb', + 'verbose_name': 'Message Type', + 'email_subject': '[{site.name}] Message Notification Subject', + 'message': ( + 'Message notification with {notification.verb} and level {notification.level}' + ' by [{notification.actor}]({notification.actor_link})' + ), + 'description': '{notification.description}', + 'message_template': 'openwisp_notifications/default_message.md', + 'email_notification': False, + 'web_notification': True, + }, } -NOTIFICATION_CHOICES = [('default', 'Default Type')] +NOTIFICATION_CHOICES = [('default', 'Default Type'), ('general_message', 'General Message Type')] NOTIFICATION_ASSOCIATED_MODELS = set() From 1025bfbececbd91f78dbb521cf116b84f481c1b6 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Tue, 14 May 2024 17:26:28 +0530 Subject: [PATCH 02/32] [qa] Fix --- .../static/openwisp-notifications/js/notifications.js | 4 +--- openwisp_notifications/types.py | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 4ecbc62e..70b6a1f9 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -332,16 +332,14 @@ function notificationWidget($) { $('.ow-dialog-overlay').removeClass('ow-hide'); if (notification.target_url) { var target_url = new URL(notification.target_url); - console.log(target_url.pathname) $(document).on('click', '.ow-message-target-redirect', function () { - window.location = target_url.pathname + window.location = target_url.pathname; }); $('.ow-message-target-redirect').removeClass('ow-hide'); } } else { window.location = elem.data('location'); } - console.log(fetchedPages) }); // Close dialog on click, keypress or esc diff --git a/openwisp_notifications/types.py b/openwisp_notifications/types.py index a02ca9c3..abadb8ee 100644 --- a/openwisp_notifications/types.py +++ b/openwisp_notifications/types.py @@ -33,7 +33,10 @@ }, } -NOTIFICATION_CHOICES = [('default', 'Default Type'), ('general_message', 'General Message Type')] +NOTIFICATION_CHOICES = [ + ('default', 'Default Type'), + ('general_message', 'General Message Type'), +] NOTIFICATION_ASSOCIATED_MODELS = set() From 2605a09ac62e84c5fd4f237f580ad2e186e48196 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Tue, 14 May 2024 18:42:32 +0530 Subject: [PATCH 03/32] [chore] Add trans --- openwisp_notifications/templates/admin/base_site.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openwisp_notifications/templates/admin/base_site.html b/openwisp_notifications/templates/admin/base_site.html index c4cb32d6..c311f962 100644 --- a/openwisp_notifications/templates/admin/base_site.html +++ b/openwisp_notifications/templates/admin/base_site.html @@ -40,8 +40,8 @@

- - + +
From a15a2840be5ddb21943ca3e58a79464e0a95c4e2 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Mon, 20 May 2024 16:48:53 +0530 Subject: [PATCH 04/32] [chore] Increase page_size --- openwisp_notifications/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openwisp_notifications/tests/test_api.py b/openwisp_notifications/tests/test_api.py index 52b2a87f..5d5481aa 100644 --- a/openwisp_notifications/tests/test_api.py +++ b/openwisp_notifications/tests/test_api.py @@ -533,7 +533,7 @@ def test_notification_setting_list_api(self): self.assertEqual(len(response.data['results']), number_of_settings) with self.subTest('Test "page_size" query'): - page_size = 1 + page_size = 2 url = f'{url}?page_size={page_size}' response = self.client.get(url) self.assertEqual(response.status_code, 200) From 3a0377d565456f551248a9cf1cf987225c02cc01 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Wed, 22 May 2024 20:55:26 +0530 Subject: [PATCH 05/32] [chore] Fix relative URL error --- .../static/openwisp-notifications/js/notifications.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 70b6a1f9..66cf938c 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -212,7 +212,7 @@ function notificationWidget($) { function notificationListItem(elem) { let klass; const datetime = dateTimeStampToDateTimeLocaleString(new Date(elem.timestamp)), - target_url = new URL(elem.target_url); + target_url = new URL(elem.target_url, window.location.href); if (!notificationReadStatus.has(elem.id)) { if (elem.unread) { @@ -331,7 +331,7 @@ function notificationWidget($) { document.querySelector('.ow-message-description').textContent = notification.description; $('.ow-dialog-overlay').removeClass('ow-hide'); if (notification.target_url) { - var target_url = new URL(notification.target_url); + var target_url = new URL(notification.target_url, window.location.href); $(document).on('click', '.ow-message-target-redirect', function () { window.location = target_url.pathname; }); From 2be703759634d6b8592d5b3f3c347eec53be5ef3 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Thu, 23 May 2024 22:23:44 +0530 Subject: [PATCH 06/32] [chore] Improvements --- openwisp_notifications/base/models.py | 4 +- openwisp_notifications/handlers.py | 4 +- .../css/notifications.css | 27 ++++----- .../js/notifications.js | 24 +++++--- .../templates/admin/base_site.html | 11 ++-- openwisp_notifications/tests/test_widget.py | 59 +++++++++++++------ openwisp_notifications/types.py | 8 +-- 7 files changed, 83 insertions(+), 54 deletions(-) diff --git a/openwisp_notifications/base/models.py b/openwisp_notifications/base/models.py index bbcf3793..8b5fb871 100644 --- a/openwisp_notifications/base/models.py +++ b/openwisp_notifications/base/models.py @@ -120,7 +120,9 @@ def get_message(self, email_message=False): try: config = get_notification_configuration(self.type) data = self.data or {} - if 'message' in config: + if 'message' in data: + md_text = data['message'] + elif 'message' in config: md_text = config['message'].format(notification=self, **data) else: md_text = render_to_string( diff --git a/openwisp_notifications/handlers.py b/openwisp_notifications/handlers.py index 8f434374..5482c879 100644 --- a/openwisp_notifications/handlers.py +++ b/openwisp_notifications/handlers.py @@ -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 diff --git a/openwisp_notifications/static/openwisp-notifications/css/notifications.css b/openwisp_notifications/static/openwisp-notifications/css/notifications.css index 52409236..e1b5bd2f 100644 --- a/openwisp_notifications/static/openwisp-notifications/css/notifications.css +++ b/openwisp_notifications/static/openwisp-notifications/css/notifications.css @@ -17,7 +17,7 @@ align-items: center; transition: opacity 0.3s; } -.ow-dialog-notifications { +.ow-dialog-notification { position: relative; background-color: white; padding: 20px; @@ -28,10 +28,19 @@ max-width: 500px; text-align: left; } +.ow-dialog-notification-level-wrapper { + display: flex; + justify-content: space-between; + color: #777; +} +.ow-dialog-notification .icon { + min-height: 15; + min-width: 15px; +} .ow-dialog-close-x { color: #333; cursor: pointer; - font-size: 1.25em; + font-size: 1.75em; position: absolute; display: block; font-weight: bold; @@ -54,19 +63,6 @@ line-height: 1.6; color: #666; } -.ow-dialog-buttons button { - margin-right: 10px; - background-color: #007bff; - color: white; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; -} -.ow-dialog-buttons button:hover { - background-color: #0056b3; -} /* Notification bell */ .ow-notifications { @@ -319,6 +315,7 @@ .ow-notification-level-text { padding: 0px 6px; text-transform: uppercase; + font-weight: bold; } .ow-notification-elem .icon, .ow-notification-toast .icon { diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 66cf938c..0c5e05d4 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -95,7 +95,7 @@ function convertMessageWithRelativeURL(htmlString) { links.forEach((link) => { let url = link.getAttribute('href'); if (url) { - url = new URL(url); + url = new URL(url, window.location.href); link.setAttribute('href', url.pathname); } }); @@ -227,13 +227,13 @@ function notificationWidget($) { data-location="${target_url.pathname}" role="link" tabindex="0">
-
-
-
${elem.level}
-
-
${datetime}
+
+
+
${elem.level}
+
+
${datetime}
- ${convertMessageWithRelativeURL(elem.message)} + ${elem.description ? elem.message.replace(/]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)}
`; } @@ -327,8 +327,16 @@ function notificationWidget($) { var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); if (notification.description) { + const datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp)); + document.querySelector('.ow-dialog-notification-level-wrapper').innerHTML = ` +
+
+
${notification.level}
+
+
${datetime}
+ `; document.querySelector('.ow-message-title').innerHTML = convertMessageWithRelativeURL(notification.message); - document.querySelector('.ow-message-description').textContent = notification.description; + document.querySelector('.ow-message-description').innerHTML = notification.description; $('.ow-dialog-overlay').removeClass('ow-hide'); if (notification.target_url) { var target_url = new URL(notification.target_url, window.location.href); diff --git a/openwisp_notifications/templates/admin/base_site.html b/openwisp_notifications/templates/admin/base_site.html index c311f962..0c62d533 100644 --- a/openwisp_notifications/templates/admin/base_site.html +++ b/openwisp_notifications/templates/admin/base_site.html @@ -34,15 +34,14 @@

{% trans 'No new notification.' %}

-
-
+
+
× +

-
- - -
+ +
diff --git a/openwisp_notifications/tests/test_widget.py b/openwisp_notifications/tests/test_widget.py index 0fc011ca..d8e1d256 100644 --- a/openwisp_notifications/tests/test_widget.py +++ b/openwisp_notifications/tests/test_widget.py @@ -1,8 +1,11 @@ +import time + from django.contrib.staticfiles.testing import StaticLiveServerTestCase from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait +from openwisp_notifications.signals import notify from openwisp_notifications.swapper import load_model from openwisp_notifications.utils import _get_object_link from openwisp_users.tests.utils import TestOrganizationMixin @@ -22,27 +25,23 @@ def setUp(self): self.admin = self._create_admin( username=self.admin_username, password=self.admin_password ) - - def test_notification_relative_link(self): - self.login() - operator = super()._create_operator() - data = dict( - email_subject='Test Email subject', - url='http://localhost:8000/admin/', - ) - notification = Notification.objects.create( - actor=self.admin, + self.operator = super()._get_operator() + self.notification_options = dict( + sender=self.admin, recipient=self.admin, - description='Test Notification Description', verb='Test Notification', - action_object=operator, - target=operator, - data=data, - ) - self.web_driver.implicitly_wait(10) - WebDriverWait(self.web_driver, 10).until( - EC.visibility_of_element_located((By.ID, 'openwisp_notifications')) + email_subject='Test Email subject', + action_object=self.operator, + target=self.operator, + type='default', ) + + def _create_notification(self): + return notify.send(**self.notification_options) + + def test_notification_relative_link(self): + self.login() + notification = self._create_notification().pop()[1][0] self.web_driver.find_element(By.ID, 'openwisp_notifications').click() WebDriverWait(self.web_driver, 10).until( EC.visibility_of_element_located((By.CLASS_NAME, 'ow-notification-elem')) @@ -54,3 +53,27 @@ def test_notification_relative_link(self): self.assertEqual( data_location_value, _get_object_link(notification, 'target', False) ) + + def test_notification_dialog(self): + self.login() + self.notification_options.update( + {'message': 'Test Message', 'description': 'Test Description'} + ) + notification = self._create_notification().pop()[1][0] + self.web_driver.find_element(By.ID, 'openwisp_notifications').click() + time.sleep(4) + WebDriverWait(self.web_driver, 10).until( + EC.visibility_of_element_located((By.ID, f'ow-{notification.id}')) + ) + self.web_driver.find_element(By.ID, f'ow-{notification.id}').click() + WebDriverWait(self.web_driver, 10).until( + EC.visibility_of_element_located((By.CLASS_NAME, 'ow-dialog-notification')) + ) + dialog = self.web_driver.find_element(By.CLASS_NAME, 'ow-dialog-notification') + self.assertEqual( + dialog.find_element(By.CLASS_NAME, 'ow-message-title').text, 'Test Message' + ) + self.assertEqual( + dialog.find_element(By.CLASS_NAME, 'ow-message-description').text, + 'Test Description', + ) diff --git a/openwisp_notifications/types.py b/openwisp_notifications/types.py index abadb8ee..3b4eafe3 100644 --- a/openwisp_notifications/types.py +++ b/openwisp_notifications/types.py @@ -19,11 +19,11 @@ }, 'general_message': { 'level': 'info', - 'verb': 'message verb', - 'verbose_name': 'Message Type', - 'email_subject': '[{site.name}] Message Notification Subject', + 'verb': 'generic verb', + 'verbose_name': 'Generic Type', + 'email_subject': '[{site.name}] Generic Notification Subject', 'message': ( - 'Message notification with {notification.verb} and level {notification.level}' + 'Generic notification with {notification.verb} and level {notification.level}' ' by [{notification.actor}]({notification.actor_link})' ), 'description': '{notification.description}', From 581cc90e6af0b55f0accf5619b3dd3ff9b41f853 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Thu, 23 May 2024 22:53:21 +0530 Subject: [PATCH 07/32] [chore] Show description with md support --- openwisp_notifications/api/serializers.py | 2 +- openwisp_notifications/base/models.py | 4 ++++ .../static/openwisp-notifications/js/notifications.js | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openwisp_notifications/api/serializers.py b/openwisp_notifications/api/serializers.py index c5060a42..2f843a2d 100644 --- a/openwisp_notifications/api/serializers.py +++ b/openwisp_notifications/api/serializers.py @@ -59,7 +59,7 @@ class Meta(NotificationSerializer.Meta): fields = [ 'id', 'message', - 'description', + 'get_description', 'unread', 'target_url', 'email_subject', diff --git a/openwisp_notifications/base/models.py b/openwisp_notifications/base/models.py index 8b5fb871..82e346a8 100644 --- a/openwisp_notifications/base/models.py +++ b/openwisp_notifications/base/models.py @@ -105,6 +105,10 @@ def target_url(self): def message(self): return self.get_message() + @cached_property + def get_description(self): + return mark_safe(markdown(self.description)) + @property def email_message(self): return self.get_message(email_message=True) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 0c5e05d4..515ca078 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -233,7 +233,7 @@ function notificationWidget($) {
${datetime}
- ${elem.description ? elem.message.replace(/
]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)} + ${elem.get_description ? elem.message.replace(/]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)} `; } @@ -326,7 +326,7 @@ function notificationWidget($) { } var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); - if (notification.description) { + if (notification.get_description) { const datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp)); document.querySelector('.ow-dialog-notification-level-wrapper').innerHTML = `
@@ -336,7 +336,7 @@ function notificationWidget($) {
${datetime}
`; document.querySelector('.ow-message-title').innerHTML = convertMessageWithRelativeURL(notification.message); - document.querySelector('.ow-message-description').innerHTML = notification.description; + document.querySelector('.ow-message-description').innerHTML = notification.get_description; $('.ow-dialog-overlay').removeClass('ow-hide'); if (notification.target_url) { var target_url = new URL(notification.target_url, window.location.href); From a7c792d49a2e03cd75cafdaa25b48d91bb250091 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Thu, 23 May 2024 22:54:48 +0530 Subject: [PATCH 08/32] [chore] Rename general to generic message types --- openwisp_notifications/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openwisp_notifications/types.py b/openwisp_notifications/types.py index 3b4eafe3..2ae7a4e9 100644 --- a/openwisp_notifications/types.py +++ b/openwisp_notifications/types.py @@ -17,7 +17,7 @@ 'email_notification': True, 'web_notification': True, }, - 'general_message': { + 'generic_message': { 'level': 'info', 'verb': 'generic verb', 'verbose_name': 'Generic Type', @@ -35,7 +35,7 @@ NOTIFICATION_CHOICES = [ ('default', 'Default Type'), - ('general_message', 'General Message Type'), + ('generic_message', 'Generic Message Type'), ] NOTIFICATION_ASSOCIATED_MODELS = set() From 9e7c1fcd527f8e61c7092fe2847a2043c544d370 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 25 May 2024 23:46:46 +0530 Subject: [PATCH 09/32] [chore] Fix menu bar --- .../css/notifications.css | 1 + .../js/notifications.js | 45 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/css/notifications.css b/openwisp_notifications/static/openwisp-notifications/css/notifications.css index e1b5bd2f..61f7fb4e 100644 --- a/openwisp_notifications/static/openwisp-notifications/css/notifications.css +++ b/openwisp_notifications/static/openwisp-notifications/css/notifications.css @@ -16,6 +16,7 @@ justify-content: center; align-items: center; transition: opacity 0.3s; + z-index: 9999; } .ow-dialog-notification { position: relative; diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 515ca078..34895fe8 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -2,6 +2,7 @@ const notificationReadStatus = new Map(); const userLanguage = navigator.language || navigator.userLanguage; const owWindowId = String(Date.now()); +var isNotificationDialogOpen = false; if (typeof gettext === 'undefined') { var gettext = function(word){ return word; }; @@ -59,7 +60,9 @@ function initNotificationDropDown($) { // Check if the clicked area is dropDown / notification-btn or not if ( $('.ow-notification-dropdown').has(e.target).length === 0 && - !$(e.target).is($('.ow-notifications')) + !$(e.target).is($('.ow-notifications')) && + !$(e.target).is($('.ow-dialog-close')) && + !isNotificationDialogOpen ) { $('.ow-notification-dropdown').addClass('ow-hide'); } @@ -69,7 +72,10 @@ function initNotificationDropDown($) { $(document).focusin(function(e){ // Hide notification widget if focus is shifted to an element outside it e.stopPropagation(); - if ($('.ow-notification-dropdown').has(e.target).length === 0){ + if ( + $('.ow-notification-dropdown').has(e.target).length === 0 && + !isNotificationDialogOpen + ) { // Don't hide if focus changes to notification bell icon if (e.target != $('#openwisp_notifications').get(0)) { $('.ow-notification-dropdown').addClass('ow-hide'); @@ -77,12 +83,20 @@ function initNotificationDropDown($) { } }); - $('.ow-notification-dropdown').on('keyup', '*', function(e){ + $(document).on('keyup', '*', function(e){ e.stopPropagation(); // Hide notification widget on "Escape" key - if (e.keyCode == 27){ - $('.ow-notification-dropdown').addClass('ow-hide'); - $('#openwisp_notifications').focus(); + if (e.keyCode == 27) { + if (isNotificationDialogOpen) { + $('.ow-dialog-overlay').addClass('ow-hide'); + $('.ow-message-target-redirect').addClass('ow-hide'); + isNotificationDialogOpen = false; + } else { + $('.ow-notification-dropdown').addClass('ow-hide'); + } + if ($(e.target).is($('.ow-notification-dropdown'))) { + $('#openwisp_notifications').focus(); + } } }); } @@ -325,8 +339,22 @@ function notificationWidget($) { markNotificationRead(elem.get(0)); } + var container = document.querySelector('#container'); + // Check if the container exists and does not already have a direct child with the class '.ow-dialog-overlay' + if (container && !Array.from(container.children).some(child => child.classList.contains('ow-dialog-overlay'))) { + const overlayElement = document.querySelector('.ow-dialog-overlay'); + + if (overlayElement) { + // Remove the overlay element from its current parent + overlayElement.parentNode.removeChild(overlayElement); + // Append the overlay element to the container + container.appendChild(overlayElement); + } + } + var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); if (notification.get_description) { + isNotificationDialogOpen = true; const datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp)); document.querySelector('.ow-dialog-notification-level-wrapper').innerHTML = `
@@ -355,10 +383,9 @@ function notificationWidget($) { if (e.type === 'keypress' && e.which !== 13 && e.which !== 27) { return; } + isNotificationDialogOpen = false; $('.ow-dialog-overlay').addClass('ow-hide'); - if (!$('.ow-message-target-redirect').hasClass('ow-hide')) { - $('.ow-message-target-redirect').addClass('ow-hide'); - } + $('.ow-message-target-redirect').addClass('ow-hide'); }); // Handler for marking notification as read on mouseout event From 2608c110a2852f27df93cdfbe1604a23d996039f Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 25 May 2024 23:48:14 +0530 Subject: [PATCH 10/32] [chore] Rename rendered_description --- openwisp_notifications/api/serializers.py | 2 +- openwisp_notifications/base/models.py | 2 +- .../static/openwisp-notifications/js/notifications.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openwisp_notifications/api/serializers.py b/openwisp_notifications/api/serializers.py index 2f843a2d..2a74301a 100644 --- a/openwisp_notifications/api/serializers.py +++ b/openwisp_notifications/api/serializers.py @@ -59,7 +59,7 @@ class Meta(NotificationSerializer.Meta): fields = [ 'id', 'message', - 'get_description', + 'rendered_description', 'unread', 'target_url', 'email_subject', diff --git a/openwisp_notifications/base/models.py b/openwisp_notifications/base/models.py index 82e346a8..8d9b6e2d 100644 --- a/openwisp_notifications/base/models.py +++ b/openwisp_notifications/base/models.py @@ -106,7 +106,7 @@ def message(self): return self.get_message() @cached_property - def get_description(self): + def rendered_description(self): return mark_safe(markdown(self.description)) @property diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 34895fe8..dd2560fd 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -247,7 +247,7 @@ function notificationWidget($) {
${datetime}
- ${elem.get_description ? elem.message.replace(/
]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)} + ${elem.rendered_description ? elem.message.replace(/]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)} `; } @@ -353,7 +353,7 @@ function notificationWidget($) { } var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); - if (notification.get_description) { + if (notification.rendered_description) { isNotificationDialogOpen = true; const datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp)); document.querySelector('.ow-dialog-notification-level-wrapper').innerHTML = ` @@ -364,7 +364,7 @@ function notificationWidget($) {
${datetime}
`; document.querySelector('.ow-message-title').innerHTML = convertMessageWithRelativeURL(notification.message); - document.querySelector('.ow-message-description').innerHTML = notification.get_description; + document.querySelector('.ow-message-description').innerHTML = notification.rendered_description; $('.ow-dialog-overlay').removeClass('ow-hide'); if (notification.target_url) { var target_url = new URL(notification.target_url, window.location.href); From 2a922a6e3121ca62472ccf1e95c33de402fa555d Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 25 May 2024 23:48:49 +0530 Subject: [PATCH 11/32] [chore] Fix test --- openwisp_notifications/tests/test_widget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openwisp_notifications/tests/test_widget.py b/openwisp_notifications/tests/test_widget.py index d8e1d256..d1d9d568 100644 --- a/openwisp_notifications/tests/test_widget.py +++ b/openwisp_notifications/tests/test_widget.py @@ -66,6 +66,7 @@ def test_notification_dialog(self): EC.visibility_of_element_located((By.ID, f'ow-{notification.id}')) ) self.web_driver.find_element(By.ID, f'ow-{notification.id}').click() + self.web_driver.find_element(By.ID, f'ow-{notification.id}').click() WebDriverWait(self.web_driver, 10).until( EC.visibility_of_element_located((By.CLASS_NAME, 'ow-dialog-notification')) ) From 0686c2d1d70aaae242662ca0b4094e3e224edecb Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sun, 26 May 2024 16:28:29 +0530 Subject: [PATCH 12/32] [chore] Just use description --- openwisp_notifications/api/serializers.py | 4 +++- .../static/openwisp-notifications/js/notifications.js | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openwisp_notifications/api/serializers.py b/openwisp_notifications/api/serializers.py index 2a74301a..d4dbd41d 100644 --- a/openwisp_notifications/api/serializers.py +++ b/openwisp_notifications/api/serializers.py @@ -55,11 +55,13 @@ def data(self): class NotificationListSerializer(NotificationSerializer): + description = serializers.CharField(source='rendered_description') + class Meta(NotificationSerializer.Meta): fields = [ 'id', 'message', - 'rendered_description', + 'description', 'unread', 'target_url', 'email_subject', diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index dd2560fd..ea8ca691 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -247,7 +247,7 @@ function notificationWidget($) {
${datetime}
- ${elem.rendered_description ? elem.message.replace(/
]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)} + ${elem.description ? elem.message.replace(/]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)} `; } @@ -353,7 +353,7 @@ function notificationWidget($) { } var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); - if (notification.rendered_description) { + if (notification.description) { isNotificationDialogOpen = true; const datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp)); document.querySelector('.ow-dialog-notification-level-wrapper').innerHTML = ` @@ -364,7 +364,7 @@ function notificationWidget($) {
${datetime}
`; document.querySelector('.ow-message-title').innerHTML = convertMessageWithRelativeURL(notification.message); - document.querySelector('.ow-message-description').innerHTML = notification.rendered_description; + document.querySelector('.ow-message-description').innerHTML = notification.description; $('.ow-dialog-overlay').removeClass('ow-hide'); if (notification.target_url) { var target_url = new URL(notification.target_url, window.location.href); From 8106f3e40ab437fc7c8b6846c455c56ffb1bde4b Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sun, 26 May 2024 16:35:39 +0530 Subject: [PATCH 13/32] [fix] Rendered description shows error for None description --- openwisp_notifications/base/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openwisp_notifications/base/models.py b/openwisp_notifications/base/models.py index 8d9b6e2d..905c7645 100644 --- a/openwisp_notifications/base/models.py +++ b/openwisp_notifications/base/models.py @@ -107,7 +107,7 @@ def message(self): @cached_property def rendered_description(self): - return mark_safe(markdown(self.description)) + return mark_safe(markdown(self.description)) if self.description else None @property def email_message(self): From cc528df039d73ff6cb5f6c797e1e33593eb9e7ea Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sun, 26 May 2024 16:42:39 +0530 Subject: [PATCH 14/32] [fix] Level icon repeat --- .../static/openwisp-notifications/css/notifications.css | 1 + 1 file changed, 1 insertion(+) diff --git a/openwisp_notifications/static/openwisp-notifications/css/notifications.css b/openwisp_notifications/static/openwisp-notifications/css/notifications.css index 61f7fb4e..204b710a 100644 --- a/openwisp_notifications/static/openwisp-notifications/css/notifications.css +++ b/openwisp_notifications/static/openwisp-notifications/css/notifications.css @@ -37,6 +37,7 @@ .ow-dialog-notification .icon { min-height: 15; min-width: 15px; + background-repeat: no-repeat; } .ow-dialog-close-x { color: #333; From aca39bc850fc26c6aec9ecaea6d10a73b373cd4a Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sun, 26 May 2024 16:43:21 +0530 Subject: [PATCH 15/32] [chore] JQuery way --- .../js/notifications.js | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index ea8ca691..984c1bf2 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -14,6 +14,7 @@ if (typeof gettext === 'undefined') { initNotificationDropDown($); initWebSockets($); owNotificationWindow.init($); + moveNotificationDialog($); }); })(django.jQuery); @@ -101,6 +102,15 @@ function initNotificationDropDown($) { }); } +function moveNotificationDialog($) { + var $container = $('#container'); + var $overlayElement = $('.ow-dialog-overlay'); + // Remove the overlay element from its current parent + $overlayElement.detach(); + // Append the overlay element to the container + $container.append($overlayElement); +} + // Used to convert absolute URLs in notification messages to relative paths function convertMessageWithRelativeURL(htmlString) { const parser = new DOMParser(), @@ -339,19 +349,6 @@ function notificationWidget($) { markNotificationRead(elem.get(0)); } - var container = document.querySelector('#container'); - // Check if the container exists and does not already have a direct child with the class '.ow-dialog-overlay' - if (container && !Array.from(container.children).some(child => child.classList.contains('ow-dialog-overlay'))) { - const overlayElement = document.querySelector('.ow-dialog-overlay'); - - if (overlayElement) { - // Remove the overlay element from its current parent - overlayElement.parentNode.removeChild(overlayElement); - // Append the overlay element to the container - container.appendChild(overlayElement); - } - } - var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); if (notification.description) { isNotificationDialogOpen = true; From 1e6b840eef72089a690cd7608a4f04db64251a33 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Mon, 27 May 2024 23:02:33 +0530 Subject: [PATCH 16/32] [fix] Fixed tests --- openwisp_notifications/tests/test_notifications.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openwisp_notifications/tests/test_notifications.py b/openwisp_notifications/tests/test_notifications.py index 87ef23a2..2154f502 100644 --- a/openwisp_notifications/tests/test_notifications.py +++ b/openwisp_notifications/tests/test_notifications.py @@ -60,7 +60,6 @@ def setUp(self): self.notification_options = dict( sender=self.admin, description='Test Notification', - level='info', verb='Test Notification', email_subject='Test Email subject', url='https://localhost:8000/admin', From 00ec345fe5b0736c20c841dd6eb7ae915ab892a7 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Tue, 28 May 2024 01:07:10 +0530 Subject: [PATCH 17/32] [chore] Bump changes --- .../js/notifications.js | 88 +++++++++---------- .../templates/admin/base_site.html | 20 ++--- openwisp_notifications/tests/test_widget.py | 5 +- openwisp_notifications/types.py | 1 - 4 files changed, 55 insertions(+), 59 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 984c1bf2..bfe93388 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -2,7 +2,6 @@ const notificationReadStatus = new Map(); const userLanguage = navigator.language || navigator.userLanguage; const owWindowId = String(Date.now()); -var isNotificationDialogOpen = false; if (typeof gettext === 'undefined') { var gettext = function(word){ return word; }; @@ -14,7 +13,6 @@ if (typeof gettext === 'undefined') { initNotificationDropDown($); initWebSockets($); owNotificationWindow.init($); - moveNotificationDialog($); }); })(django.jQuery); @@ -58,12 +56,15 @@ function initNotificationDropDown($) { $(document).click(function (e) { e.stopPropagation(); - // Check if the clicked area is dropDown / notification-btn or not if ( + // Check if the clicked area is dropDown $('.ow-notification-dropdown').has(e.target).length === 0 && + // Check notification-btn or not !$(e.target).is($('.ow-notifications')) && + // Hide the notification dropdown when a click occurs outside of it !$(e.target).is($('.ow-dialog-close')) && - !isNotificationDialogOpen + // Do not hide if the user is interacting with the notification dialog + !$('.ow-dialog-overlay').is(':visible') ) { $('.ow-notification-dropdown').addClass('ow-hide'); } @@ -75,7 +76,8 @@ function initNotificationDropDown($) { e.stopPropagation(); if ( $('.ow-notification-dropdown').has(e.target).length === 0 && - !isNotificationDialogOpen + // Do not hide if the user is interacting with the notification dialog + !$('.ow-dialog-overlay').is(':visible') ) { // Don't hide if focus changes to notification bell icon if (e.target != $('#openwisp_notifications').get(0)) { @@ -86,31 +88,22 @@ function initNotificationDropDown($) { $(document).on('keyup', '*', function(e){ e.stopPropagation(); + if (e.keyCode !== 27) { + return; + } // Hide notification widget on "Escape" key - if (e.keyCode == 27) { - if (isNotificationDialogOpen) { - $('.ow-dialog-overlay').addClass('ow-hide'); - $('.ow-message-target-redirect').addClass('ow-hide'); - isNotificationDialogOpen = false; - } else { - $('.ow-notification-dropdown').addClass('ow-hide'); - } - if ($(e.target).is($('.ow-notification-dropdown'))) { - $('#openwisp_notifications').focus(); - } + if ($('.ow-dialog-overlay').is(':visible')) { + $('.ow-dialog-overlay').addClass('ow-hide'); + $('.ow-message-target-redirect').addClass('ow-hide'); + } else { + $('.ow-notification-dropdown').addClass('ow-hide'); + } + if ($(e.target).is($('.ow-notification-dropdown'))) { + $('#openwisp_notifications').focus(); } }); } -function moveNotificationDialog($) { - var $container = $('#container'); - var $overlayElement = $('.ow-dialog-overlay'); - // Remove the overlay element from its current parent - $overlayElement.detach(); - // Append the overlay element to the container - $container.append($overlayElement); -} - // Used to convert absolute URLs in notification messages to relative paths function convertMessageWithRelativeURL(htmlString) { const parser = new DOMParser(), @@ -236,7 +229,8 @@ function notificationWidget($) { function notificationListItem(elem) { let klass; const datetime = dateTimeStampToDateTimeLocaleString(new Date(elem.timestamp)), - target_url = new URL(elem.target_url, window.location.href); + // target_url can be null or '#', so we need to handle it without any errors + target_url = new URL(elem.target_url, window.location.href); if (!notificationReadStatus.has(elem.id)) { if (elem.unread) { @@ -247,6 +241,7 @@ function notificationWidget($) { } klass = notificationReadStatus.get(elem.id); + // Remove hyperlinks from notification messages if description is present return `
@@ -257,7 +252,7 @@ function notificationWidget($) {
${datetime}
- ${elem.description ? elem.message.replace(/
]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)} + ${elem.description ? elem.message.replace(/]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)} `; } @@ -344,30 +339,36 @@ function notificationWidget($) { return; } let elem = $(this); + var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); + // If notification is unread then send read request - if (elem.hasClass('unread')) { + if (!notification.description && elem.hasClass('unread')) { markNotificationRead(elem.get(0)); } - var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); if (notification.description) { - isNotificationDialogOpen = true; - const datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp)); - document.querySelector('.ow-dialog-notification-level-wrapper').innerHTML = ` -
-
-
${notification.level}
-
-
${datetime}
- `; - document.querySelector('.ow-message-title').innerHTML = convertMessageWithRelativeURL(notification.message); - document.querySelector('.ow-message-description').innerHTML = notification.description; + var datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp)); + + $('.ow-dialog-notification-level-wrapper').html(` +
+
+
${notification.level}
+
+
${datetime}
+ `); + + $('.ow-message-title').html(convertMessageWithRelativeURL(notification.message)); + $('.ow-message-description').html(notification.description); + $('.ow-dialog-overlay').removeClass('ow-hide'); - if (notification.target_url) { - var target_url = new URL(notification.target_url, window.location.href); - $(document).on('click', '.ow-message-target-redirect', function () { + + if (notification.target_url && notification.target_url !== '#') { + var target_url = new URL(notification.target_url); + + $(document).on('click', '.ow-message-target-redirect', function() { window.location = target_url.pathname; }); + $('.ow-message-target-redirect').removeClass('ow-hide'); } } else { @@ -380,7 +381,6 @@ function notificationWidget($) { if (e.type === 'keypress' && e.which !== 13 && e.which !== 27) { return; } - isNotificationDialogOpen = false; $('.ow-dialog-overlay').addClass('ow-hide'); $('.ow-message-target-redirect').addClass('ow-hide'); }); diff --git a/openwisp_notifications/templates/admin/base_site.html b/openwisp_notifications/templates/admin/base_site.html index 0c62d533..2ef75b84 100644 --- a/openwisp_notifications/templates/admin/base_site.html +++ b/openwisp_notifications/templates/admin/base_site.html @@ -34,20 +34,20 @@

{% trans 'No new notification.' %}

-
-
- × -
-

-
- - -
-
{% endblock %} {% block footer %} +
+
+ × +
+

+
+ + +
+
{{ block.super }} {% if request.user.is_authenticated %} diff --git a/openwisp_notifications/tests/test_widget.py b/openwisp_notifications/tests/test_widget.py index d1d9d568..f977ddea 100644 --- a/openwisp_notifications/tests/test_widget.py +++ b/openwisp_notifications/tests/test_widget.py @@ -1,5 +1,3 @@ -import time - from django.contrib.staticfiles.testing import StaticLiveServerTestCase from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC @@ -61,11 +59,10 @@ def test_notification_dialog(self): ) notification = self._create_notification().pop()[1][0] self.web_driver.find_element(By.ID, 'openwisp_notifications').click() - time.sleep(4) WebDriverWait(self.web_driver, 10).until( EC.visibility_of_element_located((By.ID, f'ow-{notification.id}')) ) - self.web_driver.find_element(By.ID, f'ow-{notification.id}').click() + # self.web_driver.find_element(By.ID, f'ow-{notification.id}').click() self.web_driver.find_element(By.ID, f'ow-{notification.id}').click() WebDriverWait(self.web_driver, 10).until( EC.visibility_of_element_located((By.CLASS_NAME, 'ow-dialog-notification')) diff --git a/openwisp_notifications/types.py b/openwisp_notifications/types.py index 2ae7a4e9..5aac9814 100644 --- a/openwisp_notifications/types.py +++ b/openwisp_notifications/types.py @@ -27,7 +27,6 @@ ' by [{notification.actor}]({notification.actor_link})' ), 'description': '{notification.description}', - 'message_template': 'openwisp_notifications/default_message.md', 'email_notification': False, 'web_notification': True, }, From e69600fb94dc045a199b497b0cc838f5dbb733c0 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Tue, 28 May 2024 01:21:49 +0530 Subject: [PATCH 18/32] [chore] Add tests for notification level --- openwisp_notifications/tests/test_notifications.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openwisp_notifications/tests/test_notifications.py b/openwisp_notifications/tests/test_notifications.py index 2154f502..7227bacd 100644 --- a/openwisp_notifications/tests/test_notifications.py +++ b/openwisp_notifications/tests/test_notifications.py @@ -321,6 +321,13 @@ def test_default_notification_type(self): ) self.assertEqual(n.email_subject, '[example.com] Default Notification Subject') + def test_notification_level_kwarg_precedence(self): + # Create a notification with level kwarg set to 'warning' + self.notification_options.update({'level': 'warning'}) + self._create_notification() + n = notification_queryset.first() + self.assertEqual(n.level, 'warning') + @mock_notification_types def test_misc_notification_type_validation(self): with self.subTest('Registering with incomplete notification configuration.'): From bbff5103790757d50f9af7e1d28e6bc5e67801a9 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Tue, 28 May 2024 01:30:41 +0530 Subject: [PATCH 19/32] [chore] Remove comment --- openwisp_notifications/tests/test_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openwisp_notifications/tests/test_widget.py b/openwisp_notifications/tests/test_widget.py index f977ddea..d601c0e9 100644 --- a/openwisp_notifications/tests/test_widget.py +++ b/openwisp_notifications/tests/test_widget.py @@ -62,7 +62,6 @@ def test_notification_dialog(self): WebDriverWait(self.web_driver, 10).until( EC.visibility_of_element_located((By.ID, f'ow-{notification.id}')) ) - # self.web_driver.find_element(By.ID, f'ow-{notification.id}').click() self.web_driver.find_element(By.ID, f'ow-{notification.id}').click() WebDriverWait(self.web_driver, 10).until( EC.visibility_of_element_located((By.CLASS_NAME, 'ow-dialog-notification')) From df10915391d252ea742ee2ea154a57d73c8a2fd2 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Tue, 28 May 2024 19:42:56 +0530 Subject: [PATCH 20/32] [chore] Focus fix --- .../static/openwisp-notifications/js/notifications.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index bfe93388..17ba2048 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -97,8 +97,6 @@ function initNotificationDropDown($) { $('.ow-message-target-redirect').addClass('ow-hide'); } else { $('.ow-notification-dropdown').addClass('ow-hide'); - } - if ($(e.target).is($('.ow-notification-dropdown'))) { $('#openwisp_notifications').focus(); } }); From 6e5db98d21f694760ce5dde39fb9b6f0afaf0121 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Wed, 29 May 2024 18:45:17 -0400 Subject: [PATCH 21/32] [chores] UI/CSS improvements --- .../css/notifications.css | 120 +++++++++--------- .../js/notifications.js | 12 +- .../templates/admin/base_site.html | 12 +- 3 files changed, 75 insertions(+), 69 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/css/notifications.css b/openwisp_notifications/static/openwisp-notifications/css/notifications.css index 204b710a..a8b27932 100644 --- a/openwisp_notifications/static/openwisp-notifications/css/notifications.css +++ b/openwisp_notifications/static/openwisp-notifications/css/notifications.css @@ -5,66 +5,6 @@ border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; } -.ow-dialog-overlay { - position: fixed; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.6); - display: flex; - justify-content: center; - align-items: center; - transition: opacity 0.3s; - z-index: 9999; -} -.ow-dialog-notification { - position: relative; - background-color: white; - padding: 20px; - padding-top: 40px; - 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-level-wrapper { - display: flex; - justify-content: space-between; - color: #777; -} -.ow-dialog-notification .icon { - min-height: 15; - min-width: 15px; - background-repeat: no-repeat; -} -.ow-dialog-close-x { - color: #333; - cursor: pointer; - font-size: 1.75em; - position: absolute; - display: block; - font-weight: bold; - top: 10px; - right: 10px; -} -.ow-dialog-close-x:hover { - color: #e04343; -} -.ow-message-title { - color: #333; - margin-bottom: 10px; -} -.ow-message-title a { - color: #df5d43 !important; - font-weight: bold; -} -.ow-message-description { - margin-bottom: 20px; - line-height: 1.6; - color: #666; -} /* Notification bell */ .ow-notifications { @@ -361,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%; diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 17ba2048..69b82fbc 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -64,7 +64,7 @@ function initNotificationDropDown($) { // Hide the notification dropdown when a click occurs outside of it !$(e.target).is($('.ow-dialog-close')) && // Do not hide if the user is interacting with the notification dialog - !$('.ow-dialog-overlay').is(':visible') + !$('.ow-overlay-notification').is(':visible') ) { $('.ow-notification-dropdown').addClass('ow-hide'); } @@ -77,7 +77,7 @@ function initNotificationDropDown($) { if ( $('.ow-notification-dropdown').has(e.target).length === 0 && // Do not hide if the user is interacting with the notification dialog - !$('.ow-dialog-overlay').is(':visible') + !$('.ow-overlay-notification').is(':visible') ) { // Don't hide if focus changes to notification bell icon if (e.target != $('#openwisp_notifications').get(0)) { @@ -92,8 +92,8 @@ function initNotificationDropDown($) { return; } // Hide notification widget on "Escape" key - if ($('.ow-dialog-overlay').is(':visible')) { - $('.ow-dialog-overlay').addClass('ow-hide'); + if ($('.ow-overlay-notification').is(':visible')) { + $('.ow-overlay-notification').addClass('ow-hide'); $('.ow-message-target-redirect').addClass('ow-hide'); } else { $('.ow-notification-dropdown').addClass('ow-hide'); @@ -358,7 +358,7 @@ function notificationWidget($) { $('.ow-message-title').html(convertMessageWithRelativeURL(notification.message)); $('.ow-message-description').html(notification.description); - $('.ow-dialog-overlay').removeClass('ow-hide'); + $('.ow-overlay-notification').removeClass('ow-hide'); if (notification.target_url && notification.target_url !== '#') { var target_url = new URL(notification.target_url); @@ -379,7 +379,7 @@ function notificationWidget($) { if (e.type === 'keypress' && e.which !== 13 && e.which !== 27) { return; } - $('.ow-dialog-overlay').addClass('ow-hide'); + $('.ow-overlay-notification').addClass('ow-hide'); $('.ow-message-target-redirect').addClass('ow-hide'); }); diff --git a/openwisp_notifications/templates/admin/base_site.html b/openwisp_notifications/templates/admin/base_site.html index 2ef75b84..4861d9f1 100644 --- a/openwisp_notifications/templates/admin/base_site.html +++ b/openwisp_notifications/templates/admin/base_site.html @@ -38,14 +38,20 @@ {% endblock %} {% block footer %} -
+
×

- - +
+ + +
{{ block.super }} From 6de368f8cd235e45a0d7bb7b3cef69d3e9e32ca1 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Thu, 30 May 2024 17:59:47 +0530 Subject: [PATCH 22/32] [fix] Toast notifications dialog --- .../js/notifications.js | 78 ++++++++++--------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 69b82fbc..0e04f321 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -2,6 +2,7 @@ const notificationReadStatus = new Map(); const userLanguage = navigator.language || navigator.userLanguage; const owWindowId = String(Date.now()); +let fetchedPages = []; if (typeof gettext === 'undefined') { var gettext = function(word){ return word; }; @@ -122,7 +123,6 @@ function notificationWidget($) { let nextPageUrl = getAbsoluteUrl('/api/v1/notifications/notification/'), renderedPages = 2, busy = false, - fetchedPages = [], lastRenderedPage = 0; // 1 based indexing (0 -> no page rendered) @@ -337,41 +337,7 @@ function notificationWidget($) { return; } let elem = $(this); - var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); - - // If notification is unread then send read request - if (!notification.description && elem.hasClass('unread')) { - markNotificationRead(elem.get(0)); - } - - if (notification.description) { - var datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp)); - - $('.ow-dialog-notification-level-wrapper').html(` -
-
-
${notification.level}
-
-
${datetime}
- `); - - $('.ow-message-title').html(convertMessageWithRelativeURL(notification.message)); - $('.ow-message-description').html(notification.description); - - $('.ow-overlay-notification').removeClass('ow-hide'); - - if (notification.target_url && notification.target_url !== '#') { - var target_url = new URL(notification.target_url); - - $(document).on('click', '.ow-message-target-redirect', function() { - window.location = target_url.pathname; - }); - - $('.ow-message-target-redirect').removeClass('ow-hide'); - } - } else { - window.location = elem.data('location'); - } + notificationHandler($, elem); }); // Close dialog on click, keypress or esc @@ -409,6 +375,44 @@ function markNotificationRead(elem) { ); } +function notificationHandler($, elem) { + var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); + + // If notification is unread then send read request + if (!notification.description && elem.hasClass('unread')) { + markNotificationRead(elem.get(0)); + } + + if (notification.description) { + var datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp)); + + $('.ow-dialog-notification-level-wrapper').html(` +
+
+
${notification.level}
+
+
${datetime}
+ `); + + $('.ow-message-title').html(convertMessageWithRelativeURL(notification.message)); + $('.ow-message-description').html(notification.description); + + $('.ow-overlay-notification').removeClass('ow-hide'); + + if (notification.target_url && notification.target_url !== '#') { + var target_url = new URL(notification.target_url); + + $(document).on('click', '.ow-message-target-redirect', function() { + window.location = target_url.pathname; + }); + + $('.ow-message-target-redirect').removeClass('ow-hide'); + } + } else { + window.location = elem.data('location'); + } +} + function initWebSockets($) { notificationSocket.addEventListener('message', function (e) { let data = JSON.parse(e.data); @@ -463,7 +467,7 @@ function initWebSockets($) { // Make toast message clickable $(document).on('click', '.ow-notification-toast', function () { markNotificationRead($(this).get(0)); - window.location = $(this).data('location'); + notificationHandler($, $(this)); }); $(document).on('click', '.ow-notification-toast .ow-notify-close.btn', function (event) { event.stopPropagation(); From b0725b1ae8b43f6214d4bf2ae881eac3370f1292 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 30 May 2024 10:54:18 -0400 Subject: [PATCH 23/32] [fix] Use relative path also for notification toast --- .../js/notifications.js | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 0e04f321..a317e53d 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -376,13 +376,20 @@ function markNotificationRead(elem) { } function notificationHandler($, elem) { - var notification = fetchedPages.flat().find((notification) => notification.id == elem.get(0).id.replace('ow-', '')); + var notification = fetchedPages.flat().find((notification) => + notification.id == elem.get(0).id.replace('ow-', '')), + targetUrl = elem.data('location'); // If notification is unread then send read request if (!notification.description && elem.hasClass('unread')) { markNotificationRead(elem.get(0)); } + if (notification.target_url && notification.target_url !== '#') { + targetUrl = new URL(notification.target_url).pathname; + } + + // Notification with overlay dialog if (notification.description) { var datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp)); @@ -393,23 +400,17 @@ function notificationHandler($, elem) {
${datetime}
`); - $('.ow-message-title').html(convertMessageWithRelativeURL(notification.message)); $('.ow-message-description').html(notification.description); - $('.ow-overlay-notification').removeClass('ow-hide'); - if (notification.target_url && notification.target_url !== '#') { - var target_url = new URL(notification.target_url); - - $(document).on('click', '.ow-message-target-redirect', function() { - window.location = target_url.pathname; - }); - - $('.ow-message-target-redirect').removeClass('ow-hide'); - } + $(document).on('click', '.ow-message-target-redirect', function() { + window.location = targetUrl; + }); + $('.ow-message-target-redirect').removeClass('ow-hide'); + // standard notification } else { - window.location = elem.data('location'); + window.location = targetUrl; } } From 9ed59826e6a274714e0ccbb71514d51d4efa6e9c Mon Sep 17 00:00:00 2001 From: Dhanus Date: Thu, 30 May 2024 23:52:46 +0530 Subject: [PATCH 24/32] [chore] Add generic_message test --- openwisp_notifications/tests/test_notifications.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openwisp_notifications/tests/test_notifications.py b/openwisp_notifications/tests/test_notifications.py index eb986abb..bc25604d 100644 --- a/openwisp_notifications/tests/test_notifications.py +++ b/openwisp_notifications/tests/test_notifications.py @@ -322,6 +322,19 @@ def test_default_notification_type(self): ) self.assertEqual(n.email_subject, '[example.com] Default Notification Subject') + def test_generic_notification_type(self): + self.notification_options.pop('verb') + self.notification_options.update({'message': 'Generic Message'}) + self.notification_options.update({'type': 'generic_message'}) + self.notification_options.update({'description': 'Generic Description'}) + self._create_notification() + n = notification_queryset.first() + self.assertEqual(n.level, 'info') + self.assertEqual(n.verb, 'generic verb') + self.assertIn('Generic Message', n.message) + self.assertEqual(n.description, 'Generic Description') + self.assertEqual(n.email_subject, '[example.com] Generic Notification Subject') + def test_notification_level_kwarg_precedence(self): # Create a notification with level kwarg set to 'warning' self.notification_options.update({'level': 'warning'}) From 4a9956b38fbbd444a4e54704d323074f87ef4fa2 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 30 May 2024 14:58:15 -0400 Subject: [PATCH 25/32] [docs] Added generic_message --- README.rst | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index f7f43b7c..dc6139e8 100644 --- a/README.rst +++ b/README.rst @@ -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** | @@ -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: From 4593ef32b20c26f629adede4bcc0a4cdbc67fc37 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 31 May 2024 02:03:19 +0530 Subject: [PATCH 26/32] [feature] Support markdown in message and description of generic notification --- openwisp_notifications/base/models.py | 56 +++++++++++++++---- .../tests/test_notifications.py | 24 +++++--- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/openwisp_notifications/base/models.py b/openwisp_notifications/base/models.py index 905c7645..c2d08cf3 100644 --- a/openwisp_notifications/base/models.py +++ b/openwisp_notifications/base/models.py @@ -107,40 +107,72 @@ def message(self): @cached_property def rendered_description(self): - return mark_safe(markdown(self.description)) if self.description else None + if not self.description: + return + md_description = self._get_formatted_string(self.description) + return mark_safe(markdown(md_description)) @property def email_message(self): return self.get_message(email_message=True) + def _set_extra_attributes(self, email_message=False): + """ + This method sets extra attributes for the notification + which are required to render the notification correctly. + """ + 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 + ) + + def _delete_extra_attributes(self): + """ + This method removes the extra_attributes set by + _set_extra_attributes + """ + for attr in ('actor_link', 'action_link', 'target_link'): + delattr(self, attr) + + def _get_formatted_string(self, string, email_message=False): + self._set_extra_attributes(email_message=email_message) + data = self.data or {} + try: + formatted_output = string.format(notification=self, **data) + except AttributeError as exception: + self._invalid_notification( + self.pk, + exception, + 'Error encountered in rendering notification message', + ) + self._delete_extra_attributes() + return formatted_output + 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 data: md_text = data['message'] elif 'message' in config: - md_text = config['message'].format(notification=self, **data) + md_text = config['message'] else: + self._set_extra_attributes(email_message=email_message) md_text = render_to_string( config['message_template'], context=dict(notification=self) ).strip() - except (AttributeError, KeyError, NotificationRenderException) as exception: + self._delete_extra_attributes() + except (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)) + return mark_safe( + markdown(self._get_formatted_string(md_text, email_message)) + ) else: return self.description diff --git a/openwisp_notifications/tests/test_notifications.py b/openwisp_notifications/tests/test_notifications.py index bc25604d..ce004216 100644 --- a/openwisp_notifications/tests/test_notifications.py +++ b/openwisp_notifications/tests/test_notifications.py @@ -324,15 +324,24 @@ def test_default_notification_type(self): def test_generic_notification_type(self): self.notification_options.pop('verb') - self.notification_options.update({'message': 'Generic Message'}) - self.notification_options.update({'type': 'generic_message'}) - self.notification_options.update({'description': 'Generic Description'}) + self.notification_options.update( + { + 'message': '[{notification.actor}]({notification.actor_link})', + 'type': 'generic_message', + 'description': '[{notification.actor}]({notification.actor_link})', + } + ) self._create_notification() n = notification_queryset.first() self.assertEqual(n.level, 'info') self.assertEqual(n.verb, 'generic verb') - self.assertIn('Generic Message', n.message) - self.assertEqual(n.description, 'Generic Description') + expected_output = ( + '

admin

' + ).format( + user_path=reverse('admin:openwisp_users_user_change', args=[self.admin.pk]) + ) + self.assertEqual(n.message, expected_output) + self.assertEqual(n.rendered_description, expected_output) self.assertEqual(n.email_subject, '[example.com] Generic Notification Subject') def test_notification_level_kwarg_precedence(self): @@ -605,8 +614,9 @@ def test_related_objects_database_query(self): {'action_object': operator, 'target': operator} ) self._create_notification() - with self.assertNumQueries(2): - # 2 queries since admin is already cached + with self.assertNumQueries(1): + # 1 query since all related objects are cached + # when rendering the notification n = notification_queryset.first() self.assertEqual(n.actor, self.admin) self.assertEqual(n.action_object, operator) From 9f11de6a0a573d77c73626cd03ccc8629bfd78f5 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Fri, 31 May 2024 13:58:40 +0530 Subject: [PATCH 27/32] [fix] Open button display only when target_url is available --- .../static/openwisp-notifications/js/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index a317e53d..96c2d8c9 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -387,6 +387,7 @@ function notificationHandler($, elem) { if (notification.target_url && notification.target_url !== '#') { targetUrl = new URL(notification.target_url).pathname; + $('.ow-message-target-redirect').removeClass('ow-hide'); } // Notification with overlay dialog @@ -407,7 +408,6 @@ function notificationHandler($, elem) { $(document).on('click', '.ow-message-target-redirect', function() { window.location = targetUrl; }); - $('.ow-message-target-redirect').removeClass('ow-hide'); // standard notification } else { window.location = targetUrl; From b09cd6f5aa3982ee061c6fe16526f44d7754e6e9 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Sat, 1 Jun 2024 00:12:39 +0530 Subject: [PATCH 28/32] [changes] Use context manager to handle temporary attribtues --- openwisp_notifications/base/models.py | 110 +++++++++--------- openwisp_notifications/handlers.py | 10 +- .../tests/test_notifications.py | 24 +++- openwisp_notifications/types.py | 2 +- 4 files changed, 82 insertions(+), 64 deletions(-) diff --git a/openwisp_notifications/base/models.py b/openwisp_notifications/base/models.py index c2d08cf3..56bb0f76 100644 --- a/openwisp_notifications/base/models.py +++ b/openwisp_notifications/base/models.py @@ -1,4 +1,5 @@ import logging +from contextlib import contextmanager from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey @@ -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) @@ -103,78 +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 - md_description = self._get_formatted_string(self.description) - return mark_safe(markdown(md_description)) + 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) - - def _set_extra_attributes(self, email_message=False): - """ - This method sets extra attributes for the notification - which are required to render the notification correctly. - """ - 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 - ) - - def _delete_extra_attributes(self): - """ - This method removes the extra_attributes set by - _set_extra_attributes - """ - for attr in ('actor_link', 'action_link', 'target_link'): - delattr(self, attr) + with notification_render_attributes(self, target_link='redirect_view_url'): + return self.get_message() - def _get_formatted_string(self, string, email_message=False): - self._set_extra_attributes(email_message=email_message) - data = self.data or {} + def get_message(self): + if not self.type: + return self.description try: - formatted_output = string.format(notification=self, **data) - except AttributeError as exception: + 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', ) - self._delete_extra_attributes() - return formatted_output - - def get_message(self, email_message=False): - if self.type: - try: - config = get_notification_configuration(self.type) - data = self.data or {} - if 'message' in data: - md_text = data['message'] - elif 'message' in config: - md_text = config['message'] - else: - self._set_extra_attributes(email_message=email_message) - md_text = render_to_string( - config['message_template'], context=dict(notification=self) - ).strip() - self._delete_extra_attributes() - except (KeyError, NotificationRenderException) as exception: - self._invalid_notification( - self.pk, - exception, - 'Error encountered in rendering notification message', - ) - return mark_safe( - markdown(self._get_formatted_string(md_text, email_message)) - ) - else: - return self.description + return mark_safe(markdown(md_text)) @cached_property def email_subject(self): diff --git a/openwisp_notifications/handlers.py b/openwisp_notifications/handlers.py index 9b7d32c9..45cfc399 100644 --- a/openwisp_notifications/handlers.py +++ b/openwisp_notifications/handlers.py @@ -195,7 +195,7 @@ 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: @@ -203,14 +203,14 @@ def send_email_notification(sender, instance, created, **kwargs): 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, diff --git a/openwisp_notifications/tests/test_notifications.py b/openwisp_notifications/tests/test_notifications.py index ce004216..e159a517 100644 --- a/openwisp_notifications/tests/test_notifications.py +++ b/openwisp_notifications/tests/test_notifications.py @@ -312,7 +312,10 @@ def test_no_organization(self): def test_default_notification_type(self): self.notification_options.pop('verb') - self.notification_options.update({'type': 'default'}) + self.notification_options.pop('url') + self.notification_options.update( + {'type': 'default', 'target': self._get_org_user()} + ) self._create_notification() n = notification_queryset.first() self.assertEqual(n.level, 'info') @@ -321,6 +324,25 @@ def test_default_notification_type(self): 'Default notification with default verb and level info by', n.message ) self.assertEqual(n.email_subject, '[example.com] Default Notification Subject') + email = mail.outbox.pop() + html_email = email.alternatives[0][0] + self.assertEqual( + email.body, + ( + 'Default notification with default verb and' + ' level info by Tester Tester (test org)\n\n' + f'For more information see {n.redirect_view_url}.' + ), + ) + self.assertIn( + ( + '

Default notification with' + ' default verb and level info by' + f' ' + 'Tester Tester (test org)

' + ), + html_email, + ) def test_generic_notification_type(self): self.notification_options.pop('verb') diff --git a/openwisp_notifications/types.py b/openwisp_notifications/types.py index 5aac9814..9e7e84a9 100644 --- a/openwisp_notifications/types.py +++ b/openwisp_notifications/types.py @@ -11,7 +11,7 @@ 'email_subject': '[{site.name}] Default Notification Subject', 'message': ( 'Default notification with {notification.verb} and level {notification.level}' - ' by [{notification.actor}]({notification.actor_link})' + ' by [{notification.target}]({notification.target_link})' ), 'message_template': 'openwisp_notifications/default_message.md', 'email_notification': True, From 82292680243a61bceeaecf8162a5020a4204e0a2 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 1 Jun 2024 16:07:36 +0530 Subject: [PATCH 29/32] [chore] Add Tests for open button visibility --- openwisp_notifications/tests/test_widget.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openwisp_notifications/tests/test_widget.py b/openwisp_notifications/tests/test_widget.py index d601c0e9..48bebeca 100644 --- a/openwisp_notifications/tests/test_widget.py +++ b/openwisp_notifications/tests/test_widget.py @@ -74,3 +74,22 @@ def test_notification_dialog(self): dialog.find_element(By.CLASS_NAME, 'ow-message-description').text, 'Test Description', ) + + def test_notification_dialog_open_button_visibility(self): + self.login() + self.notification_options.pop('target') + self.notification_options.update( + {'message': 'Test Message', 'description': 'Test Description'} + ) + notification = self._create_notification().pop()[1][0] + self.web_driver.find_element(By.ID, 'openwisp_notifications').click() + WebDriverWait(self.web_driver, 10).until( + EC.visibility_of_element_located((By.ID, f'ow-{notification.id}')) + ) + self.web_driver.find_element(By.ID, f'ow-{notification.id}').click() + WebDriverWait(self.web_driver, 10).until( + EC.visibility_of_element_located((By.CLASS_NAME, 'ow-dialog-notification')) + ) + dialog = self.web_driver.find_element(By.CLASS_NAME, 'ow-dialog-notification') + # This confirms the button is hidden + dialog.find_element(By.CSS_SELECTOR, '.ow-message-target-redirect.ow-hide') From 08ab21b5c9d2f500d23cfb741adeee807c201495 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Fri, 7 Jun 2024 13:11:13 +0530 Subject: [PATCH 30/32] [fix] Keyup event handler --- .../static/openwisp-notifications/js/notifications.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 96c2d8c9..9a452e88 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -87,8 +87,7 @@ function initNotificationDropDown($) { } }); - $(document).on('keyup', '*', function(e){ - e.stopPropagation(); + $('.ow-notification-dropdown').on('keyup', function(e){ if (e.keyCode !== 27) { return; } From 0020c2166be0011b23cf13f6e5afea91726041f1 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 8 Jun 2024 00:40:52 +0530 Subject: [PATCH 31/32] [chore] Refactor message conversion to improve readability --- .../static/openwisp-notifications/js/notifications.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 9a452e88..34ed9883 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -238,7 +238,14 @@ function notificationWidget($) { } klass = notificationReadStatus.get(elem.id); - // Remove hyperlinks from notification messages if description is present + // If description is present, remove hyperlinks from the message + let message; + if (elem.description) { + message = elem.message.replace(/]*>([^<]*)<\/a>/g, '$1'); + } else { + message = convertMessageWithRelativeURL(elem.message); + } + return `
@@ -249,7 +256,7 @@ function notificationWidget($) {
${datetime}
- ${elem.description ? elem.message.replace(/
]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)} + ${message} `; } From 859734e4cf48c655f31642eb3b5663d1caa21dcd Mon Sep 17 00:00:00 2001 From: Dhanus Date: Sat, 8 Jun 2024 14:42:11 +0530 Subject: [PATCH 32/32] [chore] Comment changes --- .../static/openwisp-notifications/js/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openwisp_notifications/static/openwisp-notifications/js/notifications.js b/openwisp_notifications/static/openwisp-notifications/js/notifications.js index 34ed9883..811ed155 100644 --- a/openwisp_notifications/static/openwisp-notifications/js/notifications.js +++ b/openwisp_notifications/static/openwisp-notifications/js/notifications.js @@ -238,9 +238,9 @@ function notificationWidget($) { } klass = notificationReadStatus.get(elem.id); - // If description is present, remove hyperlinks from the message let message; if (elem.description) { + // Remove hyperlinks from generic notifications to enforce the opening of the message dialog message = elem.message.replace(/]*>([^<]*)<\/a>/g, '$1'); } else { message = convertMessageWithRelativeURL(elem.message);