diff --git a/artel/artel/settings/base.py b/artel/artel/settings/base.py index 66d3794..0b62259 100644 --- a/artel/artel/settings/base.py +++ b/artel/artel/settings/base.py @@ -26,6 +26,7 @@ BASE_DIR = os.path.dirname(PROJECT_DIR) INSTALLED_APPS = [ "home", "store", + "mailings", "blog", "search", "wagtail.contrib.forms", diff --git a/artel/mailings/__init__.py b/artel/mailings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/artel/mailings/admin.py b/artel/mailings/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/artel/mailings/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/artel/mailings/apps.py b/artel/mailings/apps.py new file mode 100644 index 0000000..c2ec5b6 --- /dev/null +++ b/artel/mailings/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MailingsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'mailings' diff --git a/artel/mailings/migrations/0001_initial.py b/artel/mailings/migrations/0001_initial.py new file mode 100644 index 0000000..3b6fdfc --- /dev/null +++ b/artel/mailings/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 4.1.9 on 2023-06-21 18:32 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="MailTemplate", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("template_name", models.CharField(max_length=255, unique=True)), + ("template", models.FileField(upload_to="mail_templates")), + ("subject", models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name="OutgoingEmail", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("sender", models.EmailField(max_length=254)), + ("recipient", models.EmailField(max_length=254)), + ("sent", models.BooleanField(default=False)), + ( + "template", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="mailings.mailtemplate"), + ), + ], + ), + ] diff --git a/artel/mailings/migrations/__init__.py b/artel/mailings/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/artel/mailings/models.py b/artel/mailings/models.py new file mode 100644 index 0000000..dd5d119 --- /dev/null +++ b/artel/mailings/models.py @@ -0,0 +1,94 @@ +from typing import Any +from dataclasses import dataclass + +from django.db import models +from django.db import transaction +from django.template import ( + Template, + Context +) +from django.core.mail import EmailMessage +from django.conf import settings + + +@dataclass +class Attachment: + name: str + content: Any + contenttype: str + + +def send_mail( + to: list[str], + attachments: list[Attachment], + subject: str, + body: str, + sender_email: str = settings.DEFAULT_FROM_EMAIL + ): + message = EmailMessage( + subject=subject, + body=body, + from_email=sender_email, + to=to + ) + for attachment in attachments: + message.attach(attachment.name, attachment.content, attachment.contenttype) + return bool(message.send()) + + +class MailTemplate(models.Model): + template_name = models.CharField(max_length=255, unique=True) + + template = models.FileField( + upload_to="mail_templates" + ) + subject = models.CharField(max_length=255) + + def delete(self, *args, **kwargs): + # delete file + super().delete(*args, **kwargs) + + @transaction.on_commit + def remove_template_file(self): + self.template.delete() + + def load_and_process_template(self, context: dict|Context): + if not self.template: + raise FileNotFoundError("Template file is missing") + if isinstance(context, dict): + context = Context(context) + with open(self.template.path, "r", encoding="utf-8") as f: + content = f.read() + template = Template(content) + return template.render(context) + + +class OutgoingEmailManager(models.Manager): + + def send( + self, template_name: str, + recipient: str, context: dict | Context, + sender:str, attachments: list[Attachment] = None + ): + template = MailTemplate.objects.get(template_name=template_name) + outgoing_email = self.create(template=template, recipient=recipient) + attachments = attachments or [] + # send email + sent = send_mail( + to=[recipient], sender_email=sender, + subject=template.subject, content=template.load_and_process_template(context), + attachments=attachments + ) + outgoing_email.sent = sent + outgoing_email.save() + return outgoing_email + + +class OutgoingEmail(models.Model): + template = models.ForeignKey(MailTemplate, on_delete=models.CASCADE) + sender = models.EmailField() + recipient = models.EmailField() + + sent = models.BooleanField(default=False) + + objects = OutgoingEmailManager() diff --git a/artel/mailings/tests.py b/artel/mailings/tests.py new file mode 100644 index 0000000..02bbbc3 --- /dev/null +++ b/artel/mailings/tests.py @@ -0,0 +1,64 @@ +from django.test import TestCase +from django.core.files.uploadedfile import SimpleUploadedFile + +from mailings.models import ( + MailTemplate, + OutgoingEmail, +) + + +class TestMailTemplate(TestCase): + + def setUp(self) -> None: + super().setUp() + self.mail_template = MailTemplate.objects.create( + template_name="test_template", + template=SimpleUploadedFile( + "test_template.html", b"{{test_var}}" + ), + subject="Test subject", + ) + + def test_load_and_process_template_success(self): + content = self.mail_template.load_and_process_template({"test_var": "test"}) + self.assertEqual(content, "test") + + def test_load_and_process_template_missing_var_failure(self): + content = self.mail_template.load_and_process_template({}) + self.assertEqual(content, "") + + def test_load_and_preprocess_template_no_template_file(self): + self.mail_template.template.delete() + self.mail_template.template = None + self.mail_template.save() + with self.assertRaises(FileNotFoundError): + self.mail_template.load_and_process_template({}) + + +class TestOutgoingEmail(TestCase): + + def setUp(self) -> None: + super().setUp() + self.mail_template = MailTemplate.objects.create( + template_name="test_template", + template=SimpleUploadedFile( + "test_template.html", b"{{test_var}}" + ), + subject="Test subject", + ) + + def test_send_success(self): + mail = OutgoingEmail.objects.send( + template_name="test_template", + recipient="test@stardust.io", context={}, + sender="sklep-test@stardust.io" + ) + self.assertEqual(mail.sent, True) + # TODO outbox + + def test_send_missing_template_failure(self): + with self.assertRaises(MailTemplate.DoesNotExist): + OutgoingEmail.objects.send( + template_name="missing_template", + recipient="", sender="", context={}\ + ) diff --git a/artel/mailings/views.py b/artel/mailings/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/artel/mailings/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/artel/requirements.txt b/artel/requirements.txt index 5a81340..7b1f252 100644 --- a/artel/requirements.txt +++ b/artel/requirements.txt @@ -1,7 +1,7 @@ Django>=4.1,<4.2 wagtail>=4.2,<4.3 wagtailmenus>=3.1.5,<=3.1.7 -psycopg2>=2.9.5,<=2.9.6 +psycopg2-binary>=2.9.5,<=2.9.6 dj-database-url<=2.0.0 djangorestframework==3.14.0 phonenumbers==8.13.13