diff --git a/entity_emailer/interface.py b/entity_emailer/interface.py index 00df989..03d9530 100644 --- a/entity_emailer/interface.py +++ b/entity_emailer/interface.py @@ -3,7 +3,7 @@ import traceback from datetime import datetime - +from django.db import transaction from django.core import mail from entity_event import context_loader @@ -94,7 +94,7 @@ def send_unsent_scheduled_emails(cls): for email in emails_to_send: try: # Send mail - connection.send_message(email.get('message')) + connection.send_messages(email.get('message')) except Exception as e: cls.save_email_exception(email.get('model'), e) @@ -123,6 +123,7 @@ def convert_events_to_emails(): Email.objects.create_email(event=event, from_address=from_address, recipients=targets) @staticmethod + @transaction.atomic def bulk_convert_events_to_emails(): """ Converts unseen events to emails and marks them as seen. Uses the create_emails method to bulk create diff --git a/entity_emailer/models.py b/entity_emailer/models.py index c61a593..e46c4ba 100644 --- a/entity_emailer/models.py +++ b/entity_emailer/models.py @@ -47,14 +47,20 @@ def create_emails(self, email_params_list): # Build list of recipient through relationships to create recipients_to_create = [] + + # Keep track of unique pairs to avoid unique constraint on through relationship + email_entity_pairs = set() for i, recipient_entities in enumerate(recipient_entities_per_email): for recipient_entity in recipient_entities: - recipients_to_create.append( - Email.recipients.through( - email_id=emails[i].id, - entity_id=recipient_entity.id, + if (emails[i].id, recipient_entity.id) not in email_entity_pairs: + email_entity_pairs.add((emails[i].id, recipient_entity.id)) + + recipients_to_create.append( + Email.recipients.through( + email_id=emails[i].id, + entity_id=recipient_entity.id, + ) ) - ) # Bulk create the recipient relationships Email.recipients.through.objects.bulk_create(recipients_to_create) diff --git a/entity_emailer/tests/interface_tests.py b/entity_emailer/tests/interface_tests.py index 4429337..3f672cf 100644 --- a/entity_emailer/tests/interface_tests.py +++ b/entity_emailer/tests/interface_tests.py @@ -298,10 +298,15 @@ def test_multiple_events_only_following_false(self): @freeze_time('2013-1-2') def test_bulk_multiple_events_only_following_false(self): + """ + Handles bulk creating events and tests the unique constraint of the duplicated subscription which would cause + a bulk create error if it wasn't handled + """ source = G(Source) e = G(Entity) other_e = G(Entity) + G(Subscription, entity=e, source=source, medium=self.email_medium, only_following=False) G(Subscription, entity=e, source=source, medium=self.email_medium, only_following=False) G(Subscription, entity=other_e, source=source, medium=self.email_medium, only_following=False) email_context = { @@ -320,6 +325,35 @@ def test_bulk_multiple_events_only_following_false(self): self.assertEquals(email.subject, '') self.assertEquals(email.scheduled, datetime(2013, 1, 2)) + @freeze_time('2013-1-2') + def test_bulk_multiple_events_only_following_true(self): + """ + Handles bulk creating events and tests the unique constraint of the duplicated subscription which would cause + a bulk create error if it wasn't handled + """ + source = G(Source) + e = G(Entity) + other_e = G(Entity) + + G(Subscription, entity=e, source=source, medium=self.email_medium, only_following=True) + G(Subscription, entity=e, source=source, medium=self.email_medium, only_following=True) + G(Subscription, entity=other_e, source=source, medium=self.email_medium, only_following=True) + email_context = { + 'entity_emailer_template': 'template', + 'entity_emailer_subject': 'hi', + } + G(Event, source=source, context=email_context) + event = G(Event, source=source, context=email_context) + G(EventActor, event=event, entity=e) + + EntityEmailerInterface.bulk_convert_events_to_emails() + + email = Email.objects.get() + self.assertEquals(set(email.recipients.all()), set([e])) + self.assertEquals(email.event.context, email_context) + self.assertEquals(email.subject, '') + self.assertEquals(email.scheduled, datetime(2013, 1, 2)) + @freeze_time('2013-1-2') def test_multiple_events_only_following_true(self): source = G(Source) @@ -371,7 +405,7 @@ def test_sends_all_scheduled_emails(self, render_mock, address_mock): with patch(settings.EMAIL_BACKEND) as mock_connection: EntityEmailerInterface.send_unsent_scheduled_emails() - self.assertEqual(2, mock_connection.return_value.__enter__.return_value.send_message.call_count) + self.assertEqual(2, mock_connection.return_value.__enter__.return_value.send_messages.call_count) @patch('entity_emailer.interface.pre_send') @patch('entity_emailer.interface.get_subscribed_email_addresses') @@ -392,7 +426,7 @@ def test_send_signals(self, render_mock, address_mock, mock_pre_send): EntityEmailerInterface.send_unsent_scheduled_emails() # Assert that we sent the email - self.assertEqual(1, mock_connection.return_value.__enter__.return_value.send_message.call_count) + self.assertEqual(1, mock_connection.return_value.__enter__.return_value.send_messages.call_count) # Assert that we called the pre send signal with the proper values name, args, kwargs = mock_pre_send.send.mock_calls[0] @@ -416,7 +450,7 @@ def test_sends_email_with_specified_from_address(self, render_mock, address_mock with patch(settings.EMAIL_BACKEND) as mock_connection: EntityEmailerInterface.send_unsent_scheduled_emails() - args = mock_connection.return_value.__enter__.return_value.send_message.call_args + args = mock_connection.return_value.__enter__.return_value.send_messages.call_args self.assertEqual(args[0][0].from_email, from_address) @patch('entity_emailer.interface.get_subscribed_email_addresses') @@ -504,7 +538,7 @@ def to_dict(self): with patch(settings.EMAIL_BACKEND) as mock_connection: # Mock side effects for sending emails - mock_connection.return_value.__enter__.return_value.send_message.side_effect = [ + mock_connection.return_value.__enter__.return_value.send_messages.side_effect = [ None, TestEmailSendMessageException('test'), ] diff --git a/entity_emailer/version.py b/entity_emailer/version.py index 3f39079..e7c12d2 100644 --- a/entity_emailer/version.py +++ b/entity_emailer/version.py @@ -1 +1 @@ -__version__ = '2.0.1' +__version__ = '2.0.3' diff --git a/release_notes.rst b/release_notes.rst index b1a2557..3546d95 100644 --- a/release_notes.rst +++ b/release_notes.rst @@ -1,10 +1,22 @@ Release Notes ============= +v2.0.3 +------ +* Merging v2 hotfixes + v2.0.1 ------ * Fix for handling single failures in a batch of outgoing emails +v2.0.0.2 +------ +* Atomic decorator on event fetching + +v2.0.0.1 +------ +* Fix unique constraint when bulk creating emails + v2.0.0 ------ * Added bulk interface for converting to emails