Commit 2c80b1b4 by Nimisha Asthagiri Committed by GitHub

Merge pull request #16339 from edx/cale/dry-schedule-tests

Dry schedule tests
parents 764e598f b461ce0c
...@@ -9,6 +9,7 @@ from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin ...@@ -9,6 +9,7 @@ from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin
class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand): class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
async_send_task = None # define in subclass async_send_task = None # define in subclass
offsets = () # define in subclass
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
...@@ -37,9 +38,6 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand): ...@@ -37,9 +38,6 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
override_recipient_email = options.get('override_recipient_email') override_recipient_email = options.get('override_recipient_email')
self.send_emails(site, current_date, override_recipient_email) 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): def enqueue(self, day_offset, site, current_date, override_recipient_email=None):
self.async_send_task.enqueue( self.async_send_task.enqueue(
site, site,
...@@ -47,3 +45,7 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand): ...@@ -47,3 +45,7 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
day_offset, day_offset,
override_recipient_email, override_recipient_email,
) )
def send_emails(self, *args, **kwargs):
for offset in self.offsets:
self.enqueue(offset, *args, **kwargs)
...@@ -4,11 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleCourseUpdate ...@@ -4,11 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleCourseUpdate
class Command(SendEmailBaseCommand): class Command(SendEmailBaseCommand):
async_send_task = ScheduleCourseUpdate async_send_task = ScheduleCourseUpdate
log_prefix = 'Course Update'
def __init__(self, *args, **kwargs): offsets = xrange(-7, -77, -7)
super(Command, self).__init__(*args, **kwargs)
self.log_prefix = 'Upgrade Reminder'
def send_emails(self, *args, **kwargs):
for day_offset in xrange(-7, -77, -7):
self.enqueue(day_offset, *args, **kwargs)
...@@ -4,11 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleRecurringNudge ...@@ -4,11 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleRecurringNudge
class Command(SendEmailBaseCommand): class Command(SendEmailBaseCommand):
async_send_task = ScheduleRecurringNudge async_send_task = ScheduleRecurringNudge
log_prefix = 'Scheduled Nudge'
def __init__(self, *args, **kwargs): offsets = (-3, -10)
super(Command, self).__init__(*args, **kwargs)
self.log_prefix = 'Scheduled Nudge'
def send_emails(self, *args, **kwargs):
for day_offset in (-3, -10):
self.enqueue(day_offset, *args, **kwargs)
...@@ -4,10 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleUpgradeReminder ...@@ -4,10 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleUpgradeReminder
class Command(SendEmailBaseCommand): class Command(SendEmailBaseCommand):
async_send_task = ScheduleUpgradeReminder async_send_task = ScheduleUpgradeReminder
log_prefix = 'Upgrade Reminder'
def __init__(self, *args, **kwargs): offsets = (2,)
super(Command, self).__init__(*args, **kwargs)
self.log_prefix = 'Upgrade Reminder'
def send_emails(self, *args, **kwargs):
self.enqueue(2, *args, **kwargs)
from copy import deepcopy
import datetime
import ddt
import logging
import attr
from django.conf import settings
from freezegun import freeze_time
from mock import Mock, patch
import pytz
from courseware.models import DynamicUpgradeDeadlineConfiguration
from edx_ace.channel import ChannelType
from edx_ace.utils.date import serialize
from edx_ace.test_utils import StubPolicy, patch_channels, patch_policies
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
from openedx.core.djangoapps.schedules import resolvers, tasks
from openedx.core.djangoapps.schedules.resolvers import _get_datetime_beginning_of_day
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory, ScheduleFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
SITE_QUERY = 1
ORG_DEADLINE_QUERY = 1
SCHEDULES_QUERY = 1
COURSE_MODES_QUERY = 1
GLOBAL_DEADLINE_SWITCH_QUERY = 1
COMMERCE_CONFIG_QUERY = 1
NUM_QUERIES_NO_ORG_LIST = 1
NUM_QUERIES_NO_MATCHING_SCHEDULES = SITE_QUERY + SCHEDULES_QUERY
NUM_QUERIES_WITH_MATCHES = (
NUM_QUERIES_NO_MATCHING_SCHEDULES +
COURSE_MODES_QUERY
)
NUM_QUERIES_FIRST_MATCH = (
NUM_QUERIES_WITH_MATCHES
+ GLOBAL_DEADLINE_SWITCH_QUERY
+ ORG_DEADLINE_QUERY
+ COMMERCE_CONFIG_QUERY
)
LOG = logging.getLogger(__name__)
@ddt.ddt
@freeze_time('2017-08-01 00:00:00', tz_offset=0, tick=True)
class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
__test__ = False
ENABLED_CACHES = ['default']
has_course_queries = False
def setUp(self):
super(ScheduleSendEmailTestBase, self).setUp()
site = SiteFactory.create()
self.site_config = SiteConfigurationFactory.create(site=site)
ScheduleConfigFactory.create(site=self.site_config.site)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
def _calculate_bin_for_user(self, user):
return user.id % self.tested_task.num_bins
def _get_dates(self, offset=None):
current_day = _get_datetime_beginning_of_day(datetime.datetime.now(pytz.UTC))
offset = offset or self.expected_offsets[0]
target_day = current_day + datetime.timedelta(days=offset)
return current_day, offset, target_day
def _get_template_overrides(self):
templates_override = deepcopy(settings.TEMPLATES)
templates_override[0]['OPTIONS']['string_if_invalid'] = "TEMPLATE WARNING - MISSING VARIABLE [%s]"
return templates_override
def test_command_task_binding(self):
self.assertEqual(self.tested_command.async_send_task, self.tested_task)
def test_handle(self):
with patch.object(self.tested_command, 'async_send_task') as mock_send:
test_day = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
self.tested_command().handle(date='2017-08-01', site_domain_name=self.site_config.site.domain)
for offset in self.expected_offsets:
mock_send.enqueue.assert_any_call(
self.site_config.site,
test_day,
offset,
None
)
@patch.object(tasks, 'ace')
def test_resolver_send(self, mock_ace):
current_day, offset, target_day = self._get_dates()
with patch.object(self.tested_task, 'apply_async') as mock_apply_async:
self.tested_task.enqueue(self.site_config.site, current_day, offset)
mock_apply_async.assert_any_call(
(self.site_config.site.id, serialize(target_day), offset, 0, None),
retry=False,
)
mock_apply_async.assert_any_call(
(self.site_config.site.id, serialize(target_day), offset, self.tested_task.num_bins - 1, None),
retry=False,
)
self.assertFalse(mock_ace.send.called)
@ddt.data(1, 10, 100)
@patch.object(tasks, 'ace')
@patch.object(resolvers, 'set_custom_metric')
def test_schedule_bin(self, schedule_count, mock_metric, mock_ace):
with patch.object(self.tested_task, 'async_send_task') as mock_schedule_send:
current_day, offset, target_day = self._get_dates()
schedules = [
ScheduleFactory.create(
start=target_day,
upgrade_deadline=target_day,
enrollment__course__self_paced=True,
) for _ in range(schedule_count)
]
bins_in_use = frozenset((self._calculate_bin_for_user(s.enrollment.user)) for s in schedules)
is_first_match = True
course_queries = len(set(s.enrollment.course.id for s in schedules)) if self.has_course_queries else 0
target_day_str = serialize(target_day)
for b in range(self.tested_task.num_bins):
LOG.debug('Running bin %d', b)
expected_queries = NUM_QUERIES_NO_MATCHING_SCHEDULES
if b in bins_in_use:
if is_first_match:
expected_queries = (
# Since this is the first match, we need to cache all of the config models, so we run a
# query for each of those...
NUM_QUERIES_FIRST_MATCH + course_queries
)
is_first_match = False
else:
expected_queries = NUM_QUERIES_WITH_MATCHES
expected_queries += NUM_QUERIES_NO_ORG_LIST
with self.assertNumQueries(expected_queries, table_blacklist=WAFFLE_TABLES):
self.tested_task.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=target_day_str, day_offset=offset, bin_num=b,
))
num_schedules = mock_metric.call_args[0][1]
if b in bins_in_use:
self.assertGreater(num_schedules, 0)
else:
self.assertEqual(num_schedules, 0)
self.assertEqual(mock_schedule_send.apply_async.call_count, schedule_count)
self.assertFalse(mock_ace.send.called)
def test_no_course_overview(self):
current_day, offset, target_day = self._get_dates()
schedule = ScheduleFactory.create(
start=target_day,
upgrade_deadline=target_day,
enrollment__course__self_paced=True,
)
schedule.enrollment.course_id = CourseKey.from_string('edX/toy/Not_2012_Fall')
schedule.enrollment.save()
with patch.object(self.tested_task, 'async_send_task') as mock_schedule_send:
for b in range(self.tested_task.num_bins):
self.tested_task.apply(kwargs=dict(
site_id=self.site_config.site.id,
target_day_str=serialize(target_day),
day_offset=offset,
bin_num=b,
))
# There is no database constraint that enforces that enrollment.course_id points
# to a valid CourseOverview object. However, in that case, schedules isn't going
# to attempt to address it, and will instead simply skip those users.
# This happens 'transparently' because django generates an inner-join between
# enrollment and course_overview, and thus will skip any rows where course_overview
# is null.
self.assertEqual(mock_schedule_send.apply_async.call_count, 0)
@ddt.data(True, False)
@patch.object(tasks, 'ace')
@patch.object(tasks, 'Message')
def test_deliver_config(self, is_enabled, mock_message, mock_ace):
schedule_config_kwargs = {
'site': self.site_config.site,
self.deliver_config: is_enabled,
}
ScheduleConfigFactory.create(**schedule_config_kwargs)
mock_msg = Mock()
self.deliver_task(self.site_config.site.id, mock_msg)
if is_enabled:
self.assertTrue(mock_ace.send.called)
else:
self.assertFalse(mock_ace.send.called)
@ddt.data(True, False)
def test_enqueue_config(self, is_enabled):
schedule_config_kwargs = {
'site': self.site_config.site,
self.enqueue_config: is_enabled,
}
ScheduleConfigFactory.create(**schedule_config_kwargs)
current_datetime = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
with patch.object(self.tested_task, 'apply_async') as mock_apply_async:
self.tested_task.enqueue(self.site_config.site, current_datetime, 3)
if is_enabled:
self.assertTrue(mock_apply_async.called)
else:
self.assertFalse(mock_apply_async.called)
@patch.object(tasks, 'ace')
@ddt.data(
((['filtered_org'], [], 1)),
(([], ['filtered_org'], 2))
)
@ddt.unpack
def test_site_config(self, this_org_list, other_org_list, expected_message_count, mock_ace):
filtered_org = 'filtered_org'
unfiltered_org = 'unfiltered_org'
this_config = SiteConfigurationFactory.create(values={'course_org_filter': this_org_list})
other_config = SiteConfigurationFactory.create(values={'course_org_filter': other_org_list})
for config in (this_config, other_config):
ScheduleConfigFactory.create(site=config.site)
user1 = UserFactory.create(id=self.tested_task.num_bins)
user2 = UserFactory.create(id=self.tested_task.num_bins * 2)
current_day, offset, target_day = self._get_dates()
ScheduleFactory.create(
upgrade_deadline=target_day,
start=target_day,
enrollment__course__org=filtered_org,
enrollment__course__self_paced=True,
enrollment__user=user1,
)
ScheduleFactory.create(
upgrade_deadline=target_day,
start=target_day,
enrollment__course__org=unfiltered_org,
enrollment__course__self_paced=True,
enrollment__user=user1,
)
ScheduleFactory.create(
upgrade_deadline=target_day,
start=target_day,
enrollment__course__org=unfiltered_org,
enrollment__course__self_paced=True,
enrollment__user=user2,
)
with patch.object(self.tested_task, 'async_send_task') as mock_schedule_send:
self.tested_task.apply(kwargs=dict(
site_id=this_config.site.id, target_day_str=serialize(target_day), day_offset=offset, bin_num=0
))
self.assertEqual(mock_schedule_send.apply_async.call_count, expected_message_count)
self.assertFalse(mock_ace.send.called)
@ddt.data(True, False)
def test_course_end(self, has_course_ended):
user1 = UserFactory.create(id=self.tested_task.num_bins)
current_day, offset, target_day = self._get_dates()
schedule = ScheduleFactory.create(
start=target_day,
upgrade_deadline=target_day,
enrollment__course__self_paced=True,
enrollment__user=user1,
)
schedule.enrollment.course.start = current_day - datetime.timedelta(days=30)
end_date_offset = -2 if has_course_ended else 2
schedule.enrollment.course.end = current_day + datetime.timedelta(days=end_date_offset)
schedule.enrollment.course.save()
with patch.object(self.tested_task, 'async_send_task') as mock_schedule_send:
self.tested_task.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset, bin_num=0,
))
if has_course_ended:
self.assertFalse(mock_schedule_send.apply_async.called)
else:
self.assertTrue(mock_schedule_send.apply_async.called)
@patch.object(tasks, 'ace')
def test_multiple_enrollments(self, mock_ace):
user = UserFactory.create()
current_day, offset, target_day = self._get_dates()
num_courses = 3
for course_index in range(num_courses):
ScheduleFactory.create(
start=target_day,
upgrade_deadline=target_day,
enrollment__course__self_paced=True,
enrollment__user=user,
enrollment__course__id=CourseKey.from_string('edX/toy/course{}'.format(course_index))
)
course_queries = num_courses if self.has_course_queries else 0
expected_query_count = NUM_QUERIES_FIRST_MATCH + course_queries + NUM_QUERIES_NO_ORG_LIST
with self.assertNumQueries(expected_query_count, table_blacklist=WAFFLE_TABLES):
with patch.object(self.tested_task, 'async_send_task') as mock_schedule_send:
self.tested_task.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset,
bin_num=self._calculate_bin_for_user(user),
))
self.assertEqual(mock_schedule_send.apply_async.call_count, 1)
self.assertFalse(mock_ace.send.called)
@ddt.data(1, 10, 100)
def test_templates(self, message_count):
for offset in self.expected_offsets:
self._assert_template_for_offset(offset, message_count)
self.clear_caches()
def _assert_template_for_offset(self, offset, message_count):
current_day, offset, target_day = self._get_dates(offset)
user = UserFactory.create()
for course_index in range(message_count):
ScheduleFactory.create(
start=target_day,
upgrade_deadline=target_day,
enrollment__course__self_paced=True,
enrollment__user=user,
enrollment__course__id=CourseKey.from_string('edX/toy/course{}'.format(course_index))
)
patch_policies(self, [StubPolicy([ChannelType.PUSH])])
mock_channel = Mock(
name='test_channel',
channel_type=ChannelType.EMAIL
)
patch_channels(self, [mock_channel])
sent_messages = []
with self.settings(TEMPLATES=self._get_template_overrides()):
with patch.object(self.tested_task, 'async_send_task') as mock_schedule_send:
mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args)
num_expected_queries = NUM_QUERIES_NO_ORG_LIST + NUM_QUERIES_FIRST_MATCH
if self.has_course_queries:
num_expected_queries += message_count
with self.assertNumQueries(num_expected_queries, table_blacklist=WAFFLE_TABLES):
self.tested_task.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset,
bin_num=self._calculate_bin_for_user(user),
))
self.assertEqual(len(sent_messages), 1)
with self.assertNumQueries(2):
for args in sent_messages:
self.deliver_task(*args)
self.assertEqual(mock_channel.deliver.call_count, 1)
for (_name, (_msg, email), _kwargs) in mock_channel.deliver.mock_calls:
for template in attr.astuple(email):
self.assertNotIn("TEMPLATE WARNING", template)
self.assertNotIn("{{", template)
self.assertNotIn("}}", template)
...@@ -4,7 +4,7 @@ from unittest import skipUnless ...@@ -4,7 +4,7 @@ from unittest import skipUnless
import ddt import ddt
import pytz import pytz
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.management.commands import SendEmailBaseCommand from openedx.core.djangoapps.schedules.management.commands import SendEmailBaseCommand
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory, SiteConfigurationFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory, SiteConfigurationFactory
...@@ -29,3 +29,18 @@ class TestSendEmailBaseCommand(CacheIsolationTestCase): ...@@ -29,3 +29,18 @@ class TestSendEmailBaseCommand(CacheIsolationTestCase):
datetime.datetime(2017, 9, 29, tzinfo=pytz.UTC), datetime.datetime(2017, 9, 29, tzinfo=pytz.UTC),
None None
) )
def test_send_emails(self):
with patch.multiple(
self.command,
offsets=(1, 3, 5),
enqueue=DEFAULT,
):
arg = Mock(name='arg')
kwarg = Mock(name='kwarg')
self.command.send_emails(arg, kwarg=kwarg)
self.assertFalse(arg.called)
self.assertFalse(kwarg.called)
for offset in self.command.offsets:
self.command.enqueue.assert_any_call(offset, arg, kwarg=kwarg)
import datetime import datetime
import itertools
from copy import deepcopy
from unittest import skipUnless from unittest import skipUnless
import attr
import ddt import ddt
import pytz
from django.conf import settings from django.conf import settings
from edx_ace.channel import ChannelType
from edx_ace.test_utils import StubPolicy, patch_channels, patch_policies
from edx_ace.utils.date import serialize from edx_ace.utils.date import serialize
from edx_ace.message import Message from edx_ace.message import Message
from mock import Mock, patch from mock import patch
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
import pytz
from course_modes.models import CourseMode from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory from course_modes.tests.factories import CourseModeFactory
from courseware.models import DynamicUpgradeDeadlineConfiguration from courseware.models import DynamicUpgradeDeadlineConfiguration
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.schedules import tasks
from openedx.core.djangoapps.schedules import resolvers, tasks
from openedx.core.djangoapps.schedules.management.commands import send_recurring_nudge as nudge from openedx.core.djangoapps.schedules.management.commands import send_recurring_nudge as nudge
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory, ScheduleFactory from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import ScheduleSendEmailTestBase
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.core.djangolib.testing.utils import skip_unless_lms
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms, FilteredQueryCountMixin
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
# 1) Load the current django site
# 2) Query the schedules to find all of the template context information
NUM_QUERIES_NO_MATCHING_SCHEDULES = 2
# 3) Query all course modes for all courses in returned schedules
NUM_QUERIES_WITH_MATCHES = NUM_QUERIES_NO_MATCHING_SCHEDULES + 1
# 4) Load the non-matching site configurations
NUM_QUERIES_NO_ORG_LIST = 1
NUM_COURSE_MODES_QUERIES = 1
@ddt.ddt @ddt.ddt
@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 TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase): class TestSendRecurringNudge(ScheduleSendEmailTestBase):
# pylint: disable=protected-access __test__ = True
ENABLED_CACHES = ['default']
def setUp(self):
super(TestSendRecurringNudge, self).setUp()
ScheduleFactory.create(start=datetime.datetime(2017, 8, 1, 15, 44, 30, tzinfo=pytz.UTC))
ScheduleFactory.create(start=datetime.datetime(2017, 8, 1, 17, 34, 30, tzinfo=pytz.UTC))
ScheduleFactory.create(start=datetime.datetime(2017, 8, 2, 15, 34, 30, tzinfo=pytz.UTC))
site = SiteFactory.create()
self.site_config = SiteConfigurationFactory.create(site=site)
ScheduleConfigFactory.create(site=self.site_config.site)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
@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)
for day in (-3, -10):
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)
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, None),
retry=False,
)
mock_apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_day), -3, resolvers.RECURRING_NUDGE_NUM_BINS - 1, None),
retry=False,
)
self.assertFalse(mock_ace.send.called)
@ddt.data(1, 10, 100)
@patch.object(tasks, 'ace')
@patch.object(tasks.ScheduleRecurringNudge, 'async_send_task')
def test_schedule_bin(self, schedule_count, mock_schedule_send, mock_ace):
schedules = [
ScheduleFactory.create(
start=datetime.datetime(2017, 8, 3, 18, 44, 30, tzinfo=pytz.UTC),
enrollment__course__id=CourseLocator('edX', 'toy', 'Bin')
) for i in range(schedule_count)
]
bins_in_use = frozenset((s.enrollment.user.id % resolvers.RECURRING_NUDGE_NUM_BINS) for s in schedules)
test_datetime = datetime.datetime(2017, 8, 3, 18, tzinfo=pytz.UTC)
test_datetime_str = serialize(test_datetime)
for b in range(resolvers.RECURRING_NUDGE_NUM_BINS):
expected_queries = NUM_QUERIES_NO_MATCHING_SCHEDULES + NUM_QUERIES_NO_ORG_LIST
if b in bins_in_use:
# to fetch course modes for valid schedules
expected_queries += NUM_COURSE_MODES_QUERIES
with self.assertNumQueries(expected_queries, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleRecurringNudge.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=-3, bin_num=b,
))
self.assertEqual(mock_schedule_send.apply_async.call_count, schedule_count)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks.ScheduleRecurringNudge, 'async_send_task')
def test_no_course_overview(self, mock_schedule_send):
schedule = ScheduleFactory.create(
start=datetime.datetime(2017, 8, 3, 20, 34, 30, tzinfo=pytz.UTC),
enrollment__user=UserFactory.create(),
)
schedule.enrollment.course_id = CourseKey.from_string('edX/toy/Not_2012_Fall')
schedule.enrollment.save()
test_datetime = datetime.datetime(2017, 8, 3, 20, tzinfo=pytz.UTC)
test_datetime_str = serialize(test_datetime)
for b in range(resolvers.RECURRING_NUDGE_NUM_BINS):
with self.assertNumQueries(NUM_QUERIES_NO_MATCHING_SCHEDULES + NUM_QUERIES_NO_ORG_LIST, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleRecurringNudge.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=-3, bin_num=b
))
# There is no database constraint that enforces that enrollment.course_id points
# to a valid CourseOverview object. However, in that case, schedules isn't going
# to attempt to address it, and will instead simply skip those users.
# This happens 'transparently' because django generates an inner-join between
# enrollment and course_overview, and thus will skip any rows where course_overview
# is null.
self.assertEqual(mock_schedule_send.apply_async.call_count, 0)
@patch.object(tasks.ScheduleRecurringNudge, 'async_send_task')
def test_send_after_course_end(self, mock_schedule_send):
user1 = UserFactory.create(id=resolvers.RECURRING_NUDGE_NUM_BINS)
schedule_start = datetime.datetime(2017, 8, 3, 20, 34, 30, tzinfo=pytz.UTC)
day_command_is_run = schedule_start + datetime.timedelta(days=3)
schedule = ScheduleFactory.create(
start=schedule_start,
enrollment__user=user1,
)
schedule.enrollment.course.start = schedule_start - datetime.timedelta(days=30)
schedule.enrollment.course.end = day_command_is_run - datetime.timedelta(days=1)
schedule.enrollment.course.save()
test_datetime = datetime.datetime(2017, 8, 3, 20, tzinfo=pytz.UTC)
test_datetime_str = serialize(test_datetime)
tasks.ScheduleRecurringNudge.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=-3, bin_num=0,
))
self.assertFalse(mock_schedule_send.apply_async.called)
@patch.object(tasks, 'ace')
def test_delivery_disabled(self, mock_ace):
ScheduleConfigFactory.create(site=self.site_config.site, deliver_recurring_nudge=False)
mock_msg = Mock()
tasks._recurring_nudge_schedule_send(self.site_config.site.id, mock_msg)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks, 'ace')
@patch.object(tasks.ScheduleUpgradeReminder, 'apply_async')
def test_enqueue_disabled(self, mock_ace, mock_apply_async):
ScheduleConfigFactory.create(site=self.site_config.site, enqueue_recurring_nudge=False)
current_datetime = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
tasks.ScheduleRecurringNudge.enqueue(
self.site_config.site,
current_datetime,
3
)
self.assertFalse(mock_apply_async.called)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks, 'ace')
@patch.object(tasks.ScheduleRecurringNudge, 'async_send_task')
@ddt.data(
((['filtered_org'], [], 1)),
(([], ['filtered_org'], 2))
)
@ddt.unpack
def test_site_config(self, this_org_list, other_org_list, expected_message_count, mock_schedule_send, mock_ace):
filtered_org = 'filtered_org'
unfiltered_org = 'unfiltered_org'
this_config = SiteConfigurationFactory.create(values={'course_org_filter': this_org_list})
other_config = SiteConfigurationFactory.create(values={'course_org_filter': other_org_list})
for config in (this_config, other_config):
ScheduleConfigFactory.create(site=config.site)
user1 = UserFactory.create(id=resolvers.RECURRING_NUDGE_NUM_BINS)
user2 = UserFactory.create(id=resolvers.RECURRING_NUDGE_NUM_BINS * 2)
ScheduleFactory.create(
start=datetime.datetime(2017, 8, 3, 17, 44, 30, tzinfo=pytz.UTC),
enrollment__course__org=filtered_org,
enrollment__user=user1,
)
ScheduleFactory.create(
start=datetime.datetime(2017, 8, 3, 17, 44, 30, tzinfo=pytz.UTC),
enrollment__course__org=unfiltered_org,
enrollment__user=user1,
)
ScheduleFactory.create(
start=datetime.datetime(2017, 8, 3, 17, 44, 30, tzinfo=pytz.UTC),
enrollment__course__org=unfiltered_org,
enrollment__user=user2,
)
test_datetime = datetime.datetime(2017, 8, 3, 17, tzinfo=pytz.UTC) # pylint: disable=protected-access
test_datetime_str = serialize(test_datetime) tested_task = tasks.ScheduleRecurringNudge
deliver_task = tasks._recurring_nudge_schedule_send
expected_queries = NUM_QUERIES_WITH_MATCHES tested_command = nudge.Command
if not this_org_list: deliver_config = 'deliver_recurring_nudge'
expected_queries += NUM_QUERIES_NO_ORG_LIST enqueue_config = 'enqueue_recurring_nudge'
expected_offsets = (-3, -10)
with self.assertNumQueries(expected_queries, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleRecurringNudge.apply(kwargs=dict(
site_id=this_config.site.id, target_day_str=test_datetime_str, day_offset=-3, bin_num=0
))
self.assertEqual(mock_schedule_send.apply_async.call_count, expected_message_count)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks, 'ace')
@patch.object(tasks.ScheduleRecurringNudge, 'async_send_task')
def test_multiple_enrollments(self, mock_schedule_send, mock_ace):
user = UserFactory.create()
schedules = [
ScheduleFactory.create(
start=datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC),
enrollment__user=user,
enrollment__course__id=CourseLocator('edX', 'toy', 'Course{}'.format(course_num))
)
for course_num in (1, 2, 3)
]
test_datetime = datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC)
test_datetime_str = serialize(test_datetime)
with self.assertNumQueries(NUM_QUERIES_WITH_MATCHES + NUM_QUERIES_NO_ORG_LIST, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleRecurringNudge.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=-3,
bin_num=user.id % resolvers.RECURRING_NUDGE_NUM_BINS,
))
self.assertEqual(mock_schedule_send.apply_async.call_count, 1)
self.assertFalse(mock_ace.send.called)
@ddt.data(*itertools.product((1, 10, 100), (-3, -10)))
@ddt.unpack
def test_templates(self, message_count, day):
user = UserFactory.create()
schedules = [
ScheduleFactory.create(
start=datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC),
enrollment__user=user,
enrollment__course__id=CourseLocator('edX', 'toy', 'Course{}'.format(course_num))
)
for course_num in range(message_count)
]
test_datetime = datetime.datetime(2017, 8, 3, 19, tzinfo=pytz.UTC)
test_datetime_str = serialize(test_datetime)
patch_policies(self, [StubPolicy([ChannelType.PUSH])])
mock_channel = Mock(
name='test_channel',
channel_type=ChannelType.EMAIL
)
patch_channels(self, [mock_channel])
sent_messages = []
with self.settings(TEMPLATES=self._get_template_overrides()):
with patch.object(tasks.ScheduleRecurringNudge, 'async_send_task') as mock_schedule_send:
mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args)
with self.assertNumQueries(NUM_QUERIES_WITH_MATCHES + NUM_QUERIES_NO_ORG_LIST, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleRecurringNudge.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=day,
bin_num=self._calculate_bin_for_user(user),
))
self.assertEqual(len(sent_messages), 1)
# Load the site
# Check the schedule config
with self.assertNumQueries(2):
for args in sent_messages:
tasks._recurring_nudge_schedule_send(*args)
self.assertEqual(mock_channel.deliver.call_count, 1)
for (_name, (_msg, email), _kwargs) in mock_channel.deliver.mock_calls:
for template in attr.astuple(email):
self.assertNotIn("TEMPLATE WARNING", template)
self.assertNotIn("{{", template)
self.assertNotIn("}}", template)
def test_user_in_course_with_verified_coursemode_receives_upsell(self): def test_user_in_course_with_verified_coursemode_receives_upsell(self):
user = UserFactory.create() user = UserFactory.create()
...@@ -344,8 +64,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase): ...@@ -344,8 +64,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
user, user,
schedule.enrollment.course.org schedule.enrollment.course.org
] ]
sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=tasks.ScheduleRecurringNudge, sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=self.tested_task,
stubbed_send_task=patch.object(tasks.ScheduleRecurringNudge, 'async_send_task'), stubbed_send_task=patch.object(self.tested_task, 'async_send_task'),
bin_task_params=bin_task_parameters) bin_task_params=bin_task_parameters)
self.assertEqual(len(sent_messages), 1) self.assertEqual(len(sent_messages), 1)
...@@ -376,8 +96,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase): ...@@ -376,8 +96,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
user, user,
schedule.enrollment.course.org schedule.enrollment.course.org
] ]
sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=tasks.ScheduleRecurringNudge, sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=self.tested_task,
stubbed_send_task=patch.object(tasks.ScheduleRecurringNudge, 'async_send_task'), stubbed_send_task=patch.object(self.tested_task, 'async_send_task'),
bin_task_params=bin_task_parameters) bin_task_params=bin_task_parameters)
self.assertEqual(len(sent_messages), 1) self.assertEqual(len(sent_messages), 1)
...@@ -415,8 +135,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase): ...@@ -415,8 +135,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
user, user,
schedule.enrollment.course.org schedule.enrollment.course.org
] ]
sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=tasks.ScheduleRecurringNudge, sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=self.tested_task,
stubbed_send_task=patch.object(tasks.ScheduleRecurringNudge, 'async_send_task'), stubbed_send_task=patch.object(self.tested_task, 'async_send_task'),
bin_task_params=bin_task_parameters) bin_task_params=bin_task_parameters)
self.assertEqual(len(sent_messages), 1) self.assertEqual(len(sent_messages), 1)
...@@ -440,15 +160,6 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase): ...@@ -440,15 +160,6 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
return sent_messages return sent_messages
def _get_template_overrides(self):
templates_override = deepcopy(settings.TEMPLATES)
templates_override[0]['OPTIONS']['string_if_invalid'] = "TEMPLATE WARNING - MISSING VARIABLE [%s]"
return templates_override
def _calculate_bin_for_user(self, user):
return user.id % resolvers.RECURRING_NUDGE_NUM_BINS
def _contains_upsell_attribute(self, msg_attr): def _contains_upsell_attribute(self, msg_attr):
msg = Message.from_string(msg_attr) msg = Message.from_string(msg_attr)
tmp = msg.context["show_upsell"]
return msg.context["show_upsell"] return msg.context["show_upsell"]
import datetime
from copy import deepcopy
import logging import logging
from unittest import skipUnless from unittest import skipUnless
import attr
import ddt import ddt
import pytz
from django.conf import settings from django.conf import settings
from edx_ace import Message from edx_ace import Message
from freezegun import freeze_time
from edx_ace.channel import ChannelType
from edx_ace.test_utils import StubPolicy, patch_channels, patch_policies
from edx_ace.utils.date import serialize from edx_ace.utils.date import serialize
from mock import Mock, patch from mock import Mock, patch
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
from course_modes.models import CourseMode from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory from openedx.core.djangoapps.schedules import tasks
from courseware.models import DynamicUpgradeDeadlineConfiguration
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.schedules import resolvers, tasks
from openedx.core.djangoapps.schedules.management.commands import send_upgrade_reminder as reminder from openedx.core.djangoapps.schedules.management.commands import send_upgrade_reminder as reminder
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory, ScheduleFactory from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import ScheduleSendEmailTestBase
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
SITE_QUERY = 1
SCHEDULES_QUERY = 1
COURSE_MODES_QUERY = 1
GLOBAL_DEADLINE_SWITCH_QUERY = 1
COMMERCE_CONFIG_QUERY = 1
NUM_QUERIES_NO_ORG_LIST = 1
NUM_QUERIES_NO_MATCHING_SCHEDULES = SITE_QUERY + SCHEDULES_QUERY
NUM_QUERIES_WITH_MATCHES = (
NUM_QUERIES_NO_MATCHING_SCHEDULES +
COURSE_MODES_QUERY
)
NUM_QUERIES_FIRST_MATCH = (
NUM_QUERIES_WITH_MATCHES
+ GLOBAL_DEADLINE_SWITCH_QUERY
+ COMMERCE_CONFIG_QUERY
)
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
...@@ -58,378 +24,58 @@ LOG = logging.getLogger(__name__) ...@@ -58,378 +24,58 @@ LOG = logging.getLogger(__name__)
@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")
@freeze_time('2017-08-01 00:00:00', tz_offset=0, tick=True) class TestUpgradeReminder(ScheduleSendEmailTestBase):
class TestUpgradeReminder(SharedModuleStoreTestCase): __test__ = True
ENABLED_CACHES = ['default']
@classmethod
def setUpClass(cls):
super(TestUpgradeReminder, cls).setUpClass()
cls.course = CourseFactory.create(
org='edX',
number='test',
display_name='Test Course',
self_paced=True,
start=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=30),
)
cls.course_overview = CourseOverview.get_from_id(cls.course.id)
def setUp(self):
super(TestUpgradeReminder, self).setUp()
CourseModeFactory(
course_id=self.course.id,
mode_slug=CourseMode.VERIFIED,
expiration_datetime=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30),
)
ScheduleFactory.create(upgrade_deadline=datetime.datetime(2017, 8, 1, 15, 44, 30, tzinfo=pytz.UTC))
ScheduleFactory.create(upgrade_deadline=datetime.datetime(2017, 8, 1, 17, 34, 30, tzinfo=pytz.UTC))
ScheduleFactory.create(upgrade_deadline=datetime.datetime(2017, 8, 2, 15, 34, 30, tzinfo=pytz.UTC))
site = SiteFactory.create()
self.site_config = SiteConfigurationFactory.create(site=site)
ScheduleConfigFactory.create(site=self.site_config.site)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True) tested_task = tasks.ScheduleUpgradeReminder
deliver_task = tasks._upgrade_reminder_schedule_send
tested_command = reminder.Command
deliver_config = 'deliver_upgrade_reminder'
enqueue_config = 'enqueue_upgrade_reminder'
expected_offsets = (2,)
@patch.object(reminder.Command, 'async_send_task') has_course_queries = True
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_send.enqueue.assert_called_with(
self.site_config.site,
test_day,
2,
None
)
@ddt.data(True, False)
@patch.object(tasks, 'ace') @patch.object(tasks, 'ace')
def test_resolver_send(self, mock_ace): def test_verified_learner(self, is_verified, mock_ace):
current_day = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC) user = UserFactory.create(id=self.tested_task.num_bins)
test_day = current_day + datetime.timedelta(days=2) current_day, offset, target_day = self._get_dates()
ScheduleFactory.create(upgrade_deadline=datetime.datetime(2017, 8, 3, 15, 34, 30, tzinfo=pytz.UTC))
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, None),
retry=False,
)
mock_apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_day), 2, resolvers.UPGRADE_REMINDER_NUM_BINS - 1, None),
retry=False,
)
self.assertFalse(mock_ace.send.called)
@ddt.data(1, 10, 100)
@patch.object(tasks, 'ace')
@patch.object(tasks.ScheduleUpgradeReminder, 'async_send_task')
def test_schedule_bin(self, schedule_count, mock_schedule_send, mock_ace):
upgrade_deadline = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
schedules = [
ScheduleFactory.create(
upgrade_deadline=upgrade_deadline,
enrollment__course=self.course_overview,
) for i in range(schedule_count)
]
bins_in_use = frozenset((self._calculate_bin_for_user(s.enrollment.user)) for s in schedules)
is_first_match = True
course_switch_queries = len(set(s.enrollment.course.id for s in schedules))
org_switch_queries = len(set(s.enrollment.course.id.org for s in schedules))
test_datetime = upgrade_deadline
test_datetime_str = serialize(test_datetime)
for b in range(resolvers.UPGRADE_REMINDER_NUM_BINS):
LOG.debug('Running bin %d', b)
expected_queries = NUM_QUERIES_NO_MATCHING_SCHEDULES
if b in bins_in_use:
if is_first_match:
expected_queries = (
# Since this is the first match, we need to cache all of the config models, so we run a query
# for each of those...
NUM_QUERIES_FIRST_MATCH
+ course_switch_queries + org_switch_queries
)
is_first_match = False
else:
expected_queries = NUM_QUERIES_WITH_MATCHES
expected_queries += NUM_QUERIES_NO_ORG_LIST
with self.assertNumQueries(expected_queries, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleUpgradeReminder.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=2, bin_num=b,
))
self.assertEqual(mock_schedule_send.apply_async.call_count, schedule_count)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks.ScheduleUpgradeReminder, 'async_send_task')
def test_no_course_overview(self, mock_schedule_send):
schedule = ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 20, 34, 30, tzinfo=pytz.UTC),
)
schedule.enrollment.course_id = CourseKey.from_string('edX/toy/Not_2012_Fall')
schedule.enrollment.save()
test_datetime = datetime.datetime(2017, 8, 3, 20, tzinfo=pytz.UTC)
test_datetime_str = serialize(test_datetime)
for b in range(resolvers.UPGRADE_REMINDER_NUM_BINS):
with self.assertNumQueries(NUM_QUERIES_NO_MATCHING_SCHEDULES + NUM_QUERIES_NO_ORG_LIST, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleUpgradeReminder.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=2, bin_num=b,
))
# There is no database constraint that enforces that enrollment.course_id points
# to a valid CourseOverview object. However, in that case, schedules isn't going
# to attempt to address it, and will instead simply skip those users.
# This happens 'transparently' because django generates an inner-join between
# enrollment and course_overview, and thus will skip any rows where course_overview
# is null.
self.assertEqual(mock_schedule_send.apply_async.call_count, 0)
@patch.object(tasks, 'ace')
def test_delivery_disabled(self, mock_ace):
ScheduleConfigFactory.create(site=self.site_config.site, deliver_upgrade_reminder=False)
mock_msg = Mock()
tasks._upgrade_reminder_schedule_send(self.site_config.site.id, mock_msg)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks, 'ace')
@patch.object(tasks.ScheduleUpgradeReminder, 'apply_async')
def test_enqueue_disabled(self, mock_ace, mock_apply_async):
ScheduleConfigFactory.create(site=self.site_config.site, enqueue_upgrade_reminder=False)
current_day = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
tasks.ScheduleUpgradeReminder.enqueue(
self.site_config.site,
current_day,
day_offset=3,
)
self.assertFalse(mock_apply_async.called)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks, 'ace')
@patch.object(tasks.ScheduleUpgradeReminder, 'async_send_task')
@ddt.data(
((['filtered_org'], [], 1)),
(([], ['filtered_org'], 2))
)
@ddt.unpack
def test_site_config(self, this_org_list, other_org_list, expected_message_count, mock_schedule_send, mock_ace):
filtered_org = 'filtered_org'
unfiltered_org = 'unfiltered_org'
this_config = SiteConfigurationFactory.create(values={'course_org_filter': this_org_list})
other_config = SiteConfigurationFactory.create(values={'course_org_filter': other_org_list})
for config in (this_config, other_config):
ScheduleConfigFactory.create(site=config.site)
user1 = UserFactory.create(id=resolvers.UPGRADE_REMINDER_NUM_BINS)
user2 = UserFactory.create(id=resolvers.UPGRADE_REMINDER_NUM_BINS * 2)
ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 17, 44, 30, tzinfo=pytz.UTC),
enrollment__course__org=filtered_org,
enrollment__course__self_paced=True,
enrollment__user=user1,
)
ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 17, 44, 30, tzinfo=pytz.UTC),
enrollment__course__org=unfiltered_org,
enrollment__course__self_paced=True,
enrollment__user=user1,
)
ScheduleFactory.create( ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 17, 44, 30, tzinfo=pytz.UTC), upgrade_deadline=target_day,
enrollment__course__org=unfiltered_org,
enrollment__course__self_paced=True, enrollment__course__self_paced=True,
enrollment__user=user2, enrollment__user=user,
enrollment__mode=CourseMode.VERIFIED if is_verified else CourseMode.AUDIT,
) )
test_datetime = datetime.datetime(2017, 8, 3, 17, tzinfo=pytz.UTC) self.tested_task.apply(kwargs=dict(
test_datetime_str = serialize(test_datetime) site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset,
bin_num=self._calculate_bin_for_user(user),
))
course_switch_queries = 1 self.assertEqual(mock_ace.send.called, not is_verified)
org_switch_queries = 1
expected_queries = NUM_QUERIES_FIRST_MATCH + course_switch_queries + org_switch_queries
if not this_org_list:
expected_queries += NUM_QUERIES_NO_ORG_LIST
with self.assertNumQueries(expected_queries, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleUpgradeReminder.apply(kwargs=dict(
site_id=this_config.site.id, target_day_str=test_datetime_str, day_offset=-3, bin_num=0
))
self.assertEqual(mock_schedule_send.apply_async.call_count, expected_message_count)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks, 'ace')
@patch.object(tasks.ScheduleUpgradeReminder, 'async_send_task')
def test_multiple_enrollments(self, mock_schedule_send, mock_ace):
user = UserFactory.create()
schedules = [
ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC),
enrollment__user=user,
enrollment__course__self_paced=True,
enrollment__course__id=CourseLocator('edX', 'toy', 'Course{}'.format(course_num))
)
for course_num in (1, 2, 3)
]
course_switch_queries = len(set(s.enrollment.course.id for s in schedules))
org_switch_queries = len(set(s.enrollment.course.id.org for s in schedules))
test_datetime = datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC)
test_datetime_str = serialize(test_datetime)
expected_query_count = (
NUM_QUERIES_FIRST_MATCH + course_switch_queries + org_switch_queries + NUM_QUERIES_NO_ORG_LIST
)
with self.assertNumQueries(expected_query_count, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleUpgradeReminder.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=2,
bin_num=self._calculate_bin_for_user(user),
))
self.assertEqual(mock_schedule_send.apply_async.call_count, 1)
self.assertFalse(mock_ace.send.called)
@ddt.data(1, 10, 100)
def test_templates(self, message_count):
now = datetime.datetime.now(pytz.UTC)
future_datetime = now + datetime.timedelta(days=21)
user = UserFactory.create()
schedules = [
ScheduleFactory.create(
upgrade_deadline=future_datetime,
enrollment__user=user,
enrollment__course__self_paced=True,
enrollment__course__end=future_datetime + datetime.timedelta(days=30),
enrollment__course__id=CourseLocator('edX', 'toy', 'Course{}'.format(course_num))
)
for course_num in range(message_count)
]
for schedule in schedules:
CourseModeFactory(
course_id=schedule.enrollment.course.id,
mode_slug=CourseMode.VERIFIED,
expiration_datetime=future_datetime
)
course_switch_queries = len(set(s.enrollment.course.id for s in schedules))
org_switch_queries = len(set(s.enrollment.course.id.org for s in schedules))
test_datetime = future_datetime
test_datetime_str = serialize(test_datetime)
patch_policies(self, [StubPolicy([ChannelType.PUSH])])
mock_channel = Mock(
name='test_channel',
channel_type=ChannelType.EMAIL
)
patch_channels(self, [mock_channel])
sent_messages = []
with self.settings(TEMPLATES=self._get_template_overrides()):
with patch.object(tasks.ScheduleUpgradeReminder, 'async_send_task') as mock_schedule_send:
mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args)
# we execute one query per course to see if it's opted out of dynamic upgrade deadlines
num_expected_queries = (
NUM_QUERIES_FIRST_MATCH + NUM_QUERIES_NO_ORG_LIST + course_switch_queries + org_switch_queries
)
with self.assertNumQueries(num_expected_queries, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleUpgradeReminder.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=2,
bin_num=self._calculate_bin_for_user(user),
))
self.assertEqual(len(sent_messages), 1)
# Load the site (which we query per message sent)
# Check the schedule config
with self.assertNumQueries(2):
for args in sent_messages:
tasks._upgrade_reminder_schedule_send(*args)
self.assertEqual(mock_channel.deliver.call_count, 1)
for (_name, (_msg, email), _kwargs) in mock_channel.deliver.mock_calls:
for template in attr.astuple(email):
self.assertNotIn("TEMPLATE WARNING", template)
self.assertNotIn("{{", template)
self.assertNotIn("}}", template)
def _get_template_overrides(self):
templates_override = deepcopy(settings.TEMPLATES)
templates_override[0]['OPTIONS']['string_if_invalid'] = "TEMPLATE WARNING - MISSING VARIABLE [%s]"
return templates_override
def _calculate_bin_for_user(self, user):
return user.id % resolvers.UPGRADE_REMINDER_NUM_BINS
@patch.object(tasks, '_upgrade_reminder_schedule_send')
def test_dont_send_to_verified_learner(self, mock_schedule_send):
upgrade_deadline = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
ScheduleFactory.create(
upgrade_deadline=upgrade_deadline,
enrollment__user=UserFactory.create(id=resolvers.UPGRADE_REMINDER_NUM_BINS),
enrollment__course=self.course_overview,
enrollment__mode=CourseMode.VERIFIED,
)
test_datetime_str = serialize(datetime.datetime.now(pytz.UTC))
tasks.ScheduleUpgradeReminder.delay(
self.site_config.site.id, target_day_str=test_datetime_str, day_offset=2, bin_num=0,
org_list=[self.course.org],
)
self.assertFalse(mock_schedule_send.called)
self.assertFalse(mock_schedule_send.apply_async.called)
def test_filter_out_verified_schedules(self): def test_filter_out_verified_schedules(self):
now = datetime.datetime.now(pytz.UTC) current_day, offset, target_day = self._get_dates()
future_datetime = now + datetime.timedelta(days=21)
user = UserFactory.create() user = UserFactory.create()
schedules = [ schedules = [
ScheduleFactory.create( ScheduleFactory.create(
upgrade_deadline=future_datetime, upgrade_deadline=target_day,
enrollment__user=user, enrollment__user=user,
enrollment__course__self_paced=True, enrollment__course__self_paced=True,
enrollment__course__end=future_datetime + datetime.timedelta(days=30),
enrollment__course__id=CourseLocator('edX', 'toy', 'Course{}'.format(i)), enrollment__course__id=CourseLocator('edX', 'toy', 'Course{}'.format(i)),
enrollment__mode=CourseMode.VERIFIED if i in (0, 3) else CourseMode.AUDIT, enrollment__mode=CourseMode.VERIFIED if i in (0, 3) else CourseMode.AUDIT,
) )
for i in range(5) for i in range(5)
] ]
for schedule in schedules:
CourseModeFactory(
course_id=schedule.enrollment.course.id,
mode_slug=CourseMode.VERIFIED,
expiration_datetime=future_datetime
)
test_datetime = future_datetime
test_datetime_str = serialize(test_datetime)
sent_messages = [] sent_messages = []
with patch.object(tasks.ScheduleUpgradeReminder, 'async_send_task') as mock_schedule_send: with patch.object(self.tested_task, 'async_send_task') as mock_schedule_send:
mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args[1]) mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args[1])
tasks.ScheduleUpgradeReminder.apply(kwargs=dict( self.tested_task.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=2, site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset,
bin_num=self._calculate_bin_for_user(user), bin_num=self._calculate_bin_for_user(user),
)) ))
......
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