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

[feat] Add unsubscribe link to email notifications #307

Open
wants to merge 102 commits into
base: notification-preferences
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 70 commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
aa7ac52
[feature] Added Email Batch Summary #132
Dhanus3133 Jul 30, 2024
6cc2835
[chore] REST API changes
Dhanus3133 Jul 9, 2024
7016f86
[chore] Add tests
Dhanus3133 Jul 14, 2024
6d410fc
[chore] Use GenericAPIView
Dhanus3133 Jul 17, 2024
e6cbb9c
[refactor] URL routes
Dhanus3133 Jul 17, 2024
531acda
[chore] Update tests
Dhanus3133 Jul 19, 2024
2dfc5a8
[chore] Add Global Notification
Dhanus3133 Jul 19, 2024
215118b
[chore] Global Notification Preference changes
Dhanus3133 Jul 25, 2024
90221ca
[feat] Notification Settings Page
Dhanus3133 Aug 1, 2024
64fe873
[ci] Run builds for gsoc24 branch
pandafy Jul 9, 2024
fdb8fdf
[qa] Check fixes
Dhanus3133 Aug 2, 2024
7c5cbeb
[chore] Notification settings page with breadcrumbs
Dhanus3133 Aug 3, 2024
dfca656
[chore] Notification settings update
Dhanus3133 Aug 4, 2024
0e5ebd2
[chore] CSS updates
Dhanus3133 Aug 5, 2024
44ca0dc
[chore] Bump changes
Dhanus3133 Aug 6, 2024
85bb0c9
[fix] Verbose errora admin user page
Dhanus3133 Aug 6, 2024
b264324
[chore] Web notification on the left, email on the right
Dhanus3133 Aug 6, 2024
b175fa1
[chore] Handling auto trigger of email without web checkboxes
Dhanus3133 Aug 10, 2024
c0d373f
[chore] Automatically open first org dropdown, label for org level em…
Dhanus3133 Aug 10, 2024
487f6dd
[chore] Fetch current user global setting preference and make it default
Dhanus3133 Aug 10, 2024
a5d8b4e
[chore] Update type_label for user-setting api
Dhanus3133 Aug 10, 2024
cb8a296
[chore] Reduced CSS and replicate admin design
Dhanus3133 Aug 11, 2024
4d5fca5
[chore] Update org name styles and add no setting available
Dhanus3133 Aug 11, 2024
5c6bf56
[QA] Fixes
Dhanus3133 Aug 11, 2024
8a8437f
[chore] Toast dialog update
Dhanus3133 Aug 12, 2024
8fb919c
[chore] Variable naming
Dhanus3133 Aug 12, 2024
7628892
[chore] Org level email/web center to the checkbox
Dhanus3133 Aug 12, 2024
946d743
[fix] Tests
Dhanus3133 Aug 12, 2024
1268fe6
[chore] Reviewed change bump
Dhanus3133 Aug 16, 2024
c8cfdf0
[chore] Add tooltips
Dhanus3133 Aug 17, 2024
f38dce3
[chore] Remove unused
Dhanus3133 Aug 17, 2024
cd40a29
[chore] Add link in notification widget
Dhanus3133 Aug 19, 2024
b99f801
[chore] Fix settings button in widget
Dhanus3133 Aug 20, 2024
3ecbd92
[chore] Drop ow-show-unread button functionality
Dhanus3133 Aug 20, 2024
33ec904
[chore] Handle UI on API errors
Dhanus3133 Aug 20, 2024
a0f08df
[chore] Remove unused
Dhanus3133 Aug 20, 2024
5a60f7d
[chore] Immediately apply setting changes and rollback on API error
Dhanus3133 Aug 21, 2024
852790b
[chore] Use switch
Dhanus3133 Aug 21, 2024
3caaeda
[chore] Bump reviewed changes
Dhanus3133 Aug 21, 2024
87f0b1f
[ci] Run builds for gsoc24-rebased branch
Dhanus3133 Aug 21, 2024
ec83771
[chore] Remove gsoc24 branch from builds
Dhanus3133 Aug 21, 2024
088abf2
[chore] Fetch upto 100 user-settings per api request
Dhanus3133 Aug 21, 2024
5f8e008
[chore] Add 'Organization:' on the header
Dhanus3133 Aug 21, 2024
1d4faae
[chore] Increase spacing around email/web global switches
Dhanus3133 Aug 21, 2024
002dddf
[chore] Create EmailTokenGenerator
Dhanus3133 Aug 21, 2024
7322a73
[chore] Unsubscribe Implementation
Dhanus3133 Aug 24, 2024
070e36c
Merge branch 'master' into gsoc24-rebased
Dhanus3133 Aug 28, 2024
aa813a7
Merge branch 'gsoc24-rebased' into notification-preferences
Dhanus3133 Aug 28, 2024
40471ba
[chore] Add space around settings
Dhanus3133 Aug 28, 2024
1e1978f
[docs] Update Notification Preference page
Dhanus3133 Aug 31, 2024
6290e16
[chore] Remove token time expiry
Dhanus3133 Sep 1, 2024
cfff9aa
[chore] Handle logic for any one email setting type enabled even when…
Dhanus3133 Sep 1, 2024
286b22b
[chore] Translatable i18n and js file refactor
Dhanus3133 Sep 1, 2024
b82f441
[chore] Add tests
Dhanus3133 Sep 1, 2024
ede83b3
Merge branch 'gsoc24-rebased' into feat/manage-notifications-unsubscribe
Dhanus3133 Sep 1, 2024
21b9362
Merge branch 'notification-preferences' into feat/manage-notification…
Dhanus3133 Sep 1, 2024
3b96255
[chore] Handle check_token function for older django version
Dhanus3133 Sep 1, 2024
27952c7
[ci] Add notification-preferences target branches PR to build actions
Dhanus3133 Sep 1, 2024
bcf58fb
[docs] Added docs for Email Batches #306
Dhanus3133 Sep 2, 2024
c912883
[chores] Small UI and view improvements
nemesifier Sep 2, 2024
afb3be9
Merge branch 'gsoc24-rebased' into notification-preferences
nemesifier Sep 2, 2024
3fc3533
[chore] Bump review changes
Dhanus3133 Sep 3, 2024
06bc604
[qa] Fix checks
Dhanus3133 Sep 3, 2024
72211c8
[fix] URL update
Dhanus3133 Sep 3, 2024
5f265be
[chore] Add test for missing notification_preference get api
Dhanus3133 Sep 3, 2024
7982603
[chore] Add missing tests
Dhanus3133 Sep 3, 2024
6ebb734
Merge branch 'notification-preferences' into feat/manage-notification…
Dhanus3133 Sep 3, 2024
890d764
[chore] Bump changes
Dhanus3133 Sep 3, 2024
90a6056
[chore] Bump changes
Dhanus3133 Sep 5, 2024
735c4d0
[chore] Add tests
Dhanus3133 Sep 5, 2024
7528ed7
[chore] Add selenium tests
Dhanus3133 Sep 6, 2024
fcb9e23
[chore] View Notification preferences button in user admin model for …
Dhanus3133 Sep 6, 2024
6dd8ca5
[chore] Show loader
Dhanus3133 Sep 6, 2024
d1c5215
[chore] Add selenium test
Dhanus3133 Sep 6, 2024
082c967
[fix] Tests
Dhanus3133 Sep 6, 2024
c0b7357
[fix] Tests
Dhanus3133 Sep 6, 2024
6106f9b
[fix] Tests
Dhanus3133 Sep 6, 2024
234627e
[fix] Tests
Dhanus3133 Sep 7, 2024
3d53083
[fix] Should fix Tests
Dhanus3133 Sep 7, 2024
9661ae6
[chore] Bump changes
Dhanus3133 Sep 7, 2024
0ecaa74
[chore] Reuse serializer
Dhanus3133 Sep 7, 2024
4ec65b7
[chore] Increase waiting time for selenium tests
Dhanus3133 Sep 7, 2024
b9d62b4
[chore] Update preference page org dropdown icon position
Dhanus3133 Sep 7, 2024
b37fce5
Merge branch 'notification-preferences' into feat/manage-notification…
Dhanus3133 Sep 7, 2024
2f802be
[chore] Bump changes
Dhanus3133 Sep 7, 2024
5c9df62
[fix] JS console
Dhanus3133 Sep 7, 2024
8b46ca7
[fix] Tests
Dhanus3133 Sep 7, 2024
35ea832
[chore] Reuse base_entrance.html
Dhanus3133 Sep 11, 2024
baa6111
[fix] Error displaying __str__ for global notification setting
Dhanus3133 Sep 11, 2024
c10952e
[chore] Verify global setting creation in test_post_migration_handler…
Dhanus3133 Sep 11, 2024
7efa79e
[chore] Remove transition on accordion
Dhanus3133 Sep 11, 2024
0e687b4
[chore] Update tests
Dhanus3133 Sep 11, 2024
9156700
[chore] Bump changes
Dhanus3133 Sep 13, 2024
5413859
[fix] Import error
Dhanus3133 Sep 13, 2024
a796372
[chore] Bump changes
Dhanus3133 Sep 17, 2024
c3f01e5
[chore] Update org level changes
Dhanus3133 Sep 20, 2024
ad8ff56
[chore] Use full_clean instead of saving directly
Dhanus3133 Sep 20, 2024
6c53bf6
[chore] Update global notification setting changes
Dhanus3133 Sep 20, 2024
2836bbd
[chore] UI changes
Dhanus3133 Sep 30, 2024
dfe62ed
Merge branch 'notification-preferences' into feat/manage-notification…
Dhanus3133 Oct 2, 2024
4171bd4
[chore] CSS changes
Dhanus3133 Oct 2, 2024
2afa9fe
[qa] Fixes
Dhanus3133 Oct 2, 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 .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ on:
branches:
- master
- dev
- gsoc24-rebased
pull_request:
branches:
- master
- dev
- gsoc24-rebased
- notification-preferences

