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 17 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 @@ -5,6 +5,66 @@
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 {
Expand Down Expand Up @@ -257,6 +317,7 @@
.ow-notification-level-text {
padding: 0px 6px;
text-transform: uppercase;
font-weight: bold;
}
.ow-notification-elem .icon,
.ow-notification-toast .icon {
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());
var isNotificationDialogOpen = false;

if (typeof gettext === 'undefined') {
var gettext = function(word){ return word; };
Expand All @@ -13,6 +14,7 @@ if (typeof gettext === 'undefined') {
initNotificationDropDown($);
initWebSockets($);
owNotificationWindow.init($);
moveNotificationDialog($);
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
});
})(django.jQuery);

Expand Down Expand Up @@ -59,7 +61,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
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
) {
$('.ow-notification-dropdown').addClass('ow-hide');
}
Expand All @@ -69,24 +73,59 @@ 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 &&
!isNotificationDialogOpen
) {
// Don't hide if focus changes to notification bell icon
if (e.target != $('#openwisp_notifications').get(0)) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we make these checks re-usable? We also use these for click event?

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 change makes the checks reusable, but it might add unnecessary complexity and confusion for now ig. If you need to then I will do it.

$('.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!

// 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();
}
}
});
}

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(),
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/'),
Expand Down Expand Up @@ -197,7 +236,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);
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved

if (!notificationReadStatus.has(elem.id)) {
if (elem.unread) {
Expand All @@ -208,32 +247,17 @@ 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 `<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)}
Copy link
Member

Choose a reason for hiding this comment

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

It is not clear to me what are we doing here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, in the call, we discussed on the generic message type we don't want to allow users to click on the target_url directly something like this on the Org 2
image
So, with this regex, we eliminate this ability. The users view the description and then proceed to the target_url via the dialog. (Will add comments there)

</div>
</div>`;
}
Expand Down Expand Up @@ -324,7 +348,41 @@ 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) {
isNotificationDialogOpen = true;
const datetime = dateTimeStampToDateTimeLocaleString(new Date(notification.timestamp));
document.querySelector('.ow-dialog-notification-level-wrapper').innerHTML = `
<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>
`;
document.querySelector('.ow-message-title').innerHTML = convertMessageWithRelativeURL(notification.message);
document.querySelector('.ow-message-description').innerHTML = notification.description;
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
$('.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 () {
window.location = target_url.pathname;
});
$('.ow-message-target-redirect').removeClass('ow-hide');
}
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
} else {
window.location = elem.data('location');
}
});

// 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;
}
isNotificationDialogOpen = false;
$('.ow-dialog-overlay').addClass('ow-hide');
$('.ow-message-target-redirect').addClass('ow-hide');
});

// Handler for marking notification as read on mouseout event
Expand Down
10 changes: 10 additions & 0 deletions openwisp_notifications/templates/admin/base_site.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@
<div class="ow-no-notifications ow-round-bottom-border ow-hide">
<p>{% trans 'No new notification.' %}</p>
</div>
<div class="ow-dialog-overlay 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>
<button class="ow-message-target-redirect ow-hide button default">{% trans 'Open' %}</button>
<button class="ow-dialog-close button default">{% trans 'Close' %}</button>
</div>
</div>
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
</div>
{% endblock %}

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
1 change: 0 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 @@ -60,7 +60,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
Loading
Loading