def _get_course_duration_info(self, course_key):
        """
        Fetch course duration information from database
        """
        try:
            key = CourseKey.from_string(course_key)
            course = CourseOverview.objects.values('display_name').get(id=key)
            duration_config = CourseDurationLimitConfig.current(course_key=key)
            gating_config = ContentTypeGatingConfig.current(course_key=key)
            duration_enabled = CourseDurationLimitConfig.enabled_for_course(course_key=key)
            gating_enabled = ContentTypeGatingConfig.enabled_for_course(course_key=key)

            gating_dict = {
                'enabled': gating_enabled,
                'enabled_as_of': str(gating_config.enabled_as_of) if gating_config.enabled_as_of else 'N/A',
                'reason': gating_config.provenances['enabled'].value
            }
            duration_dict = {
                'enabled': duration_enabled,
                'enabled_as_of': str(duration_config.enabled_as_of) if duration_config.enabled_as_of else 'N/A',
                'reason': duration_config.provenances['enabled'].value
            }

            return {
                'course_id': course_key,
                'course_name': course.get('display_name'),
                'gating_config': gating_dict,
                'duration_config': duration_dict,
            }

        except (ObjectDoesNotExist, InvalidKeyError):
            return {}
    def test_config_overrides(self, global_setting, site_setting, org_setting, course_setting, reverse_order):
        """
        Test that the stacked configuration overrides happen in the correct order and priority.

        This is tested by exhaustively setting each combination of contexts, and validating that only
        the lowest level context that is set to not-None is applied.
        """
        # Add a bunch of configuration outside the contexts that are being tested, to make sure
        # there are no leaks of configuration across contexts
        non_test_course_enabled = CourseOverviewFactory.create(org='non-test-org-enabled')
        non_test_course_disabled = CourseOverviewFactory.create(org='non-test-org-disabled')
        non_test_site_cfg_enabled = SiteConfigurationFactory.create(
            values={'course_org_filter': non_test_course_enabled.org}
        )
        non_test_site_cfg_disabled = SiteConfigurationFactory.create(
            values={'course_org_filter': non_test_course_disabled.org}
        )

        CourseDurationLimitConfig.objects.create(course=non_test_course_enabled, enabled=True)
        CourseDurationLimitConfig.objects.create(course=non_test_course_disabled, enabled=False)
        CourseDurationLimitConfig.objects.create(org=non_test_course_enabled.org, enabled=True)
        CourseDurationLimitConfig.objects.create(org=non_test_course_disabled.org, enabled=False)
        CourseDurationLimitConfig.objects.create(site=non_test_site_cfg_enabled.site, enabled=True)
        CourseDurationLimitConfig.objects.create(site=non_test_site_cfg_disabled.site, enabled=False)

        # Set up test objects
        test_course = CourseOverviewFactory.create(org='test-org')
        test_site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': test_course.org})

        if reverse_order:
            CourseDurationLimitConfig.objects.create(site=test_site_cfg.site, enabled=site_setting)
            CourseDurationLimitConfig.objects.create(org=test_course.org, enabled=org_setting)
            CourseDurationLimitConfig.objects.create(course=test_course, enabled=course_setting)
            CourseDurationLimitConfig.objects.create(enabled=global_setting)
        else:
            CourseDurationLimitConfig.objects.create(enabled=global_setting)
            CourseDurationLimitConfig.objects.create(course=test_course, enabled=course_setting)
            CourseDurationLimitConfig.objects.create(org=test_course.org, enabled=org_setting)
            CourseDurationLimitConfig.objects.create(site=test_site_cfg.site, enabled=site_setting)

        expected_global_setting = self._resolve_settings([global_setting])
        expected_site_setting = self._resolve_settings([global_setting, site_setting])
        expected_org_setting = self._resolve_settings([global_setting, site_setting, org_setting])
        expected_course_setting = self._resolve_settings([global_setting, site_setting, org_setting, course_setting])

        self.assertEqual(expected_global_setting, CourseDurationLimitConfig.current().enabled)
        self.assertEqual(expected_site_setting, CourseDurationLimitConfig.current(site=test_site_cfg.site).enabled)
        self.assertEqual(expected_org_setting, CourseDurationLimitConfig.current(org=test_course.org).enabled)
        self.assertEqual(expected_course_setting, CourseDurationLimitConfig.current(course_key=test_course.id).enabled)
    def test_enabled_for_course(
        self,
        before_enabled,
    ):
        config = CourseDurationLimitConfig.objects.create(
            enabled=True,
            course=self.course_overview,
            enabled_as_of=timezone.now(),
        )

        # Tweak the datetime to check for course enablement so it is either
        # before or after when the configuration was enabled
        if before_enabled:
            target_datetime = config.enabled_as_of - timedelta(days=1)
        else:
            target_datetime = config.enabled_as_of + timedelta(days=1)

        course_key = self.course_overview.id

        self.assertEqual(
            not before_enabled,
            CourseDurationLimitConfig.enabled_for_course(
                course_key=course_key,
                target_datetime=target_datetime,
            )
        )
 def test_course_messaging_for_staff(self):
     """
     Staff users will not see the expiration banner when course duration limits
     are on for the course.
     """
     config = CourseDurationLimitConfig(
         course=CourseOverview.get_from_id(self.course.id),
         enabled=True,
         enabled_as_of=datetime(2018, 1, 1)
     )
     config.save()
     url = course_home_url(self.course)
     CourseEnrollment.enroll(self.staff_user, self.course.id)
     response = self.client.get(url)
     bannerText = get_expiration_banner_text(self.staff_user, self.course)
     self.assertNotContains(response, bannerText, html=True)
Beispiel #5
0
 def test_enabled_for_enrollment_flag_override(self):
     self.assertTrue(CourseDurationLimitConfig.enabled_for_enrollment(
         None,
         None,
         None
     ))
     self.assertTrue(CourseDurationLimitConfig.enabled_for_enrollment(
         Mock(name='enrollment'),
         Mock(name='user'),
         None
     ))
     self.assertTrue(CourseDurationLimitConfig.enabled_for_enrollment(
         Mock(name='enrollment'),
         None,
         Mock(name='course_key')
     ))
