commit
9dd2933444
|
@ -26,6 +26,7 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
|
|||
INSTALLED_APPS = [
|
||||
"home",
|
||||
"store",
|
||||
"mailings",
|
||||
"blog",
|
||||
"search",
|
||||
"wagtail.contrib.forms",
|
||||
|
@ -198,4 +199,4 @@ EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
|
|||
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", '')
|
||||
EMAIL_PORT = os.environ.get('EMAIL_PORT', 587)
|
||||
EMAIL_USE_TLS = True
|
||||
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'mtyton@tepewu.pl')
|
||||
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'artel-sklep@tepewu.pl')
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
from django.forms import fields
|
||||
|
||||
from wagtail.contrib.modeladmin.options import (
|
||||
ModelAdmin,
|
||||
ModelAdminGroup,
|
||||
modeladmin_register
|
||||
)
|
||||
|
||||
from mailings import models
|
||||
|
||||
|
||||
class MailTemplateAdmin(ModelAdmin):
|
||||
model = models.MailTemplate
|
||||
menu_label = "Mail templates"
|
||||
menu_icon = 'mail'
|
||||
menu_order = 100
|
||||
add_to_settings_menu = False
|
||||
exclude_from_explorer = False
|
||||
list_display = (
|
||||
"template_name",
|
||||
)
|
||||
search_fields = (
|
||||
"template_name",
|
||||
)
|
||||
list_filter = (
|
||||
"template_name",
|
||||
)
|
||||
form_fields = (
|
||||
"template_name",
|
||||
"template",
|
||||
)
|
||||
|
||||
|
||||
class OutgoingMailAdmin(ModelAdmin):
|
||||
model = models.OutgoingEmail
|
||||
menu_label = "Outgoing mails"
|
||||
menu_icon = 'mail'
|
||||
menu_order = 100
|
||||
add_to_settings_menu = False
|
||||
exclude_from_explorer = False
|
||||
list_display = (
|
||||
"subject",
|
||||
"to",
|
||||
"sent",
|
||||
)
|
||||
search_fields = (
|
||||
"subject",
|
||||
"to",
|
||||
)
|
||||
list_filter = (
|
||||
"subject",
|
||||
"sender",
|
||||
"recipient",
|
||||
"template__template_name",
|
||||
"sent",
|
||||
)
|
||||
readonly_fields = (
|
||||
"subject",
|
||||
"sender",
|
||||
"recipient",
|
||||
"sent"
|
||||
)
|
||||
|
||||
|
||||
class MailingGroup(ModelAdminGroup):
|
||||
menu_label = "Mailings"
|
||||
menu_icon = 'mail'
|
||||
menu_order = 200
|
||||
items = (
|
||||
MailTemplateAdmin,
|
||||
OutgoingMailAdmin
|
||||
)
|
||||
|
||||
|
||||
modeladmin_register(MailingGroup)
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MailingsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'mailings'
|
|
@ -0,0 +1,35 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-22 14:09
|
||||
|
||||
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")),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="OutgoingEmail",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("subject", models.CharField(max_length=255)),
|
||||
("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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,98 @@
|
|||
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,
|
||||
content: str,
|
||||
sender_email: str = settings.DEFAULT_FROM_EMAIL
|
||||
):
|
||||
message = EmailMessage(
|
||||
subject=subject,
|
||||
body=content,
|
||||
from_email=sender_email,
|
||||
to=to
|
||||
)
|
||||
message.content_subtype = 'html'
|
||||
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"
|
||||
)
|
||||
|
||||
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, subject: 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, subject=subject,
|
||||
sender=sender
|
||||
)
|
||||
attachments = attachments or []
|
||||
# send email
|
||||
sent = send_mail(
|
||||
to=[recipient], sender_email=sender,
|
||||
subject=subject, content=template.load_and_process_template(context),
|
||||
attachments=attachments
|
||||
)
|
||||
outgoing_email.sent = sent
|
||||
outgoing_email.save()
|
||||
return outgoing_email
|
||||
|
||||
|
||||
class OutgoingEmail(models.Model):
|
||||
subject = models.CharField(max_length=255)
|
||||
template = models.ForeignKey(MailTemplate, on_delete=models.CASCADE)
|
||||
sender = models.EmailField()
|
||||
recipient = models.EmailField()
|
||||
|
||||
sent = models.BooleanField(default=False)
|
||||
|
||||
objects = OutgoingEmailManager()
|
|
@ -0,0 +1,10 @@
|
|||
from factory.django import DjangoModelFactory
|
||||
from factory import Faker
|
||||
|
||||
|
||||
class MailTemplateFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = "mailings.MailTemplate"
|
||||
|
||||
template_name = Faker("name")
|
||||
template = Faker("file_name", extension="html")
|
|
@ -0,0 +1,66 @@
|
|||
from django.test import TestCase
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core import mail
|
||||
|
||||
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"<html>{{test_var}}</html>"
|
||||
)
|
||||
)
|
||||
|
||||
def test_load_and_process_template_success(self):
|
||||
content = self.mail_template.load_and_process_template({"test_var": "test"})
|
||||
self.assertEqual(content, "<html>test</html>")
|
||||
|
||||
def test_load_and_process_template_missing_var_failure(self):
|
||||
content = self.mail_template.load_and_process_template({})
|
||||
self.assertEqual(content, "<html></html>")
|
||||
|
||||
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"<html>{{test_var}}</html>"
|
||||
)
|
||||
)
|
||||
|
||||
def test_send_success(self):
|
||||
email = OutgoingEmail.objects.send(
|
||||
template_name="test_template",
|
||||
recipient="test@stardust.io", context={},
|
||||
sender="sklep-test@stardust.io",
|
||||
subject="Test subject"
|
||||
)
|
||||
self.assertEqual(email.sent, True)
|
||||
self.assertEqual(mail.outbox[0].subject, "Test subject")
|
||||
|
||||
def test_send_missing_template_failure(self):
|
||||
with self.assertRaises(MailTemplate.DoesNotExist):
|
||||
OutgoingEmail.objects.send(
|
||||
template_name="missing_template",
|
||||
recipient="", sender="", context={},
|
||||
subject="Test subject"
|
||||
)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
|
@ -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
|
||||
|
|
|
@ -26,11 +26,13 @@ class ProductCategoryParamAdmin(ModelAdmin):
|
|||
|
||||
|
||||
class ProductTemplateAdmin(ModelAdmin):
|
||||
menu_label = "Product design"
|
||||
model = models.ProductTemplate
|
||||
list_display = ("title", "code")
|
||||
|
||||
|
||||
class ProductAdmin(ModelAdmin):
|
||||
menu_label = "Product variant"
|
||||
model = models.Product
|
||||
list_display = ("title", "price")
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-22 16:40
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0005_order_order_number"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="orderdocument",
|
||||
name="sent",
|
||||
),
|
||||
]
|
|
@ -31,6 +31,10 @@ from store.utils import (
|
|||
notify_user_about_order,
|
||||
notify_manufacturer_about_order
|
||||
)
|
||||
from mailings.models import (
|
||||
OutgoingEmail,
|
||||
Attachment
|
||||
)
|
||||
|
||||
|
||||
class PersonalData(models.Model):
|
||||
|
@ -233,6 +237,44 @@ class OrderManager(models.Manager):
|
|||
year = datetime.datetime.now().year
|
||||
return f"{author.id}/{number_of_prev_orders:06}/{year}"
|
||||
|
||||
def _send_notifications(
|
||||
self, order: models.Model, author: ProductAuthor,
|
||||
customer_data: dict[str, Any], docs: list[models.Model]
|
||||
):
|
||||
# for user
|
||||
attachments = [
|
||||
Attachment(
|
||||
content=doc.generate_document({"customer_data": customer_data}),
|
||||
contenttype="application/pdf",
|
||||
name=f"{doc.template.doc_type}_{order.order_number}.pdf"
|
||||
) for doc in docs
|
||||
]
|
||||
mail_subject = f"Wygenerowano umowę numer {order.order_number} z dnia {order.created_at.strftime('%d.%m.%Y')}"
|
||||
user_mail = OutgoingEmail.objects.send(
|
||||
recipient=customer_data["email"],
|
||||
subject=mail_subject,
|
||||
context = {
|
||||
"docs": docs,
|
||||
"order_number": order.order_number,
|
||||
"customer_email": customer_data["email"],
|
||||
}, sender=settings.DEFAULT_FROM_EMAIL,
|
||||
template_name="order_created_user",
|
||||
attachments=attachments
|
||||
)
|
||||
# for author
|
||||
author_mail = OutgoingEmail.objects.send(
|
||||
recipient=author.email,
|
||||
subject=mail_subject,
|
||||
context = {
|
||||
"docs": docs,
|
||||
"order_number": order.order_number,
|
||||
"manufacturer_email": author.email,
|
||||
}, sender=settings.DEFAULT_FROM_EMAIL,
|
||||
template_name="order_created_author",
|
||||
attachments=attachments
|
||||
)
|
||||
return user_mail is not None and author_mail is not None
|
||||
|
||||
def create_from_cart(
|
||||
self, cart_items: list[dict[str, str|dict]],
|
||||
payment_method: models.Model| None,
|
||||
|
@ -242,8 +284,9 @@ class OrderManager(models.Manager):
|
|||
orders_pks = []
|
||||
|
||||
payment_method = payment_method or PaymentMethod.objects.first()
|
||||
agreement_template = DocumentTemplate.objects.get(doc_type=DocumentTypeChoices.AGREEMENT)
|
||||
receipt_template = DocumentTemplate.objects.get(doc_type=DocumentTypeChoices.RECEIPT)
|
||||
doc_templates = DocumentTemplate.objects.filter(
|
||||
doc_type__in=[DocumentTypeChoices.AGREEMENT, DocumentTypeChoices.RECEIPT]
|
||||
)
|
||||
|
||||
for item in cart_items:
|
||||
author = item["author"]
|
||||
|
@ -255,39 +298,18 @@ class OrderManager(models.Manager):
|
|||
)
|
||||
OrderProduct.objects.create_from_cart(author_products, order)
|
||||
orders_pks.append(order.pk)
|
||||
agreement = OrderDocument.objects.create(
|
||||
order=order,
|
||||
template=agreement_template
|
||||
)
|
||||
receipt = OrderDocument.objects.create(
|
||||
order=order,
|
||||
template=receipt_template
|
||||
)
|
||||
extra_document_kwargs = {
|
||||
"customer_data": customer_data
|
||||
}
|
||||
default_kwargs ={
|
||||
"docs": [
|
||||
agreement.generate_document(extra_document_kwargs),
|
||||
receipt.generate_document(extra_document_kwargs)
|
||||
],
|
||||
"order_number": order.order_number
|
||||
}
|
||||
user_kwargs = {
|
||||
"customer_email": customer_data["email"],
|
||||
}
|
||||
user_kwargs.update(default_kwargs)
|
||||
user_notified = notify_user_about_order(**user_kwargs)
|
||||
manufacturer_kwargs = {
|
||||
"manufacturer_email": author.email,
|
||||
}
|
||||
manufacturer_kwargs.update(default_kwargs)
|
||||
manufacturer_notified = notify_manufacturer_about_order(**manufacturer_kwargs)
|
||||
sent = user_notified and manufacturer_notified
|
||||
agreement.sent = sent
|
||||
receipt.sent = sent
|
||||
agreement.save()
|
||||
receipt.save()
|
||||
docs = []
|
||||
for template in doc_templates:
|
||||
doc = OrderDocument.objects.create(
|
||||
order=order, template=template
|
||||
)
|
||||
docs.append(doc)
|
||||
sent = self._send_notifications(order, author, customer_data, docs)
|
||||
|
||||
if not sent:
|
||||
# TODO - store data temporarily
|
||||
raise Exception("Error while sending emails")
|
||||
|
||||
return Order.objects.filter(pk__in=orders_pks)
|
||||
|
||||
|
||||
|
@ -346,7 +368,6 @@ class DocumentTemplate(models.Model):
|
|||
class OrderDocument(models.Model):
|
||||
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="documents")
|
||||
template = models.ForeignKey(DocumentTemplate, on_delete=models.CASCADE)
|
||||
sent = models.BooleanField(default=False)
|
||||
|
||||
def get_document_context(self):
|
||||
_context = {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
|
||||
from unittest.mock import patch
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.core import mail
|
||||
|
||||
from store.tests import factories
|
||||
from store import models as store_models
|
||||
from mailings.tests.factories import MailTemplateFactory
|
||||
|
||||
|
||||
# TODO - this is fine for now, but we'll want to use factoryboy for this:
|
||||
|
@ -72,8 +75,11 @@ class OrderTestCase(TestCase):
|
|||
self.payment_method = factories.PaymentMethodFactory()
|
||||
factories.DocumentTemplateFactory()
|
||||
factories.DocumentTemplateFactory(doc_type="receipt")
|
||||
MailTemplateFactory(template_name="order_created_user")
|
||||
MailTemplateFactory(template_name="order_created_author")
|
||||
|
||||
def test_create_from_cart_success_single_author(self):
|
||||
@patch("mailings.models.MailTemplate.load_and_process_template", return_value="test")
|
||||
def test_create_from_cart_success_single_author(self, mocked_load):
|
||||
product = factories.ProductFactory(template__author=self.author, price=100)
|
||||
cart_items = [{
|
||||
"author": self.author,
|
||||
|
@ -86,10 +92,13 @@ class OrderTestCase(TestCase):
|
|||
)
|
||||
self.assertEqual(orders.count(), 1)
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
self.assertEqual(mail.outbox[0].subject, f"Zamówienie {orders[0].order_number}")
|
||||
self.assertEqual(
|
||||
mail.outbox[0].subject,
|
||||
f"Wygenerowano umowę numer {orders[0].order_number} z dnia {orders[0].created_at.strftime('%d.%m.%Y')}"
|
||||
)
|
||||
|
||||
|
||||
def test_create_from_cart_success_multpile_authors(self):
|
||||
@patch("mailings.models.MailTemplate.load_and_process_template", return_value="test")
|
||||
def test_create_from_cart_success_multpile_authors(self, mocked_load):
|
||||
product = factories.ProductFactory(template__author=self.second_author, price=100)
|
||||
cart_items = [
|
||||
{
|
||||
|
@ -107,5 +116,11 @@ class OrderTestCase(TestCase):
|
|||
)
|
||||
self.assertEqual(orders.count(), 2)
|
||||
self.assertEqual(len(mail.outbox), 4)
|
||||
self.assertEqual(mail.outbox[0].subject, f"Zamówienie {orders[0].order_number}")
|
||||
self.assertEqual(mail.outbox[2].subject, f"Zamówienie {orders[1].order_number}")
|
||||
self.assertEqual(
|
||||
mail.outbox[0].subject,
|
||||
f"Wygenerowano umowę numer {orders[0].order_number} z dnia {orders[0].created_at.strftime('%d.%m.%Y')}"
|
||||
)
|
||||
self.assertEqual(
|
||||
mail.outbox[2].subject,
|
||||
f"Wygenerowano umowę numer {orders[1].order_number} z dnia {orders[1].created_at.strftime('%d.%m.%Y')}"
|
||||
)
|
||||
|
|
Ładowanie…
Reference in New Issue