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() diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index da737a73..25578cdd 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): @@ -241,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. ''' @@ -314,3 +324,44 @@ 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 + + ''' + + # 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/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) diff --git a/registrasion/tests/test_invoice.py b/registrasion/tests/test_invoice.py index f1ed6ed0..bd8c4340 100644 --- a/registrasion/tests/test_invoice.py +++ b/registrasion/tests/test_invoice.py @@ -533,3 +533,40 @@ 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) + 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"])