Beispiel #6
0
    def get_audit_access_expires(self, model):
        """
        Returns expiration date for a course audit expiration, if any or null
        """
        if not CourseDurationLimitConfig.enabled_for_enrollment(user=model.user, course_key=model.course.id):
            return None

        return get_user_course_expiration_date(model.user, model.course)
    def test_all_current_course_configs(self):
        # Set up test objects
        for global_setting in (True, False, None):
            CourseDurationLimitConfig.objects.create(enabled=global_setting, enabled_as_of=datetime(2018, 1, 1))
            for site_setting in (True, False, None):
                test_site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': []})
                CourseDurationLimitConfig.objects.create(
                    site=test_site_cfg.site, enabled=site_setting, enabled_as_of=datetime(2018, 1, 1)
                )

                for org_setting in (True, False, None):
                    test_org = "{}-{}".format(test_site_cfg.id, org_setting)
                    test_site_cfg.values['course_org_filter'].append(test_org)
                    test_site_cfg.save()

                    CourseDurationLimitConfig.objects.create(
                        org=test_org, enabled=org_setting, enabled_as_of=datetime(2018, 1, 1)
                    )

                    for course_setting in (True, False, None):
                        test_course = CourseOverviewFactory.create(
                            org=test_org,
                            id=CourseLocator(test_org, 'test_course', 'run-{}'.format(course_setting))
                        )
                        CourseDurationLimitConfig.objects.create(
                            course=test_course, enabled=course_setting, enabled_as_of=datetime(2018, 1, 1)
                        )

            with self.assertNumQueries(4):
                all_configs = CourseDurationLimitConfig.all_current_course_configs()

        # Deliberatly using the last all_configs that was checked after the 3rd pass through the global_settings loop
        # We should be creating 3^4 courses (3 global values * 3 site values * 3 org values * 3 course values)
        # Plus 1 for the edX/toy/2012_Fall course
        self.assertEqual(len(all_configs), 3**4 + 1)

        # Point-test some of the final configurations
        self.assertEqual(
            all_configs[CourseLocator('7-True', 'test_course', 'run-None')],
            {
                'enabled': (True, Provenance.org),
                'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run),
            }
        )
        self.assertEqual(
            all_configs[CourseLocator('7-True', 'test_course', 'run-False')],
            {
                'enabled': (False, Provenance.run),
                'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run),
            }
        )
        self.assertEqual(
            all_configs[CourseLocator('7-None', 'test_course', 'run-None')],
            {
                'enabled': (True, Provenance.site),
                'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run),
            }
        )
Beispiel #8
0
def get_audit_access_expiration(user, course):
    """
    Return the expiration date for the user's audit access to this course.
    """
    if AUDIT_DEADLINE_FLAG.is_enabled():
        if not CourseDurationLimitConfig.enabled_for_enrollment(user=user, course_key=course.id):
            return None

        return get_user_course_expiration_date(user, course)
    return None
Beispiel #9
0
    def test_caching_global(self):
        global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
        global_config.save()

        # Check that the global value is not retrieved from cache after save
        with self.assertNumQueries(1):
            self.assertTrue(CourseDurationLimitConfig.current().enabled)

        # Check that the global value can be retrieved from cache after read
        with self.assertNumQueries(0):
            self.assertTrue(CourseDurationLimitConfig.current().enabled)

        global_config.enabled = False
        global_config.save()

        # Check that the global value in cache was deleted on save
        with self.assertNumQueries(1):
            self.assertFalse(CourseDurationLimitConfig.current().enabled)
Beispiel #10
0
    def test_course_expiration_banner_with_unicode(self, mock_strftime_localized, mock_get_date_string):
        """
        Ensure that switching to other languages that have unicode in their
        date representations will not cause the course home page to 404.
        """
        fake_unicode_start_time = u"üñîçø∂é_ßtå®t_tîµé"
        mock_strftime_localized.return_value = fake_unicode_start_time
        date_string = u'<span class="localized-datetime" data-format="shortDate" \
        data-datetime="{formatted_date}" data-language="{language}">{formatted_date_localized}</span>'
        mock_get_date_string.return_value = date_string

        config = CourseDurationLimitConfig(
            course=CourseOverview.get_from_id(self.course.id),
            enabled=True,
            enabled_as_of=datetime(2018, 1, 1)
        )
        config.save()
        url = course_home_url(self.course)
        user = self.create_user_for_course(self.course, CourseUserType.UNENROLLED)
        CourseEnrollment.enroll(user, self.course.id)

        language = 'eo'
        DarkLangConfig(
            released_languages=language,
            changed_by=user,
            enabled=True
        ).save()

        response = self.client.get(url, HTTP_ACCEPT_LANGUAGE=language)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response['Content-Language'], language)

        # Check that if the string is incorrectly not marked as unicode we still get the error
        with mock.patch("openedx.features.course_duration_limits.access.get_date_string",
                        return_value=date_string.encode('utf-8')):
            response = self.client.get(url, HTTP_ACCEPT_LANGUAGE=language)
            self.assertEqual(response.status_code, 500)
    def test_enabled_for_enrollment(
        self,
        already_enrolled,
        pass_enrollment,
        enrolled_before_enabled,
    ):

        # Tweak the datetime to enable the config so that it is either before
        # or after now (which is when the enrollment will be created)
        if enrolled_before_enabled:
            enabled_as_of = timezone.now() + timedelta(days=1)
        else:
            enabled_as_of = timezone.now() - timedelta(days=1)

        CourseDurationLimitConfig.objects.create(
            enabled=True,
            course=self.course_overview,
            enabled_as_of=enabled_as_of,
        )

        if already_enrolled:
            existing_enrollment = CourseEnrollmentFactory.create(
                user=self.user,
                course=self.course_overview,
            )
        else:
            existing_enrollment = None

        if pass_enrollment:
            enrollment = existing_enrollment
            user = None
            course_key = None
        else:
            enrollment = None
            user = self.user
            course_key = self.course_overview.id

        query_count = 7
        if pass_enrollment and already_enrolled:
            query_count = 6

        with self.assertNumQueries(query_count):
            enabled = CourseDurationLimitConfig.enabled_for_enrollment(
                enrollment=enrollment,
                user=user,
                course_key=course_key,
            )
            self.assertEqual(not enrolled_before_enabled, enabled)
Beispiel #12
0
def check_course_expired(user, course):
    """
    Check if the course expired for the user.
    """
    # masquerading course staff should always have access
    if get_course_masquerade(user, course.id):
        return ACCESS_GRANTED

    if not CourseDurationLimitConfig.enabled_for_enrollment(user=user, course_key=course.id):
        return ACCESS_GRANTED

    expiration_date = get_user_course_expiration_date(user, course)
    if expiration_date and timezone.now() > expiration_date:
        return AuditExpiredError(user, course, expiration_date)

    return ACCESS_GRANTED
 def test_enabled_for_enrollment_failure(self):
     with self.assertRaises(ValueError):
         CourseDurationLimitConfig.enabled_for_enrollment(None, None, None)
     with self.assertRaises(ValueError):
         CourseDurationLimitConfig.enabled_for_enrollment(
             Mock(name='enrollment'),
             Mock(name='user'),
             None
         )
     with self.assertRaises(ValueError):
         CourseDurationLimitConfig.enabled_for_enrollment(
             Mock(name='enrollment'),
             None,
             Mock(name='course_key')
         )
    def _get_course_duration_info(self, course_key):
        """
        Fetch course duration information from database
        """
        results = []

        try:
            key = CourseKey.from_string(course_key)
            course = CourseOverview.objects.values('display_name').get(id=key)
            duration_config = CourseDurationLimitConfig.current(course_key=key)
            gating_config = ContentTypeGatingConfig.current(course_key=key)
            partially_enabled = duration_config.enabled != gating_config.enabled

            if partially_enabled:
                if duration_config.enabled:
                    enabled = 'Course Duration Limits Only'
                    enabled_as_of = str(duration_config.enabled_as_of) if duration_config.enabled_as_of else 'N/A'
                    reason = 'Course duration limits are enabled for this course, but content type gating is disabled.'
                elif gating_config.enabled:
                    enabled = 'Content Type Gating Only'
                    enabled_as_of = str(gating_config.enabled_as_of) if gating_config.enabled_as_of else 'N/A'
                    reason = 'Content type gating is enabled for this course, but course duration limits are disabled.'
            else:
                enabled = duration_config.enabled or False
                enabled_as_of = str(duration_config.enabled_as_of) if duration_config.enabled_as_of else 'N/A'
                reason = duration_config.provenances['enabled']

            data = {
                'course_id': course_key,
                'course_name': course.get('display_name'),
                'enabled': enabled,
                'enabled_as_of': enabled_as_of,
                'reason': reason,
            }
            results.append(data)

        except (ObjectDoesNotExist, InvalidKeyError):
            pass

        return results
