Merge branch 'chrisjrn/email_invoices'
This commit is contained in:
		
						commit
						64e897919e
					
				
					 6 changed files with 178 additions and 3 deletions
				
			
		
							
								
								
									
										0
									
								
								registrasion/contrib/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								registrasion/contrib/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										63
									
								
								registrasion/contrib/mail.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								registrasion/contrib/mail.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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() | ||||||
|  | @ -5,6 +5,8 @@ from django.db import transaction | ||||||
| from django.db.models import Sum | from django.db.models import Sum | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| 
 | 
 | ||||||
|  | from registrasion.contrib.mail import send_email | ||||||
|  | 
 | ||||||
| from registrasion.models import commerce | from registrasion.models import commerce | ||||||
| from registrasion.models import conditions | from registrasion.models import conditions | ||||||
| from registrasion.models import people | from registrasion.models import people | ||||||
|  | @ -149,6 +151,8 @@ class InvoiceController(ForId, object): | ||||||
| 
 | 
 | ||||||
|         invoice.save() |         invoice.save() | ||||||
| 
 | 
 | ||||||
|  |         cls.email_on_invoice_creation(invoice) | ||||||
|  | 
 | ||||||
|         return invoice |         return invoice | ||||||
| 
 | 
 | ||||||
|     def can_view(self, user=None, access_code=None): |     def can_view(self, user=None, access_code=None): | ||||||
|  | @ -241,6 +245,12 @@ class InvoiceController(ForId, object): | ||||||
|         if residual != 0: |         if residual != 0: | ||||||
|             CreditNoteController.generate_from_invoice(self.invoice, residual) |             CreditNoteController.generate_from_invoice(self.invoice, residual) | ||||||
| 
 | 
 | ||||||
|  |         self.email_on_invoice_change( | ||||||
|  |             self.invoice, | ||||||
|  |             old_status, | ||||||
|  |             self.invoice.status, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|     def _mark_paid(self): |     def _mark_paid(self): | ||||||
|         ''' Marks the invoice as paid, and updates the attached cart if |         ''' Marks the invoice as paid, and updates the attached cart if | ||||||
|         necessary. ''' |         necessary. ''' | ||||||
|  | @ -314,3 +324,44 @@ class InvoiceController(ForId, object): | ||||||
| 
 | 
 | ||||||
|         CreditNoteController.generate_from_invoice(self.invoice, amount) |         CreditNoteController.generate_from_invoice(self.invoice, amount) | ||||||
|         self.update_status() |         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") | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| 
 | 
 | ||||||
|  | from registrasion.contrib import mail | ||||||
| 
 | 
 | ||||||
| class SetTimeMixin(object): | class SetTimeMixin(object): | ||||||
|     ''' Patches timezone.now() for the duration of a test case. Allows us to |     ''' 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): |     def new_timezone_now(self): | ||||||
|         return self.now |         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 | ||||||
|  | @ -16,12 +16,12 @@ from registrasion.controllers.batch import BatchController | ||||||
| from registrasion.controllers.product import ProductController | from registrasion.controllers.product import ProductController | ||||||
| 
 | 
 | ||||||
| from controller_helpers import TestingCartController | from controller_helpers import TestingCartController | ||||||
| from patch_datetime import SetTimeMixin | from patches import MixInPatches | ||||||
| 
 | 
 | ||||||
| UTC = pytz.timezone('UTC') | UTC = pytz.timezone('UTC') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class RegistrationCartTestCase(SetTimeMixin, TestCase): | class RegistrationCartTestCase(MixInPatches, TestCase): | ||||||
| 
 | 
 | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super(RegistrationCartTestCase, self).setUp() |         super(RegistrationCartTestCase, self).setUp() | ||||||
|  | @ -377,7 +377,7 @@ class BasicCartTests(RegistrationCartTestCase): | ||||||
|             # Memoise the cart |             # Memoise the cart | ||||||
|             same_cart = TestingCartController.for_user(self.USER_1) |             same_cart = TestingCartController.for_user(self.USER_1) | ||||||
|             # Do nothing on exit |             # Do nothing on exit | ||||||
|          | 
 | ||||||
|         rev_1 = self.reget(cart.cart).revision |         rev_1 = self.reget(cart.cart).revision | ||||||
|         self.assertEqual(rev_0, rev_1) |         self.assertEqual(rev_0, rev_1) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -533,3 +533,40 @@ class InvoiceTestCase(RegistrationCartTestCase): | ||||||
|         # Now that we don't have CAT_1, we can't checkout this cart |         # Now that we don't have CAT_1, we can't checkout this cart | ||||||
|         with self.assertRaises(ValidationError): |         with self.assertRaises(ValidationError): | ||||||
|             invoice = TestingInvoiceController.for_cart(cart.cart) |             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"]) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer