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.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -71,19 +71,44 @@
|
||||
<div class="register-choice register-choice-certificate">
|
||||
<div class="wrapper-copy">
|
||||
<span class="deco-ribbon"></span>
|
||||
<h4 class="title">${_("Pursue a Verified Certificate")}</h4>
|
||||
% if has_credit_upsell:
|
||||
<h4 class="title">${_("Pursue Academic Credit with a Verified Certificate")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<p>${_("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.")}</p>
|
||||
<p>${_("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)}</p>
|
||||
<p>
|
||||
<div class="wrapper-copy-inline">
|
||||
<div class="copy-inline">
|
||||
<h4>
|
||||
${_("Benefits of a verified Certificate")}
|
||||
</h4>
|
||||
<h4>${_("Benefits of a Verified Certificate")}</h4>
|
||||
<ul>
|
||||
<li>${_("{b_start}Eligible for credit:{b_end} Receive academic credit after successfully completing the course").format(b_start='<b>', b_end='</b>')}</li>
|
||||
<li>${_("{b_start}Official:{b_end} Receive an instructor-signed certificate with the institution's logo").format(b_start='<b>', b_end='</b>')}</li>
|
||||
<li>${_("{b_start}Easily shareable:{b_end} Add the certificate to your CV or resume, or post it directly on LinkedIn").format(b_start='<b>', b_end='</b>')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="copy-inline list-actions">
|
||||
<ul class="list-actions">
|
||||
<li class="action action-select">
|
||||
<input type="hidden" name="contribution" value="${min_price}" />
|
||||
<input type="submit" name="verified_mode" value="${_('Pursue a Verified Certificate')} ($${min_price})" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
% else:
|
||||
<h4 class="title">${_("Pursue a Verified Certificate")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<p>${_("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.")}</p>
|
||||
<p>
|
||||
<div class="wrapper-copy-inline">
|
||||
<div class="copy-inline">
|
||||
<h4>${_("Benefits of a Verified Certificate")}</h4>
|
||||
<ul>
|
||||
<li>${_("{b_start}Official: {b_end}Receive an instructor-signed certificate with the institution's logo").format(b_start='<b>', b_end='</b>')}</li>
|
||||
<li>${_("{b_start}Easily sharable: {b_end}Add the certificate to your CV or resume, or post it directly on LinkedIn").format(b_start='<b>', b_end='</b>')}</li>
|
||||
<li>${_("{b_start}Easily shareable: {b_end}Add the certificate to your CV or resume, or post it directly on LinkedIn").format(b_start='<b>', b_end='</b>')}</li>
|
||||
<li>${_("{b_start}Motivating: {b_end}Give yourself an additional incentive to complete the course").format(b_start='<b>', b_end='</b>')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -98,6 +123,7 @@
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
Reference in New Issue
Block a user