Skip to content
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
20 changes: 17 additions & 3 deletions common/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin
from django.contrib.auth.models import User

from .models import EmailVerificationTable, MaintenanceMode, UserProfile
from .models import (
EmailSendingTable,
EmailVerificationTable,
MaintenanceMode,
UserProfile,
)


class UserProfileInline(admin.StackedInline):
Expand All @@ -19,6 +24,16 @@ class EmailVerificationTableInline(admin.StackedInline):
fk_name = "user"


class EmailSendingTableAdmin(admin.ModelAdmin):
list_display = ("from_email", "to_email", "to_user", "subject", "message", "template", "status", "created_at")
search_fields = ("to_email", "subject")
list_filter = ("status", "to_user")
readonly_fields = ("to_email", "status")

def get_queryset(self, request):
return super().get_queryset(request).select_related("to_user")


class UserAdmin(DefaultUserAdmin):
inlines = (UserProfileInline, EmailVerificationTableInline)
list_display = ("email", "first_name", "last_name", "is_active", "is_staff", "get_affiliation", "date_joined")
Expand All @@ -33,5 +48,4 @@ def get_affiliation(self, instance):
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
admin.site.register(MaintenanceMode)

# Register your models here.
admin.site.register(EmailSendingTable, EmailSendingTableAdmin)
77 changes: 77 additions & 0 deletions common/migrations/0002_emailsendingtable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Generated by Django 5.1.1 on 2025-06-24 15:25

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("common", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="EmailSendingTable",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"from_email",
models.EmailField(
choices=[
("[email protected]", "[email protected]"),
("[email protected]", "[email protected]"),
],
max_length=254,
),
),
("to_email", models.EmailField(max_length=254)),
(
"subject",
models.CharField(
help_text="Subject of the email.If there is already exists a ticket on Edge, you can use it's subject to track email history through it.",
max_length=255,
),
),
(
"message",
models.TextField(
blank=True,
default="",
help_text="Email message to be sent. If base template is selected, this will be rendered using the template. You can use HTML tags here.",
null=True,
),
),
(
"template",
models.CharField(
blank=True,
choices=[
("admin/email/base.html", "admin/email/base.html"),
(
"admin/email/user-in-not-from-a-swedish-uni.html",
"admin/email/user-in-not-from-a-swedish-uni.html",
),
("admin/email/account-enabled-email.html", "admin/email/account-enabled-email.html"),
],
max_length=100,
null=True,
),
),
(
"status",
models.CharField(
choices=[("sent", "Sent"), ("failed", "Failed")], default="pending", max_length=10
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"to_user",
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
),
),
],
),
]
50 changes: 50 additions & 0 deletions common/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.conf import settings
from django.contrib.auth.models import AbstractUser, User
from django.core.mail import send_mail
from django.db import models
from django.template.loader import render_to_string

from studio.utils import get_logger

Expand Down Expand Up @@ -41,6 +43,54 @@ def send_verification_email(self):
send_verification_email_task(self.user.email, self.token)


class EmailSendingTable(models.Model):
EMAIL_TEMPLATES = {file_path: file_path for file_path in settings.EMAIL_TEMPLATES}
from_email = models.EmailField(
choices=[
(settings.DEFAULT_FROM_EMAIL, settings.DEFAULT_FROM_EMAIL),
(settings.EMAIL_FROM, settings.EMAIL_FROM),
]
)
to_user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
to_email = models.EmailField()
subject = models.CharField(
max_length=255,
help_text="Subject of the email."
"If there is already exists a ticket on Edge, you can use it's subject"
" to track email history through it.",
)
message = models.TextField(
help_text="Email message to be sent. If base template is selected, "
"this will be rendered using the template. You can use HTML tags here.",
blank=True,
null=True,
default="",
)
template = models.CharField(max_length=100, choices=EMAIL_TEMPLATES, null=True, blank=True)
status = models.CharField(choices=[("sent", "Sent"), ("failed", "Failed")], default="pending", max_length=10)
created_at = models.DateTimeField(auto_now_add=True)

def send_email(self):
message = self.message
html_message = None
if self.template:
# If a template is selected, render the message using the template

html_message = render_to_string(self.template, {"message": self.message, "user": self.to_user})
message = html_message # Use the rendered HTML message as the plain text message
else:
if not message:
raise ValueError("Message cannot be empty if no template is selected.")
send_mail(
subject=self.subject,
message=message,
from_email=self.from_email,
recipient_list=[self.to_email, settings.DEFAULT_FROM_EMAIL],
html_message=html_message,
fail_silently=False,
)


