Commit 88d2ff24 by Calen Pennington Committed by GitHub

Merge pull request #16293 from edx/cale/refactor-upgrade-email-resolvers

Cale/refactor upgrade email resolvers
parents 5cf016cf 58bff7ed
...@@ -8,7 +8,7 @@ from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin ...@@ -8,7 +8,7 @@ from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin
class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand): class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
resolver_class = None # define in subclass async_send_task = None # define in subclass
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
...@@ -23,20 +23,27 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand): ...@@ -23,20 +23,27 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
parser.add_argument('site_domain_name') parser.add_argument('site_domain_name')
def handle(self, *args, **options): def handle(self, *args, **options):
resolver = self.make_resolver(*args, **options) self.log_debug('Args = %r', options)
self.send_emails(resolver, *args, **options)
def make_resolver(self, *args, **options):
current_date = datetime.datetime( current_date = datetime.datetime(
*[int(x) for x in options['date'].split('-')], *[int(x) for x in options['date'].split('-')],
tzinfo=pytz.UTC tzinfo=pytz.UTC
) )
self.log_debug('Args = %r', options)
self.log_debug('Current date = %s', current_date.isoformat()) self.log_debug('Current date = %s', current_date.isoformat())
site = Site.objects.get(domain__iexact=options['site_domain_name']) site = Site.objects.get(domain__iexact=options['site_domain_name'])
self.log_debug('Running for site %s', site.domain) self.log_debug('Running for site %s', site.domain)
return self.resolver_class(site, current_date)
def send_emails(self, resolver, *args, **options): override_recipient_email = options.get('override_recipient_email')
pass # define in subclass self.send_emails(site, current_date, override_recipient_email)
def send_emails(self, *args, **kwargs):
raise NotImplementedError
def enqueue(self, day_offset, site, current_date, override_recipient_email=None):
self.async_send_task.enqueue(
site,
current_date,
day_offset,
override_recipient_email,
)
from openedx.core.djangoapps.schedules.management.commands import SendEmailBaseCommand from openedx.core.djangoapps.schedules.management.commands import SendEmailBaseCommand
from openedx.core.djangoapps.schedules.resolvers import CourseUpdateResolver from openedx.core.djangoapps.schedules.tasks import ScheduleCourseUpdate
class Command(SendEmailBaseCommand): class Command(SendEmailBaseCommand):
resolver_class = CourseUpdateResolver async_send_task = ScheduleCourseUpdate
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs) super(Command, self).__init__(*args, **kwargs)
self.log_prefix = 'Upgrade Reminder' self.log_prefix = 'Upgrade Reminder'
def send_emails(self, resolver, *args, **options): def send_emails(self, *args, **kwargs):
for day_offset in xrange(-7, -77, -7): for day_offset in xrange(-7, -77, -7):
resolver.send(day_offset, options.get('override_recipient_email')) self.enqueue(day_offset, *args, **kwargs)
from openedx.core.djangoapps.schedules.management.commands import SendEmailBaseCommand from openedx.core.djangoapps.schedules.management.commands import SendEmailBaseCommand
from openedx.core.djangoapps.schedules.resolvers import ScheduleStartResolver from openedx.core.djangoapps.schedules.tasks import ScheduleRecurringNudge
class Command(SendEmailBaseCommand): class Command(SendEmailBaseCommand):
resolver_class = ScheduleStartResolver async_send_task = ScheduleRecurringNudge
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs) super(Command, self).__init__(*args, **kwargs)
self.log_prefix = 'Scheduled Nudge' self.log_prefix = 'Scheduled Nudge'
def send_emails(self, resolver, *args, **options): def send_emails(self, *args, **kwargs):
for day_offset in (-3, -10): for day_offset in (-3, -10):
resolver.send(day_offset, options.get('override_recipient_email')) self.enqueue(day_offset, *args, **kwargs)
from openedx.core.djangoapps.schedules.management.commands import SendEmailBaseCommand from openedx.core.djangoapps.schedules.management.commands import SendEmailBaseCommand
from openedx.core.djangoapps.schedules.resolvers import UpgradeReminderResolver from openedx.core.djangoapps.schedules.tasks import ScheduleUpgradeReminder
class Command(SendEmailBaseCommand): class Command(SendEmailBaseCommand):
resolver_class = UpgradeReminderResolver async_send_task = ScheduleUpgradeReminder
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs) super(Command, self).__init__(*args, **kwargs)
self.log_prefix = 'Upgrade Reminder' self.log_prefix = 'Upgrade Reminder'
def send_emails(self, resolver, *args, **options): def send_emails(self, *args, **kwargs):
resolver.send(2, options.get('override_recipient_email')) self.enqueue(2, *args, **kwargs)
...@@ -18,23 +18,13 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_un ...@@ -18,23 +18,13 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_un
class TestSendEmailBaseCommand(CacheIsolationTestCase): class TestSendEmailBaseCommand(CacheIsolationTestCase):
def setUp(self): def setUp(self):
self.command = SendEmailBaseCommand() self.command = SendEmailBaseCommand()
self.site = SiteFactory()
def test_init_resolver_class(self):
assert self.command.resolver_class is None
def test_make_resolver(self):
with patch.object(self.command, 'resolver_class') as resolver_class:
example_site = SiteFactory(domain='example.com')
self.command.make_resolver(site_domain_name='example.com', date='2017-09-29')
resolver_class.assert_called_once_with(
example_site,
datetime.datetime(2017, 9, 29, tzinfo=pytz.UTC)
)
def test_handle(self): def test_handle(self):
with patch.object(self.command, 'make_resolver') as make_resolver: with patch.object(self.command, 'send_emails') as send_emails:
make_resolver.return_value = 'resolver' self.command.handle(site_domain_name=self.site.domain, date='2017-09-29')
with patch.object(self.command, 'send_emails') as send_emails: send_emails.assert_called_once_with(
self.command.handle(date='2017-09-29') self.site,
make_resolver.assert_called_once_with(date='2017-09-29') datetime.datetime(2017, 9, 29, tzinfo=pytz.UTC),
send_emails.assert_called_once_with('resolver', date='2017-09-29') None
)
...@@ -9,3 +9,17 @@ class ScheduleMessageType(MessageType): ...@@ -9,3 +9,17 @@ class ScheduleMessageType(MessageType):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ScheduleMessageType, self).__init__(*args, **kwargs) super(ScheduleMessageType, self).__init__(*args, **kwargs)
self.log_level = logging.DEBUG if DEBUG_MESSAGE_WAFFLE_FLAG.is_enabled() else None self.log_level = logging.DEBUG if DEBUG_MESSAGE_WAFFLE_FLAG.is_enabled() else None
class RecurringNudge(ScheduleMessageType):
def __init__(self, day, *args, **kwargs):
super(RecurringNudge, self).__init__(*args, **kwargs)
self.name = "recurringnudge_day{}".format(day)
class UpgradeReminder(ScheduleMessageType):
pass
class CourseUpdate(ScheduleMessageType):
pass
...@@ -3,10 +3,10 @@ from unittest import skipUnless ...@@ -3,10 +3,10 @@ from unittest import skipUnless
import ddt import ddt
from django.conf import settings from django.conf import settings
from mock import patch from mock import patch, DEFAULT, Mock
from openedx.core.djangoapps.schedules.resolvers import BinnedSchedulesBaseResolver from openedx.core.djangoapps.schedules.tasks import ScheduleMessageBaseTask
from openedx.core.djangoapps.schedules.tasks import DEFAULT_NUM_BINS from openedx.core.djangoapps.schedules.resolvers import DEFAULT_NUM_BINS
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
...@@ -16,68 +16,61 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_un ...@@ -16,68 +16,61 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_un
@skip_unless_lms @skip_unless_lms
@skipUnless('openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS, @skipUnless('openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS,
"Can't test schedules if the app isn't installed") "Can't test schedules if the app isn't installed")
class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase): class TestScheduleMessageBaseTask(CacheIsolationTestCase):
def setUp(self): def setUp(self):
super(TestBinnedSchedulesBaseResolver, self).setUp() super(TestScheduleMessageBaseTask, self).setUp()
self.site = SiteFactory.create() self.site = SiteFactory.create()
self.site_config = SiteConfigurationFactory.create(site=self.site) self.site_config = SiteConfigurationFactory.create(site=self.site)
self.schedule_config = ScheduleConfigFactory.create(site=self.site) self.schedule_config = ScheduleConfigFactory.create(site=self.site)
self.basetask = ScheduleMessageBaseTask
def setup_resolver(self, site=None, current_date=None):
if site is None:
site = self.site
if current_date is None:
current_date = datetime.datetime.now()
resolver = BinnedSchedulesBaseResolver(self.site, current_date)
return resolver
def test_init_site(self):
resolver = self.setup_resolver()
assert resolver.site == self.site
def test_init_current_date(self):
current_time = datetime.datetime.now()
resolver = self.setup_resolver(current_date=current_time)
current_date = current_time.replace(hour=0, minute=0, second=0)
assert resolver.current_date == current_date
def test_init_async_send_task(self):
resolver = self.setup_resolver()
assert resolver.async_send_task is None
def test_init_num_bins(self):
resolver = self.setup_resolver()
assert resolver.num_bins == DEFAULT_NUM_BINS
def test_send_enqueue_disabled(self): def test_send_enqueue_disabled(self):
resolver = self.setup_resolver() send = Mock(name='async_send_task')
resolver.is_enqueue_enabled = lambda: False with patch.multiple(
with patch.object(resolver, 'async_send_task') as send: self.basetask,
with patch.object(resolver, 'log_debug') as log_debug: is_enqueue_enabled=Mock(return_value=False),
resolver.send(day_offset=2) log_debug=DEFAULT,
log_debug.assert_called_once_with('Message queuing disabled for site %s', self.site.domain) run=send,
send.apply_async.assert_not_called() ) as patches:
self.basetask.enqueue(
site=self.site,
current_date=datetime.datetime.now(),
day_offset=2
)
patches['log_debug'].assert_called_once_with(
'Message queuing disabled for site %s', self.site.domain)
send.apply_async.assert_not_called()
@ddt.data(0, 2, -3) @ddt.data(0, 2, -3)
def test_send_enqueue_enabled(self, day_offset): def test_send_enqueue_enabled(self, day_offset):
resolver = self.setup_resolver() send = Mock(name='async_send_task')
resolver.is_enqueue_enabled = lambda: True current_date = datetime.datetime.now()
resolver.get_course_org_filter = lambda: (False, None) with patch.multiple(
with patch.object(resolver, 'async_send_task') as send: self.basetask,
with patch.object(resolver, 'log_debug') as log_debug: is_enqueue_enabled=Mock(return_value=True),
resolver.send(day_offset=day_offset) get_course_org_filter=Mock(return_value=(False, None)),
target_date = resolver.current_date + datetime.timedelta(day_offset) log_debug=DEFAULT,
log_debug.assert_any_call('Target date = %s', target_date.isoformat()) run=send,
assert send.apply_async.call_count == DEFAULT_NUM_BINS ) as patches:
self.basetask.enqueue(
site=self.site,
current_date=current_date,
day_offset=day_offset
)
target_date = current_date.replace(hour=0, minute=0, second=0, microsecond=0) + \
datetime.timedelta(day_offset)
print(patches['log_debug'].mock_calls)
patches['log_debug'].assert_any_call(
'Target date = %s', target_date.isoformat())
assert send.call_count == DEFAULT_NUM_BINS
@ddt.data(True, False) @ddt.data(True, False)
def test_is_enqueue_enabled(self, enabled): def test_is_enqueue_enabled(self, enabled):
resolver = self.setup_resolver() with patch.object(self.basetask, 'enqueue_config_var', 'enqueue_recurring_nudge'):
resolver.enqueue_config_var = 'enqueue_recurring_nudge' self.schedule_config.enqueue_recurring_nudge = enabled
self.schedule_config.enqueue_recurring_nudge = enabled self.schedule_config.save()
self.schedule_config.save() assert self.basetask.is_enqueue_enabled(self.site) == enabled
assert resolver.is_enqueue_enabled() == enabled
@ddt.unpack @ddt.unpack
@ddt.data( @ddt.data(
...@@ -85,10 +78,9 @@ class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase): ...@@ -85,10 +78,9 @@ class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase):
(['course1', 'course2'], ['course1', 'course2']) (['course1', 'course2'], ['course1', 'course2'])
) )
def test_get_course_org_filter_include(self, course_org_filter, expected_org_list): def test_get_course_org_filter_include(self, course_org_filter, expected_org_list):
resolver = self.setup_resolver()
self.site_config.values['course_org_filter'] = course_org_filter self.site_config.values['course_org_filter'] = course_org_filter
self.site_config.save() self.site_config.save()
exclude_orgs, org_list = resolver.get_course_org_filter() exclude_orgs, org_list = self.basetask.get_course_org_filter(self.site)
assert not exclude_orgs assert not exclude_orgs
assert org_list == expected_org_list assert org_list == expected_org_list
...@@ -99,12 +91,9 @@ class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase): ...@@ -99,12 +91,9 @@ class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase):
(['course1', 'course2'], [u'course1', u'course2']) (['course1', 'course2'], [u'course1', u'course2'])
) )
def test_get_course_org_filter_exclude(self, course_org_filter, expected_org_list): def test_get_course_org_filter_exclude(self, course_org_filter, expected_org_list):
resolver = self.setup_resolver() SiteConfigurationFactory.create(
self.other_site = SiteFactory.create()
self.other_site_config = SiteConfigurationFactory.create(
site=self.other_site,
values={'course_org_filter': course_org_filter}, values={'course_org_filter': course_org_filter},
) )
exclude_orgs, org_list = resolver.get_course_org_filter() exclude_orgs, org_list = self.basetask.get_course_org_filter(self.site)
assert exclude_orgs assert exclude_orgs
self.assertItemsEqual(org_list, expected_org_list) self.assertItemsEqual(org_list, expected_org_list)
...@@ -6,9 +6,12 @@ LOG = logging.getLogger(__name__) ...@@ -6,9 +6,12 @@ LOG = logging.getLogger(__name__)
# TODO: consider using a LoggerAdapter instead of this mixin: # TODO: consider using a LoggerAdapter instead of this mixin:
# https://docs.python.org/2/library/logging.html#logging.LoggerAdapter # https://docs.python.org/2/library/logging.html#logging.LoggerAdapter
class PrefixedDebugLoggerMixin(object): class PrefixedDebugLoggerMixin(object):
log_prefix = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PrefixedDebugLoggerMixin, self).__init__(*args, **kwargs) super(PrefixedDebugLoggerMixin, self).__init__(*args, **kwargs)
self.log_prefix = self.__class__.__name__ if self.log_prefix is None:
self.log_prefix = self.__class__.__name__
def log_debug(self, message, *args, **kwargs): def log_debug(self, message, *args, **kwargs):
LOG.debug(self.log_prefix + ': ' + message, *args, **kwargs) LOG.debug(self.log_prefix + ': ' + message, *args, **kwargs)
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
# * @edx/ospr - to check licensing # * @edx/ospr - to check licensing
# * @edx/devops - to check system requirements # * @edx/devops - to check system requirements
attrs==17.2.0
beautifulsoup4==4.1.3 beautifulsoup4==4.1.3
beautifulsoup==3.2.1 beautifulsoup==3.2.1
bleach==1.4 bleach==1.4
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment