def setUp(self): """ Fixtures. """ super(TestSetDueDateExtension, self).setUp() self.due = due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=UTC) course = CourseFactory.create() week1 = ItemFactory.create(due=due, parent=course) week2 = ItemFactory.create(due=due, parent=course) week3 = ItemFactory.create(parent=course) homework = ItemFactory.create(parent=week1) assignment = ItemFactory.create(parent=homework, due=due) handlers.extract_dates(None, course.id) user = UserFactory.create() self.course = course self.week1 = week1 self.homework = homework self.assignment = assignment self.week2 = week2 self.week3 = week3 self.user = user ScheduleFactory.create(enrollment__user=self.user, enrollment__course_id=self.course.id) inject_field_data((course, week1, week2, week3, homework, assignment), course, user)
def test_schedule_start_date_in_past(self): """ Test that when schedule start date is before course start or enrollment date, content_availability_date is set to max of course start or enrollment date """ enrollment = CourseEnrollmentFactory.create( course__start=datetime(2018, 1, 1, tzinfo=UTC), course__self_paced=True, ) CourseModeFactory.create( course_id=enrollment.course.id, mode_slug=CourseMode.VERIFIED, ) CourseModeFactory.create( course_id=enrollment.course.id, mode_slug=CourseMode.AUDIT, ) ScheduleFactory.create( enrollment=enrollment, start_date=datetime(2017, 1, 1, tzinfo=UTC), ) content_availability_date = max(enrollment.created, enrollment.course.start) access_duration = get_user_course_duration(enrollment.user, enrollment.course) expected_course_expiration_date = content_availability_date + access_duration duration_limit_upgrade_deadline = get_user_course_expiration_date( enrollment.user, enrollment.course) self.assertIsNotNone(duration_limit_upgrade_deadline) self.assertEqual(duration_limit_upgrade_deadline, expected_course_expiration_date)
def setUp(self): """ Fixtures. """ super(TestDataDumps, self).setUp() due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=UTC) course = CourseFactory.create() week1 = ItemFactory.create(due=due, parent=course) week2 = ItemFactory.create(due=due, parent=course) homework = ItemFactory.create(parent=week1, due=due) user1 = UserFactory.create() user2 = UserFactory.create() self.course = course self.week1 = week1 self.homework = homework self.week2 = week2 self.user1 = user1 self.user2 = user2 ScheduleFactory.create(enrollment__user=self.user1, enrollment__course_id=self.course.id) ScheduleFactory.create(enrollment__user=self.user2, enrollment__course_id=self.course.id) handlers.extract_dates(None, course.id)
def test_site_config(self, org_list, exclude_orgs, expected_message_count, mock_schedule_send, mock_ace): filtered_org = 'filtered_org' unfiltered_org = 'unfiltered_org' site1 = SiteFactory.create(domain='foo1.bar', name='foo1.bar') limited_config = SiteConfigurationFactory.create(values={'course_org_filter': [filtered_org]}, site=site1) site2 = SiteFactory.create(domain='foo2.bar', name='foo2.bar') unlimited_config = SiteConfigurationFactory.create(values={'course_org_filter': []}, site=site2) for config in (limited_config, unlimited_config): ScheduleConfigFactory.create(site=config.site) ScheduleFactory.create( start=datetime.datetime(2017, 8, 2, 17, 44, 30, tzinfo=pytz.UTC), enrollment__course__org=filtered_org, ) for _ in range(2): ScheduleFactory.create( start=datetime.datetime(2017, 8, 2, 17, 44, 30, tzinfo=pytz.UTC), enrollment__course__org=unfiltered_org, ) test_time_str = serialize(datetime.datetime(2017, 8, 2, 17, tzinfo=pytz.UTC)) with self.assertNumQueries(2): tasks.recurring_nudge_schedule_hour( limited_config.site.id, 3, test_time_str, org_list=org_list, exclude_orgs=exclude_orgs, ) self.assertEqual(mock_schedule_send.apply_async.call_count, expected_message_count) self.assertFalse(mock_ace.send.called)
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)
def test_generate_course_expired_message(self, offsets): now = timezone.now() schedule_offset, course_offset = offsets # Set a timezone and request, to test that the message looks at the user's setting request = RequestFactory().get('/') request.user = UserFactory() set_current_request(request) self.addCleanup(set_current_request, None) set_user_preference(request.user, 'time_zone', 'Asia/Tokyo') if schedule_offset is not None: schedule_upgrade_deadline = now + timedelta(days=schedule_offset) else: schedule_upgrade_deadline = None if course_offset is not None: course_upgrade_deadline = now + timedelta(days=course_offset) else: course_upgrade_deadline = None enrollment = CourseEnrollmentFactory.create( course__start=datetime(2018, 1, 1, tzinfo=UTC), course__self_paced=True, ) CourseModeFactory.create( course_id=enrollment.course.id, mode_slug=CourseMode.VERIFIED, expiration_datetime=course_upgrade_deadline, ) CourseModeFactory.create( course_id=enrollment.course.id, mode_slug=CourseMode.AUDIT, ) ScheduleFactory.create( enrollment=enrollment, upgrade_deadline=schedule_upgrade_deadline, ) duration_limit_upgrade_deadline = get_user_course_expiration_date( enrollment.user, enrollment.course) self.assertIsNotNone(duration_limit_upgrade_deadline) message = generate_course_expired_message(enrollment.user, enrollment.course) self.assertDateInMessage(duration_limit_upgrade_deadline, message) self.assertIn('data-timezone="Asia/Tokyo"', message) soft_upgradeable = schedule_upgrade_deadline is not None and now < schedule_upgrade_deadline upgradeable = course_upgrade_deadline is None or now < course_upgrade_deadline has_upgrade_deadline = course_upgrade_deadline is not None if upgradeable and soft_upgradeable: self.assertDateInMessage(schedule_upgrade_deadline, message) elif upgradeable and has_upgrade_deadline: self.assertDateInMessage(course_upgrade_deadline, message) else: self.assertNotIn("Upgrade by", message)
def setUpTestData(cls): """Set up and enroll our fake user in the course.""" cls.user = UserFactory(password=TEST_PASSWORD) for course in cls.courses: enrollment = CourseEnrollment.enroll(cls.user, course.id) ScheduleFactory.create(start_date=timezone.now() - datetime.timedelta(days=1), enrollment=enrollment)
def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called """Set up and enroll our fake user in the course.""" cls.user = UserFactory(password=TEST_PASSWORD) for course in cls.courses: enrollment = CourseEnrollment.enroll(cls.user, course.id) ScheduleFactory.create(start_date=timezone.now() - datetime.timedelta(days=1), enrollment=enrollment)
def test_site_config(self, org_list, exclude_orgs, expected_message_count, mock_schedule_send, mock_ace): filtered_org = 'filtered_org' unfiltered_org = 'unfiltered_org' site1 = SiteFactory.create(domain='foo1.bar', name='foo1.bar') limited_config = SiteConfigurationFactory.create( values={'course_org_filter': [filtered_org]}, site=site1) site2 = SiteFactory.create(domain='foo2.bar', name='foo2.bar') unlimited_config = SiteConfigurationFactory.create( values={'course_org_filter': []}, site=site2) for config in (limited_config, unlimited_config): ScheduleConfigFactory.create(site=config.site) filtered_sched = ScheduleFactory.create( start=datetime.datetime(2017, 8, 2, 17, 44, 30, tzinfo=pytz.UTC), enrollment__course__org=filtered_org, ) unfiltered_scheds = [ ScheduleFactory.create( start=datetime.datetime(2017, 8, 2, 17, 44, 30, tzinfo=pytz.UTC), enrollment__course__org=unfiltered_org, ) for _ in range(2) ] print(filtered_sched.enrollment) print(filtered_sched.enrollment.course) print(filtered_sched.enrollment.course.org) print(unfiltered_scheds[0].enrollment) print(unfiltered_scheds[0].enrollment.course) print(unfiltered_scheds[0].enrollment.course.org) print(unfiltered_scheds[1].enrollment) print(unfiltered_scheds[1].enrollment.course) print(unfiltered_scheds[1].enrollment.course.org) test_time_str = serialize( datetime.datetime(2017, 8, 2, 17, tzinfo=pytz.UTC)) with self.assertNumQueries(1): tasks.recurring_nudge_schedule_hour( limited_config.site.id, 3, test_time_str, org_list=org_list, exclude_orgs=exclude_orgs, ) print(mock_schedule_send.mock_calls) self.assertEqual(mock_schedule_send.apply_async.call_count, expected_message_count) self.assertFalse(mock_ace.send.called)
def test_generate_course_expired_message(self, offsets): now = timezone.now() schedule_offset, course_offset = offsets if schedule_offset is not None: schedule_upgrade_deadline = now + timedelta(days=schedule_offset) else: schedule_upgrade_deadline = None if course_offset is not None: course_upgrade_deadline = now + timedelta(days=course_offset) else: course_upgrade_deadline = None def format_date(date): return strftime_localized(date, u'%b %-d, %Y') enrollment = CourseEnrollmentFactory.create( course__start=datetime(2018, 1, 1, tzinfo=UTC), course__self_paced=True, ) CourseModeFactory.create( course_id=enrollment.course.id, mode_slug=CourseMode.VERIFIED, expiration_datetime=course_upgrade_deadline, ) CourseModeFactory.create( course_id=enrollment.course.id, mode_slug=CourseMode.AUDIT, ) ScheduleFactory.create( enrollment=enrollment, upgrade_deadline=schedule_upgrade_deadline, ) duration_limit_upgrade_deadline = get_user_course_expiration_date( enrollment.user, enrollment.course) self.assertIsNotNone(duration_limit_upgrade_deadline) message = generate_course_expired_message(enrollment.user, enrollment.course) self.assertIn(format_date(duration_limit_upgrade_deadline), message) soft_upgradeable = schedule_upgrade_deadline is not None and now < schedule_upgrade_deadline upgradeable = course_upgrade_deadline is None or now < course_upgrade_deadline has_upgrade_deadline = course_upgrade_deadline is not None if upgradeable and soft_upgradeable: self.assertIn(format_date(schedule_upgrade_deadline), message) elif upgradeable and has_upgrade_deadline: self.assertIn(format_date(course_upgrade_deadline), message) else: self.assertNotIn("Upgrade by", message)
def test_generate_course_expired_message(self, offsets): now = timezone.now() schedule_offset, course_offset = offsets if schedule_offset is not None: schedule_upgrade_deadline = now + timedelta(days=schedule_offset) else: schedule_upgrade_deadline = None if course_offset is not None: course_upgrade_deadline = now + timedelta(days=course_offset) else: course_upgrade_deadline = None def format_date(date): return strftime_localized(date, u'%b %-d, %Y') enrollment = CourseEnrollmentFactory.create( course__start=datetime(2018, 1, 1, tzinfo=UTC), course__self_paced=True, ) CourseModeFactory.create( course_id=enrollment.course.id, mode_slug=CourseMode.VERIFIED, expiration_datetime=course_upgrade_deadline, ) CourseModeFactory.create( course_id=enrollment.course.id, mode_slug=CourseMode.AUDIT, ) ScheduleFactory.create( enrollment=enrollment, upgrade_deadline=schedule_upgrade_deadline, ) duration_limit_upgrade_deadline = get_user_course_expiration_date(enrollment.user, enrollment.course) self.assertIsNotNone(duration_limit_upgrade_deadline) message = generate_course_expired_message(enrollment.user, enrollment.course) self.assertIn(format_date(duration_limit_upgrade_deadline), message) soft_upgradeable = schedule_upgrade_deadline is not None and now < schedule_upgrade_deadline upgradeable = course_upgrade_deadline is None or now < course_upgrade_deadline has_upgrade_deadline = course_upgrade_deadline is not None if upgradeable and soft_upgradeable: self.assertIn(format_date(schedule_upgrade_deadline), message) elif upgradeable and has_upgrade_deadline: self.assertIn(format_date(course_upgrade_deadline), message) else: self.assertNotIn("Upgrade by", message)
def _assert_template_for_offset(self, offset, message_count): current_day, offset, target_day, upgrade_deadline = self._get_dates( offset) user = UserFactory.create() for course_index in range(message_count): ScheduleFactory.create( start=target_day, upgrade_deadline=upgrade_deadline, 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.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_FIRST_MATCH if self.queries_deadline_for_each_course: num_expected_queries += (message_count - 1) with self.assertNumQueries(num_expected_queries, table_blacklist=WAFFLE_TABLES): self.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), )) num_expected_messages = 1 if self.consolidates_emails_for_learner else message_count self.assertEqual(len(sent_messages), num_expected_messages) with self.assertNumQueries(2): self.deliver_task(*sent_messages[0]) 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_add_entitlement_and_upgrade_audit_enrollment_with_dynamic_deadline( self, mock_get_course_runs): """ Verify that if an entitlement is added for a user, if the user has one upgradeable enrollment that enrollment is upgraded to the mode of the entitlement and linked to the entitlement regardless of dynamic upgrade deadline being set. """ DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True) course = CourseFactory.create(self_paced=True) course_uuid = uuid.uuid4() course_mode = CourseModeFactory( course_id=course.id, mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1)) # Set up Entitlement entitlement_data = self._get_data_set(self.user, str(course_uuid)) mock_get_course_runs.return_value = [{'key': str(course.id)}] # Add an audit course enrollment for user. enrollment = CourseEnrollment.enroll(self.user, course.id, mode=CourseMode.AUDIT) # Set an upgrade schedule so that dynamic upgrade deadlines are used ScheduleFactory.create( enrollment=enrollment, upgrade_deadline=course_mode.expiration_datetime + timedelta(days=-3)) # The upgrade should complete and ignore the deadline response = self.client.post( self.entitlements_list_url, data=json.dumps(entitlement_data), content_type='application/json', ) assert response.status_code == 201 results = response.data course_entitlement = CourseEntitlement.objects.get( user=self.user, course_uuid=course_uuid) # Assert that enrollment mode is now verified enrollment_mode = CourseEnrollment.enrollment_mode_for_user( self.user, course.id)[0] assert enrollment_mode == course_entitlement.mode assert course_entitlement.enrollment_course_run == enrollment assert results == CourseEntitlementSerializer(course_entitlement).data
def test_resolver_send(self, mock_schedule_bin, 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)) reminder.UpgradeReminderResolver(self.site_config.site, current_day).send(2) self.assertFalse(mock_schedule_bin.called) mock_schedule_bin.apply_async.assert_any_call( (self.site_config.site.id, serialize(test_day), 2, 0, [], True, None), retry=False, ) mock_schedule_bin.apply_async.assert_any_call( (self.site_config.site.id, serialize(test_day), 2, tasks.UPGRADE_REMINDER_NUM_BINS - 1, [], True, None), retry=False, ) self.assertFalse(mock_ace.send.called)
def test_upsell(self, enable_config, testcase): DynamicUpgradeDeadlineConfiguration.objects.create( enabled=enable_config) current_day, offset, target_day, _ = self._get_dates() upgrade_deadline = None if testcase.set_deadline: upgrade_deadline = current_day + datetime.timedelta( days=testcase.deadline_offset) schedule = ScheduleFactory.create( start=target_day, upgrade_deadline=upgrade_deadline, enrollment__course__self_paced=True, ) sent_messages = [] with patch.object(self.task, 'async_send_task') as mock_schedule_send: mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append( args[1]) self.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(schedule.enrollment.user), )) self.assertEqual(len(sent_messages), 1) found_upsell = self._contains_upsell(sent_messages[0]) expect_upsell = enable_config and testcase.expect_upsell self.assertEqual(found_upsell, expect_upsell)
def test_schedule_bin(self, schedule_count, mock_schedule_send, mock_ace): schedules = [ ScheduleFactory.create( upgrade_deadline=datetime.datetime(2017, 8, 3, 18, 44, 30, tzinfo=pytz.UTC), enrollment__user=UserFactory.create(), enrollment__course__id=CourseLocator('edX', 'toy', 'Bin')) for _ in range(schedule_count) ] test_time = datetime.datetime(2017, 8, 3, 18, tzinfo=pytz.UTC) test_time_str = serialize(test_time) for b in range(tasks.UPGRADE_REMINDER_NUM_BINS): # waffle flag takes an extra query before it is cached with self.assertNumQueries(3 if b == 0 else 2): tasks.upgrade_reminder_schedule_bin( self.site_config.site.id, target_day_str=test_time_str, day_offset=2, bin_num=b, org_list=[schedules[0].enrollment.course.org], ) self.assertEqual(mock_schedule_send.apply_async.call_count, schedule_count) self.assertFalse(mock_ace.send.called)
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 % tasks.RECURRING_NUDGE_NUM_BINS) for s in schedules) test_time = datetime.datetime(2017, 8, 3, 18, tzinfo=pytz.UTC) test_time_str = serialize(test_time) for b in range(tasks.RECURRING_NUDGE_NUM_BINS): expected_queries = NUM_QUERIES_NO_MATCHING_SCHEDULES 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.recurring_nudge_schedule_bin( self.site_config.site.id, target_day_str=test_time_str, day_offset=-3, bin_num=b, org_list=[schedules[0].enrollment.course.org], ) self.assertEqual(mock_schedule_send.apply_async.call_count, schedule_count) self.assertFalse(mock_ace.send.called)
def test_no_upsell_button_when_DUDConfiguration_is_off(self): DynamicUpgradeDeadlineConfiguration.objects.create(enabled=False) user = UserFactory.create() course_id = CourseLocator('edX', 'toy', 'Course1') first_day_of_schedule = datetime.datetime.now(pytz.UTC) target_day = first_day_of_schedule target_hour_as_string = serialize(target_day) nudge_day = 3 schedule = ScheduleFactory.create(start=first_day_of_schedule, enrollment__user=user, enrollment__course__id=course_id) schedule.enrollment.course.self_paced = True schedule.enrollment.course.save() bin_task_parameters = [ target_hour_as_string, nudge_day, user, schedule.enrollment.course.org ] sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=tasks.recurring_nudge_schedule_bin, stubbed_send_task=patch.object(tasks, '_recurring_nudge_schedule_send'), bin_task_params=bin_task_parameters) self.assertEqual(len(sent_messages), 1) message_attributes = sent_messages[0][1] self.assertFalse(self._contains_upsell_attribute(message_attributes))
def test_no_course_overview(self, mock_schedule_send): schedule = ScheduleFactory.create(start=datetime.datetime( 2017, 8, 1, 20, 34, 30, tzinfo=pytz.UTC), ) schedule.enrollment.course_id = CourseKey.from_string( 'edX/toy/Not_2012_Fall') schedule.enrollment.save() test_time_str = serialize( datetime.datetime(2017, 8, 1, 20, tzinfo=pytz.UTC)) with self.assertNumQueries(2): tasks.recurring_nudge_schedule_hour( self.site_config.site.id, 3, test_time_str, [schedule.enrollment.course.org], ) # 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)
def test_multiple_enrollments(self, test_hour, messages_sent, mock_schedule_send, mock_ace): user = UserFactory.create() schedules = [ ScheduleFactory.create(start=datetime.datetime(2017, 8, 1, hour, 44, 30, tzinfo=pytz.UTC), enrollment__user=user, enrollment__course__id=CourseLocator( 'edX', 'toy', 'Hour{}'.format(hour))) for hour in (19, 20, 21) ] test_time_str = serialize( datetime.datetime(2017, 8, 1, test_hour, tzinfo=pytz.UTC)) with self.assertNumQueries(2): tasks.recurring_nudge_schedule_hour( self.site_config.site.id, 3, test_time_str, [schedules[0].enrollment.course.org], ) self.assertEqual(mock_schedule_send.apply_async.call_count, messages_sent) self.assertFalse(mock_ace.send.called)
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)
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 % tasks.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(tasks.RECURRING_NUDGE_NUM_BINS): expected_queries = NUM_QUERIES_NO_MATCHING_SCHEDULES 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.recurring_nudge_schedule_bin( self.site_config.site.id, target_day_str=test_datetime_str, day_offset=-3, bin_num=b, org_list=[schedules[0].enrollment.course.org], ) 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)
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__id=CourseLocator( 'edX', 'toy', 'Course{}'.format(course_num))) for course_num in (1, 2, 3) ] test_time = datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC) test_time_str = serialize(test_time) with self.assertNumQueries(NUM_QUERIES_WITH_MATCHES, table_blacklist=WAFFLE_TABLES): tasks.upgrade_reminder_schedule_bin( self.site_config.site.id, target_day_str=test_time_str, day_offset=2, bin_num=user.id % tasks.UPGRADE_REMINDER_NUM_BINS, org_list=[schedules[0].enrollment.course.org], ) self.assertEqual(mock_schedule_send.apply_async.call_count, 3) self.assertFalse(mock_ace.send.called)
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_time = datetime.datetime(2017, 8, 3, 20, tzinfo=pytz.UTC) test_time_str = serialize(test_time) for b in range(tasks.UPGRADE_REMINDER_NUM_BINS): with self.assertNumQueries(NUM_QUERIES_NO_MATCHING_SCHEDULES, table_blacklist=WAFFLE_TABLES): tasks.upgrade_reminder_schedule_bin( self.site_config.site.id, target_day_str=test_time_str, day_offset=2, bin_num=b, org_list=[schedule.enrollment.course.org], ) # 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)
def test_reset_deadlines_with_masquerade(self): """ Staff users should be able to masquerade as a learner and reset the learner's schedule """ course = CourseFactory.create(self_paced=True) student_username = self.user.username student_user_id = self.user.id student_enrollment = CourseEnrollment.enroll(self.user, course.id) student_schedule = ScheduleFactory.create( start_date=timezone.now() - datetime.timedelta(days=100), enrollment=student_enrollment ) staff_schedule = ScheduleFactory( start_date=timezone.now() - datetime.timedelta(days=30), enrollment__course__id=course.id, enrollment__user=self.staff_user, ) self.switch_to_staff() self.update_masquerade(course=course, username=student_username) with patch('openedx.features.course_experience.api.v1.views.dates_banner_should_display', return_value=(True, False)): self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': course.id}) updated_schedule = Schedule.objects.get(id=student_schedule.id) self.assertEqual(updated_schedule.start_date.date(), datetime.datetime.today().date()) updated_staff_schedule = Schedule.objects.get(id=staff_schedule.id) self.assertEqual(updated_staff_schedule.start_date, staff_schedule.start_date) self.assert_event_emitted( 'edx.ui.lms.reset_deadlines.clicked', courserun_key=str(course.id), is_masquerading=True, is_staff=False, org_key=course.org, user_id=student_user_id, )
def test_filter_out_verified_schedules(self): current_day, offset, target_day, upgrade_deadline = self._get_dates() user = UserFactory.create() schedules = [ ScheduleFactory.create( upgrade_deadline=upgrade_deadline, enrollment__user=user, enrollment__course__self_paced=True, 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) ] sent_messages = [] with patch.object(self.task, 'async_send_task') as mock_schedule_send: mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append( args[1]) self.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), )) messages = [Message.from_string(m) for m in sent_messages] self.assertEqual(len(messages), 1) message = messages[0] self.assertItemsEqual( message.context['course_ids'], [str(schedules[i].enrollment.course.id) for i in (1, 2, 4)])
def test_no_course_overview(self, mock_schedule_send): schedule = ScheduleFactory.create( start=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_time = datetime.datetime(2017, 8, 3, 20, tzinfo=pytz.UTC) test_time_str = serialize(test_time) for b in range(tasks.RECURRING_NUDGE_NUM_BINS): # waffle flag takes an extra query before it is cached with self.assertNumQueries(3 if b == 0 else 2): tasks.recurring_nudge_schedule_bin( self.site_config.site.id, target_day_str=test_time_str, day_offset=-3, bin_num=b, org_list=[schedule.enrollment.course.org], ) # 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)
def test_add_entitlement_and_upgrade_audit_enrollment_with_dynamic_deadline(self, mock_get_course_runs): """ Verify that if an entitlement is added for a user, if the user has one upgradeable enrollment that enrollment is upgraded to the mode of the entitlement and linked to the entitlement regardless of dynamic upgrade deadline being set. """ DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True) course = CourseFactory.create(self_paced=True) course_uuid = uuid.uuid4() course_mode = CourseModeFactory( course_id=course.id, mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1) ) # Set up Entitlement entitlement_data = self._get_data_set(self.user, str(course_uuid)) mock_get_course_runs.return_value = [{'key': str(course.id)}] # Add an audit course enrollment for user. enrollment = CourseEnrollment.enroll(self.user, course.id, mode=CourseMode.AUDIT) # Set an upgrade schedule so that dynamic upgrade deadlines are used ScheduleFactory.create( enrollment=enrollment, upgrade_deadline=course_mode.expiration_datetime + timedelta(days=-3) ) # The upgrade should complete and ignore the deadline response = self.client.post( self.entitlements_list_url, data=json.dumps(entitlement_data), content_type='application/json', ) assert response.status_code == 201 results = response.data course_entitlement = CourseEntitlement.objects.get( user=self.user, course_uuid=course_uuid ) # Assert that enrollment mode is now verified enrollment_mode = CourseEnrollment.enrollment_mode_for_user(self.user, course.id)[0] assert enrollment_mode == course_entitlement.mode assert course_entitlement.enrollment_course_run == enrollment assert results == CourseEntitlementSerializer(course_entitlement).data
def test_templates(self, message_count, day): user = UserFactory.create() schedules = [ ScheduleFactory.create( upgrade_deadline=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_time = datetime.datetime(2017, 8, 3, 19, tzinfo=pytz.UTC) test_time_str = serialize(test_time) patch_policies(self, [StubPolicy([ChannelType.PUSH])]) mock_channel = Mock(name='test_channel', channel_type=ChannelType.EMAIL) patch_channels(self, [mock_channel]) sent_messages = [] templates_override = deepcopy(settings.TEMPLATES) templates_override[0]['OPTIONS'][ 'string_if_invalid'] = "TEMPLATE WARNING - MISSING VARIABLE [%s]" with self.settings(TEMPLATES=templates_override): with patch.object( tasks, '_upgrade_reminder_schedule_send') as mock_schedule_send: mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append( args) with self.assertNumQueries(3): tasks.upgrade_reminder_schedule_bin( self.site_config.site.id, target_day_str=test_time_str, day_offset=day, bin_num=user.id % tasks.UPGRADE_REMINDER_NUM_BINS, org_list=[schedules[0].enrollment.course.org], ) self.assertEqual(len(sent_messages), message_count) for args in sent_messages: tasks._upgrade_reminder_schedule_send(*args) self.assertEqual(mock_channel.deliver.call_count, message_count) for (_name, (_msg, email), _kwargs) in mock_channel.deliver.mock_calls: for template in attr.astuple(email): self.assertNotIn("TEMPLATE WARNING", template)
def test_verified_learner(self, is_verified, mock_ace): user = UserFactory.create(id=self.task.num_bins) current_day, offset, target_day, upgrade_deadline = self._get_dates() ScheduleFactory.create( upgrade_deadline=upgrade_deadline, enrollment__course__self_paced=True, enrollment__user=user, enrollment__mode=CourseMode.VERIFIED if is_verified else CourseMode.AUDIT, ) self.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_ace.send.called, not is_verified)
def test_templates(self, message_count, day): user = UserFactory.create() schedules = [ ScheduleFactory.create(start=datetime.datetime(2017, 8, 1, 19, 44, 30, tzinfo=pytz.UTC), enrollment__user=user, enrollment__course__id=CourseLocator( 'edX', 'toy', 'Hour{}'.format(idx))) for idx in range(message_count) ] test_time_str = serialize( datetime.datetime(2017, 8, 1, 19, tzinfo=pytz.UTC)) patch_policies(self, [StubPolicy([ChannelType.PUSH])]) mock_channel = Mock(name='test_channel', channel_type=ChannelType.EMAIL) patch_channels(self, [mock_channel]) sent_messages = [] templates_override = deepcopy(settings.TEMPLATES) templates_override[0]['OPTIONS'][ 'string_if_invalid'] = "TEMPLATE WARNING - MISSING VARIABLE [%s]" with self.settings(TEMPLATES=templates_override): with patch.object( tasks, '_recurring_nudge_schedule_send') as mock_schedule_send: mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append( args) with self.assertNumQueries(2): tasks.recurring_nudge_schedule_hour( self.site_config.site.id, day, test_time_str, [schedules[0].enrollment.course.org], ) self.assertEqual(len(sent_messages), 1) 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)
def test_resolver_send(self, mock_schedule_bin, mock_ace): current_time = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC) test_time = current_time + datetime.timedelta(days=2) ScheduleFactory.create(upgrade_deadline=datetime.datetime( 2017, 8, 3, 15, 34, 30, tzinfo=pytz.UTC)) reminder.UpgradeReminderResolver(self.site_config.site, current_time).send(2) self.assertFalse(mock_schedule_bin.called) mock_schedule_bin.apply_async.assert_any_call( (self.site_config.site.id, serialize(test_time), 2, 0, [], True, None), retry=False, ) mock_schedule_bin.apply_async.assert_any_call( (self.site_config.site.id, serialize(test_time), 2, tasks.UPGRADE_REMINDER_NUM_BINS - 1, [], True, None), retry=False, ) self.assertFalse(mock_ace.send.called)
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_schedule_hour(self, schedule_count, mock_schedule_send, mock_ace): schedules = [ ScheduleFactory.create(start=datetime.datetime(2017, 8, 1, 18, 34, 30, tzinfo=pytz.UTC)) for _ in range(schedule_count) ] test_time_str = serialize(datetime.datetime(2017, 8, 1, 18, tzinfo=pytz.UTC)) with self.assertNumQueries(2): tasks.recurring_nudge_schedule_hour( self.site_config.site.id, 3, test_time_str, [schedules[0].enrollment.course.org], ) self.assertEqual(mock_schedule_send.apply_async.call_count, schedule_count) self.assertFalse(mock_ace.send.called)
def setUp(self): 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)
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)
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, '_recurring_nudge_schedule_send') as mock_schedule_send: mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args) with self.assertNumQueries(NUM_QUERIES_WITH_MATCHES, table_blacklist=WAFFLE_TABLES): tasks.recurring_nudge_schedule_bin( self.site_config.site.id, target_day_str=test_datetime_str, day_offset=day, bin_num=self._calculate_bin_for_user(user), org_list=[schedules[0].enrollment.course.org], ) 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)
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)
def test_multiple_enrollments(self, test_hour, messages_sent, mock_schedule_send, mock_ace): user = UserFactory.create() schedules = [ ScheduleFactory.create( start=datetime.datetime(2017, 8, 1, hour, 44, 30, tzinfo=pytz.UTC), enrollment__user=user, enrollment__course__id=CourseLocator('edX', 'toy', 'Hour{}'.format(hour)) ) for hour in (19, 20, 21) ] test_time_str = serialize(datetime.datetime(2017, 8, 1, test_hour, tzinfo=pytz.UTC)) with self.assertNumQueries(2): tasks.recurring_nudge_schedule_hour( self.site_config.site.id, 3, test_time_str, [schedules[0].enrollment.course.org], ) self.assertEqual(mock_schedule_send.apply_async.call_count, messages_sent) self.assertFalse(mock_ace.send.called)
def test_send_after_course_end(self, mock_schedule_send): user1 = UserFactory.create(id=tasks.RECURRING_NUDGE_NUM_BINS) schedule = ScheduleFactory.create( start=datetime.datetime(2017, 8, 3, 20, 34, 30, tzinfo=pytz.UTC), enrollment__user=user1, ) schedule.enrollment.course = CourseOverviewFactory() schedule.enrollment.course.end = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=1) test_datetime = datetime.datetime(2017, 8, 3, 20, tzinfo=pytz.UTC) test_datetime_str = serialize(test_datetime) tasks.recurring_nudge_schedule_bin.apply_async( self.site_config.site.id, target_day_str=test_datetime_str, day_offset=-3, bin_num=0, org_list=[schedule.enrollment.course.org], ) self.assertFalse(mock_schedule_send.apply_async.called)
def test_templates(self, message_count, day): settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'] = "TEMPLATE WARNING - MISSING VARIABLE [%s]" user = UserFactory.create() schedules = [ ScheduleFactory.create( start=datetime.datetime(2017, 8, 1, 19, 44, 30, tzinfo=pytz.UTC), enrollment__user=user, enrollment__course__id=CourseLocator('edX', 'toy', 'Hour{}'.format(idx)) ) for idx in range(message_count) ] test_time_str = serialize(datetime.datetime(2017, 8, 1, 19, tzinfo=pytz.UTC)) patch_policies(self, [StubPolicy([ChannelType.PUSH])]) mock_channel = Mock( name='test_channel', channel_type=ChannelType.EMAIL ) patch_channels(self, [mock_channel]) sent_messages = [] with patch.object(tasks, '_recurring_nudge_schedule_send') as mock_schedule_send: mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args) with self.assertNumQueries(2): tasks.recurring_nudge_schedule_hour( self.site_config.site.id, day, test_time_str, [schedules[0].enrollment.course.org], ) self.assertEqual(len(sent_messages), 1) 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)
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, table_blacklist=WAFFLE_TABLES): tasks.recurring_nudge_schedule_bin( self.site_config.site.id, target_day_str=test_datetime_str, day_offset=-3, bin_num=user.id % tasks.RECURRING_NUDGE_NUM_BINS, org_list=[schedules[0].enrollment.course.org], ) self.assertEqual(mock_schedule_send.apply_async.call_count, 1) self.assertFalse(mock_ace.send.called)
def test_no_course_overview(self, mock_schedule_send): schedule = ScheduleFactory.create( start=datetime.datetime(2017, 8, 1, 20, 34, 30, tzinfo=pytz.UTC), ) schedule.enrollment.course_id = CourseKey.from_string('edX/toy/Not_2012_Fall') schedule.enrollment.save() test_time_str = serialize(datetime.datetime(2017, 8, 1, 20, tzinfo=pytz.UTC)) with self.assertNumQueries(2): tasks.recurring_nudge_schedule_hour( self.site_config.site.id, 3, test_time_str, [schedule.enrollment.course.org], ) # 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)
def test_user_with_no_upgrade_deadline_is_not_upsold(self): user = UserFactory.create() course_id = CourseLocator('edX', 'toy', 'Course1') first_day_of_schedule = datetime.datetime.now(pytz.UTC) target_day = first_day_of_schedule target_hour_as_string = serialize(target_day) nudge_day = 3 schedule = ScheduleFactory.create(start=first_day_of_schedule, upgrade_deadline=None, enrollment__user=user, enrollment__course__id=course_id) schedule.enrollment.course.self_paced = True schedule.enrollment.course.save() verification_deadline = first_day_of_schedule + datetime.timedelta(days=21) CourseModeFactory( course_id=course_id, mode_slug=CourseMode.VERIFIED, expiration_datetime=verification_deadline ) schedule.upgrade_deadline = verification_deadline bin_task_parameters = [ target_hour_as_string, nudge_day, user, schedule.enrollment.course.org ] sent_messages = self._stub_sender_and_collect_sent_messages(bin_task=tasks.recurring_nudge_schedule_bin, stubbed_send_task=patch.object(tasks, '_recurring_nudge_schedule_send'), bin_task_params=bin_task_parameters) self.assertEqual(len(sent_messages), 1) message_attributes = sent_messages[0][1] self.assertFalse(self._contains_upsell_attribute(message_attributes))
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(tasks.RECURRING_NUDGE_NUM_BINS): with self.assertNumQueries(NUM_QUERIES_NO_MATCHING_SCHEDULES, table_blacklist=WAFFLE_TABLES): tasks.recurring_nudge_schedule_bin( self.site_config.site.id, target_day_str=test_datetime_str, day_offset=-3, bin_num=b, org_list=[schedule.enrollment.course.org], ) # 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)
def test_site_config(self, org_list, exclude_orgs, expected_message_count, mock_schedule_send, mock_ace): filtered_org = 'filtered_org' unfiltered_org = 'unfiltered_org' site1 = SiteFactory.create(domain='foo1.bar', name='foo1.bar') limited_config = SiteConfigurationFactory.create(values={'course_org_filter': [filtered_org]}, site=site1) site2 = SiteFactory.create(domain='foo2.bar', name='foo2.bar') unlimited_config = SiteConfigurationFactory.create(values={'course_org_filter': []}, site=site2) for config in (limited_config, unlimited_config): ScheduleConfigFactory.create(site=config.site) user1 = UserFactory.create(id=tasks.RECURRING_NUDGE_NUM_BINS) user2 = UserFactory.create(id=tasks.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) with self.assertNumQueries(NUM_QUERIES_WITH_MATCHES, table_blacklist=WAFFLE_TABLES): tasks.recurring_nudge_schedule_bin( limited_config.site.id, target_day_str=test_datetime_str, day_offset=-3, bin_num=0, org_list=org_list, exclude_orgs=exclude_orgs, ) self.assertEqual(mock_schedule_send.apply_async.call_count, expected_message_count) self.assertFalse(mock_ace.send.called)
def test_templates(self, message_count, day): 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__id=CourseLocator('edX', 'toy', 'Course{}'.format(course_num)) ) for course_num in range(message_count) ] for schedule in schedules: schedule.enrollment.course.self_paced = True schedule.enrollment.course.end = future_datetime + datetime.timedelta(days=30) schedule.enrollment.course.save() 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) 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, '_upgrade_reminder_schedule_send') 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, however, # since we create a new course for each schedule in this test, we expect there to be one per message num_expected_queries = NUM_QUERIES_WITH_MATCHES + NUM_QUERIES_WITH_DEADLINE + message_count with self.assertNumQueries(num_expected_queries, table_blacklist=WAFFLE_TABLES): tasks.upgrade_reminder_schedule_bin( self.site_config.site.id, target_day_str=test_datetime_str, day_offset=day, bin_num=self._calculate_bin_for_user(user), org_list=[schedules[0].enrollment.course.org], ) self.assertEqual(len(sent_messages), message_count) # Load the site (which we query per message sent) # Check the schedule config with self.assertNumQueries(1 + message_count): for args in sent_messages: tasks._upgrade_reminder_schedule_send(*args) self.assertEqual(mock_channel.deliver.call_count, message_count) for (_name, (_msg, email), _kwargs) in mock_channel.deliver.mock_calls: for template in attr.astuple(email): self.assertNotIn("TEMPLATE WARNING", template)