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 6 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
7 changes: 6 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"
DATASTEWARD_MAILING_LIST = "" # used when there are notifications errors

# Placeholders on login page
# LOGIN_USERNAME_PLACEHOLDER = ''
Expand Down Expand Up @@ -84,5 +85,9 @@
"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_all_notifications_for_user_upcoming_events",
Fancien marked this conversation as resolved.
Show resolved Hide resolved
"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)
2 changes: 1 addition & 1 deletion 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 Down
15 changes: 4 additions & 11 deletions notification/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
Notification class does not have any target at the moment.
"""
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 @@ -94,15 +92,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)])
raise Exception("No url defined for this content type")
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
196 changes: 196 additions & 0 deletions notification/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
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_datasteward(user):
"""
Send upcoming events notifications errors report for datasteward, if any.

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

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

# Send email to data steward 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.DATASTEWARD_MAILING_LIST,
"Notifications",
"notification/email_datasteward_notifications_error",
context,
)
except Exception as e:
logger.error(
f"Failed: An error occurred while sending Email notification error report for data-stewards."
f" Error: {e}"
)
print(
f"Failed: An error occurred while sending Email notification error report for data-stewards."
f" Error: {e}"
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This could be removed no? Also, is it intentional for the error to not be raised again?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes it could be removed. The idea was that we log the error and then send datastewards an email in case of notification error and continue to the next user. you think we should raise the error anyways?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It makes sense. I think the error email should be sent at the very end though, or we risk sending an email for every user on some days.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i thought it is one email per user. I felt it would be easier for data stewards to track which user notifications errors they checked and which they didn't. Otherwise. if all users in one email it might be long and maybe they will not be able to keep track of where they stopped.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems the print is back :/

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sorry, removed it now.



@shared_task
def send_notifications_for_user_upcoming_events(execution_date: str = None):
"""
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)
"""

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:
notifications_exec_date = Notification.objects.filter(
recipient=user.id,
dispatch_by_email=True,
processing_date=None,
time__date=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 data-stewards, if any
report_notifications_upcoming_events_errors_for_datasteward(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}"
)
print(
f"Failed: An error occurred while sending upcoming events Email notification for user {user.full_name}."
f" Error: {e}"
)
Fancien marked this conversation as resolved.
Show resolved Hide resolved
finally:
# report error to data-stewards
report_notifications_upcoming_events_errors_for_datasteward(user)
continue


@shared_task
def send_all_notifications_for_user_upcoming_events(execution_date: str = None):
Fancien marked this conversation as resolved.
Show resolved Hide resolved
"""
Send all 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)
"""

logger.info("Sending all 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:
notifications_lte_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_lte_exec_date:
# Checks if there is missed notification for this user and report errors to data-stewards, if any
report_notifications_upcoming_events_errors_for_datasteward(user)
continue

# group notifications per content type and set processed to today
notifications_by_content_type = defaultdict(list)
for notif in notifications_lte_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_lte_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}"
)
print(
f"Failed: An error occurred while sending upcoming events Email notification for user {user.full_name}."
f" Error: {e}"
)
finally:
# report error to data-stewards
report_notifications_upcoming_events_errors_for_datasteward(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 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 notifications settings in your <a href="{{profile_url}}" style="color: #f47d20;">profile</a>.
Fancien marked this conversation as resolved.
Show resolved Hide resolved
{% endblock %}
</td>
</tr>
</table>
</body>
</html>
2 changes: 0 additions & 2 deletions notification/templates/notification/email.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
DAISY - {% block title %}{% endblock %}

{% block content %}{% endblock %}

{% block footer %}
Expand Down
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