Beispiel #15
0
def register_course_expired_message(request, course):
    """
    Add a banner notifying the user of the user course expiration date if it exists.
    """
    if not CourseDurationLimitConfig.enabled_for_enrollment(user=request.user, course_key=course.id):
        return

    expiration_date = get_user_course_expiration_date(request.user, course)
    if not expiration_date:
        return

    if is_masquerading_as_student(request.user, course.id) and timezone.now() > expiration_date:
        upgrade_message = _('This learner would not have access to this course. '
                            'Their access expired on {expiration_date}.')
        PageLevelMessages.register_warning_message(
            request,
            HTML(upgrade_message).format(
                expiration_date=expiration_date.strftime('%b %-d')
            )
        )
    else:
        upgrade_message = _('Your access to this course expires on {expiration_date}. \
                    {a_open}Upgrade now {sronly_span_open}to retain access past {expiration_date}.\
                    {span_close}{a_close}{sighted_only_span_open}for unlimited access.{span_close}')
        PageLevelMessages.register_info_message(
            request,
            Text(upgrade_message).format(
                a_open=HTML('<a href="{upgrade_link}">').format(
                    upgrade_link=verified_upgrade_deadline_link(user=request.user, course=course)
                ),
                sronly_span_open=HTML('<span class="sr-only">'),
                sighted_only_span_open=HTML('<span aria-hidden="true">'),
                span_close=HTML('</span>'),
                a_close=HTML('</a>'),
                expiration_date=expiration_date.strftime('%b %-d'),
            )
        )
Beispiel #16
0
    def test_all_current_course_configs(self):
        # Set up test objects
        for global_setting in (True, False, None):
            CourseDurationLimitConfig.objects.create(enabled=global_setting,
                                                     enabled_as_of=datetime(
                                                         2018, 1, 1))
            for site_setting in (True, False, None):
                test_site_cfg = SiteConfigurationFactory.create(
                    values={'course_org_filter': []})
                CourseDurationLimitConfig.objects.create(
                    site=test_site_cfg.site,
                    enabled=site_setting,
                    enabled_as_of=datetime(2018, 1, 1))

                for org_setting in (True, False, None):
                    test_org = "{}-{}".format(test_site_cfg.id, org_setting)
                    test_site_cfg.values['course_org_filter'].append(test_org)
                    test_site_cfg.save()

                    CourseDurationLimitConfig.objects.create(
                        org=test_org,
                        enabled=org_setting,
                        enabled_as_of=datetime(2018, 1, 1))

                    for course_setting in (True, False, None):
                        test_course = CourseOverviewFactory.create(
                            org=test_org,
                            id=CourseLocator(test_org, 'test_course',
                                             'run-{}'.format(course_setting)))
                        CourseDurationLimitConfig.objects.create(
                            course=test_course,
                            enabled=course_setting,
                            enabled_as_of=datetime(2018, 1, 1))

            with self.assertNumQueries(4):
                all_configs = CourseDurationLimitConfig.all_current_course_configs(
                )

        # Deliberatly using the last all_configs that was checked after the 3rd pass through the global_settings loop
        # We should be creating 3^4 courses (3 global values * 3 site values * 3 org values * 3 course values)
        # Plus 1 for the edX/toy/2012_Fall course
        self.assertEqual(len(all_configs), 3**4 + 1)

        # Point-test some of the final configurations
        self.assertEqual(
            all_configs[CourseLocator('7-True', 'test_course', 'run-None')], {
                'enabled': (True, Provenance.org),
                'enabled_as_of':
                (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.course),
            })
        self.assertEqual(
            all_configs[CourseLocator('7-True', 'test_course', 'run-False')], {
                'enabled': (False, Provenance.course),
                'enabled_as_of':
                (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.course),
            })
        self.assertEqual(
            all_configs[CourseLocator('7-None', 'test_course', 'run-None')], {
                'enabled': (True, Provenance.site),
                'enabled_as_of':
                (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.course),
            })
    def test_caching_course(self):
        course = CourseOverviewFactory.create(org='test-org')
        site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': course.org})
        course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1))
        course_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the org value is not retrieved from cache after save
        with self.assertNumQueries(2):
            self.assertTrue(CourseDurationLimitConfig.current(course_key=course.id).enabled)

        RequestCache.clear_all_namespaces()

        # Check that the org value can be retrieved from cache after read
        with self.assertNumQueries(0):
            self.assertTrue(CourseDurationLimitConfig.current(course_key=course.id).enabled)

        course_config.enabled = False
        course_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the org value in cache was deleted on save
        with self.assertNumQueries(2):
            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)

        global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
        global_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the org value is not updated in cache by changing the global value
        with self.assertNumQueries(0):
            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)

        site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
        site_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the org value is not updated in cache by changing the site value
        with self.assertNumQueries(0):
            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)

        org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
        org_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the org value is not updated in cache by changing the site value
        with self.assertNumQueries(0):
            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)
    def test_caching_site(self):
        site_cfg = SiteConfigurationFactory()
        site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
        site_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the site value is not retrieved from cache after save
        with self.assertNumQueries(1):
            self.assertTrue(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)

        RequestCache.clear_all_namespaces()

        # Check that the site value can be retrieved from cache after read
        with self.assertNumQueries(0):
            self.assertTrue(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)

        site_config.enabled = False
        site_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the site value in cache was deleted on save
        with self.assertNumQueries(1):
            self.assertFalse(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)

        global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
        global_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the site value is not updated in cache by changing the global value
        with self.assertNumQueries(0):
            self.assertFalse(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)
