Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic subscription #367

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
4 changes: 3 additions & 1 deletion newsletter/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def admin_messages(self, obj):
admin_messages.short_description = ''

def admin_subscriptions(self, obj):
return self._admin_url(obj, Subscription, _("Subscriptions"))
link_text = _("Subscriptions") if not obj.subscription_generator_class \
else obj.subscription_generator_class.rsplit('.', 1)[-1]
return self._admin_url(obj, Subscription, link_text)
admin_subscriptions.short_description = ''

def admin_submissions(self, obj):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-27 03:29

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('newsletter', '0008_longer_subscription_name'),
]

operations = [
migrations.AddField(
model_name='newsletter',
name='subscription_generator_class',
field=models.CharField(blank=True, max_length=200, null=True),
),
]
50 changes: 48 additions & 2 deletions newsletter/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import logging
import os
import time
import importlib
from datetime import datetime
from abc import abstractmethod

import django

Expand Down Expand Up @@ -44,6 +46,8 @@ class Newsletter(models.Model):
max_length=200, verbose_name=_('sender'), help_text=_('Sender name')
)

subscription_generator_class = models.CharField(max_length=200, blank=True, null=True)

visible = models.BooleanField(
default=True, verbose_name=_('visible'), db_index=True
)
Expand Down Expand Up @@ -97,6 +101,17 @@ def get_templates(self, action):

return subject_template, text_template, html_template

def get_subscription_generator(self):
if self.subscription_generator_class:
if "." not in self.subscription_generator_class:
raise ModuleNotFoundError("missing module for subscription generator class")
module_name, class_name = self.subscription_generator_class.rsplit(".", 1)
module = importlib.import_module(module_name)
self.subscription_generator = getattr(module, class_name)()
return self.subscription_generator
else:
return None

def __str__(self):
return self.title

Expand Down Expand Up @@ -135,6 +150,14 @@ def get_default(cls):
return None


def newsletter_presave(sender, instance, **kwargs):
if instance.subscription_generator_class:
instance.get_subscription_generator()


models.signals.pre_save.connect(newsletter_presave, Newsletter)


class Subscription(models.Model):
user = models.ForeignKey(
AUTH_USER_MODEL, blank=True, null=True, verbose_name=_('user'),
Expand Down Expand Up @@ -547,6 +570,21 @@ def get_default(cls):
return None


class SubscriptionGenerator:
"""
Interface for subscription generators.
Users must implement the generate_subscriptions method.
"""
@abstractmethod
def generate_subscriptions(self, submission):
"""
:param submission: the submission for which we are generating the subscription list
:return: the list of Subscription objects.
They may just be in memory Subscription objects, no need to save them to the DB.
"""
raise NotImplementedError()


class Submission(models.Model):
"""
Submission represents a particular Message as it is being submitted
Expand Down Expand Up @@ -574,11 +612,19 @@ def extra_headers(self):
}

def submit(self):
subscriptions = self.subscriptions.filter(subscribed=True)
subscriptions = list(self.subscriptions.filter(subscribed=True).all())

if self.newsletter.subscription_generator_class:
logger.info("Dynamically generating subscriptions")
subscribed_emails = {s.email for s in subscriptions}
unsubscribed_emails = {s.email for s in self.newsletter.subscription_set.filter(unsubscribed=True).all()}
dynamic_subscriptions = self.newsletter.get_subscription_generator().generate_subscriptions(self)
subscriptions += (s for s in dynamic_subscriptions
if s.email not in subscribed_emails and s.email not in unsubscribed_emails)

logger.info(
gettext("Submitting %(submission)s to %(count)d people"),
{'submission': self, 'count': subscriptions.count()}
{'submission': self, 'count': len(subscriptions)}
)

assert self.publish_date < now(), \
Expand Down
53 changes: 52 additions & 1 deletion tests/test_mailing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django.utils.timezone import now

from newsletter.models import (
Newsletter, Subscription, Submission, Message, Article, get_default_sites, Attachment
Newsletter, Subscription, Submission, Message, Article, get_default_sites, Attachment, SubscriptionGenerator
)
from newsletter.utils import ACTIONS

Expand Down Expand Up @@ -172,6 +172,57 @@ def test_submission_unsubscribed_unactivated(self):
self.assertEqual(list(subscriptions), [self.s2])


class TestingSubscriptionGenerator(SubscriptionGenerator):
def generate_subscriptions(self, submission):
return [
Subscription(newsletter=submission.newsletter, name_field='name 2', email_field='[email protected]'),
Subscription(newsletter=submission.newsletter, name_field='name 3', email_field='[email protected]'),
Subscription(newsletter=submission.newsletter, name_field='name 4', email_field='[email protected]'),
]


class SubscriptionGeneratorTestCase(MailingTestCase):
def setUp(self):
super().setUp()
self.n.subscription_generator_class = 'tests.test_mailing.TestingSubscriptionGenerator'
self.n.save()
self.sub = Submission.from_message(self.m)
self.sub.save()

def test_subscription_generator(self):
""" Test the dynamic generation of subscriptors """
# Manually add some subscriptions, including an unsubscription
Subscription.objects.filter(newsletter=self.n).delete()
sub1 = Subscription.objects.create(name='name 1', email='[email protected]', newsletter=self.n, subscribed=True)
sub2 = Subscription.objects.create(name='name 2', email='[email protected]', newsletter=self.n, subscribed=True)
sub3 = Subscription.objects.create(name='name 3', email='[email protected]', newsletter=self.n, unsubscribed=True)
self.sub.subscriptions.add(sub1)
self.sub.subscriptions.add(sub2)
self.sub.subscriptions.add(sub3)

self.sub.submit()
Submission.submit_queue()
submission = Submission.objects.get(pk=self.sub.pk)
self.assertTrue(submission.sent)
self.assertEqual(len(mail.outbox), 3)

def test_nonexistent_generator_class(self):
""" Test failure when generator class does not exist """
try:
self.n.subscription_generator_class = 'nonexistent_module.SomeClass'
self.n.save()
self.fail('Should not reach this')
except ModuleNotFoundError:
pass

try:
self.n.subscription_generator_class = 'newsletter.models.NonexistentClass'
self.n.save()
self.fail('Should not reach this')
except AttributeError:
pass


class SubmitSubmissionTestCase(MailingTestCase):
def setUp(self):
super().setUp()
Expand Down
Loading