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

Email notification tasks and templates #472 #485

Merged
merged 16 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 core/models/contract.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse

from core import constants
from core.permissions.mapping import PERMISSION_MAPPING
Expand Down Expand Up @@ -118,6 +119,8 @@ class AppMeta:
)

# metadata = JSONField(null=True, blank=True, default=dict)
def get_absolute_url(self):
return reverse("contract", args=[str(self.pk)])

def add_partner_with_role(self, partner, role, contact=None):
partner_role = self.partners_roles.create(partner=partner)
Expand Down
4 changes: 4 additions & 0 deletions core/models/data_declaration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import uuid

from django.db import models
from django.urls import reverse
from enumchoicefield import EnumChoiceField, ChoiceEnum

from core import constants
Expand Down Expand Up @@ -217,6 +218,9 @@ class Meta:
help_text="This is the unique identifier used by DAISY for this dataset. This field annot be edited.",
)

def get_absolute_url(self):
return reverse("data_declaration", args=[str(self.pk)])

def copy(
self, source_data_declaration, excluded_fields=None, ignore_many_to_many=False
):
Expand Down
4 changes: 4 additions & 0 deletions core/models/project.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.module_loading import import_string
from guardian.models import GroupObjectPermissionBase, UserObjectPermissionBase
Expand Down Expand Up @@ -203,6 +204,9 @@ class AppMeta:
def __str__(self):
return self.acronym or self.title or "undefined"

def get_absolute_url(self):
return reverse("project", args=[str(self.pk)])

@property
def is_published(self):
return any(dataset.is_published for dataset in self.datasets.all())
Expand Down
2 changes: 1 addition & 1 deletion elixir_daisy/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@
}

# by default, notifications by email are disabled
NOTIFICATIONS_DISABLED = False
NOTIFICATIONS_DISABLED = True

LOGIN_USERNAME_PLACEHOLDER = ""
LOGIN_PASSWORD_PLACEHOLDER = ""
Expand Down
12 changes: 11 additions & 1 deletion elixir_daisy/settings_local.template.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
COMPANY = "LCSB" # Used for generating some models' verbose names

HELPDESK_EMAIL = "lcsb-sysadmins@uni.lu"
ADMIN_NOTIFICATIONS_EMAIL = "" # used when there are notifications errors

# Placeholders on login page
# LOGIN_USERNAME_PLACEHOLDER = ''
Expand Down Expand Up @@ -79,10 +80,19 @@
# KEYCLOAK_USER = 'your service user for daisy'
# KEYCLOAK_PASS = 'the password for the service user'

# Email related setting
EMAIL_HOST = ""
EMAIL_PORT = 25
EMAIL_SENDER = ""

# Celery beat setting to schedule tasks on docker creation
CELERY_BEAT_SCHEDULE = {
"clean-accesses-every-day": {
"task": "core.tasks.check_accesses_expiration",
"schedule": crontab(minute=0, hour=0), # Execute task at midnight
}
},
"notifications-email-every-day": {
"task": "notification.tasks.send_notifications_for_user_upcoming_events",
"schedule": crontab(minute=0, hour=0), # Execute task at midnight
},
}
7 changes: 6 additions & 1 deletion notification/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@

from notification.models import Notification, NotificationSetting

admin.site.register(Notification)

class NotificationAdmin(admin.ModelAdmin):
readonly_fields = ("processing_date",)


admin.site.register(Notification, NotificationAdmin)
admin.site.register(NotificationSetting)
8 changes: 5 additions & 3 deletions notification/email_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

def send_the_email(sender_email, recipients, subject, template, context):
"""
Send an email to the recipents using the templates,
Send an email to the recipients using the templates,
"""
# recipients can be a list or single email
if not isinstance(recipients, (list, tuple)):
Expand All @@ -22,8 +22,10 @@ def send_the_email(sender_email, recipients, subject, template, context):
settings.SERVER_SCHEME,
settings.SERVER_URL,
)
if "profile_url" not in context:
context["profile_url"] = reverse("profile")
if "notifications_settings_url" not in context:
context["notifications_settings_url"] = context["server_url"] + reverse(
"notifications_settings"
)