Beispiel #19
0
    def get(self, request, course_id, error=None):
        """Displays the course mode choice page.

        Args:
            request (`Request`): The Django Request object.
            course_id (unicode): The slash-separated course key.

        Keyword Args:
            error (unicode): If provided, display this error message
                on the page.

        Returns:
            Response

        """
        course_key = CourseKey.from_string(course_id)

        # Check whether the user has access to this course
        # based on country access rules.
        embargo_redirect = embargo_api.redirect_if_blocked(
            course_key,
            user=request.user,
            ip_address=get_ip(request),
            url=request.path)
        if embargo_redirect:
            return redirect(embargo_redirect)

        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
            request.user, course_key)

        increment('track-selection.{}.{}'.format(
            enrollment_mode, 'active' if is_active else 'inactive'))
        increment('track-selection.views')

        if enrollment_mode is None:
            LOG.info(
                'Rendering track selection for unenrolled user, referred by %s',
                request.META.get('HTTP_REFERER'))

        modes = CourseMode.modes_for_course_dict(course_key)
        ecommerce_service = EcommerceService()

        # We assume that, if 'professional' is one of the modes, it should be the *only* mode.
        # If there are both modes, default to non-id-professional.
        has_enrolled_professional = (
            CourseMode.is_professional_slug(enrollment_mode) and is_active)
        if CourseMode.has_professional_mode(
                modes) and not has_enrolled_professional:
            purchase_workflow = request.GET.get("purchase_workflow", "single")
            verify_url = reverse(
                'verify_student_start_flow',
                kwargs={'course_id': six.text_type(course_key)})
            redirect_url = "{url}?purchase_workflow={workflow}".format(
                url=verify_url, workflow=purchase_workflow)
            if ecommerce_service.is_enabled(request.user):
                professional_mode = modes.get(
                    CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(
                        CourseMode.PROFESSIONAL)
                if purchase_workflow == "single" and professional_mode.sku:
                    redirect_url = ecommerce_service.get_checkout_page_url(
                        professional_mode.sku)
                if purchase_workflow == "bulk" and professional_mode.bulk_sku:
                    redirect_url = ecommerce_service.get_checkout_page_url(
                        professional_mode.bulk_sku)
            return redirect(redirect_url)

        course = modulestore().get_course(course_key)

        # If there isn't a verified mode available, then there's nothing
        # to do on this page.  Send the user to the dashboard.
        if not CourseMode.has_verified_mode(modes):
            return redirect(reverse('dashboard'))

        # If a user has already paid, redirect them to the dashboard.
        if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES +
                          [CourseMode.NO_ID_PROFESSIONAL_MODE]):
            # If the course has started redirect to course home instead
            if course.has_started():
                return redirect(
                    reverse('openedx.course_experience.course_home',
                            kwargs={'course_id': course_key}))
            return redirect(reverse('dashboard'))

        donation_for_course = request.session.get("donation_for_course", {})
        chosen_price = donation_for_course.get(six.text_type(course_key), None)

        if CourseEnrollment.is_enrollment_closed(request.user, course):
            locale = to_locale(get_language())
            enrollment_end_date = format_datetime(course.enrollment_end,
                                                  'short',
                                                  locale=locale)
            params = six.moves.urllib.parse.urlencode(
                {'course_closed': enrollment_end_date})
            return redirect('{0}?{1}'.format(reverse('dashboard'), params))

        # When a credit mode is available, students will be given the option
        # to upgrade from a verified mode to a credit mode at the end of the course.
        # This allows students who have completed photo verification to be eligible
        # for university credit.
        # Since credit isn't one of the selectable options on the track selection page,
        # we need to check *all* available course modes in order to determine whether
        # a credit mode is available.  If so, then we show slightly different messaging
        # for the verified track.
        has_credit_upsell = any(
            CourseMode.is_credit_mode(mode)
            for mode in CourseMode.modes_for_course(course_key,
                                                    only_selectable=False))
        course_id = text_type(course_key)

        context = {
            "course_modes_choose_url":
            reverse("course_modes_choose", kwargs={'course_id': course_id}),
            "modes":
            modes,
            "has_credit_upsell":
            has_credit_upsell,
            "course_name":
            course.display_name_with_default,
            "course_org":
            course.display_org_with_default,
            "course_num":
            course.display_number_with_default,
            "chosen_price":
            chosen_price,
            "error":
            error,
            "responsive":
            True,
            "nav_hidden":
            True,
            "content_gating_enabled":
            ContentTypeGatingConfig.enabled_for_enrollment(
                user=request.user, course_key=course_key),
            "course_duration_limit_enabled":
            CourseDurationLimitConfig.enabled_for_enrollment(
                user=request.user, course_key=course_key),
        }
        context.update(
            get_experiment_user_metadata_context(
                course,
                request.user,
            ))

        title_content = _(
            "Congratulations!  You are now enrolled in {course_name}").format(
                course_name=course.display_name_with_default)

        context["title_content"] = title_content

        if "verified" in modes:
            verified_mode = modes["verified"]
            context["suggested_prices"] = [
                decimal.Decimal(x.strip())
                for x in verified_mode.suggested_prices.split(",")
                if x.strip()
            ]
            price_before_discount = verified_mode.min_price

            context["currency"] = verified_mode.currency.upper()
            context["min_price"] = price_before_discount
            context["verified_name"] = verified_mode.name
            context["verified_description"] = verified_mode.description

            if verified_mode.sku:
                context[
                    "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(
                        request.user)
                context[
                    "ecommerce_payment_page"] = ecommerce_service.payment_page_url(
                    )
                context["sku"] = verified_mode.sku
                context["bulk_sku"] = verified_mode.bulk_sku

        context['currency_data'] = []
        if waffle.switch_is_active('local_currency'):
            if 'edx-price-l10n' not in request.COOKIES:
                currency_data = get_currency_data()
                try:
                    context['currency_data'] = json.dumps(currency_data)
                except TypeError:
                    pass
        return render_to_response("course_modes/choose.html", context)
Beispiel #20
0
def register_course_expired_message(request, course):
    """
    Add a banner notifying the user of the user course expiration date if it exists.
    """
    if not CourseDurationLimitConfig.enabled_for_enrollment(
            user=request.user, course_key=course.id):
        return

    expiration_date = get_user_course_expiration_date(request.user, course)
    if not expiration_date:
        return

    if is_masquerading_as_specific_student(
            request.user, course.id) and timezone.now() > expiration_date:
        upgrade_message = _(
            'This learner does not have access to this course. '
            'Their access expired on {expiration_date}.')
        PageLevelMessages.register_warning_message(
            request,
            HTML(upgrade_message).format(expiration_date=strftime_localized(
                expiration_date, '%b. %-d, %Y')))
    else:
        enrollment = CourseEnrollment.get_enrollment(request.user, course.id)
        if enrollment is None:
            return

        upgrade_deadline = enrollment.upgrade_deadline
        now = timezone.now()
        course_upgrade_deadline = enrollment.course_upgrade_deadline
        if (not upgrade_deadline) or (upgrade_deadline < now):
            upgrade_deadline = course_upgrade_deadline

        expiration_message = _(
            '{strong_open}Audit Access Expires {expiration_date}{strong_close}'
            '{line_break}You lose all access to this course, including your progress, on '
            '{expiration_date}.')
        upgrade_deadline_message = _(
            '{line_break}Upgrade by {upgrade_deadline} to get unlimited access to the course '
            'as long as it exists on the site. {a_open}Upgrade now{sronly_span_open} to '
            'retain access past {expiration_date}{span_close}{a_close}')
        full_message = expiration_message
        if upgrade_deadline and now < upgrade_deadline:
            full_message += upgrade_deadline_message
            using_upgrade_messaging = True
        else:
            using_upgrade_messaging = False

        language = get_language()
        language_is_es = language and language.split('-')[0].lower() == 'es'
        if language_is_es:
            formatted_expiration_date = strftime_localized(
                expiration_date, '%-d de %b. de %Y').lower()
        else:
            formatted_expiration_date = strftime_localized(
                expiration_date, '%b. %-d, %Y')

        if using_upgrade_messaging:
            if language_is_es:
                formatted_upgrade_deadline = strftime_localized(
                    upgrade_deadline, '%-d de %b. de %Y').lower()
            else:
                formatted_upgrade_deadline = strftime_localized(
                    upgrade_deadline, '%b. %-d, %Y')

            PageLevelMessages.register_info_message(
                request,
                Text(full_message).format(
                    a_open=HTML('<a href="{upgrade_link}">').format(
                        upgrade_link=verified_upgrade_deadline_link(
                            user=request.user, course=course)),
                    sronly_span_open=HTML('<span class="sr-only">'),
                    span_close=HTML('</span>'),
                    a_close=HTML('</a>'),
                    expiration_date=formatted_expiration_date,
                    strong_open=HTML('<strong>'),
                    strong_close=HTML('</strong>'),
                    line_break=HTML('<br>'),
                    upgrade_deadline=formatted_upgrade_deadline))
        else:
            PageLevelMessages.register_info_message(
                request,
                Text(full_message).format(
                    span_close=HTML('</span>'),
                    expiration_date=formatted_expiration_date,
                    strong_open=HTML('<strong>'),
                    strong_close=HTML('</strong>'),
                    line_break=HTML('<br>'),
                ))
