From 1faa608425f9509eadde495c49120d6b41d022c8 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 21 Aug 2016 15:40:05 +1000 Subject: [PATCH 1/6] Adds email framework shamelessly stolen from Symposion --- registrasion/contrib/__init__.py | 0 registrasion/contrib/mail.py | 63 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 registrasion/contrib/__init__.py create mode 100644 registrasion/contrib/mail.py diff --git a/registrasion/contrib/__init__.py b/registrasion/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/registrasion/contrib/mail.py b/registrasion/contrib/mail.py new file mode 100644 index 00000000..06ca8fd2 --- /dev/null +++ b/registrasion/contrib/mail.py @@ -0,0 +1,63 @@ +import os + +from django.conf import settings +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils.html import strip_tags + +from django.contrib.sites.models import Site + + +class Sender(object): + ''' Class for sending e-mails under a templete prefix. ''' + + def __init__(self, template_prefix): + self.template_prefix = template_prefix + + def send_email(self, to, kind, **kwargs): + ''' Sends an e-mail to the given address. + + to: The address + kind: the ID for an e-mail kind; it should point to a subdirectory of + self.template_prefix containing subject.txt and message.html, which + are django templates for the subject and HTML message respectively. + + context: a context for rendering the e-mail. + + ''' + + return __send_email__(self.template_prefix, to, kind, **kwargs) + + +send_email = Sender("registrasion/emails").send_email + + +def __send_email__(template_prefix, to, kind, **kwargs): + + current_site = Site.objects.get_current() + + ctx = { + "current_site": current_site, + "STATIC_URL": settings.STATIC_URL, + } + ctx.update(kwargs.get("context", {})) + subject_template = os.path.join(template_prefix, "%s/subject.txt" % kind) + message_template = os.path.join(template_prefix, "%s/message.html" % kind) + subject = "[%s] %s" % ( + current_site.name, + render_to_string(subject_template, ctx).strip() + ) + + message_html = render_to_string(message_template, ctx) + message_plaintext = strip_tags(message_html) + + from_email = settings.DEFAULT_FROM_EMAIL + + try: + bcc_email = settings.ENVELOPE_BCC_LIST + except AttributeError: + bcc_email = None + + email = EmailMultiAlternatives(subject, message_plaintext, from_email, to, bcc=bcc_email) + email.attach_alternative(message_html, "text/html") + email.send() From 155f6d42d9f38c9aa95be240d077002bcb2e3a7c Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 21 Aug 2016 16:39:57 +1000 Subject: [PATCH 2/6] Renames patch_datetime to patches, adds e-mail patching bits --- .../tests/{patch_datetime.py => patches.py} | 24 +++++++++++++++++++ registrasion/tests/test_cart.py | 6 ++--- 2 files changed, 27 insertions(+), 3 deletions(-) rename registrasion/tests/{patch_datetime.py => patches.py} (53%) diff --git a/registrasion/tests/patch_datetime.py b/registrasion/tests/patches.py similarity index 53% rename from registrasion/tests/patch_datetime.py rename to registrasion/tests/patches.py index 8b64b606..7d7cd66c 100644 --- a/registrasion/tests/patch_datetime.py +++ b/registrasion/tests/patches.py @@ -1,5 +1,6 @@ from django.utils import timezone +from registrasion.contrib import mail class SetTimeMixin(object): ''' Patches timezone.now() for the duration of a test case. Allows us to @@ -23,3 +24,26 @@ class SetTimeMixin(object): def new_timezone_now(self): return self.now + + +class SendEmailMixin(object): + + def setUp(self): + super(SendEmailMixin, self).setUp() + + self._old_sender = mail.__send_email__ + mail.__send_email__ = self._send_email + self.emails = [] + + def _send_email(self, template_prefix, to, kind, **kwargs): + args = {"to": to, "kind": kind} + args.update(kwargs) + self.emails.append(args) + + def tearDown(self): + mail.__send_email__ = self._old_sender + super(SendEmailMixin, self).tearDown() + + +class MixInPatches(SetTimeMixin, SendEmailMixin): + pass diff --git a/registrasion/tests/test_cart.py b/registrasion/tests/test_cart.py index 619b9074..bee94322 100644 --- a/registrasion/tests/test_cart.py +++ b/registrasion/tests/test_cart.py @@ -16,12 +16,12 @@ from registrasion.controllers.batch import BatchController from registrasion.controllers.product import ProductController from controller_helpers import TestingCartController -from patch_datetime import SetTimeMixin +from patches import MixInPatches UTC = pytz.timezone('UTC') -class RegistrationCartTestCase(SetTimeMixin, TestCase): +class RegistrationCartTestCase(MixInPatches, TestCase): def setUp(self): super(RegistrationCartTestCase, self).setUp() @@ -377,7 +377,7 @@ class BasicCartTests(RegistrationCartTestCase): # Memoise the cart same_cart = TestingCartController.for_user(self.USER_1) # Do nothing on exit - + rev_1 = self.reget(cart.cart).revision self.assertEqual(rev_0, rev_1) From e946af0f0487f85e2e8036051fc43c9f7a0d732b Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 21 Aug 2016 16:56:05 +1000 Subject: [PATCH 3/6] Adds functions for mailing invoices when certain events occur. --- registrasion/controllers/invoice.py | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index da737a73..03be199a 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -5,6 +5,8 @@ from django.db import transaction from django.db.models import Sum from django.utils import timezone +from registrasion.contrib.mail import send_email + from registrasion.models import commerce from registrasion.models import conditions from registrasion.models import people @@ -149,6 +151,8 @@ class InvoiceController(ForId, object): invoice.save() + cls.email_on_invoice_creation(invoice) + return invoice def can_view(self, user=None, access_code=None): @@ -314,3 +318,33 @@ class InvoiceController(ForId, object): CreditNoteController.generate_from_invoice(self.invoice, amount) self.update_status() + + @classmethod + def email(cls, invoice, kind): + ''' Sends out an e-mail notifying the user about something to do + with that invoice. ''' + + context = { + "invoice": invoice, + } + + send_email(invoice.user.email, kind, context=context) + + @classmethod + def email_on_invoice_creation(cls, invoice): + ''' Sends out an e-mail notifying the user that an invoice has been + created. ''' + + cls.email(invoice, "invoice_created") + + @classmethod + def email_on_invoice_change(cls, invoice, old_status, new_status): + ''' Sends out all of the necessary notifications that the status of the + invoice has changed to: + + - Invoice is now paid + - Invoice is now refunded + + ''' + + cls.email(invoice, "invoice_updated") From 924906d38cc0898121fac31cd2bd20532209687f Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 21 Aug 2016 16:56:15 +1000 Subject: [PATCH 4/6] Adds test for e-mails being sent when invoices are generated. --- registrasion/tests/test_invoice.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/registrasion/tests/test_invoice.py b/registrasion/tests/test_invoice.py index f1ed6ed0..32ec6c2e 100644 --- a/registrasion/tests/test_invoice.py +++ b/registrasion/tests/test_invoice.py @@ -533,3 +533,11 @@ class InvoiceTestCase(RegistrationCartTestCase): # Now that we don't have CAT_1, we can't checkout this cart with self.assertRaises(ValidationError): invoice = TestingInvoiceController.for_cart(cart.cart) + + def test_sends_email_on_invoice_creation(self): + invoice = self._invoice_containing_prod_1(1) + assert(1, len(self.emails)) + email = self.emails[0] + self.assertEquals(self.USER_1.email, email["to"]) + self.assertEquals("invoice_created", email["kind"]) + self.assertEquals(invoice.invoice, email["context"]["invoice"]) From 7bf372f92a87274c924001215b98d582c72a8f2c Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 21 Aug 2016 17:14:19 +1000 Subject: [PATCH 5/6] Invoices now send e-mails when created, paid, or refunded. --- registrasion/controllers/invoice.py | 17 ++++++++++++++++ registrasion/tests/test_invoice.py | 31 ++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index 03be199a..1b70640e 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -245,6 +245,12 @@ class InvoiceController(ForId, object): if residual != 0: CreditNoteController.generate_from_invoice(self.invoice, residual) + self.email_on_invoice_change( + self.invoice, + old_status, + self.invoice.status, + ) + def _mark_paid(self): ''' Marks the invoice as paid, and updates the attached cart if necessary. ''' @@ -347,4 +353,15 @@ class InvoiceController(ForId, object): ''' + # The statuses that we don't care about. + silent_status = [ + commerce.Invoice.STATUS_VOID, + commerce.Invoice.STATUS_UNPAID, + ] + + if old_status == new_status: + return + if False and new_status in silent_status: + pass + cls.email(invoice, "invoice_updated") diff --git a/registrasion/tests/test_invoice.py b/registrasion/tests/test_invoice.py index 32ec6c2e..98d479b6 100644 --- a/registrasion/tests/test_invoice.py +++ b/registrasion/tests/test_invoice.py @@ -536,8 +536,37 @@ class InvoiceTestCase(RegistrationCartTestCase): def test_sends_email_on_invoice_creation(self): invoice = self._invoice_containing_prod_1(1) - assert(1, len(self.emails)) + self.assertEquals(1, len(self.emails)) email = self.emails[0] self.assertEquals(self.USER_1.email, email["to"]) self.assertEquals("invoice_created", email["kind"]) self.assertEquals(invoice.invoice, email["context"]["invoice"]) + + def test_sends_first_change_email_on_invoice_fully_paid(self): + invoice = self._invoice_containing_prod_1(1) + + self.assertEquals(1, len(self.emails)) + invoice.pay("Partial", invoice.invoice.value - 1) + # Should have an "invoice_created" email and nothing else. + self.assertEquals(1, len(self.emails)) + invoice.pay("Remainder", 1) + self.assertEquals(2, len(self.emails)) + + email = self.emails[1] + self.assertEquals(self.USER_1.email, email["to"]) + self.assertEquals("invoice_updated", email["kind"]) + self.assertEquals(invoice.invoice, email["context"]["invoice"]) + + def test_sends_email_when_invoice_refunded(self): + invoice = self._invoice_containing_prod_1(1) + + self.assertEquals(1, len(self.emails)) + invoice.pay("Payment", invoice.invoice.value) + self.assertEquals(2, len(self.emails)) + invoice.refund() + self.assertEquals(3, len(self.emails)) + + email = self.emails[2] + self.assertEquals(self.USER_1.email, email["to"]) + self.assertEquals("invoice_updated", email["kind"]) + self.assertEquals(invoice.invoice, email["context"]["invoice"]) From 4f16e4b9d0e9f1691e58d719c3f34d2a4f6d3176 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 21 Aug 2016 18:28:16 +1000 Subject: [PATCH 6/6] Oops. --- registrasion/controllers/invoice.py | 2 +- registrasion/tests/test_invoice.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index 1b70640e..25578cdd 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -334,7 +334,7 @@ class InvoiceController(ForId, object): "invoice": invoice, } - send_email(invoice.user.email, kind, context=context) + send_email([invoice.user.email], kind, context=context) @classmethod def email_on_invoice_creation(cls, invoice): diff --git a/registrasion/tests/test_invoice.py b/registrasion/tests/test_invoice.py index 98d479b6..bd8c4340 100644 --- a/registrasion/tests/test_invoice.py +++ b/registrasion/tests/test_invoice.py @@ -538,7 +538,7 @@ class InvoiceTestCase(RegistrationCartTestCase): invoice = self._invoice_containing_prod_1(1) self.assertEquals(1, len(self.emails)) email = self.emails[0] - self.assertEquals(self.USER_1.email, email["to"]) + self.assertEquals([self.USER_1.email], email["to"]) self.assertEquals("invoice_created", email["kind"]) self.assertEquals(invoice.invoice, email["context"]["invoice"]) @@ -553,7 +553,7 @@ class InvoiceTestCase(RegistrationCartTestCase): self.assertEquals(2, len(self.emails)) email = self.emails[1] - self.assertEquals(self.USER_1.email, email["to"]) + self.assertEquals([self.USER_1.email], email["to"]) self.assertEquals("invoice_updated", email["kind"]) self.assertEquals(invoice.invoice, email["context"]["invoice"]) @@ -567,6 +567,6 @@ class InvoiceTestCase(RegistrationCartTestCase): self.assertEquals(3, len(self.emails)) email = self.emails[2] - self.assertEquals(self.USER_1.email, email["to"]) + self.assertEquals([self.USER_1.email], email["to"]) self.assertEquals("invoice_updated", email["kind"]) self.assertEquals(invoice.invoice, email["context"]["invoice"])