# prepare email
subject = f"{SUBJECT_PREFIX} {subject}"
Expand Down
16 changes: 4 additions & 12 deletions notification/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
from datetime import datetime

from django.db import models
from django.urls import reverse
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth import get_user_model
Expand Down Expand Up @@ -96,16 +94,10 @@ class Meta:
objects = NotificationManager()

def get_absolute_url(self):
if self.content_type.model_class() == apps.get_model("core.Dataset"):
return reverse("dataset", args=[str(self.object_id)])
elif self.content_type.model_class() == apps.get_model("core.DataDeclaration"):
return reverse("data_declaration", args=[str(self.object_id)])
elif self.content_type.model_class() == apps.get_model("core.Contract"):
return reverse("contract", args=[str(self.object_id)])
elif self.content_type.model_class() == apps.get_model("core.Project"):
return reverse("project", args=[str(self.object_id)])
else:
return None
model_class = self.content_type.model_class()
if hasattr(model_class, "get_absolute_url"):
return model_class.objects.get(pk=self.object_id).get_absolute_url()
return None

def get_full_url(self):
"""
Expand Down
138 changes: 138 additions & 0 deletions notification/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import smtplib
from collections import defaultdict
from datetime import datetime

from celery import shared_task
from django.conf import settings
from django.contrib.auth import get_user_model
from notification.email_sender import send_the_email
from notification.models import Notification
from celery.utils.log import get_task_logger

logger = get_task_logger(__name__)


def report_notifications_upcoming_events_errors_for_admin(user):
"""
Send upcoming events notifications errors report for admin, if any.

Params:
user: The user for which the notifications sending failed
"""
logger.info("Sending upcoming events notifications errors for admin")

notifications_not_processed = Notification.objects.filter(
recipient=user.id, dispatch_by_email=True, processing_date=None
)

# Send email to admin in case of errors
if notifications_not_processed:
logger.error("Some notification are not processed: ")
logger.error(notifications_not_processed)

notifications_not_processed_by_content_type = defaultdict(list)

for notif in notifications_not_processed:
notifications_not_processed_by_content_type[notif.content_type].append(
notif
)

context = {
"notifications": dict(notifications_not_processed_by_content_type),
"error_message": "Please find below the notifications that failed to be sent to user: "
+ user.full_name,
}
try:
send_the_email(
settings.EMAIL_DONOTREPLY,
settings.ADMIN_NOTIFICATIONS_EMAIL,
"Notifications",
"notification/email_admin_notifications_error",
context,
)
except Exception as e:
logger.error(
f"Failed: An error occurred while sending Email notification error report for admin."
f" Error: {e}"
)


@shared_task
def send_notifications_for_user_upcoming_events(
execution_date: str = None, only_one_day: bool = False
):
"""
Send upcoming events notification report for all users, if any.