Beispiel #21
0
def generate_course_expired_message(user, course):
    """
    Generate the message for the user course expiration date if it exists.
    """
    if not CourseDurationLimitConfig.enabled_for_enrollment(user=user, course_key=course.id):
        return

    expiration_date = get_user_course_expiration_date(user, course)
    if not expiration_date:
        return

    if is_masquerading_as_specific_student(user, course.id) and timezone.now() > expiration_date:
        upgrade_message = _('This learner does not have access to this course. '
                            u'Their access expired on {expiration_date}.')
        return HTML(upgrade_message).format(
            expiration_date=strftime_localized(expiration_date, EXPIRATION_DATE_FORMAT_STR)
        )
    else:
        enrollment = CourseEnrollment.get_enrollment(user, course.id)
        if enrollment is None:
            return

        upgrade_deadline = enrollment.upgrade_deadline
        now = timezone.now()
        course_upgrade_deadline = enrollment.course_upgrade_deadline
        if (not upgrade_deadline) or (upgrade_deadline < now):
            upgrade_deadline = course_upgrade_deadline

        expiration_message = _(u'{strong_open}Audit Access Expires {expiration_date}{strong_close}'
                               u'{line_break}You lose all access to this course, including your progress, on '
                               u'{expiration_date}.')
        upgrade_deadline_message = _(u'{line_break}Upgrade by {upgrade_deadline} to get unlimited access to the course '
                                     u'as long as it exists on the site. {a_open}Upgrade now{sronly_span_open} to '
                                     u'retain access past {expiration_date}{span_close}{a_close}')
        full_message = expiration_message
        if upgrade_deadline and now < upgrade_deadline:
            full_message += upgrade_deadline_message
            using_upgrade_messaging = True
        else:
            using_upgrade_messaging = False

        language = get_language()
        date_string = get_date_string()
        formatted_expiration_date = date_string.format(
            language=language,
            formatted_date=expiration_date.strftime("%Y-%m-%d"),
            formatted_date_localized=strftime_localized(expiration_date, EXPIRATION_DATE_FORMAT_STR)
        )
        if using_upgrade_messaging:
            formatted_upgrade_deadline = date_string.format(
                language=language,
                formatted_date=upgrade_deadline.strftime("%Y-%m-%d"),
                formatted_date_localized=strftime_localized(upgrade_deadline, EXPIRATION_DATE_FORMAT_STR)
            )

            return HTML(full_message).format(
                a_open=HTML(u'<a href="{upgrade_link}">').format(
                    upgrade_link=verified_upgrade_deadline_link(user=user, course=course)
                ),
                sronly_span_open=HTML('<span class="sr-only">'),
                span_close=HTML('</span>'),
                a_close=HTML('</a>'),
                expiration_date=HTML(formatted_expiration_date),
                strong_open=HTML('<strong>'),
                strong_close=HTML('</strong>'),
                line_break=HTML('<br>'),
                upgrade_deadline=HTML(formatted_upgrade_deadline)
            )

        else:
            return HTML(full_message).format(
                span_close=HTML('</span>'),
                expiration_date=HTML(formatted_expiration_date),
                strong_open=HTML('<strong>'),
                strong_close=HTML('</strong>'),
                line_break=HTML('<br>'),
            )
 def date(self):
     if not CourseDurationLimitConfig.enabled_for_enrollment(
             user=self.user, course_key=self.course_id):
         return
     return get_user_course_expiration_date(self.user, self.course)
    def test_course_messaging(self):
        """
        Ensure that the following four use cases work as expected

        1) Anonymous users are shown a course message linking them to the login page
        2) Unenrolled users are shown a course message allowing them to enroll
        3) Enrolled users who show up on the course page after the course has begun
        are not shown a course message.
        4) Enrolled users who show up on the course page after the course has begun will
        see the course expiration banner if course duration limits are on for the course.
        5) Enrolled users who show up on the course page before the course begins
        are shown a message explaining when the course starts as well as a call to
        action button that allows them to add a calendar event.
        """
        # Verify that anonymous users are shown a login link in the course message
        url = course_home_url(self.course)
        response = self.client.get(url)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE_ANONYMOUS)

        # Verify that unenrolled users are shown an enroll call to action message
        user = self.create_user_for_course(self.course,
                                           CourseUserType.UNENROLLED)
        url = course_home_url(self.course)
        response = self.client.get(url)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED)

        # Verify that enrolled users are not shown any state warning message when enrolled and course has begun.
        CourseEnrollment.enroll(user, self.course.id)
        url = course_home_url(self.course)
        response = self.client.get(url)
        self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_ANONYMOUS)
        self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED)
        self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)

        # Verify that enrolled users are shown the course expiration banner if content gating is enabled

        # We use .save() explicitly here (rather than .objects.create) in order to force the
        # cache to refresh.
        config = CourseDurationLimitConfig(course=CourseOverview.get_from_id(
            self.course.id),
                                           enabled=True,
                                           enabled_as_of=datetime(2018,
                                                                  1,
                                                                  1,
                                                                  tzinfo=UTC))
        config.save()

        url = course_home_url(self.course)
        response = self.client.get(url)
        bannerText = get_expiration_banner_text(user, self.course)
        self.assertContains(response, bannerText, html=True)

        # Verify that enrolled users are not shown the course expiration banner if content gating is disabled
        config.enabled = False
        config.save()
        url = course_home_url(self.course)
        response = self.client.get(url)
        bannerText = get_expiration_banner_text(user, self.course)
        self.assertNotContains(response, bannerText, html=True)

        # Verify that enrolled users are shown 'days until start' message before start date
        future_course = self.create_future_course()
        CourseEnrollment.enroll(user, future_course.id)
        url = course_home_url(future_course)
        response = self.client.get(url)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)
