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
class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
async_send_task = None # define in subclass
offsets = () # define in subclass
def add_arguments(self, parser):
parser.add_argument(
......@@ -37,9 +38,6 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
override_recipient_email = options.get('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):
self.async_send_task.enqueue(
site,
......@@ -47,3 +45,7 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
day_offset,
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
class Command(SendEmailBaseCommand):
async_send_task = ScheduleCourseUpdate
def __init__(self, *args, **kwargs):
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)
log_prefix = 'Course Update'
offsets = xrange(-7, -77, -7)
......@@ -4,11 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleRecurringNudge
class Command(SendEmailBaseCommand):
async_send_task = ScheduleRecurringNudge
def __init__(self, *args, **kwargs):
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)
log_prefix = 'Scheduled Nudge'
offsets = (-3, -10)
......@@ -4,10 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleUpgradeReminder
class Command(SendEmailBaseCommand):
async_send_task = ScheduleUpgradeReminder
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
self.log_prefix = 'Upgrade Reminder'
def send_emails(self, *args, **kwargs):
self.enqueue(2, *args, **kwargs)
log_prefix = 'Upgrade Reminder'
offsets = (2,)
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
import ddt
import pytz
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.site_configuration.tests.factories import SiteFactory, SiteConfigurationFactory
......@@ -29,3 +29,18 @@ class TestSendEmailBaseCommand(CacheIsolationTestCase):
datetime.datetime(2017, 9, 29, tzinfo=pytz.UTC),
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 itertools
from copy import deepcopy
from unittest import skipUnless
import attr
import ddt
import pytz
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.message import Message
from mock import Mock, patch
from opaque_keys.edx.keys import CourseKey
from mock import patch
from opaque_keys.edx.locator import CourseLocator
import pytz
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from courseware.models import DynamicUpgradeDeadlineConfiguration
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.schedules import resolvers, tasks
from openedx.core.djangoapps.schedules import tasks
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.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms, FilteredQueryCountMixin
from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import ScheduleSendEmailTestBase
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
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
@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 TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
# pylint: disable=protected-access
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)
test_datetime_str = serialize(test_datetime)
expected_queries = NUM_QUERIES_WITH_MATCHES
if not this_org_list:
expected_queries += NUM_QUERIES_NO_ORG_LIST
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)
class TestSendRecurringNudge(ScheduleSendEmailTestBase):
__test__ = True
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)
# pylint: disable=protected-access
tested_task = tasks.ScheduleRecurringNudge
deliver_task = tasks._recurring_nudge_schedule_send
tested_command = nudge.Command
deliver_config = 'deliver_recurring_nudge'
enqueue_config = 'enqueue_recurring_nudge'
expected_offsets = (-3, -10)
def test_user_in_course_with_verified_coursemode_receives_upsell(self):
user = UserFactory.create()
......@@ -344,8 +64,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
user,
schedule.enrollment.course.org
]
sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=tasks.ScheduleRecurringNudge,
stubbed_send_task=patch.object(tasks.ScheduleRecurringNudge, 'async_send_task'),
sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=self.tested_task,
stubbed_send_task=patch.object(self.tested_task, 'async_send_task'),
bin_task_params=bin_task_parameters)
self.assertEqual(len(sent_messages), 1)
......@@ -376,8 +96,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
user,
schedule.enrollment.course.org
]
sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=tasks.ScheduleRecurringNudge,
stubbed_send_task=patch.object(tasks.ScheduleRecurringNudge, 'async_send_task'),
sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=self.tested_task,
stubbed_send_task=patch.object(self.tested_task, 'async_send_task'),
bin_task_params=bin_task_parameters)
self.assertEqual(len(sent_messages), 1)
......@@ -415,8 +135,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
user,
schedule.enrollment.course.org
]
sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=tasks.ScheduleRecurringNudge,
stubbed_send_task=patch.object(tasks.ScheduleRecurringNudge, 'async_send_task'),
sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=self.tested_task,
stubbed_send_task=patch.object(self.tested_task, 'async_send_task'),
bin_task_params=bin_task_parameters)
self.assertEqual(len(sent_messages), 1)
......@@ -440,15 +160,6 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
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):
msg = Message.from_string(msg_attr)
tmp = msg.context["show_upsell"]
return msg.context["show_upsell"]
import datetime
from copy import deepcopy
import logging
from unittest import skipUnless
import attr
import ddt
import pytz
from django.conf import settings
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 mock import Mock, patch
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import CourseLocator
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
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 import tasks
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.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import ScheduleSendEmailTestBase
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
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__)
......@@ -58,378 +24,58 @@ LOG = logging.getLogger(__name__)
@skip_unless_lms
@skipUnless('openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS,
"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(SharedModuleStoreTestCase):
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)
@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_send.enqueue.assert_called_with(
self.site_config.site,
test_day,
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))
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(
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=user2,
)
class TestUpgradeReminder(ScheduleSendEmailTestBase):
__test__ = True
test_datetime = datetime.datetime(2017, 8, 3, 17, tzinfo=pytz.UTC)
test_datetime_str = serialize(test_datetime)
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,)
course_switch_queries = 1
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)
has_course_queries = True
@ddt.data(True, False)
@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 = [
def test_verified_learner(self, is_verified, mock_ace):
user = UserFactory.create(id=self.tested_task.num_bins)
current_day, offset, target_day = self._get_dates()
ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC),
enrollment__user=user,
upgrade_deadline=target_day,
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
enrollment__mode=CourseMode.VERIFIED if is_verified else CourseMode.AUDIT,
)
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,
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)
# 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)
self.assertEqual(mock_ace.send.called, not is_verified)
def test_filter_out_verified_schedules(self):
now = datetime.datetime.now(pytz.UTC)
future_datetime = now + datetime.timedelta(days=21)
current_day, offset, target_day = self._get_dates()
user = UserFactory.create()
schedules = [
ScheduleFactory.create(
upgrade_deadline=future_datetime,
upgrade_deadline=target_day,
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(i)),
enrollment__mode=CourseMode.VERIFIED if i in (0, 3) else CourseMode.AUDIT,
)
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 = []
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])
tasks.ScheduleUpgradeReminder.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=2,
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),
))
......
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