Commit d222b2d7 by Calen Pennington

Move bin-task enqueuing into a classmethod (rather than having it on the RecipientResolvers

parent 352fa067
......@@ -8,7 +8,6 @@ from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin
class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
resolver_class = None # define in subclass
async_send_task = None # define in subclass
def add_arguments(self, parser):
......@@ -24,20 +23,27 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
parser.add_argument('site_domain_name')
def handle(self, *args, **options):
resolver = self.make_resolver(*args, **options)
self.send_emails(resolver, *args, **options)
self.log_debug('Args = %r', options)
def make_resolver(self, *args, **options):
current_date = datetime.datetime(
*[int(x) for x in options['date'].split('-')],
tzinfo=pytz.UTC
)
self.log_debug('Args = %r', options)
self.log_debug('Current date = %s', current_date.isoformat())
site = Site.objects.get(domain__iexact=options['site_domain_name'])
self.log_debug('Running for site %s', site.domain)
return self.resolver_class(site, current_date, async_send_task=self.async_send_task)
def send_emails(self, resolver, *args, **options):
override_recipient_email = options.get('override_recipient_email')
self.send_emails(site, current_date, override_recipient_email)
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,
)
def send_emails(self, *args, **kwargs):
pass # define in subclass
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):
resolver_class = CourseUpdateResolver
async_send_task = ScheduleCourseUpdate
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
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):
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.resolvers import ScheduleStartResolver
from openedx.core.djangoapps.schedules.tasks import ScheduleRecurringNudge
class Command(SendEmailBaseCommand):
resolver_class = ScheduleStartResolver
async_send_task = ScheduleRecurringNudge
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
self.log_prefix = 'Scheduled Nudge'
def send_emails(self, resolver, *args, **options):
def send_emails(self, *args, **kwargs):
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.resolvers import UpgradeReminderResolver
from openedx.core.djangoapps.schedules.tasks import ScheduleUpgradeReminder
class Command(SendEmailBaseCommand):
resolver_class = UpgradeReminderResolver
async_send_task = ScheduleUpgradeReminder
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
self.log_prefix = 'Upgrade Reminder'
def send_emails(self, resolver, *args, **options):
resolver.send(2, options.get('override_recipient_email'))
def send_emails(self, *args, **kwargs):
self.enqueue(2, *args, **kwargs)
......@@ -18,24 +18,13 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_un
class TestSendEmailBaseCommand(CacheIsolationTestCase):
def setUp(self):
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,
def test_handle(self):
with patch.object(self.command, 'send_emails') as send_emails:
self.command.handle(site_domain_name=self.site.domain, date='2017-09-29')
send_emails.assert_called_once_with(
self.site,
datetime.datetime(2017, 9, 29, tzinfo=pytz.UTC),
async_send_task=None,
None
)
def test_handle(self):
with patch.object(self.command, 'make_resolver') as make_resolver:
make_resolver.return_value = 'resolver'
with patch.object(self.command, 'send_emails') as send_emails:
self.command.handle(date='2017-09-29')
make_resolver.assert_called_once_with(date='2017-09-29')
send_emails.assert_called_once_with('resolver', date='2017-09-29')
......@@ -60,35 +60,33 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
@patch.object(nudge.Command, 'resolver_class')
def test_handle(self, mock_resolver):
@patch.object(nudge.Command, 'async_send_task')
def test_handle(self, mock_send):
test_day = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
nudge.Command().handle(date='2017-08-01', site_domain_name=self.site_config.site.domain)
mock_resolver.assert_called_with(
self.site_config.site,
test_day,
async_send_task=nudge.Command.async_send_task,
)
for day in (-3, -10):
mock_resolver().send.assert_any_call(day, None)
mock_send.enqueue.assert_any_call(
self.site_config.site,
test_day,
day,
None
)
@patch.object(tasks, 'ace')
def test_resolver_send(self, mock_ace):
current_day = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
mock_schedule_bin = Mock()
nudge.ScheduleStartResolver(self.site_config.site, current_day, mock_schedule_bin).send(-3)
test_day = current_day + datetime.timedelta(days=-3)
self.assertFalse(mock_schedule_bin.called)
mock_schedule_bin.apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_day), -3, 0, [], True, None),
retry=False,
)
mock_schedule_bin.apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_day), -3, resolvers.RECURRING_NUDGE_NUM_BINS - 1, [], True, None),
retry=False,
)
self.assertFalse(mock_ace.send.called)
with patch.object(tasks.ScheduleRecurringNudge, 'apply_async') as mock_apply_async:
tasks.ScheduleRecurringNudge.enqueue(self.site_config.site, current_day, -3)
test_day = current_day + datetime.timedelta(days=-3)
mock_apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_day), -3, 0, [], True, None),
retry=False,
)
mock_apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_day), -3, resolvers.RECURRING_NUDGE_NUM_BINS - 1, [], True, None),
retry=False,
)
self.assertFalse(mock_ace.send.called)
@ddt.data(1, 10, 100)
@patch.object(tasks, 'ace')
......@@ -180,11 +178,12 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
mock_schedule_bin = Mock()
current_datetime = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
nudge.ScheduleStartResolver(
tasks.ScheduleRecurringNudge.enqueue(
self.site_config.site,
current_datetime,
mock_schedule_bin,
).send(3)
3
)
self.assertFalse(mock_schedule_bin.called)
self.assertFalse(mock_schedule_bin.apply_async.called)
self.assertFalse(mock_ace.send.called)
......
......@@ -63,36 +63,34 @@ class TestUpgradeReminder(FilteredQueryCountMixin, CacheIsolationTestCase):
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
@patch.object(reminder.Command, 'resolver_class')
def test_handle(self, mock_resolver):
@patch.object(reminder.Command, 'async_send_task')
def test_handle(self, mock_send):
test_day = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
reminder.Command().handle(date='2017-08-01', site_domain_name=self.site_config.site.domain)
mock_resolver.assert_called_with(
mock_send.enqueue.assert_called_with(
self.site_config.site,
test_day,
async_send_task=reminder.Command.async_send_task,
2,
None
)
mock_resolver().send.assert_any_call(2, None)
@patch.object(tasks, 'ace')
def test_resolver_send(self, mock_ace):
current_day = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
test_day = current_day + datetime.timedelta(days=2)
ScheduleFactory.create(upgrade_deadline=datetime.datetime(2017, 8, 3, 15, 34, 30, tzinfo=pytz.UTC))
mock_schedule_bin = Mock()
reminder.UpgradeReminderResolver(self.site_config.site, current_day, mock_schedule_bin).send(2)
self.assertFalse(mock_schedule_bin.called)
mock_schedule_bin.apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_day), 2, 0, [], True, None),
retry=False,
)
mock_schedule_bin.apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_day), 2, resolvers.UPGRADE_REMINDER_NUM_BINS - 1, [], True, None),
retry=False,
)
self.assertFalse(mock_ace.send.called)
with patch.object(tasks.ScheduleUpgradeReminder, 'apply_async') as mock_apply_async:
tasks.ScheduleUpgradeReminder.enqueue(self.site_config.site, current_day, 2)
mock_apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_day), 2, 0, [], True, None),
retry=False,
)
mock_apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_day), 2, resolvers.UPGRADE_REMINDER_NUM_BINS - 1, [], True, None),
retry=False,
)
self.assertFalse(mock_ace.send.called)
@ddt.data(1, 10, 100)
@patch.object(tasks, 'ace')
......@@ -164,11 +162,11 @@ class TestUpgradeReminder(FilteredQueryCountMixin, CacheIsolationTestCase):
mock_schedule_bin = Mock()
current_day = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
reminder.UpgradeReminderResolver(
tasks.ScheduleUpgradeReminder.enqueue(
self.site_config.site,
current_day,
async_send_task=mock_schedule_bin,
).send(3)
day_offset=3,
)
self.assertFalse(mock_schedule_bin.called)
self.assertFalse(mock_schedule_bin.apply_async.called)
self.assertFalse(mock_ace.send.called)
......
......@@ -13,13 +13,14 @@ from django.utils.formats import dateformat, get_format
from edx_ace import ace
from edx_ace.message import Message
from edx_ace.recipient import Recipient
from edx_ace.utils.date import deserialize
from edx_ace.utils.date import deserialize, serialize
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.monitoring_utils import set_custom_metric
from openedx.core.djangoapps.schedules.models import Schedule, ScheduleConfig
from openedx.core.djangoapps.schedules import resolvers
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
LOG = logging.getLogger(__name__)
......@@ -68,12 +69,91 @@ def _recurring_nudge_schedule_send(site_id, msg_str):
class ScheduleMessageBaseTask(Task):
ignore_result = True
routing_key = ROUTING_KEY
num_bins = resolvers.DEFAULT_NUM_BINS
enqueue_config_var = None # define in subclass
log_prefix = None
@classmethod
def log_debug(cls, message, *args, **kwargs):
LOG.debug(cls.log_prefix + ': ' + message, *args, **kwargs)
@classmethod
def enqueue(cls, site, current_date, day_offset, override_recipient_email=None):
current_date = current_date.replace(hour=0, minute=0, second=0)
if not cls.is_enqueue_enabled(site):
cls.log_debug(
'Message queuing disabled for site %s', site.domain)
return
exclude_orgs, org_list = cls.get_course_org_filter(site)
target_date = current_date + datetime.timedelta(days=day_offset)
cls.log_debug('Target date = %s', target_date.isoformat())
for bin in range(cls.num_bins):
task_args = (
site.id,
serialize(target_date),
day_offset,
bin,
org_list,
exclude_orgs,
override_recipient_email,
)
cls.log_debug('Launching task with args = %r', task_args)
cls.apply_async(
task_args,
retry=False,
)
@classmethod
def is_enqueue_enabled(cls, site):
if cls.enqueue_config_var:
return getattr(ScheduleConfig.current(site), cls.enqueue_config_var)
return False
@classmethod
def get_course_org_filter(cls, site):
"""
Given the configuration of sites, get the list of orgs that should be included or excluded from this send.
Returns:
tuple: Returns a tuple (exclude_orgs, org_list). If exclude_orgs is True, then org_list is a list of the
only orgs that should be included in this send. If exclude_orgs is False, then org_list is a list of
orgs that should be excluded from this send. All other orgs should be included.
"""
try:
site_config = SiteConfiguration.objects.get(site_id=site.id)
org_list = site_config.get_value('course_org_filter')
exclude_orgs = False
if not org_list:
not_orgs = set()
for other_site_config in SiteConfiguration.objects.all():
other = other_site_config.get_value('course_org_filter')
if not isinstance(other, list):
if other is not None:
not_orgs.add(other)
else:
not_orgs.update(other)
org_list = list(not_orgs)
exclude_orgs = True
elif not isinstance(org_list, list):
org_list = [org_list]
except SiteConfiguration.DoesNotExist:
org_list = None
exclude_orgs = False
finally:
return exclude_orgs, org_list
class ScheduleRecurringNudge(ScheduleMessageBaseTask):
num_bins = resolvers.RECURRING_NUDGE_NUM_BINS
enqueue_config_var = 'enqueue_recurring_nudge'
log_prefix = 'Scheduled Nudge'
def run(
self, site_id, target_day_str, day_offset, bin_num, org_list, exclude_orgs=False, override_recipient_email=None,
):
return resolvers.recurring_nudge_schedule_bin(
return resolvers.ScheduleStartResolver().recurring_nudge_schedule_bin(
_recurring_nudge_schedule_send,
site_id,
target_day_str,
......@@ -86,10 +166,15 @@ class ScheduleRecurringNudge(ScheduleMessageBaseTask):
class ScheduleUpgradeReminder(ScheduleMessageBaseTask):
num_bins = resolvers.UPGRADE_REMINDER_NUM_BINS
enqueue_config_var = 'enqueue_upgrade_reminder'
log_prefix = 'Course Update'
def run(
self, site_id, target_day_str, day_offset, bin_num, org_list, exclude_orgs=False, override_recipient_email=None,
):
return resolvers.upgrade_reminder_schedule_bin(
return resolvers.UpgradeReminderResolver().upgrade_reminder_schedule_bin(
_upgrade_reminder_schedule_send,
site_id,
target_day_str,
......@@ -111,10 +196,14 @@ def _upgrade_reminder_schedule_send(site_id, msg_str):
class ScheduleCourseUpdate(ScheduleMessageBaseTask):
num_bins = resolvers.COURSE_UPDATE_NUM_BINS
enqueue_config_var = 'enqueue_course_update'
log_prefix = 'Course Update'
def run(
self, site_id, target_day_str, day_offset, bin_num, org_list, exclude_orgs=False, override_recipient_email=None,
):
return resolvers.course_update_schedule_bin(
return resolvers.CourseUpdateResolver().course_update_schedule_bin(
_course_update_schedule_send,
site_id,
target_day_str,
......
......@@ -3,9 +3,9 @@ from unittest import skipUnless
import ddt
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.resolvers import DEFAULT_NUM_BINS
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
......@@ -16,68 +16,61 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_un
@skip_unless_lms
@skipUnless('openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS,
"Can't test schedules if the app isn't installed")
class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase):
class TestScheduleMessageBaseTask(CacheIsolationTestCase):
def setUp(self):
super(TestBinnedSchedulesBaseResolver, self).setUp()
super(TestScheduleMessageBaseTask, self).setUp()
self.site = SiteFactory.create()
self.site_config = SiteConfigurationFactory.create(site=self.site)
self.schedule_config = ScheduleConfigFactory.create(site=self.site)
def setup_resolver(self, site=None, current_date=None, async_send_task=None):
if site is None:
site = self.site
if current_date is None:
current_date = datetime.datetime.now()
resolver = BinnedSchedulesBaseResolver(self.site, current_date, async_send_task)
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
self.basetask = ScheduleMessageBaseTask
def test_send_enqueue_disabled(self):
resolver = self.setup_resolver()
resolver.is_enqueue_enabled = lambda: False
with patch.object(resolver, 'async_send_task') as send:
with patch.object(resolver, 'log_debug') as log_debug:
resolver.send(day_offset=2)
log_debug.assert_called_once_with('Message queuing disabled for site %s', self.site.domain)
send.apply_async.assert_not_called()
send = Mock(name='async_send_task')
with patch.multiple(
self.basetask,
is_enqueue_enabled=Mock(return_value=False),
log_debug=DEFAULT,
run=send,
) 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)
def test_send_enqueue_enabled(self, day_offset):
resolver = self.setup_resolver()
resolver.is_enqueue_enabled = lambda: True
resolver.get_course_org_filter = lambda: (False, None)
with patch.object(resolver, 'async_send_task') as send:
with patch.object(resolver, 'log_debug') as log_debug:
resolver.send(day_offset=day_offset)
target_date = resolver.current_date + datetime.timedelta(day_offset)
log_debug.assert_any_call('Target date = %s', target_date.isoformat())
assert send.apply_async.call_count == DEFAULT_NUM_BINS
send = Mock(name='async_send_task')
current_date = datetime.datetime.now()
with patch.multiple(
self.basetask,
is_enqueue_enabled=Mock(return_value=True),
get_course_org_filter=Mock(return_value=(False, None)),
log_debug=DEFAULT,
run=send,
) 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) + \
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)
def test_is_enqueue_enabled(self, enabled):
resolver = self.setup_resolver()
resolver.enqueue_config_var = 'enqueue_recurring_nudge'
self.schedule_config.enqueue_recurring_nudge = enabled
self.schedule_config.save()
assert resolver.is_enqueue_enabled() == enabled
with patch.object(self.basetask, 'enqueue_config_var', 'enqueue_recurring_nudge'):
self.schedule_config.enqueue_recurring_nudge = enabled
self.schedule_config.save()
assert self.basetask.is_enqueue_enabled(self.site) == enabled
@ddt.unpack
@ddt.data(
......@@ -85,10 +78,9 @@ class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase):
(['course1', 'course2'], ['course1', 'course2'])
)
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.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 org_list == expected_org_list
......@@ -99,12 +91,11 @@ class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase):
(['course1', 'course2'], [u'course1', u'course2'])
)
def test_get_course_org_filter_exclude(self, course_org_filter, expected_org_list):
resolver = self.setup_resolver()
self.other_site = SiteFactory.create()
self.other_site_config = SiteConfigurationFactory.create(
site=self.other_site,
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
self.assertItemsEqual(org_list, expected_org_list)
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