Beispiel #24
0
    def get(self, request, course_id, error=None):  # lint-amnesty, pylint: disable=too-many-statements
        """Displays the course mode choice page.

        Args:
            request (`Request`): The Django Request object.
            course_id (unicode): The slash-separated course key.

        Keyword Args:
            error (unicode): If provided, display this error message
                on the page.

        Returns:
            Response

        """
        course_key = CourseKey.from_string(course_id)

        # Check whether the user has access to this course
        # based on country access rules.
        embargo_redirect = embargo_api.redirect_if_blocked(
            course_key,
            user=request.user,
            ip_address=get_client_ip(request)[0],
            url=request.path)
        if embargo_redirect:
            return redirect(embargo_redirect)

        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
            request.user, course_key)

        increment('track-selection.{}.{}'.format(
            enrollment_mode, 'active' if is_active else 'inactive'))
        increment('track-selection.views')

        if enrollment_mode is None:
            LOG.info(
                'Rendering track selection for unenrolled user, referred by %s',
                request.META.get('HTTP_REFERER'))

        modes = CourseMode.modes_for_course_dict(course_key)
        ecommerce_service = EcommerceService()

        # We assume that, if 'professional' is one of the modes, it should be the *only* mode.
        # If there are both modes, default to 'no-id-professional'.
        has_enrolled_professional = (
            CourseMode.is_professional_slug(enrollment_mode) and is_active)
        if CourseMode.has_professional_mode(
                modes) and not has_enrolled_professional:
            purchase_workflow = request.GET.get("purchase_workflow", "single")
            redirect_url = IDVerificationService.get_verify_location(
                course_id=course_key)
            if ecommerce_service.is_enabled(request.user):
                professional_mode = modes.get(
                    CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(
                        CourseMode.PROFESSIONAL)
                if purchase_workflow == "single" and professional_mode.sku:
                    redirect_url = ecommerce_service.get_checkout_page_url(
                        professional_mode.sku)
                if purchase_workflow == "bulk" and professional_mode.bulk_sku:
                    redirect_url = ecommerce_service.get_checkout_page_url(
                        professional_mode.bulk_sku)
            return redirect(redirect_url)

        course = modulestore().get_course(course_key)

        # If there isn't a verified mode available, then there's nothing
        # to do on this page.  Send the user to the dashboard.
        if not CourseMode.has_verified_mode(modes):
            return self._redirect_to_course_or_dashboard(
                course, course_key, request.user)

        # If a user has already paid, redirect them to the dashboard.
        if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES +
                          [CourseMode.NO_ID_PROFESSIONAL_MODE]):
            return self._redirect_to_course_or_dashboard(
                course, course_key, request.user)

        donation_for_course = request.session.get("donation_for_course", {})
        chosen_price = donation_for_course.get(str(course_key), None)

        if CourseEnrollment.is_enrollment_closed(request.user, course):
            locale = to_locale(get_language())
            enrollment_end_date = format_datetime(course.enrollment_end,
                                                  'short',
                                                  locale=locale)
            params = six.moves.urllib.parse.urlencode(
                {'course_closed': enrollment_end_date})
            return redirect('{}?{}'.format(reverse('dashboard'), params))

        # When a credit mode is available, students will be given the option
        # to upgrade from a verified mode to a credit mode at the end of the course.
        # This allows students who have completed photo verification to be eligible
        # for university credit.
        # Since credit isn't one of the selectable options on the track selection page,
        # we need to check *all* available course modes in order to determine whether
        # a credit mode is available.  If so, then we show slightly different messaging
        # for the verified track.
        has_credit_upsell = any(
            CourseMode.is_credit_mode(mode)
            for mode in CourseMode.modes_for_course(course_key,
                                                    only_selectable=False))
        course_id = str(course_key)
        gated_content = ContentTypeGatingConfig.enabled_for_enrollment(
            user=request.user, course_key=course_key)
        context = {
            "course_modes_choose_url":
            reverse("course_modes_choose", kwargs={'course_id': course_id}),
            "modes":
            modes,
            "has_credit_upsell":
            has_credit_upsell,
            "course_name":
            course.display_name_with_default,
            "course_org":
            course.display_org_with_default,
            "course_num":
            course.display_number_with_default,
            "chosen_price":
            chosen_price,
            "error":
            error,
            "responsive":
            True,
            "nav_hidden":
            True,
            "content_gating_enabled":
            gated_content,
            "course_duration_limit_enabled":
            CourseDurationLimitConfig.enabled_for_enrollment(
                request.user, course),
        }
        context.update(
            get_experiment_user_metadata_context(
                course,
                request.user,
            ))

        title_content = ''
        if enrollment_mode:
            title_content = _(
                "Congratulations!  You are now enrolled in {course_name}"
            ).format(course_name=course.display_name_with_default)

        context["title_content"] = title_content

        if "verified" in modes:
            verified_mode = modes["verified"]
            context["suggested_prices"] = [
                decimal.Decimal(x.strip())
                for x in verified_mode.suggested_prices.split(",")
                if x.strip()
            ]
            price_before_discount = verified_mode.min_price
            course_price = price_before_discount
            enterprise_customer = enterprise_customer_for_request(request)
            LOG.info(
                '[e-commerce calculate API] Going to hit the API for user [%s] linked to [%s] enterprise',
                request.user.username,
                enterprise_customer.get('name') if isinstance(
                    enterprise_customer, dict) else None  # Test Purpose
            )
            if enterprise_customer and verified_mode.sku:
                course_price = get_course_final_price(request.user,
                                                      verified_mode.sku,
                                                      price_before_discount)

            context["currency"] = verified_mode.currency.upper()
            context["currency_symbol"] = get_currency_symbol(
                verified_mode.currency.upper())
            context["min_price"] = course_price
            context["verified_name"] = verified_mode.name
            context["verified_description"] = verified_mode.description
            # if course_price is equal to price_before_discount then user doesn't entitle to any discount.
            if course_price != price_before_discount:
                context["price_before_discount"] = price_before_discount

            if verified_mode.sku:
                context[
                    "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(
                        request.user)
                context[
                    "ecommerce_payment_page"] = ecommerce_service.payment_page_url(
                    )
                context["sku"] = verified_mode.sku
                context["bulk_sku"] = verified_mode.bulk_sku

        # REV-2415 TODO: remove [Track Selection Check] logs introduced by REV-2355 for error handling check
        context['currency_data'] = []
        if waffle.switch_is_active('local_currency'):
            if 'edx-price-l10n' not in request.COOKIES:
                currency_data = get_currency_data()
                LOG.info(
                    '[Track Selection Check] Currency data: [%s], for course [%s]',
                    currency_data, course_id)
                try:
                    context['currency_data'] = json.dumps(currency_data)
                except TypeError:
                    pass

        language = get_language()
        context['track_links'] = get_verified_track_links(language)

        duration = get_user_course_duration(request.user, course)
        deadline = duration and get_user_course_expiration_date(
            request.user, course)
        if deadline:
            formatted_audit_access_date = strftime_localized_html(
                deadline, 'SHORT_DATE')
            context['audit_access_deadline'] = formatted_audit_access_date
        fbe_is_on = deadline and gated_content

        # Route to correct Track Selection page.
        # REV-2133 TODO Value Prop: remove waffle flag after testing is completed
        # and happy path version is ready to be rolled out to all users.
        if VALUE_PROP_TRACK_SELECTION_FLAG.is_enabled():
            if not error:  # TODO: Remove by executing REV-2355
                if not enterprise_customer_for_request(
                        request):  # TODO: Remove by executing REV-2342
                    if fbe_is_on:
                        return render_to_response("course_modes/fbe.html",
                                                  context)
                    else:
                        return render_to_response("course_modes/unfbe.html",
                                                  context)

        # If error or enterprise_customer, failover to old choose.html page
        return render_to_response("course_modes/choose.html", context)
