Пример #1
0
    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)
Пример #2
0
    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)
Пример #3
0
    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)
Пример #5
0
    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)
Пример #6
0
    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)
Пример #7
0
 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)
Пример #8
0
 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)
Пример #10
0
    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)
Пример #11
0
    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)
Пример #12
0
    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)
Пример #13
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_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)
Пример #15
0
    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)
Пример #16
0
    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)
Пример #17
0
    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)
Пример #21
0
    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)
Пример #23
0
    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)
Пример #24
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)
Пример #25
0
    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))
Пример #26
0
    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)
Пример #27
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)])
Пример #29
0
    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)
Пример #30
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
Пример #31
0
    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)
Пример #34
0
    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)
Пример #35
0
    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)
Пример #38
0
    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 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)