jobs:

Expand Down
3 changes: 2 additions & 1 deletion docs/user/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ include:

- :doc:`sending-notifications`
- :ref:`notifications_web_notifications`
- :ref:`notifications_email_notifications`
- :ref:`notifications_email_notifications` and
:ref:`notifications_batches`
- :doc:`notification-types`
- :doc:`User notification preferences <notification-preferences>`
- :ref:`Silencing notifications for specific objects temporarily or
Expand Down
8 changes: 6 additions & 2 deletions docs/user/notification-preferences.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Notification Preferences
========================

.. image:: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/notification-settings.png
:target: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/notification-settings.png
.. image:: https://i.imgur.com/lIGqry5.png
:target: https://i.imgur.com/lIGqry5.png
:align: center

OpenWISP Notifications enables users to customize their notification
Expand All @@ -12,6 +12,10 @@ organized by notification type and organization, allowing users to tailor
their notification experience by opting to receive updates only from
specific organizations or notification types.

Users can access and manage their notification preferences by visiting the
``/notification/preferences/``. Alternatively, this page can also be
accessed directly from the notification widget.

Notification settings are automatically generated for all notification
types and organizations for every user. Superusers have the ability to
manage notification settings for all users, including adding or deleting
Expand Down
31 changes: 31 additions & 0 deletions docs/user/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,34 @@ The default configuration is as follows:
# Maximum interval after which the notification widget should get updated (in seconds)
"max_allowed_backoff": 15,
}

.. _openwisp_notifications_email_batch_interval:

``OPENWISP_NOTIFICATIONS_EMAIL_BATCH_INTERVAL``
-----------------------------------------------

======= =================================
Type ``int``
Default ``1800`` (30 minutes, in seconds)
======= =================================

This setting determines the :ref:`interval of the email batching feature
<notifications_batches>`.

The interval is specified in seconds.

To send email notifications immediately without batching, set this value
to ``0``.

.. _openwisp_notifications_email_batch_display_limit:

``OPENWISP_NOTIFICATIONS_EMAIL_BATCH_DISPLAY_LIMIT``
----------------------------------------------------

======= =======
Type ``int``
Default ``15``
======= =======

This setting specifies the maximum number of email notifications that can
be included in a single :ref:`email batch <notifications_batches>`.
35 changes: 35 additions & 0 deletions docs/user/web-email-notifications.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,38 @@ Email Notifications
Along with web notifications OpenWISP Notifications also sends email
notifications leveraging the :ref:`send_email feature of OpenWISP Utils
<utils_send_email>`.

.. _notifications_batches:

Email Batches
~~~~~~~~~~~~~

.. figure:: https://i.imgur.com/W5P009W.png
:target: https://i.imgur.com/W5P009W.png
:align: center

Batching email notifications helps manage the flow of emails sent to
users, especially during periods of increased alert activity. By grouping
emails into batches, the system minimizes the risk of emails being marked
as spam and prevents inboxes from rejecting alerts due to high volumes.

Key aspects of the batch email notification feature include:

- When multiple emails are triggered for the same user within a short time
frame, subsequent emails are grouped into a summary.
- The sending of individual emails is paused for a specified batch
interval when batching is enabled.

.. note::

If new alerts are received while a batch is pending, they will be
added to the current summary without resetting the timer. The batched
email will be sent when the initial batch interval expires.

You can customize the behavior of batch email notifications using the
following settings:

- :ref:`OPENWISP_NOTIFICATIONS_EMAIL_BATCH_INTERVAL
<openwisp_notifications_email_batch_interval>`.
- :ref:`OPENWISP_NOTIFICATIONS_EMAIL_BATCH_DISPLAY_LIMIT
<openwisp_notifications_email_batch_display_limit>`.
22 changes: 0 additions & 22 deletions openwisp_notifications/admin.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,3 @@
from django.contrib import admin

from openwisp_notifications.base.admin import NotificationSettingAdminMixin
from openwisp_notifications.swapper import load_model
from openwisp_notifications.widgets import _add_object_notification_widget
from openwisp_users.admin import UserAdmin
from openwisp_utils.admin import AlwaysHasChangedMixin

Notification = load_model('Notification')
NotificationSetting = load_model('NotificationSetting')


class NotificationSettingInline(
NotificationSettingAdminMixin, AlwaysHasChangedMixin, admin.TabularInline
):
model = NotificationSetting
extra = 0

def has_change_permission(self, request, obj=None):
return request.user.is_superuser or request.user == obj


UserAdmin.inlines = [NotificationSettingInline] + UserAdmin.inlines

_add_object_notification_widget()
5 changes: 5 additions & 0 deletions openwisp_notifications/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ class NotificationSettingFilter(OrganizationMembershipFilter):
class Meta(OrganizationMembershipFilter.Meta):
model = NotificationSetting
fields = OrganizationMembershipFilter.Meta.fields + ['type']

@property
def qs(self):
parent_qs = super().qs
return parent_qs.exclude(organization__isnull=True, type__isnull=True)
16 changes: 16 additions & 0 deletions openwisp_notifications/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from rest_framework.permissions import BasePermission


class PreferencesPermission(BasePermission):
"""
Permission class for the notification preferences.

Permission is granted only in these two cases:
1. Superusers can change the notification preferences of any user.
2. Regular users can only change their own preferences.
"""

def has_permission(self, request, view):
return request.user.is_superuser or request.user.id == view.kwargs.get(
'user_id'
)
16 changes: 16 additions & 0 deletions openwisp_notifications/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ class Meta(NotificationSerializer.Meta):


class NotificationSettingSerializer(serializers.ModelSerializer):
organization_name = serializers.CharField(
source='organization.name', read_only=True
)
type_label = serializers.CharField(source='get_type_display', read_only=True)

class Meta:
model = NotificationSetting
exclude = ['user']
Expand All @@ -87,3 +92,14 @@ class Meta:
'object_content_type',
'object_id',
]


class NotificationSettingUpdateSerializer(serializers.Serializer):
email = serializers.BooleanField()
web = serializers.BooleanField()

def update(self, instance, validated_data):
instance.email = validated_data.get('email', instance.email)
instance.web = validated_data.get('web', instance.web)
instance.save()
return instance
49 changes: 39 additions & 10 deletions openwisp_notifications/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,61 @@ def get_api_urls(api_views=None):
if not api_views:
api_views = views
return [
path('', views.notifications_list, name='notifications_list'),
path('read/', views.notifications_read_all, name='notifications_read_all'),
path('<uuid:pk>/', views.notification_detail, name='notification_detail'),
path('notification/', views.notifications_list, name='notifications_list'),
path(
'<uuid:pk>/redirect/',
'notification/read/',
views.notifications_read_all,
name='notifications_read_all',
),
path(
'notification/<uuid:pk>/',
views.notification_detail,
name='notification_detail',
),
path(
'notification/<uuid:pk>/redirect/',
views.notification_read_redirect,
name='notification_read_redirect',
),
path(
'user-setting/',
'user/<uuid:user_id>/user-setting/',
views.notification_setting_list,
name='notification_setting_list',
name='user_notification_setting_list',
),
path(
'user-setting/<uuid:pk>/',
'user/<uuid:user_id>/user-setting/<uuid:pk>/',
views.notification_setting,
name='notification_setting',
name='user_notification_setting',
),
path(
'ignore/',
'notification/ignore/',
views.ignore_object_notification_list,
name='ignore_object_notification_list',
),
path(
'ignore/<str:app_label>/<str:model_name>/<uuid:object_id>/',
'notification/ignore/<str:app_label>/<str:model_name>/<uuid:object_id>/',
views.ignore_object_notification,
name='ignore_object_notification',
),
path(
'user/<uuid:user_id>/organization/<uuid:organization_id>/setting/',
views.organization_notification_setting,
name='organization_notification_setting',
),
path(
'user/<uuid:user_id>/preference/',
views.notification_preference,
name='notification_preference',
),
# DEPRECATED
path(
'user/user-setting/',
views.notification_setting_list,
name='notification_setting_list',
),
path(
'user/user-setting/<uuid:pk>/',
views.notification_setting,
name='notification_setting',
),
]
63 changes: 62 additions & 1 deletion openwisp_notifications/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from openwisp_notifications.api.permissions import PreferencesPermission
from openwisp_notifications.api.serializers import (
IgnoreObjectNotificationSerializer,
NotificationListSerializer,
NotificationSerializer,
NotificationSettingSerializer,
NotificationSettingUpdateSerializer,
)
from openwisp_notifications.swapper import load_model
from openwisp_users.api.authentication import BearerAuthentication
Expand Down Expand Up @@ -114,11 +116,17 @@ class BaseNotificationSettingView(GenericAPIView):
model = NotificationSetting
serializer_class = NotificationSettingSerializer
authentication_classes = [BearerAuthentication, SessionAuthentication]
permission_classes = [IsAuthenticated]
permission_classes = [PreferencesPermission]

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:
return NotificationSetting.objects.filter(user_id=user_id)

return NotificationSetting.objects.filter(user=self.request.user)


Expand Down Expand Up @@ -198,11 +206,64 @@ def perform_create(self, serializer):
)


class OrganizationNotificationSettingView(GenericAPIView):
permission_classes = [IsAuthenticated, PreferencesPermission]
serializer_class = NotificationSettingUpdateSerializer

def post(self, request, user_id, organization_id):
notification_settings = NotificationSetting.objects.filter(
organization_id=organization_id, user_id=user_id
)
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
for notification_setting in notification_settings:
serializer.update(notification_setting, serializer.validated_data)
return Response(status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class NotificationPreferenceView(GenericAPIView):
permission_classes = [IsAuthenticated, PreferencesPermission]
serializer_class = NotificationSettingUpdateSerializer

def get(self, request, user_id):
notification_settings, created = NotificationSetting.objects.get_or_create(
user_id=user_id,
organization=None,
type=None,
defaults={'email': True, 'web': True},
)
serializer = self.get_serializer(notification_settings)
return Response(serializer.data, status=status.HTTP_200_OK)

def post(self, request, user_id):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data.get('email')
web = serializer.validated_data.get('web')
(
notification_settings,
created,
) = NotificationSetting.objects.update_or_create(
user_id=user_id,
organization=None,
type=None,
defaults={'email': email, 'web': web},
)
NotificationSetting.objects.filter(user_id=user_id).update(
email=email, web=web
)
return Response(status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


notifications_list = NotificationListView.as_view()
notification_detail = NotificationDetailView.as_view()
notifications_read_all = NotificationReadAllView.as_view()
notification_read_redirect = NotificationReadRedirect.as_view()
notification_setting_list = NotificationSettingListView.as_view()
notification_setting = NotificationSettingView.as_view()
organization_notification_setting = OrganizationNotificationSettingView.as_view()
ignore_object_notification_list = IgnoreObjectNotificationListView.as_view()
ignore_object_notification = IgnoreObjectNotificationView.as_view()
notification_preference = NotificationPreferenceView.as_view()
2 changes: 1 addition & 1 deletion openwisp_notifications/base/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ def get_queryset(self, request):
super()
.get_queryset(request)
.filter(deleted=False)
.exclude(organization=None)
.prefetch_related('organization')
)

class Media:
extends = True
js = [
'admin/js/jquery.init.js',
'openwisp-notifications/js/notification-settings.js',
]
Loading