From 3b160eefcbf71d74e5894d9ff623cd0456b35a18 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Tue, 14 Apr 2015 08:37:18 -0400 Subject: [PATCH] Credit message on track selection page. * Adds a credit course mode to indicate that a course has a credit option. * Hides the credit option from the track selection and pay-and-verify pages. * Shows different messaging for the verified track if it's possible to upgrade from verified to credit at the end of the course. --- common/djangoapps/course_modes/admin.py | 3 +- common/djangoapps/course_modes/models.py | 53 ++++++++++++++++--- .../course_modes/tests/test_models.py | 22 ++++++++ .../course_modes/tests/test_views.py | 20 +++++++ common/djangoapps/course_modes/views.py | 15 ++++++ common/templates/course_modes/choose.html | 38 ++++++++++--- 6 files changed, 138 insertions(+), 13 deletions(-) diff --git a/common/djangoapps/course_modes/admin.py b/common/djangoapps/course_modes/admin.py index 7645ccb3a1..3b4cebdb9c 100644 --- a/common/djangoapps/course_modes/admin.py +++ b/common/djangoapps/course_modes/admin.py @@ -22,7 +22,8 @@ class CourseModeForm(forms.ModelForm): COURSE_MODE_SLUG_CHOICES = ( [(CourseMode.DEFAULT_MODE_SLUG, CourseMode.DEFAULT_MODE_SLUG)] + [(mode_slug, mode_slug) for mode_slug in CourseMode.VERIFIED_MODES] + - [(CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.NO_ID_PROFESSIONAL_MODE)] + [(CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.NO_ID_PROFESSIONAL_MODE)] + + [(mode_slug, mode_slug) for mode_slug in CourseMode.CREDIT_MODES] ) mode_slug = forms.ChoiceField(choices=COURSE_MODE_SLUG_CHOICES) diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 70d6963b67..2a80fe7f57 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -71,6 +71,7 @@ class CourseMode(models.Model): VERIFIED = "verified" AUDIT = "audit" NO_ID_PROFESSIONAL_MODE = "no-id-professional" + CREDIT_MODE = "credit" DEFAULT_MODE = Mode(HONOR, _('Honor Code Certificate'), 0, '', 'usd', None, None, None) DEFAULT_MODE_SLUG = HONOR @@ -78,6 +79,9 @@ class CourseMode(models.Model): # Modes that allow a student to pursue a verified certificate VERIFIED_MODES = [VERIFIED, PROFESSIONAL] + # Modes that allow a student to earn credit with a university partner + CREDIT_MODES = [CREDIT_MODE] + class Meta: """ meta attributes of this model """ unique_together = ('course_id', 'mode_slug', 'currency') @@ -162,23 +166,45 @@ class CourseMode(models.Model): return [mode.to_tuple() for mode in found_course_modes] @classmethod - def modes_for_course(cls, course_id): + def modes_for_course(cls, course_id, only_selectable=True): """ Returns a list of the non-expired modes for a given course id If no modes have been set in the table, returns the default mode + + Arguments: + course_id (CourseKey): Search for course modes for this course. + + Keyword Arguments: + only_selectable (bool): If True, include only modes that are shown + to users on the track selection page. (Currently, "credit" modes + aren't available to users until they complete the course, so + they are hidden in track selection.) + + Returns: + list of `Mode` tuples + """ now = datetime.now(pytz.UTC) - found_course_modes = cls.objects.filter(Q(course_id=course_id) & - (Q(expiration_datetime__isnull=True) | - Q(expiration_datetime__gte=now))) + found_course_modes = cls.objects.filter( + Q(course_id=course_id) & (Q(expiration_datetime__isnull=True) | Q(expiration_datetime__gte=now)) + ) + + # Credit course modes are currently not shown on the track selection page; + # they're available only when students complete a course. For this reason, + # we exclude them from the list if we're only looking for selectable modes + # (e.g. on the track selection page or in the payment/verification flows). + if only_selectable: + found_course_modes = found_course_modes.exclude(mode_slug__in=cls.CREDIT_MODES) + modes = ([mode.to_tuple() for mode in found_course_modes]) if not modes: modes = [cls.DEFAULT_MODE] + return modes @classmethod - def modes_for_course_dict(cls, course_id, modes=None): + def modes_for_course_dict(cls, course_id, modes=None, only_selectable=True): """Returns the non-expired modes for a particular course. Arguments: @@ -189,12 +215,18 @@ class CourseMode(models.Model): of course modes. This can be used to avoid an additional database query if you have already loaded the modes list. + only_selectable (bool): If True, include only modes that are shown + to users on the track selection page. (Currently, "credit" modes + aren't available to users until they complete the course, so + they are hidden in track selection.) + Returns: dict: Keys are mode slugs, values are lists of `Mode` namedtuples. """ if modes is None: - modes = cls.modes_for_course(course_id) + modes = cls.modes_for_course(course_id, only_selectable=only_selectable) + return {mode.slug: mode for mode in modes} @classmethod @@ -349,6 +381,15 @@ class CourseMode(models.Model): """ return mode_slug in cls.VERIFIED_MODES + @classmethod + def is_credit_mode(cls, course_mode_tuple): + """Check whether this is a credit mode. + + Students enrolled in a credit mode are eligible to + receive university credit upon completion of a course. + """ + return course_mode_tuple.slug in cls.CREDIT_MODES + @classmethod def has_payment_options(cls, course_id): """Determines if there is any mode that has payment options diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 17f7758e77..1587928993 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -298,6 +298,28 @@ class CourseModeModelTest(TestCase): self._enrollment_display_modes_dicts(mode) ) + @ddt.data( + (['honor', 'verified', 'credit'], ['honor', 'verified']), + (['professional', 'credit'], ['professional']), + ) + @ddt.unpack + def test_hide_credit_modes(self, available_modes, expected_selectable_modes): + # Create the course modes + for mode in available_modes: + CourseMode.objects.create( + course_id=self.course_key, + mode_display_name=mode, + mode_slug=mode, + ) + + # Check the selectable modes, which should exclude credit + selectable_modes = CourseMode.modes_for_course_dict(self.course_key) + self.assertItemsEqual(selectable_modes.keys(), expected_selectable_modes) + + # When we get all unexpired modes, we should see credit as well + all_modes = CourseMode.modes_for_course_dict(self.course_key, only_selectable=False) + self.assertItemsEqual(all_modes.keys(), available_modes) + def _enrollment_display_modes_dicts(self, dict_type): """ Helper function to generate the enrollment display mode dict. diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 36910917dd..3883a5acd8 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -131,6 +131,26 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): # TODO: Fix it so that response.templates works w/ mako templates, and then assert # that the right template rendered + @ddt.data( + (['honor', 'verified', 'credit'], True), + (['honor', 'verified'], False), + ) + @ddt.unpack + def test_credit_upsell_message(self, available_modes, show_upsell): + # Create the course modes + for mode in available_modes: + CourseModeFactory(mode_slug=mode, course_id=self.course.id) + + # Check whether credit upsell is shown on the page + # This should *only* be shown when a credit mode is available + url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + response = self.client.get(url) + + if show_upsell: + self.assertContains(response, "Credit") + else: + self.assertNotContains(response, "Credit") + @ddt.data('professional', 'no-id-professional') def test_professional_enrollment(self, mode): # The only course mode is professional ed diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index e71db7d4b7..b09b4c6be2 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -96,9 +96,24 @@ class ChooseModeView(View): chosen_price = donation_for_course.get(unicode(course_key), None) course = modulestore().get_course(course_key) + + # 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) + ) + context = { "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}), "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, diff --git a/common/templates/course_modes/choose.html b/common/templates/course_modes/choose.html index d0bd48e585..8e75a5c4e6 100644 --- a/common/templates/course_modes/choose.html +++ b/common/templates/course_modes/choose.html @@ -71,19 +71,44 @@
-

${_("Pursue a Verified Certificate")}

+ % if has_credit_upsell: +

${_("Pursue Academic Credit with a Verified Certificate")}

-

${_("Highlight you new knowledge and skills with a verified certificate. Use this valuable credential to improve your job prospects and advance your career, or highlight your certificate in school applications.")}

+

${_("Become eligible for academic credit and highlight your new skills and knowledge with a verified certificate. Use this valuable credential to qualify for academic credit from {org}, advance your career, or strengthen your school applications.").format(org=course_org)}

-

- ${_("Benefits of a verified Certificate")} -

+

${_("Benefits of a Verified Certificate")}

+
    +
  • ${_("{b_start}Eligible for credit:{b_end} Receive academic credit after successfully completing the course").format(b_start='', b_end='')}
  • +
  • ${_("{b_start}Official:{b_end} Receive an instructor-signed certificate with the institution's logo").format(b_start='', b_end='')}
  • +
  • ${_("{b_start}Easily shareable:{b_end} Add the certificate to your CV or resume, or post it directly on LinkedIn").format(b_start='', b_end='')}
  • +
+
+
+
    +
  • + + +
  • +
+
+
+

+
+ % else: +

${_("Pursue a Verified Certificate")}

+ +
+

${_("Highlight your new knowledge and skills with a verified certificate. Use this valuable credential to improve your job prospects and advance your career, or highlight your certificate in school applications.")}

+

+

+
+

${_("Benefits of a Verified Certificate")}

  • ${_("{b_start}Official: {b_end}Receive an instructor-signed certificate with the institution's logo").format(b_start='', b_end='')}
  • -
  • ${_("{b_start}Easily sharable: {b_end}Add the certificate to your CV or resume, or post it directly on LinkedIn").format(b_start='', b_end='')}
  • +
  • ${_("{b_start}Easily shareable: {b_end}Add the certificate to your CV or resume, or post it directly on LinkedIn").format(b_start='', b_end='')}
  • ${_("{b_start}Motivating: {b_end}Give yourself an additional incentive to complete the course").format(b_start='', b_end='')}
@@ -98,6 +123,7 @@

+ % endif
% endif