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] Batch email notifications #276

Merged
merged 25 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2c780c8
[chore] Basic implementation
Dhanus3133 Jun 4, 2024
949ea24
[chore] Updated changes
Dhanus3133 Jun 6, 2024
305d6c8
[chore] Add tests
Dhanus3133 Jun 7, 2024
0e54696
Merge branch 'master' into feat/batch-email
Dhanus3133 Jun 7, 2024
58bdc9d
[chore] Update Readme
Dhanus3133 Jun 8, 2024
4abc717
[chore] New changes
Dhanus3133 Jun 15, 2024
caf3031
Merge branch 'master' into feat/batch-email
Dhanus3133 Jun 15, 2024
233a1e7
[chore] Improvements
Dhanus3133 Jun 17, 2024
69365bb
[chore] Fix cache delete
Dhanus3133 Jun 17, 2024
9d38db1
[chore] New changes
Dhanus3133 Jun 18, 2024
01182c4
[chore] Updates
Dhanus3133 Jun 18, 2024
483df00
[chore] Add extra tests
Dhanus3133 Jun 18, 2024
68a1604
[fix] Tests
Dhanus3133 Jun 21, 2024
6757f7b
[chore] Bump changes
Dhanus3133 Jul 6, 2024
a826d5e
[feat] Automatically open notification widget
Dhanus3133 Jul 6, 2024
7a55502
[chore] Open notifications widget with #notifications
Dhanus3133 Jul 10, 2024
67de9e5
[tests] Fixed test_without_batch_email_notification
nemesifier Jul 15, 2024
12288de
[fix] Updated
Dhanus3133 Jul 17, 2024
3760173
[chore] Update tests
Dhanus3133 Jul 17, 2024
2087b3e
Merge branch 'gsoc24' into feat/batch-email
Dhanus3133 Jul 17, 2024
56ac4cf
[chore] Update email title notifications count
Dhanus3133 Jul 17, 2024
12d19e2
[QA] Checks
Dhanus3133 Jul 17, 2024
49087b2
[chore] Update batch_email txt
Dhanus3133 Jul 18, 2024
a34920a
[fix] Use email_message instead of message
Dhanus3133 Jul 24, 2024
65dbdc1
[chore] Add email content tests
Dhanus3133 Jul 26, 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
98 changes: 67 additions & 31 deletions openwisp_notifications/handlers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

from celery.exceptions import OperationalError
from celery.result import AsyncResult
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
Expand All @@ -26,6 +27,7 @@
logger = logging.getLogger(__name__)

EXTRA_DATA = app_settings.get_config()['USE_JSONFIELD']
EMAIL_BATCH_INTERVAL = app_settings.get_config()['EMAIL_BATCH_INTERVAL']
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved

User = get_user_model()

Expand Down Expand Up @@ -161,39 +163,37 @@ def notify_handler(**kwargs):
return notification_list


@receiver(post_save, sender=Notification, dispatch_uid='send_email_notification')
def send_email_notification(sender, instance, created, **kwargs):
# Abort if a new notification is not created
if not created:
return
# Get email preference of user for this type of notification.
def can_send_email(instance):
# Get email preference of the user for this type of notification
target_org = getattr(getattr(instance, 'target', None), 'organization_id', None)
if instance.type and target_org:
try:
notification_setting = instance.recipient.notificationsetting_set.get(
organization=target_org, type=instance.type
)
except NotificationSetting.DoesNotExist:
return
return False
email_preference = notification_setting.email_notification
else:
# We can not check email preference if notification type is absent,
# or if target_org is not present
# therefore send email anyway.
# Send email anyway if notification type or target_org is absent
email_preference = True

# Check if email is verified
email_verified = instance.recipient.emailaddress_set.filter(
verified=True, email=instance.recipient.email
).exists()

# Verify email preference and email verification status
if not (email_preference and instance.recipient.email and email_verified):
return
return False

# Verify the email subject
try:
subject = instance.email_subject
except NotificationRenderException:
# Do not send email if notification is malformed.
return
return False

# Get the URL and description
url = instance.data.get('url', '') if instance.data else None
description = instance.message
if url:
Expand All @@ -203,25 +203,61 @@ def send_email_notification(sender, instance, created, **kwargs):
else:
target_url = None
if target_url:
description += _('\n\nFor more information see %(target_url)s.') % {
'target_url': target_url
}

