Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Added generic message notification type (shown in dialg box) #275

Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c207c9d
[feature] Description of notification shown in a dialog admin site
Dhanus3133 May 14, 2024
1025bfb
[qa] Fix
Dhanus3133 May 14, 2024
2605a09
[chore] Add trans
Dhanus3133 May 14, 2024
bf3ba25
Merge branch 'master' into feat/display-description-notification-dialog
Dhanus3133 May 18, 2024
a15a284
[chore] Increase page_size
Dhanus3133 May 20, 2024
3a0377d
[chore] Fix relative URL error
Dhanus3133 May 22, 2024
2be7037
[chore] Improvements
Dhanus3133 May 23, 2024
581cc90
[chore] Show description with md support
Dhanus3133 May 23, 2024
a7c792d
[chore] Rename general to generic message types
Dhanus3133 May 23, 2024
9e7c1fc
[chore] Fix menu bar
Dhanus3133 May 25, 2024
2608c11
[chore] Rename rendered_description
Dhanus3133 May 25, 2024
2a922a6
[chore] Fix test
Dhanus3133 May 25, 2024
0686c2d
[chore] Just use description
Dhanus3133 May 26, 2024
8106f3e
[fix] Rendered description shows error for None description
Dhanus3133 May 26, 2024
cc528df
[fix] Level icon repeat
Dhanus3133 May 26, 2024
aca39bc
[chore] JQuery way
Dhanus3133 May 26, 2024
1e6b840
[fix] Fixed tests
pandafy May 27, 2024
00ec345
[chore] Bump changes
Dhanus3133 May 27, 2024
e69600f
[chore] Add tests for notification level
Dhanus3133 May 27, 2024
bbff510
[chore] Remove comment
Dhanus3133 May 27, 2024
8dfcfce
Merge branch 'master' into feat/display-description-notification-dialog
Dhanus3133 May 28, 2024
df10915
[chore] Focus fix
Dhanus3133 May 28, 2024
6e5db98
[chores] UI/CSS improvements
nemesifier May 29, 2024
6de368f
[fix] Toast notifications dialog
Dhanus3133 May 30, 2024
b0725b1
[fix] Use relative path also for notification toast
nemesifier May 30, 2024
9ed5982
[chore] Add generic_message test
Dhanus3133 May 30, 2024
4a9956b
[docs] Added generic_message
nemesifier May 30, 2024
4593ef3
[feature] Support markdown in message and description of generic noti…
pandafy May 30, 2024
9f11de6
[fix] Open button display only when target_url is available
Dhanus3133 May 31, 2024
b09cd6f
[changes] Use context manager to handle temporary attribtues
pandafy May 31, 2024
8229268
[chore] Add Tests for open button visibility
Dhanus3133 Jun 1, 2024
08ab21b
[fix] Keyup event handler
Dhanus3133 Jun 7, 2024
69bfb39
Merge branch 'master' into feat/display-description-notification-dialog
nemesifier Jun 7, 2024
0020c21
[chore] Refactor message conversion to improve readability
Dhanus3133 Jun 7, 2024
859734e
[chore] Comment changes
Dhanus3133 Jun 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
8 changes: 7 additions & 1 deletion openwisp_notifications/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ def target_url(self):
def message(self):
return self.get_message()

@cached_property
def rendered_description(self):
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
return mark_safe(markdown(self.description)) if self.description else None

@property
def email_message(self):
return self.get_message(email_message=True)
Expand All @@ -120,7 +124,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(
Expand Down
4 changes: 2 additions & 2 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
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
Original file line number Diff line number Diff line change
Expand Up @@ -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; };
Expand Down Expand Up @@ -56,10 +57,15 @@ function initNotificationDropDown($) {

$(document).click(function (e) {
e.stopPropagation();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pandafy I see a similar thing here 😨

// 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 &&
!$(e.target).is($('.ow-notifications'))
// 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')) &&
// Do not hide if the user is interacting with the notification dialog
!$('.ow-overlay-notification').is(':visible')
) {
$('.ow-notification-dropdown').addClass('ow-hide');
}
Expand All @@ -69,30 +75,54 @@ function initNotificationDropDown($) {
$(document).focusin(function(e){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pandafy do you think we could avoid binding on document here too? That stopPropagation() below scares me. We could incur in the same problem we had for the keyup event in other modules if we need to use the same JS event. The problem will be that to find this will probably take hours because by that time we'll have forgotten.
We can open a new issue for this.

// 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 &&
// Do not hide if the user is interacting with the notification dialog
!$('.ow-overlay-notification').is(':visible')
) {
// Don't hide if focus changes to notification bell icon
if (e.target != $('#openwisp_notifications').get(0)) {
$('.ow-notification-dropdown').addClass('ow-hide');
}
}
});

$('.ow-notification-dropdown').on('keyup', '*', function(e){
$(document).on('keyup', '*', function(e){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fire on every keyup event that will happen in OpenWISP. Are you sure this change is intended?
Can't we narrow down the firing a bit to make sure it's not fired unless needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows the notification dialog or widget to close whenever the ESC is pressed. I would like to know if there is any other way to reduce it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this is a useful, yet basic feature. I don't know how I feel about triggering an event handler for every key press. This JS is loaded on all the pages of OpenWISP. so this event handler will get executed when filling out any field in OpenWISP.

If we plan to to keep this, we shall adopt the concept of early return. In this case, return from the function if the key is not Esc.

e.stopPropagation();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid doing such practices because it would break functionalities added by other modules.

This is the reason for openwisp/openwisp-controller#868 bug.

I apologise that this exist!!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we find a way to avoid to bind to the document like that, it should be possible to specify to which element to bind, or filter the elements instead of using *, probably also removing e.stopPropagation(); may be a good idea, depending if we bind this event to an HTML container shared with the rest of the UI or with some other HTML container which is specific to the notifications module.

@Dhanus3133 please let me know if the suggestion above is clear.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated, let me know if that requires some changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works, thanks!

if (e.keyCode !== 27) {
return;
}
// Hide notification widget on "Escape" key
if (e.keyCode == 27){
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');
$('#openwisp_notifications').focus();
}
});
}

// 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, window.location.href);
link.setAttribute('href', url.pathname);
}
});
return doc.body.innerHTML;
}