Beispiel #25
0
    def test_config_overrides(self, global_setting, site_setting, org_setting,
                              course_setting, reverse_order):
        """
        Test that the stacked configuration overrides happen in the correct order and priority.

        This is tested by exhaustively setting each combination of contexts, and validating that only
        the lowest level context that is set to not-None is applied.
        """
        # Add a bunch of configuration outside the contexts that are being tested, to make sure
        # there are no leaks of configuration across contexts
        non_test_course_enabled = CourseOverviewFactory.create(
            org='non-test-org-enabled')
        non_test_course_disabled = CourseOverviewFactory.create(
            org='non-test-org-disabled')
        non_test_site_cfg_enabled = SiteConfigurationFactory.create(
            values={'course_org_filter': non_test_course_enabled.org})
        non_test_site_cfg_disabled = SiteConfigurationFactory.create(
            values={'course_org_filter': non_test_course_disabled.org})

        CourseDurationLimitConfig.objects.create(
            course=non_test_course_enabled, enabled=True)
        CourseDurationLimitConfig.objects.create(
            course=non_test_course_disabled, enabled=False)
        CourseDurationLimitConfig.objects.create(
            org=non_test_course_enabled.org, enabled=True)
        CourseDurationLimitConfig.objects.create(
            org=non_test_course_disabled.org, enabled=False)
        CourseDurationLimitConfig.objects.create(
            site=non_test_site_cfg_enabled.site, enabled=True)
        CourseDurationLimitConfig.objects.create(
            site=non_test_site_cfg_disabled.site, enabled=False)

        # Set up test objects
        test_course = CourseOverviewFactory.create(org='test-org')
        test_site_cfg = SiteConfigurationFactory.create(
            values={'course_org_filter': test_course.org})

        if reverse_order:
            CourseDurationLimitConfig.objects.create(site=test_site_cfg.site,
                                                     enabled=site_setting)
            CourseDurationLimitConfig.objects.create(org=test_course.org,
                                                     enabled=org_setting)
            CourseDurationLimitConfig.objects.create(course=test_course,
                                                     enabled=course_setting)
            CourseDurationLimitConfig.objects.create(enabled=global_setting)
        else:
            CourseDurationLimitConfig.objects.create(enabled=global_setting)
            CourseDurationLimitConfig.objects.create(course=test_course,
                                                     enabled=course_setting)
            CourseDurationLimitConfig.objects.create(org=test_course.org,
                                                     enabled=org_setting)
            CourseDurationLimitConfig.objects.create(site=test_site_cfg.site,
                                                     enabled=site_setting)

        expected_global_setting = self._resolve_settings([global_setting])
        expected_site_setting = self._resolve_settings(
            [global_setting, site_setting])
        expected_org_setting = self._resolve_settings(
            [global_setting, site_setting, org_setting])
        expected_course_setting = self._resolve_settings(
            [global_setting, site_setting, org_setting, course_setting])

        self.assertEqual(expected_global_setting,
                         CourseDurationLimitConfig.current().enabled)
        self.assertEqual(
            expected_site_setting,
            CourseDurationLimitConfig.current(site=test_site_cfg.site).enabled)
        self.assertEqual(
            expected_org_setting,
            CourseDurationLimitConfig.current(org=test_course.org).enabled)
        self.assertEqual(
            expected_course_setting,
            CourseDurationLimitConfig.current(
                course_key=test_course.id).enabled)
Beispiel #26
0
    def test_caching_course(self):
        course = CourseOverviewFactory.create(org='test-org')
        site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': course.org})
        course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1))
        course_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the org value is not retrieved from cache after save
        with self.assertNumQueries(2):
            self.assertTrue(CourseDurationLimitConfig.current(course_key=course.id).enabled)

        RequestCache.clear_all_namespaces()

        # Check that the org value can be retrieved from cache after read
        with self.assertNumQueries(0):
            self.assertTrue(CourseDurationLimitConfig.current(course_key=course.id).enabled)

        course_config.enabled = False
        course_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the org value in cache was deleted on save
        with self.assertNumQueries(2):
            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)

        global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
        global_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the org value is not updated in cache by changing the global value
        with self.assertNumQueries(0):
            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)

        site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
        site_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the org value is not updated in cache by changing the site value
        with self.assertNumQueries(0):
            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)

        org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
        org_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the org value is not updated in cache by changing the site value
        with self.assertNumQueries(0):
            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)
Beispiel #27
0
    def test_course_messaging(self):
        """
        Ensure that the following four use cases work as expected

        1) Anonymous users are shown a course message linking them to the login page
        2) Unenrolled users are shown a course message allowing them to enroll
        3) Enrolled users who show up on the course page after the course has begun
        are not shown a course message.
        4) Enrolled users who show up on the course page after the course has begun will
        see the course expiration banner if course duration limits are on for the course.
        5) Enrolled users who show up on the course page before the course begins
        are shown a message explaining when the course starts as well as a call to
        action button that allows them to add a calendar event.
        """
        # Verify that anonymous users are shown a login link in the course message
        url = course_home_url(self.course)
        response = self.client.get(url)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE_ANONYMOUS)

        # Verify that unenrolled users are shown an enroll call to action message
        user = self.create_user_for_course(self.course, CourseUserType.UNENROLLED)
        url = course_home_url(self.course)
        response = self.client.get(url)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED)

        # Verify that enrolled users are not shown any state warning message when enrolled and course has begun.
        CourseEnrollment.enroll(user, self.course.id)
        url = course_home_url(self.course)
        response = self.client.get(url)
        self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_ANONYMOUS)
        self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED)
        self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)

        # Verify that enrolled users are shown the course expiration banner if content gating is enabled

        # We use .save() explicitly here (rather than .objects.create) in order to force the
        # cache to refresh.
        config = CourseDurationLimitConfig(
            course=CourseOverview.get_from_id(self.course.id),
            enabled=True,
            enabled_as_of=datetime(2018, 1, 1)
        )
        config.save()

        url = course_home_url(self.course)
        response = self.client.get(url)
        bannerText = get_expiration_banner_text(user, self.course)
        self.assertContains(response, bannerText, html=True)
        self.assertContains(response, TEST_BANNER_CLASS)

        # Verify that enrolled users are not shown the course expiration banner if content gating is disabled
        config.enabled = False
        config.save()
        url = course_home_url(self.course)
        response = self.client.get(url)
        bannerText = get_expiration_banner_text(user, self.course)
        self.assertNotContains(response, bannerText, html=True)

        # Verify that enrolled users are shown 'days until start' message before start date
        future_course = self.create_future_course()
        CourseEnrollment.enroll(user, future_course.id)
        url = course_home_url(future_course)
        response = self.client.get(url)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
        self.assertContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)
Beispiel #28
0
    def test_caching_site(self):
        site_cfg = SiteConfigurationFactory()
        site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
        site_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the site value is not retrieved from cache after save
        with self.assertNumQueries(1):
            self.assertTrue(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)

        RequestCache.clear_all_namespaces()

        # Check that the site value can be retrieved from cache after read
        with self.assertNumQueries(0):
            self.assertTrue(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)

        site_config.enabled = False
        site_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the site value in cache was deleted on save
        with self.assertNumQueries(1):
            self.assertFalse(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)

        global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
        global_config.save()

        RequestCache.clear_all_namespaces()

        # Check that the site value is not updated in cache by changing the global value
        with self.assertNumQueries(0):
            self.assertFalse(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)
