From f79ad0f2c713894504b24a38b89957eef6787500 Mon Sep 17 00:00:00 2001 From: Dhanus Date: Thu, 1 Aug 2024 19:20:39 +0530 Subject: [PATCH] [feat] Notification Settings Page --- openwisp_notifications/api/urls.py | 20 ++- openwisp_notifications/api/views.py | 9 ++ openwisp_notifications/base/views.py | 10 ++ .../openwisp-notifications/css/settings.css | 62 ++++++++ .../openwisp-notifications/js/settings.js | 149 ++++++++++++++++++ .../openwisp_notifications/settings.html | 41 +++++ openwisp_notifications/tests/test_api.py | 116 +++++++++++++- openwisp_notifications/urls.py | 7 +- 8 files changed, 407 insertions(+), 7 deletions(-) create mode 100644 openwisp_notifications/base/views.py create mode 100644 openwisp_notifications/static/openwisp-notifications/css/settings.css create mode 100644 openwisp_notifications/static/openwisp-notifications/js/settings.js create mode 100644 openwisp_notifications/templates/openwisp_notifications/settings.html diff --git a/openwisp_notifications/api/urls.py b/openwisp_notifications/api/urls.py index 18bcc2eb..482c8001 100644 --- a/openwisp_notifications/api/urls.py +++ b/openwisp_notifications/api/urls.py @@ -25,16 +25,15 @@ def get_api_urls(api_views=None): views.notification_read_redirect, name='notification_read_redirect', ), - # WIP path( - 'user/user-setting/', + 'user//user-setting/', views.notification_setting_list, - name='notification_setting_list', + name='user_notification_setting_list', ), path( - 'user/user-setting//', + 'user//user-setting//', views.notification_setting, - name='notification_setting', + name='user_notification_setting', ), path( 'notification/ignore/', @@ -56,4 +55,15 @@ def get_api_urls(api_views=None): views.notification_preference, name='notification_preference', ), + # DEPRECATED + path( + 'user/user-setting/', + views.notification_setting_list, + name='notification_setting_list', + ), + path( + 'user/user-setting//', + views.notification_setting, + name='notification_setting', + ), ] diff --git a/openwisp_notifications/api/views.py b/openwisp_notifications/api/views.py index 8578b45c..2f218b14 100644 --- a/openwisp_notifications/api/views.py +++ b/openwisp_notifications/api/views.py @@ -1,4 +1,5 @@ from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponseRedirect from django.urls import reverse from django_filters.rest_framework import DjangoFilterBackend @@ -123,6 +124,14 @@ class BaseNotificationSettingView(GenericAPIView): def get_queryset(self): if getattr(self, 'swagger_fake_view', False): return NotificationSetting.objects.none() # pragma: no cover + + user_id = self.kwargs.get('user_id') + + if user_id: + if not (self.request.user.id == user_id or self.request.user.is_staff): + raise PermissionDenied() + return NotificationSetting.objects.filter(user_id=user_id) + return NotificationSetting.objects.filter(user=self.request.user) diff --git a/openwisp_notifications/base/views.py b/openwisp_notifications/base/views.py new file mode 100644 index 00000000..6a188e10 --- /dev/null +++ b/openwisp_notifications/base/views.py @@ -0,0 +1,10 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import TemplateView + + +class NotificationSettingPage(LoginRequiredMixin, TemplateView): + template_name = 'openwisp_notifications/settings.html' + login_url = '/admin/login/' + + +notifiation_setting_page = NotificationSettingPage.as_view() diff --git a/openwisp_notifications/static/openwisp-notifications/css/settings.css b/openwisp_notifications/static/openwisp-notifications/css/settings.css new file mode 100644 index 00000000..90933110 --- /dev/null +++ b/openwisp_notifications/static/openwisp-notifications/css/settings.css @@ -0,0 +1,62 @@ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} +h2 { + margin-bottom: 10px; +} +.global-settings { + margin-bottom: 20px; +} +.org-panel { + background-color: #fff; + border: 1px solid #ddd; +} +.org-header { + background-color: #e0e0e0; + padding: 10px; + font-weight: bold; + display: flex; + justify-content: space-between; + cursor: pointer; +} +.org-content { + padding: 10px; + display: none; +} +.org-content.active { + display: block; +} +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} +th, +td { + padding: 8px; + text-align: left; + border-bottom: 1px solid #ddd; +} +th { + background-color: #f2f2f2; +} +th:not(:last-child), +td:not(:last-child) { + border-right: 1px solid #ddd; +} +th:not(:first-child), +td:not(:first-child) { + text-align: center; +} +.checkbox { + width: 15px; + height: 15px; + text-align: center; +} +.no-settings { + padding: 10px; + text-align: center; + color: #666; +} diff --git a/openwisp_notifications/static/openwisp-notifications/js/settings.js b/openwisp_notifications/static/openwisp-notifications/js/settings.js new file mode 100644 index 00000000..7ee24481 --- /dev/null +++ b/openwisp_notifications/static/openwisp-notifications/js/settings.js @@ -0,0 +1,149 @@ +'use strict'; + +(function ($) { + $(document).ready(function () { + fetchNotificationSettings(); + }); + + function fetchNotificationSettings() { + $.ajax({ + url: "/api/v1/notifications/user/user-setting", + method: "GET", + success: function (data) { + const groupedData = groupByOrganization(data.results); + renderNotificationSettings(groupedData); + initializeEventListeners(); + }, + }); + } + + function groupByOrganization(settings) { + const grouped = {}; + settings.forEach((setting) => { + if (!grouped[setting.organization_name]) { + grouped[setting.organization_name] = []; + } + grouped[setting.organization_name].push(setting); + }); + return grouped; + } + + function renderNotificationSettings(data) { + const orgPanelsContainer = $("#org-panels"); + orgPanelsContainer.empty(); // Clear existing content + + Object.keys(data) + .sort() + .forEach((orgName) => { + const orgSettings = data[orgName].sort((a, b) => + a.type.localeCompare(b.type) + ); + const orgPanel = $(` +
+
+ ${orgName} +
+
+
+ `); + const orgContent = orgPanel.find(".org-content"); + + if (orgSettings.length > 0) { + const table = $(` + + + + + + +
Settings + Email + + Web +
+ `); + orgSettings.forEach((setting) => { + const row = $(` + + ${setting.type} + + + + `); + table.append(row); + }); + orgContent.append(table); + updateMainCheckboxes(table); + } else { + orgContent.append(`
No settings available for this organization
`); + } + + orgPanelsContainer.append(orgPanel); + }); + } + + function updateMainCheckboxes(table) { + const emailCheckboxes = table.find('.email-checkbox'); + const webCheckboxes = table.find('.web-checkbox'); + const emailMainCheckbox = table.find('.main-checkbox[data-column="email"]'); + const webMainCheckbox = table.find('.main-checkbox[data-column="web"]'); + + emailMainCheckbox.prop('checked', emailCheckboxes.length === emailCheckboxes.filter(':checked').length); + webMainCheckbox.prop('checked', webCheckboxes.length === webCheckboxes.filter(':checked').length); + } + + function initializeEventListeners() { + $(document).on('click', '.org-header', function () { + const toggle = $(this).find(".toggle"); + toggle.text(toggle.text() === "▼" ? "▲" : "▼"); + $(this).next(".org-content").toggleClass("active"); + }); + + $(document).on('change', '.main-checkbox', function () { + const column = $(this).data("column"); + $(this).closest("table").find(`.${column}-checkbox`).prop("checked", $(this).prop("checked")); + showToast('success', 'Settings updated successfully.'); + }); + + $(document).on('change', '.email-checkbox, .web-checkbox', function () { + const column = $(this).hasClass("email-checkbox") ? "email" : "web"; + const mainCheckbox = $(this).closest("table").find(`.main-checkbox[data-column="${column}"]`); + const checkboxes = $(this).closest("table").find(`.${column}-checkbox`); + mainCheckbox.prop("checked", checkboxes.length === checkboxes.filter(":checked").length); + showToast('success', 'Settings updated successfully.'); + }); + + $("#global-email, #global-web").change(function () { + const isEmail = $(this).attr("id") === "global-email"; + const columnClass = isEmail ? "email-checkbox" : "web-checkbox"; + $(`.${columnClass}`).prop("checked", $(this).prop("checked")); + $(`.main-checkbox[data-column="${isEmail ? "email" : "web"}"]`).prop("checked", $(this).prop("checked")); + showToast('success', 'Global settings updated successfully.'); + }); + } + + function showToast(level, message) { + const toast = $(` +
+
+
+
+ ${message} +
+
+ `); + $('.ow-notification-toast-wrapper').prepend(toast); + // toast.slideDown('slow', function () { + // setTimeout(function () { + // toast.slideUp('slow', function () { + // toast.remove(); + // }); + // }, 3000); + // }); + + $(document).on('click', '.ow-notification-toast .ow-notify-close.btn', function () { + toast.remove(); + }); + } + +})(django.jQuery); diff --git a/openwisp_notifications/templates/openwisp_notifications/settings.html b/openwisp_notifications/templates/openwisp_notifications/settings.html new file mode 100644 index 00000000..42280ca5 --- /dev/null +++ b/openwisp_notifications/templates/openwisp_notifications/settings.html @@ -0,0 +1,41 @@ +{% extends "admin/base_site.html" %} + +{% load i18n %} +{% load static %} + +{% block title %}{% trans "Notification Settings" %}{% endblock %} + +{% block extrastyle %} +{{ block.super }} + +{% endblock extrastyle %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +
+

Notification Preferences

+ +
+ Global Settings: + + +
+ +
+ +
+
+{% endblock content %} + +{% block footer %} + {{ block.super }} + {% if request.user.is_authenticated %} + + {% endif %} +{% endblock footer %} diff --git a/openwisp_notifications/tests/test_api.py b/openwisp_notifications/tests/test_api.py index 9ff0d366..d013d105 100644 --- a/openwisp_notifications/tests/test_api.py +++ b/openwisp_notifications/tests/test_api.py @@ -580,6 +580,7 @@ def test_notification_setting_list_api(self): def test_list_notification_setting_filtering(self): url = self._get_path('notification_setting_list') + tester = self._create_user() with self.subTest('Test listing notification setting without filters'): count = NotificationSetting.objects.exclude( @@ -617,10 +618,33 @@ def test_list_notification_setting_filtering(self): ns = response.data['results'].pop() self.assertEqual(ns['type'], 'default') + with self.subTest('Test filtering by user_id as admin'): + user_url = self._get_path('user_notification_setting_list', tester.pk) + response = self.client.get(user_url) + self.assertEqual(response.status_code, 200) + + with self.subTest('Test with user_id by user_id as the same user'): + self.client.force_login(tester) + user_url = self._get_path('user_notification_setting_list', tester.pk) + response = self.client.get(user_url) + self.assertEqual(response.status_code, 200) + + with self.subTest('Test with user_id as a different non-admin user'): + self.client.force_login(tester) + user_url = self._get_path('user_notification_setting_list', self.admin.pk) + response = self.client.get(user_url) + self.assertEqual(response.status_code, 403) + def test_retreive_notification_setting_api(self): notification_setting = NotificationSetting.objects.exclude( organization__isnull=True ).first() + tester = self._create_user() + tester_notification_setting = NotificationSetting.objects.create( + user=tester, + type='default', + organization=Organization.objects.first(), + ) with self.subTest('Test for non-existing notification setting'): url = self._get_path('notification_setting', uuid.uuid4()) @@ -644,10 +668,49 @@ def test_retreive_notification_setting_api(self): self.assertEqual(data['web'], notification_setting.web) self.assertEqual(data['email'], notification_setting.email) + with self.subTest( + 'Test retrieving details for existing notification setting as admin' + ): + url = self._get_path( + 'user_notification_setting', tester.pk, tester_notification_setting.pk + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + data = response.data + self.assertEqual(data['id'], str(tester_notification_setting.id)) + + with self.subTest( + 'Test retrieving details for existing notification setting as the same user' + ): + self.client.force_login(tester) + url = self._get_path( + 'user_notification_setting', tester.pk, tester_notification_setting.pk + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + data = response.data + self.assertEqual(data['id'], str(tester_notification_setting.id)) + + with self.subTest( + 'Test retrieving details for existing notification setting as different non-admin user' + ): + self.client.force_login(tester) + url = self._get_path( + 'user_notification_setting', self.admin.pk, notification_setting.pk + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 403) + def test_update_notification_setting_api(self): notification_setting = NotificationSetting.objects.exclude( organization__isnull=True ).first() + tester = self._create_user() + tester_notification_setting = NotificationSetting.objects.create( + user=tester, + type='default', + organization=Organization.objects.first(), + ) update_data = {'web': False} with self.subTest('Test for non-existing notification setting'): @@ -659,7 +722,7 @@ def test_update_notification_setting_api(self): {'detail': NOT_FOUND_ERROR}, ) - with self.subTest('Test retrieving details for existing notification setting'): + with self.subTest('Test updating details for existing notification setting'): url = self._get_path( 'notification_setting', notification_setting.pk, @@ -675,6 +738,57 @@ def test_update_notification_setting_api(self): self.assertEqual(data['web'], notification_setting.web) self.assertEqual(data['email'], notification_setting.email) + with self.subTest( + 'Test updating details for existing notification setting as admin' + ): + url = self._get_path( + 'user_notification_setting', tester.pk, tester_notification_setting.pk + ) + response = self.client.put( + url, update_data, content_type='application/json' + ) + self.assertEqual(response.status_code, 200) + data = response.data + tester_notification_setting.refresh_from_db() + self.assertEqual(data['id'], str(tester_notification_setting.id)) + self.assertEqual( + data['organization'], tester_notification_setting.organization.pk + ) + self.assertEqual(data['web'], tester_notification_setting.web) + self.assertEqual(data['email'], tester_notification_setting.email) + + with self.subTest( + 'Test updating details for existing notification setting as the same user' + ): + self.client.force_login(tester) + url = self._get_path( + 'user_notification_setting', tester.pk, tester_notification_setting.pk + ) + response = self.client.put( + url, update_data, content_type='application/json' + ) + self.assertEqual(response.status_code, 200) + data = response.data + tester_notification_setting.refresh_from_db() + self.assertEqual(data['id'], str(tester_notification_setting.id)) + self.assertEqual( + data['organization'], tester_notification_setting.organization.pk + ) + self.assertEqual(data['web'], tester_notification_setting.web) + self.assertEqual(data['email'], tester_notification_setting.email) + + with self.subTest( + 'Test updating details for existing notification setting as a different non-admin user' + ): + self.client.force_login(tester) + url = self._get_path( + 'user_notification_setting', self.admin.pk, notification_setting.pk + ) + response = self.client.put( + url, update_data, content_type='application/json' + ) + self.assertEqual(response.status_code, 403) + def test_notification_redirect_api(self): def _unread_notification(notification): notification.unread = True diff --git a/openwisp_notifications/urls.py b/openwisp_notifications/urls.py index 04a8f9d7..acc1043f 100644 --- a/openwisp_notifications/urls.py +++ b/openwisp_notifications/urls.py @@ -1,6 +1,7 @@ from django.urls import include, path from .api.urls import get_api_urls +from .base.views import notifiation_setting_page def get_urls(api_views=None, social_views=None): @@ -9,7 +10,11 @@ def get_urls(api_views=None, social_views=None): Arguments: api_views(optional): views for Notifications API """ - urls = [path('api/v1/notifications/', include(get_api_urls(api_views)))] + urls = [ + path('api/v1/notifications/', include(get_api_urls(api_views))), + path('notifications/settings/', notifiation_setting_page), + path('notifications/user//settings/', notifiation_setting_page), + ] return urls