function notificationWidget($) {

let nextPageUrl = getAbsoluteUrl('/api/v1/notifications/notification/'),
renderedPages = 2,
busy = false,
fetchedPages = [],
lastRenderedPage = 0;
// 1 based indexing (0 -> no page rendered)

Expand Down Expand Up @@ -197,7 +227,8 @@ function notificationWidget($) {
function notificationListItem(elem) {
let klass;
const datetime = dateTimeStampToDateTimeLocaleString(new Date(elem.timestamp)),
target_url = new URL(elem.target_url);
// 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) {
Expand All @@ -208,32 +239,18 @@ 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;
}

// Remove hyperlinks from notification messages if description is present
return `<div class="ow-notification-elem ${klass}" id=ow-${elem.id}
data-location="${target_url.pathname}" role="link" tabindex="0">
<div class="ow-notification-inner">
<div class="ow-notification-meta">
<div class="ow-notification-level-wrapper">
<div class="ow-notify-${elem.level} icon"></div>
<div class="ow-notification-level-text">${elem.level}</div>
</div>
<div class="ow-notification-date">${datetime}</div>
<div class="ow-notification-level-wrapper">
<div class="ow-notify-${elem.level} icon"></div>
<div class="ow-notification-level-text">${elem.level}</div>
</div>
<div class="ow-notification-date">${datetime}</div>
</div>
${convertMessageWithRelativeURL(elem.message)}
${elem.description ? elem.message.replace(/<a [^>]*>([^<]*)<\/a>/g, '$1') : convertMessageWithRelativeURL(elem.message)}
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>`;
}
Expand Down Expand Up @@ -320,11 +337,16 @@ function notificationWidget($) {
return;
}
let elem = $(this);
// If notification is unread then send read request
if (elem.hasClass('unread')) {
markNotificationRead(elem.get(0));
notificationHandler($, elem);
});

// 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;
}
window.location = elem.data('location');
$('.ow-overlay-notification').addClass('ow-hide');
$('.ow-message-target-redirect').addClass('ow-hide');
});

// Handler for marking notification as read on mouseout event
Expand Down Expand Up @@ -353,6 +375,45 @@ function markNotificationRead(elem) {
);
}

function notificationHandler($, elem) {
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));

$('.ow-dialog-notification-level-wrapper').html(`
<div class="ow-notification-level-wrapper">
<div class="ow-notify-${notification.level} icon"></div>
<div class="ow-notification-level-text">${notification.level}</div>
</div>
<div class="ow-notification-date">${datetime}</div>
`);
$('.ow-message-title').html(convertMessageWithRelativeURL(notification.message));
$('.ow-message-description').html(notification.description);
$('.ow-overlay-notification').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 = targetUrl;
}
}

function initWebSockets($) {
notificationSocket.addEventListener('message', function (e) {
let data = JSON.parse(e.data);
Expand Down Expand Up @@ -407,7 +468,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();
Expand Down
16 changes: 16 additions & 0 deletions openwisp_notifications/templates/admin/base_site.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@
{% endblock %}

{% block footer %}
<div class="ow-overlay ow-overlay-notification ow-overlay-inner ow-hide">
<div class="ow-dialog-notification">
<span class="ow-dialog-close ow-dialog-close-x">&times;</span>
<div class="ow-notification-level-wrapper ow-dialog-notification-level-wrapper"></div>
<h2 class="ow-message-title"></h2>
<div class="ow-message-description"></div>
<div class="ow-dialog-buttons">
<button class="ow-message-target-redirect ow-hide button default success-btn">
{% trans 'Open' %}
</button>
<button class="ow-dialog-close button default">
{% trans 'Close' %}
</button>
</div>
</div>
</div>
{{ block.super }}
{% if request.user.is_authenticated %}
<script type="text/javascript" src="{% static 'openwisp-notifications/js/vendor/reconnecting-websocket.min.js' %}"></script>
Expand Down
2 changes: 1 addition & 1 deletion openwisp_notifications/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
nemesifier marked this conversation as resolved.
Show resolved Hide resolved
url = f'{url}?page_size={page_size}'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
Expand Down
8 changes: 7 additions & 1 deletion openwisp_notifications/tests/test_notifications.py
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ def setUp(self):
self.notification_options = dict(
sender=self.admin,
description='Test Notification',
level='info',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and fixed the reason for failing test. This would allow us to close this PR quickly.

In previous commits, we changed the logic of the notify handler to give preference to the level kwarg passed to the notify signal.

@Dhanus3133 we need to add a test case in test_models.py which verifies that the preference of level is given to the level kwargs over the level defined in notification type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the file is test_notifications.py. Updated with the changes there.

verb='Test Notification',
email_subject='Test Email subject',
url='https://localhost:8000/admin',
Expand Down Expand Up @@ -323,6 +322,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.'):
Expand Down
Loading
Loading