Beispiel #29
0
    def get(self, request, course_id, error=None):
        """Displays the course mode choice page.

        Args:
            request (`Request`): The Django Request object.
            course_id (unicode): The slash-separated course key.

        Keyword Args:
            error (unicode): If provided, display this error message
                on the page.

        Returns:
            Response

        """
        course_key = CourseKey.from_string(course_id)

        # Check whether the user has access to this course
        # based on country access rules.
        embargo_redirect = embargo_api.redirect_if_blocked(
            course_key,
            user=request.user,
            ip_address=get_ip(request),
            url=request.path
        )
        if embargo_redirect:
            return redirect(embargo_redirect)

        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key)
        modes = CourseMode.modes_for_course_dict(course_key)
        ecommerce_service = EcommerceService()

        # We assume that, if 'professional' is one of the modes, it should be the *only* mode.
        # If there are both modes, default to non-id-professional.
        has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active)
        if CourseMode.has_professional_mode(modes) and not has_enrolled_professional:
            purchase_workflow = request.GET.get("purchase_workflow", "single")
            verify_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)})
            redirect_url = "{url}?purchase_workflow={workflow}".format(url=verify_url, workflow=purchase_workflow)
            if ecommerce_service.is_enabled(request.user):
                professional_mode = modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(CourseMode.PROFESSIONAL)
                if purchase_workflow == "single" and professional_mode.sku:
                    redirect_url = ecommerce_service.get_checkout_page_url(professional_mode.sku)
                if purchase_workflow == "bulk" and professional_mode.bulk_sku:
                    redirect_url = ecommerce_service.get_checkout_page_url(professional_mode.bulk_sku)
            return redirect(redirect_url)

        course = modulestore().get_course(course_key)

        # If there isn't a verified mode available, then there's nothing
        # to do on this page.  Send the user to the dashboard.
        if not CourseMode.has_verified_mode(modes):
            return redirect(reverse('dashboard'))

        # If a user has already paid, redirect them to the dashboard.
        if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]):
            # If the course has started redirect to course home instead
            if course.has_started():
                return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key}))
            return redirect(reverse('dashboard'))

        donation_for_course = request.session.get("donation_for_course", {})
        chosen_price = donation_for_course.get(unicode(course_key), None)

        if CourseEnrollment.is_enrollment_closed(request.user, course):
            locale = to_locale(get_language())
            enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale)
            params = urllib.urlencode({'course_closed': enrollment_end_date})
            return redirect('{0}?{1}'.format(reverse('dashboard'), params))

        # When a credit mode is available, students will be given the option
        # to upgrade from a verified mode to a credit mode at the end of the course.
        # This allows students who have completed photo verification to be eligible
        # for univerity credit.
        # Since credit isn't one of the selectable options on the track selection page,
        # we need to check *all* available course modes in order to determine whether
        # a credit mode is available.  If so, then we show slightly different messaging
        # for the verified track.
        has_credit_upsell = any(
            CourseMode.is_credit_mode(mode) for mode
            in CourseMode.modes_for_course(course_key, only_selectable=False)
        )
        course_id = text_type(course_key)

        context = {
            "course_modes_choose_url": reverse(
                "course_modes_choose",
                kwargs={'course_id': course_id}
            ),
            "modes": modes,
            "has_credit_upsell": has_credit_upsell,
            "course_name": course.display_name_with_default,
            "course_org": course.display_org_with_default,
            "course_num": course.display_number_with_default,
            "chosen_price": chosen_price,
            "error": error,
            "responsive": True,
            "nav_hidden": True,
            "content_gating_enabled": ContentTypeGatingConfig.enabled_for_enrollment(
                user=request.user,
                course_key=course_key
            ),
            "course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment(
                user=request.user,
                course_key=course_key
            ),
        }
        context.update(
            get_experiment_user_metadata_context(
                course,
                request.user,
            )
        )

        title_content = _("Congratulations!  You are now enrolled in {course_name}").format(
            course_name=course.display_name_with_default
        )

        context["title_content"] = title_content

        if "verified" in modes:
            verified_mode = modes["verified"]
            context["suggested_prices"] = [
                decimal.Decimal(x.strip())
                for x in verified_mode.suggested_prices.split(",")
                if x.strip()
            ]
            context["currency"] = verified_mode.currency.upper()
            context["min_price"] = verified_mode.min_price
            context["verified_name"] = verified_mode.name
            context["verified_description"] = verified_mode.description

            if verified_mode.sku:
                context["use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(request.user)
                context["ecommerce_payment_page"] = ecommerce_service.payment_page_url()
                context["sku"] = verified_mode.sku
                context["bulk_sku"] = verified_mode.bulk_sku

        context['currency_data'] = []
        if waffle.switch_is_active('local_currency'):
            if 'edx-price-l10n' not in request.COOKIES:
                currency_data = get_currency_data()
                try:
                    context['currency_data'] = json.dumps(currency_data)
                except TypeError:
                    pass
        return render_to_response("course_modes/choose.html", context)
Beispiel #30
0
def generate_course_expired_message(user, course):
    """
    Generate the message for the user course expiration date if it exists.
    """
    if not CourseDurationLimitConfig.enabled_for_enrollment(
            user=user, course_key=course.id):
        return

    expiration_date = get_user_course_expiration_date(user, course)
    if not expiration_date:
        return

    if is_masquerading_as_specific_student(
            user, course.id) and timezone.now() > expiration_date:
        upgrade_message = _(
            'This learner does not have access to this course. '
            u'Their access expired on {expiration_date}.')
        return HTML(upgrade_message).format(expiration_date=strftime_localized(
            expiration_date, EXPIRATION_DATE_FORMAT_STR))
    else:
        enrollment = CourseEnrollment.get_enrollment(user, course.id)
        if enrollment is None:
            return

        upgrade_deadline = enrollment.upgrade_deadline
        now = timezone.now()
        course_upgrade_deadline = enrollment.course_upgrade_deadline
        if (not upgrade_deadline) or (upgrade_deadline < now):
            upgrade_deadline = course_upgrade_deadline

        expiration_message = _(
            u'{strong_open}Audit Access Expires {expiration_date}{strong_close}'
            u'{line_break}You lose all access to this course, including your progress, on '
            u'{expiration_date}.')
        upgrade_deadline_message = _(
            u'{line_break}Upgrade by {upgrade_deadline} to get unlimited access to the course '
            u'as long as it exists on the site. {a_open}Upgrade now{sronly_span_open} to '
            u'retain access past {expiration_date}{span_close}{a_close}')
        full_message = expiration_message
        if upgrade_deadline and now < upgrade_deadline:
            full_message += upgrade_deadline_message
            using_upgrade_messaging = True
        else:
            using_upgrade_messaging = False

        language = get_language()
        date_string = get_date_string()
        formatted_expiration_date = date_string.format(
            language=language,
            formatted_date=expiration_date.strftime("%Y-%m-%d"),
            formatted_date_localized=strftime_localized(
                expiration_date, EXPIRATION_DATE_FORMAT_STR))
        if using_upgrade_messaging:
            formatted_upgrade_deadline = date_string.format(
                language=language,
                formatted_date=upgrade_deadline.strftime("%Y-%m-%d"),
                formatted_date_localized=strftime_localized(
                    upgrade_deadline, EXPIRATION_DATE_FORMAT_STR))

            return HTML(full_message).format(
                a_open=HTML(u'<a href="{upgrade_link}">').format(
                    upgrade_link=verified_upgrade_deadline_link(
                        user=user, course=course)),
                sronly_span_open=HTML('<span class="sr-only">'),
                span_close=HTML('</span>'),
                a_close=HTML('</a>'),
                expiration_date=HTML(formatted_expiration_date),
                strong_open=HTML('<strong>'),
                strong_close=HTML('</strong>'),
                line_break=HTML('<br>'),
                upgrade_deadline=HTML(formatted_upgrade_deadline))

        else:
            return HTML(full_message).format(
                span_close=HTML('</span>'),
                expiration_date=HTML(formatted_expiration_date),
                strong_open=HTML('<strong>'),
                strong_close=HTML('</strong>'),
                line_break=HTML('<br>'),
            )