diff --git a/.travis.yml b/.travis.yml index 2f320dd..50a7367 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,18 +8,15 @@ python: env: matrix: - - DJANGO=2.0 - - DJANGO=2.1 - DJANGO=2.2 + - DJANGO=3.0 + - DJANGO=3.1 - DJANGO=master addons: postgresql: '9.6' matrix: - exclude: - - { python: "3.7", env: DJANGO=2.0 } - include: - { python: "3.6", env: TOXENV=flake8 } diff --git a/entity_emailer/interface.py b/entity_emailer/interface.py index aae5baf..b0a071e 100644 --- a/entity_emailer/interface.py +++ b/entity_emailer/interface.py @@ -120,3 +120,33 @@ def convert_events_to_emails(): # Create the emails Email.objects.create_email(event=event, from_address=from_address, recipients=targets) + + @staticmethod + def bulk_convert_events_to_emails(): + """ + Converts unseen events to emails and marks them as seen. Uses the create_emails method to bulk create + emails and recipient relationships + """ + + # Get the email medium + email_medium = get_medium() + + # Get the default from email + default_from_email = get_from_email_address() + + email_params_list = [] + + # Find any unseen events and create unsent email objects + for event, targets in email_medium.events_targets(seen=False, mark_seen=True): + + # Check the event's context for a from_address, otherwise fallback to default + from_address = event.context.get('from_address') or default_from_email + + email_params_list.append(dict( + event=event, + from_address=from_address, + recipients=targets + )) + + # Bulk create the emails + Email.objects.create_emails(email_params_list) diff --git a/entity_emailer/models.py b/entity_emailer/models.py index f4290b2..c61a593 100644 --- a/entity_emailer/models.py +++ b/entity_emailer/models.py @@ -1,6 +1,6 @@ from datetime import datetime -from django.db import models +from django.db import models, transaction from entity.models import Entity from entity_event.models import Event import uuid @@ -26,6 +26,41 @@ def create_email(self, recipients=None, **kwargs): email.save() return email + @transaction.atomic + def create_emails(self, email_params_list): + """ + :param email_params_list: A list of dicts containing the keys for the create_email method + :return: list of Email objects that were created + """ + emails_to_create = [] + recipient_entities_per_email = [] + + # Build the emails to create and keep track of recipients + for kwargs in email_params_list: + scheduled = kwargs.pop('scheduled', datetime.utcnow()) + recipients = kwargs.pop('recipients', []) + emails_to_create.append(Email(scheduled=scheduled, **kwargs)) + recipient_entities_per_email.append(recipients) + + # Bulk create the emails + emails = Email.objects.bulk_create(emails_to_create) + + # Build list of recipient through relationships to create + recipients_to_create = [] + 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, + ) + ) + + # Bulk create the recipient relationships + Email.recipients.through.objects.bulk_create(recipients_to_create) + + return emails + class Email(models.Model): """Save an Email object and it is sent automagically! diff --git a/entity_emailer/tests/interface_tests.py b/entity_emailer/tests/interface_tests.py index d5bd1bd..79a4ff2 100644 --- a/entity_emailer/tests/interface_tests.py +++ b/entity_emailer/tests/interface_tests.py @@ -294,6 +294,30 @@ def test_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_false(self): + 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=other_e, source=source, medium=self.email_medium, only_following=False) + email_context = { + 'entity_emailer_template': 'template', + 'entity_emailer_subject': 'hi', + } + G(Event, source=source, context=email_context) + G(Event, source=source, context=email_context) + + EntityEmailerInterface.bulk_convert_events_to_emails() + + self.assertEquals(Email.objects.count(), 2) + for email in Email.objects.all(): + self.assertEquals(set(email.recipients.all()), set([e, other_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) diff --git a/entity_emailer/version.py b/entity_emailer/version.py index 7b344ec..afced14 100644 --- a/entity_emailer/version.py +++ b/entity_emailer/version.py @@ -1 +1 @@ -__version__ = '1.1.2' +__version__ = '2.0.0' diff --git a/release_notes.rst b/release_notes.rst index 671679f..30d5525 100644 --- a/release_notes.rst +++ b/release_notes.rst @@ -1,6 +1,11 @@ Release Notes ============= +v2.0.0 +------ +* Added bulk interface for converting to emails +* Add support for django 3.0, 3.1 + v1.1.2 ------ * Handle email render exceptions diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index 2608f0b..1dd1fa8 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,7 +1,7 @@ -coverage -django-dynamic-fixture<=2.0.0 -django-nose -flake8 -freezegun -mock -psycopg2 +coverage==4.5.1 +django-dynamic-fixture +django-nose==1.4.5 +flake8==3.5.0 +freezegun==0.3.12 +mock==2.0.0 +psycopg2>=2.7.7 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9bbaff2..c9f24ba 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,8 @@ -beautifulsoup4>=4.3.2 -Django>=2.0,<3.0 -django-db-mutex>=1.2.0 -django-entity>=4.2.0 -django-entity-event>=1.2.0 +Django>=2.2 + +django-db-mutex>=2.0.0 +django-entity>=5.0.0 +django-entity-event>=2.0.0 ambition-django-uuidfield>=0.5.0 + +beautifulsoup4>=4.3.2 diff --git a/setup.py b/setup.py index 02c32d2..bee6cea 100644 --- a/setup.py +++ b/setup.py @@ -48,13 +48,14 @@ def get_requirements(requirements_file): 'Programming Language :: Python', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Framework :: Django', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', 'Framework :: Django :: 2.2', + 'Framework :: Django :: 3.0', + 'Framework :: Django :: 3.1', ], license='MIT', install_requires=get_requirements('requirements.txt'), diff --git a/tox.ini b/tox.ini index 0d8bf3a..eb5cd8c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,18 +1,18 @@ [tox] envlist = flake8 - py{36}-django20 - py{36,37}-django21 py{36,37}-django22 + py{36,37}-django30 + py{36,37}-django31 py{36,37}-djangomaster [testenv] setenv = DB = postgres deps = - django20: Django>=2.0,<2.1 - django21: Django>=2.1,<2.2 - django22: Django>=2.2,<2.3 + django22: Django>=2.2,<3.0 + django30: Django>=3.0,<3.1 + django31: Django>=3.1,<3.2 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt commands = @@ -25,7 +25,7 @@ commands = flake8 entity_emailer [travis:env] DJANGO = - 2.0: django20 - 2.1: django21 2.2: django22 + 3.0: django30 + 3.1: django31 master: djangomaster