diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 0721322508..43d57f3e97 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -4,7 +4,6 @@ Tests for course_modes views. import decimal -import mock import unittest from datetime import datetime, timedelta from unittest.mock import patch @@ -35,6 +34,12 @@ from xmodule.modulestore.tests.factories import CourseFactory from ..views import VALUE_PROP_TRACK_SELECTION_FLAG +# Name of the method to mock for Content Type Gating. +GATING_METHOD_NAME = 'openedx.features.content_type_gating.models.ContentTypeGatingConfig.enabled_for_enrollment' + +# Name of the method to mock for Course Duration Limits. +CDL_METHOD_NAME = 'openedx.features.course_duration_limits.models.CourseDurationLimitConfig.enabled_for_enrollment' + @ddt.ddt @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @@ -511,64 +516,118 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest redirect_url = reverse('dashboard') + '?course_closed=1%2F1%2F15%2C+12%3A00+AM' self.assertRedirects(response, redirect_url) - # Value Prop TODO (REV-2378): remove waffle flag from tests once the new Track Selection template is rolled out. - # Other tests may need to be updated/removed to reflect the new page. - # The below test can be separated into multiple tests once un-happy path is implemented. - def test_new_track_selection(self): - # For the new track selection template to render, FBE must be fully on (gated_content and audit_access_deadline) - # and happy path conditions must be met: - # User can upgrade, FBE is fully on, and user is not an enterprise user. + def _assert_fbe_page(self, response, min_price=None, **_): + """ + Assert fbe.html was rendered. + """ + self.assertContains(response, "Choose a path for your course in") - # Create the course modes and enroll the user + # Check if it displays the upgrade price for verified track and "Free" for audit track + self.assertContains(response, min_price) + self.assertContains(response, "Free") + + # Check for specific HTML elements + self.assertContains(response, '') + self.assertContains(response, '') + self.assertContains(response, '') + + # Check for upgrade button ID + self.assertContains(response, 'track_selection_upgrade') + + # Check for audit button ID + self.assertContains(response, 'track_selection_audit') + + # Check for happy path messaging - verified + self.assertContains(response, '
  • ') + self.assertContains(response, 'access to all course activities') + self.assertContains(response, 'Full access') + + # Check for happy path messaging - audit + self.assertContains(response, "discussion forums and non-graded assignments") + self.assertContains(response, "Get temporary access") + self.assertContains(response, "Access expires and all progress will be lost") + + def _assert_unfbe_page(self, response, min_price=None, **_): + """ + Assert track_selection.html and unfbe.html were rendered. + """ + # Check for string unique to track_selection.html. + self.assertContains(response, "| Upgrade Now") + # This string only occurs in lms/templates/course_modes/track_selection.html + # and related theme and translation files. + + # Check for string unique to unfbe.html. + self.assertContains(response, "Some graded content may be lost") + # This string only occurs in lms/templates/course_modes/unfbe.html + # and related theme and translation files. + + # Check min_price was correctly passed in. + self.assertContains(response, min_price) + + def _assert_legacy_page(self, response, **_): + """ + Assert choose.html was rendered. + """ + # Check for string unique to the legacy choose.html. + self.assertContains(response, "Choose Your Track") + # This string only occurs in lms/templates/course_modes/choose.html + # and related theme and translation files. + + @ddt.data( + # gated_content_on, course_duration_limits_on, waffle_flag_on, expected_page_assertion_function + (True, True, True, _assert_fbe_page), + (True, False, True, _assert_unfbe_page), + (False, True, True, _assert_unfbe_page), + (False, False, True, _assert_unfbe_page), + (True, True, False, _assert_legacy_page), + (True, False, False, _assert_legacy_page), + (False, True, False, _assert_legacy_page), + (False, False, False, _assert_legacy_page), + ) + @ddt.unpack + def test_track_selection_types( + self, + gated_content_on, + course_duration_limits_on, + waffle_flag_on, + expected_page_assertion_function + ): + """ + Feature-based enrollment (FBE) is when gated content and course duration + limits are enabled when a user is auditing a course. + + When prompted to perform track selection (choosing between the audit and + verified course modes), the learner may view 3 different pages: + 1. fbe.html - full FBE + 2. unfbe.html - partial or no FBE + 3. choose.html - legacy track selection page + + This test checks that the right template is rendered. + + """ + # The active course mode already exists. Create verified course mode: verified_mode = CourseModeFactory.create( mode_slug='verified', course_id=self.course_that_started.id, min_price=149, ) + + # Enroll the test user in the audit mode: CourseEnrollmentFactory( is_active=True, course_id=self.course_that_started.id, user=self.user ) + # Value Prop TODO (REV-2378): remove waffle flag from tests once the new Track Selection template is rolled out. # Check whether new track selection template is rendered. # This should *only* be shown when the waffle flag is on. - with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=True): - with mock.patch( - 'openedx.features.content_type_gating.models.ContentTypeGatingConfig.enabled_for_enrollment', - return_value=True - ): - with mock.patch( - 'openedx.features.course_duration_limits.models.CourseDurationLimitConfig.enabled_for_enrollment', - return_value=True - ): + with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=waffle_flag_on): + with patch(GATING_METHOD_NAME, return_value=gated_content_on): + with patch(CDL_METHOD_NAME, return_value=course_duration_limits_on): url = reverse('course_modes_choose', args=[str(self.course_that_started.id)]) response = self.client.get(url) - - self.assertContains(response, "Choose a path for your course in") - - # Check if it displays the upgrade price for verified track and "Free" for audit track - self.assertContains(response, verified_mode.min_price) - self.assertContains(response, "Free") - - # Check for specific HTML elements - self.assertContains(response, '') - self.assertContains(response, '') - self.assertContains(response, '') - - # Check for upgrade button ID - self.assertContains(response, 'track_selection_upgrade') - # Check for audit button ID - self.assertContains(response, 'track_selection_audit') - - # Check for happy path messaging - verified - self.assertContains(response, '
  • ') - self.assertContains(response, 'access to all course activities') - self.assertContains(response, 'Full access') - # Check for happy path messaging - audit - self.assertContains(response, "discussion forums and non-graded assignments") - self.assertContains(response, "Get temporary access") - self.assertContains(response, "Access expires and all progress will be lost") + expected_page_assertion_function(self, response, min_price=verified_mode.min_price) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 9195eb1679..bac49fe9fc 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -139,14 +139,11 @@ class ChooseModeView(View): # 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')) + 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]): - # 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')) + 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) @@ -258,15 +255,18 @@ class ChooseModeView(View): 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(): - # First iteration of happy path does not handle errors. If there are enrollment errors for a learner that is - # technically considered happy path, old Track Selection page will be displayed. - if not error: - # Happy path conditions. - if verified_mode and fbe_is_on and not enterprise_customer: - return render_to_response("course_modes/track_selection.html", context) + if not error: # TODO: Remove by executing REV-2355 + if not enterprise_customer: # 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) @method_decorator(transaction.non_atomic_requests) @@ -309,17 +309,11 @@ class ChooseModeView(View): # system, such as third-party discovery. These workflows result in learners arriving # directly at this screen, and they will not necessarily be pre-enrolled in the audit mode. CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT) - # 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')) + return self._redirect_to_course_or_dashboard(course, course_key, user) if requested_mode == 'honor': CourseEnrollment.enroll(user, course_key, mode=requested_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')) + return self._redirect_to_course_or_dashboard(course, course_key, user) mode_info = allowed_modes[requested_mode] @@ -366,6 +360,24 @@ class ChooseModeView(View): else: return None + def _redirect_to_course_or_dashboard(self, course, course_key, user): + """Perform a redirect to the course if the user is able to access the course. + + If the user is not able to access the course, redirect the user to the dashboard. + + Args: + course: modulestore object for course + course_key: course_id converted to a course_key + user: request.user, the current user for the request + + Returns: + 302 to the course if possible or the dashboard if not. + """ + if course.has_started() or user.is_staff: + return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key})) + else: + return redirect(reverse('dashboard')) + def create_mode(request, course_id): """Add a mode to the course corresponding to the given course ID. diff --git a/lms/templates/course_modes/fbe.html b/lms/templates/course_modes/fbe.html new file mode 100644 index 0000000000..08a62736df --- /dev/null +++ b/lms/templates/course_modes/fbe.html @@ -0,0 +1,68 @@ +## This template is for track selection with Feature Based Enrollment (FBE). +## This means a learner in the default audit track will have a limited time to +## complete the course (course access duration) and may have some of the content +## of the course unavailable (gated content). + +<%page expression_filter="h"/> +<%inherit file="track_selection.html" /> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> +<%block name="track_selection_certificate_bullets"> +
    +
      +
    • ${Text(_("Showcase a {link_start}verified certificate{link_end} of completion on your resumé to advance your career")).format( + link_start=HTML('').format(track_verified_url=track_links['verified_certificate']), + link_end=HTML('') + )} +
    • +
    • ${Text(_("Get {start_bold}access to all course activities{end_bold}, including both graded and non-graded assignments, while the course is running")).format( + start_bold=HTML(''), + end_bold=HTML(''), + )}
    • + +
    • + ${Text(_("{start_bold}Full access{end_bold} to course content and materials, even after the course ends")).format( + start_bold=HTML(''), + end_bold=HTML(''), + )} + + + + + + ${Text(_("{link_start}Learn more{link_end} about course access")).format( + link_start=HTML('').format(track_comparison_url=track_links['learn_more']), + link_end=HTML('') + )} + + +
    • +
    • ${Text(_("Support our {start_bold}mission{end_bold} to increase access to high-quality education for everyone, everywhere")).format( + start_bold=HTML(''), + end_bold=HTML(''), + )}
    • +
    +
    + + +<%block name="track_selection_audit_bullets"> +
    +
      +
    • ${Text(_("Get temporary access to {start_bold}non-graded{end_bold} activities, including discussion forums and non-graded assignments")).format( + start_bold=HTML(''), + end_bold=HTML(''), + )}
    • +
    • ${Text(_("Get {start_bold}temporary access{end_bold} to the course material, including videos and readings")).format( + start_bold=HTML(''), + end_bold=HTML(''), + )}
    • + % if audit_access_deadline: +
    • ${_("Access expires and all progress will be lost on")} ${audit_access_deadline}
    • + % else: +
    • ${_("Access expires and all progress will be lost")}
    • + % endif +
    +
    + diff --git a/lms/templates/course_modes/track_selection.html b/lms/templates/course_modes/track_selection.html index f710fc167e..5e88555338 100644 --- a/lms/templates/course_modes/track_selection.html +++ b/lms/templates/course_modes/track_selection.html @@ -95,43 +95,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string

    ${currency_symbol}${min_price} ${currency}

    ${_("Earn a certificate")}

    -
    -
      -
    • - ${Text(_("Showcase a {link_start}verified certificate{link_end} of completion on your resumé to advance your career")).format( - link_start=HTML('').format(track_verified_url=track_links['verified_certificate']), - link_end=HTML('') - )} -
    • -
    • ${Text(_("Get {start_bold}access to all course activities{end_bold}, including both graded and non-graded assignments, while the course is running")).format( - start_bold=HTML(''), - end_bold=HTML(''), - )}
    • - -
    • - ${Text(_("{start_bold}Full access{end_bold} to course content and materials, even after the course ends")).format( - start_bold=HTML(''), - end_bold=HTML(''), - )} - - - - - - ${Text(_("{link_start}Learn more{link_end} about course access")).format( - link_start=HTML('').format(track_comparison_url=track_links['learn_more']), - link_end=HTML('') - )} - - -
    • -
    • ${Text(_("Support our {start_bold}mission{end_bold} to increase access to high-quality education for everyone, everywhere")).format( - start_bold=HTML(''), - end_bold=HTML(''), - )}
    • -
    -
    - + <%block name="track_selection_certificate_bullets"/>
    • @@ -167,23 +131,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string

      ${_("Free")}

      ${_("Access this course")}

      -
      -
        -
      • ${Text(_("Get temporary access to {start_bold}non-graded{end_bold} activities, including discussion forums and non-graded assignments")).format( - start_bold=HTML(''), - end_bold=HTML(''), - )}
      • -
      • ${Text(_("Get {start_bold}temporary access{end_bold} to the course material, including videos and readings")).format( - start_bold=HTML(''), - end_bold=HTML(''), - )}
      • - % if audit_access_deadline: -
      • ${_("Access expires and all progress will be lost on")} ${audit_access_deadline}
      • - % else: -
      • ${_("Access expires and all progress will be lost")}
      • - % endif -
      -
      + <%block name="track_selection_audit_bullets"/>