send_email(
subject,
description,
instance.message,
recipients=[instance.recipient.email],
extra_context={
'call_to_action_url': target_url,
'call_to_action_text': _('Find out more'),
},
)
description += _('\n\nFor more information see %(target_url)s.') % {'target_url': target_url}

# flag as emailed
instance.emailed = True
# bulk_update is used to prevent emitting post_save signal
Notification.objects.bulk_update([instance], ['emailed'])
return True, subject, description, target_url


@receiver(post_save, sender=Notification, dispatch_uid='send_email_notification')
def send_email_notification(sender, instance, created, **kwargs):
# Abort if a new notification is not created
if not created:
return

can_send, subject, description, target_url = can_send_email(instance)
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved

if not can_send:
return

recipient_id = instance.recipient.id
cache_key = f'email_batch_{recipient_id}'
cache_data = cache.get(cache_key, {'last_email_sent_time': None, 'batch_scheduled': False})
is_send_email = False

if cache_data['last_email_sent_time']:
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
if (timezone.now() - cache_data['last_email_sent_time']).seconds < EMAIL_BATCH_INTERVAL:
if not cache_data['batch_scheduled']:
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
# Schedule batch email notification task
tasks.batch_email_notification.apply_async((recipient_id,), countdown=EMAIL_BATCH_INTERVAL)
cache_data['batch_scheduled'] = True
cache.set(cache_key, cache_data, timeout=EMAIL_BATCH_INTERVAL)
return
else:
# If the interval has passed, send the email immediately
is_send_email = True
cache_data['last_email_sent_time'] = timezone.now()
cache_data['batch_scheduled'] = False
else:
# If no email has been sent yet, send the email immediately
is_send_email = True
cache_data['last_email_sent_time'] = timezone.now()

cache.set(cache_key, cache_data, timeout=EMAIL_BATCH_INTERVAL)
if is_send_email:
send_email(
subject,
description,
instance.message,
recipients=[instance.recipient.email],
extra_context={
'call_to_action_url': target_url,
'call_to_action_text': _('Find out more'),
},
)
# flag as emailed
instance.emailed = True
# bulk_update is used to prevent emitting post_save signal
Notification.objects.bulk_update([instance], ['emailed'])


@receiver(post_save, sender=Notification, dispatch_uid='clear_notification_cache_saved')
Expand Down
6 changes: 6 additions & 0 deletions openwisp_notifications/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
'openwisp-notifications/audio/notification_bell.mp3',
)

EMAIL_BATCH_INTERVAL = getattr(
settings,
'EMAIL_BATCH_INTERVAL',
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
30 * 60 # 30 minutes
)

# Remove the leading "/static/" here as it will
# conflict with the "static()" call in context_processors.py.
# This is done for backward compatibility.
Expand Down
19 changes: 19 additions & 0 deletions openwisp_notifications/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@
from celery import shared_task
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.db.models import Q
from django.db.utils import OperationalError
from django.utils import timezone

from openwisp_notifications import types
from openwisp_notifications import settings as app_settings
from openwisp_notifications import handlers
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
from openwisp_notifications.swapper import load_model, swapper_load_model
from openwisp_notifications import settings as app_settings
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
from openwisp_utils.admin_theme.email import send_email
from openwisp_utils.tasks import OpenwispCeleryTask

EMAIL_BATCH_INTERVAL = app_settings.get_config()['EMAIL_BATCH_INTERVAL']
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved

User = get_user_model()

Notification = load_model('Notification')
Expand Down Expand Up @@ -202,3 +209,15 @@ def delete_ignore_object_notification(instance_id):
Deletes IgnoreObjectNotification object post it's expiration.
"""
IgnoreObjectNotification.objects.filter(id=instance_id).delete()


@shared_task(base=OpenwispCeleryTask)
def batch_email_notification(instance_id):
Dhanus3133 marked this conversation as resolved.
Show resolved Hide resolved
# Get all unsent notifications for the user in the last 30 minutes
time_threshold = timezone.now() - timedelta(minutes=30)
unsent_notifications = Notification.objects.filter(
emailed=False,
recipient_id=instance_id,
created__gte=time_threshold
)
print("Sending email....")
Loading