Params:
execution_date: The date of the execution of the task. FORMAT: YYYY-MM-DD (DEFAULT: Today)
only_one_day: If true send the notifications of the execution date only.
"""

if settings.NOTIFICATIONS_DISABLED:
logger.info(
"No notifications sent. Notifications sending is disabled in settings.py"
)
return

logger.info("Sending notification for user upcoming events")

if not execution_date:
exec_date = datetime.now().date()
else:
exec_date = datetime.strptime(execution_date, "%Y-%m-%d").date()

users = get_user_model().objects.all()

for user in users:
if only_one_day:
notifications_exec_date = Notification.objects.filter(
recipient=user.id,
dispatch_by_email=True,
processing_date=None,
time__date=exec_date,
)
else:
notifications_exec_date = Notification.objects.filter(
recipient=user.id,
dispatch_by_email=True,
processing_date=None,
time__date__lte=exec_date,
)

# send notification report to user, if any
if not notifications_exec_date:
# Checks if there is missed notification for this user and report errors to admin, if any
report_notifications_upcoming_events_errors_for_admin(user)
continue

# group notifications per content type and set processed to today
notifications_by_content_type = defaultdict(list)
for notif in notifications_exec_date:
notifications_by_content_type[notif.content_type].append(notif)

context = {
"user": user.full_name,
"notifications": dict(notifications_by_content_type),
}

try:
send_the_email(
settings.EMAIL_DONOTREPLY,
user.email,
"Notifications",
"notification/email_list_notifications",
context,
)
for notif in notifications_exec_date:
notif.processing_date = datetime.now().date()
notif.save()
except Exception as e:
logger.error(
f"Failed: An error occurred while sending upcoming events Email notification for user {user.full_name}."
f" Error: {e}"
)
finally:
# report error to admin
report_notifications_upcoming_events_errors_for_admin(user)
continue
46 changes: 26 additions & 20 deletions notification/templates/notification/email.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@

{% load static %}

<!DOCTYPE html>
<html lang="en">
<head>
Expand All @@ -15,26 +18,29 @@
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }
</style>
</head>
<body style="background-color: #fafafa; margin: 0 !important; padding: 60px 0 60px 0 !important;">
<table border="0" cellspacing="0" cellpadding="0" role="presentation" width="100%">
<tr>
<td bgcolor="#fafafa" style="font-size: 0;">&​nbsp;</td>
<td bgcolor="#fafafa" width="600" style="border-radius: 4px; color: grey; font-family: sans-serif; font-size: 18px; line-height: 28px; padding: 40px 40px;">
<article>
<h1 style="background-color: #009688; color: #fff; font-size: 32px; font-weight: bold; line-height: 36px; padding: 10px 30px 10px 30px">
DAISY - {% block title %}{% endblock %}
</h1>
<p style="margin: 30px 0 30px 0;">{% block content %}{% endblock %}</p>
<p style="background-color: #009688; color: #fff; font-weight: bold; line-height: 36px; padding: 10px 20px 10px 20px;">
{% block footer %}
This email has been generated from <a href="{{server_url}}">DAISY</a>.
You can change your notifications settings in your <a href="{{profile_url}}">profile</a>.
{% endblock %}
</p>
</article>
</td>
<td bgcolor="#fafafa" style="font-size: 0;">&​nbsp;</td>
</tr>
<body style="background-color: #fff; margin: 0 !important; padding:0 !important;">
<!-- Header -->
<table style="margin-bottom: 15px" role="presentation" border="0" cellspacing="0" cellpadding="0" width="100%">
<tr>
<td bgcolor="#fafafa" align="center">
<img src="{{server_url}}{% static '/images/banner.png' %}" alt="Daisy Banner" width="100%">
</td>
</tr>
</table>
<!-- Body -->
<table role="presentation" border="0" width="100%" cellspacing="0" style="color: #212529; font-family: sans-serif; font-size: 16px;">
{% block content %}{% endblock %}
</table>
<!-- footer -->
<table role="presentation" border="0" width="100%" cellspacing="0">
<tr>
<td style="color: #212529; font-size:13px; padding-left: 10px;" width="100%">
{% block footer %}
This email has been generated from <a href="{{server_url}}" style="color: #f47d20;">DAISY</a>.
You can change your <a href="{{notifications_settings_url}}" style="color: #f47d20;">notifications settings </a> here.
{% endblock %}
</td>
</tr>
</table>
</body>
</html>
4 changes: 1 addition & 3 deletions notification/templates/notification/email.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
DAISY - {% block title %}{% endblock %}

{% block content %}{% endblock %}

{% block footer %}
**********************************************************************************
This email has been generated from DAISY: {{server_url}}.
You can change your notifications settings in your>profile {{profile_url}}.
You can change your notifications settings here {{notifications_settings_url}}.
**********************************************************************************
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends 'notification/email.txt' %}

{% block content %}
Hello, {{ error_message }}

{% for content_type, notifs in notifications.items %}
- {{content_type.name|title}}
{% for notif in notifs %}
* {{notif.message}}:
- ID: {{notif.content_object.id}} | Name: {{notif.content_object}} | Notification Date:{{ notif.on}}.
{% endfor %}
{% endfor %}
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends 'notification/email.txt' %}

{% block content %}
Hello, {{ error_message }}

{% for content_type, notifs in notifications.items %}
- {{content_type.name|title}}
{% for notif in notifs %}
* {{notif.message}}:
- ID: {{notif.content_object.id}} | Name: {{notif.content_object}} | Notification Date:{{ notif.on}}.
{% endfor %}
{% endfor %}
{% endblock %}
Loading
Loading