class FixtureVersion(models.Model):
filename = models.CharField(max_length=255, unique=True)
hash = models.CharField(max_length=64) # Length of a SHA-256 hash
Expand Down
19 changes: 18 additions & 1 deletion common/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver

from common.models import EmailVerificationTable
from common.models import EmailSendingTable, EmailVerificationTable


@receiver(pre_save, sender=User)
Expand Down Expand Up @@ -33,3 +33,20 @@ def send_verification_email(sender, instance, created, **kwargs):
"""
if created:
instance.send_verification_email()


@receiver(pre_save, sender=EmailSendingTable)
def send_manual_email(sender, instance: EmailSendingTable, **kwargs):
"""
This function is used to send manual email to the user.

It is registered by ``common.apps.CommonConfig.ready()``
"""
instance.to_email = instance.to_user.email if instance.to_user else instance.to_email
try:
instance.send_email()
instance.status = "sent"
except Exception as e:
# Log the error or handle it as needed
print(f"Error sending email: {e}")
instance.status = "failed"
9 changes: 7 additions & 2 deletions common/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,20 @@ def alert_pause_dormant_users() -> None:

@app.task(ignore_result=True)
def send_email_task(
subject: str, message: str, recipient_list: list[str], html_message: str | None = None, fail_silently: bool = False
subject: str,
message: str,
recipient_list: list[str],
html_message: str | None = None,
fail_silently: bool = False,
from_email: str = settings.EMAIL_FROM,
) -> None:
"""
Send message content if html_message is None, otherwise send html_message.
"""
logger.info("Sending email to %s", recipient_list)
mail_subject = subject
mail_message = message if html_message is None else html_message
email = EmailMessage(mail_subject, mail_message, settings.EMAIL_FROM, to=recipient_list)
email = EmailMessage(mail_subject, mail_message, from_email, to=recipient_list)
email.content_subtype = "html" if html_message else "plain"
email.send(fail_silently=fail_silently)

Expand Down
5 changes: 5 additions & 0 deletions studio/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@
},
},
]
EMAIL_TEMPLATES = []
for path in sorted((Path(BASE_DIR, "templates", "admin", "email")).iterdir(), key=lambda p: p.name):
if path.is_file() and path.suffix in [".html", ".txt"]:
EMAIL_TEMPLATES.append(f"admin/email/{path.name}")

TEMPLATE_LOADERS = (
"django.template.loaders.filesystem.Loader",
Expand Down Expand Up @@ -429,6 +433,7 @@
EMAIL_BACKEND = (
"gmailapi_backend.service.GmailApiBackend" if not DEBUG else "django.core.mail.backends.console.EmailBackend"
)
DEFAULT_FROM_EMAIL = "[email protected]"
EMAIL_FROM = "[email protected]"
GMAIL_USER = "[email protected]"
GMAIL_SCOPES = ["https://www.googleapis.com/auth/gmail.send"]
Expand Down
20 changes: 20 additions & 0 deletions templates/admin/email/account-enabled-email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends 'admin/email/base.html' %}
{% block email-title %}Account Enabled Notification{% endblock %}

{% block content %}
<p>Dear {{ user.first_name }},</p>

<p>
We have approved your account on <a href="https://serve.scilifelab.se/">SciLifeLab Serve</a>. This means that you can now log in and use it.
</p>

<p>
We recommend following the relevant user guide for your purpose. If you run into issues, do not hesitate to get in touch with us (<a href="mailto:[email protected]">[email protected]</a>), we can take a look and help out.
</p>

<p>
Kind regards,<br>
SciLifeLab Serve team
</p>

{% endblock %}
41 changes: 41 additions & 0 deletions templates/admin/email/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{% load get_setting %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block email-title %}{% endblock %}</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333;
padding: 20px;
}
.email-container {
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
max-width: 600px;
margin: auto;
}
.button {
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="email-container">
{% block content %}{% endblock %}
</div>
</body>
</html>
20 changes: 20 additions & 0 deletions templates/admin/email/user-in-not-from-a-swedish-uni.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends 'admin/email/base.html' %}

{% block email-title %}Account Not Approved Notification{% endblock %}

{% block content %}
<p>Dear {{ user.first_name }},</p>

<p>
You have requested an account on our service SciLifeLab Serve (<a href="https://serve.scilifelab.se">https://serve.scilifelab.se</a>). Our service is only available to researchers affiliated with a Swedish university or their collaborators who need to work with them on the service. Your email is not related to a Swedish university, however.
</p>

<p>
Can you please let us know if you are currently collaborating with someone in Sweden?
</p>

<p>
Kind regards,<br>
SciLifeLab Serve team
</p>
{% endblock %}