Merge pull request #1483 from edx/diana/certs-upgrade-path
Verified Certificate Upsell
This commit is contained in:
@@ -9,7 +9,7 @@ from collections import namedtuple
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db.models import Q
|
||||
|
||||
Mode = namedtuple('Mode', ['slug', 'name', 'min_price', 'suggested_prices', 'currency'])
|
||||
Mode = namedtuple('Mode', ['slug', 'name', 'min_price', 'suggested_prices', 'currency', 'expiration_date'])
|
||||
|
||||
|
||||
class CourseMode(models.Model):
|
||||
@@ -39,7 +39,7 @@ class CourseMode(models.Model):
|
||||
# turn this mode off after the given expiration date
|
||||
expiration_date = models.DateField(default=None, null=True, blank=True)
|
||||
|
||||
DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd')
|
||||
DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd', None)
|
||||
DEFAULT_MODE_SLUG = 'honor'
|
||||
|
||||
class Meta:
|
||||
@@ -57,8 +57,14 @@ class CourseMode(models.Model):
|
||||
found_course_modes = cls.objects.filter(Q(course_id=course_id) &
|
||||
(Q(expiration_date__isnull=True) |
|
||||
Q(expiration_date__gte=now)))
|
||||
modes = ([Mode(mode.mode_slug, mode.mode_display_name, mode.min_price, mode.suggested_prices, mode.currency)
|
||||
for mode in found_course_modes])
|
||||
modes = ([Mode(
|
||||
mode.mode_slug,
|
||||
mode.mode_display_name,
|
||||
mode.min_price,
|
||||
mode.suggested_prices,
|
||||
mode.currency,
|
||||
mode.expiration_date
|
||||
) for mode in found_course_modes])
|
||||
if not modes:
|
||||
modes = [cls.DEFAULT_MODE]
|
||||
return modes
|
||||
|
||||
@@ -49,7 +49,7 @@ class CourseModeModelTest(TestCase):
|
||||
|
||||
self.create_mode('verified', 'Verified Certificate')
|
||||
modes = CourseMode.modes_for_course(self.course_id)
|
||||
mode = Mode(u'verified', u'Verified Certificate', 0, '', 'usd')
|
||||
mode = Mode(u'verified', u'Verified Certificate', 0, '', 'usd', None)
|
||||
self.assertEqual([mode], modes)
|
||||
|
||||
modes_dict = CourseMode.modes_for_course_dict(self.course_id)
|
||||
@@ -61,8 +61,8 @@ class CourseModeModelTest(TestCase):
|
||||
"""
|
||||
Finding the modes when there's multiple modes
|
||||
"""
|
||||
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd')
|
||||
mode2 = Mode(u'verified', u'Verified Certificate', 0, '', 'usd')
|
||||
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None)
|
||||
mode2 = Mode(u'verified', u'Verified Certificate', 0, '', 'usd', None)
|
||||
set_modes = [mode1, mode2]
|
||||
for mode in set_modes:
|
||||
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices)
|
||||
@@ -81,9 +81,9 @@ class CourseModeModelTest(TestCase):
|
||||
self.assertEqual(0, CourseMode.min_course_price_for_currency(self.course_id, 'usd'))
|
||||
|
||||
# create some modes
|
||||
mode1 = Mode(u'honor', u'Honor Code Certificate', 10, '', 'usd')
|
||||
mode2 = Mode(u'verified', u'Verified Certificate', 20, '', 'usd')
|
||||
mode3 = Mode(u'honor', u'Honor Code Certificate', 80, '', 'cny')
|
||||
mode1 = Mode(u'honor', u'Honor Code Certificate', 10, '', 'usd', None)
|
||||
mode2 = Mode(u'verified', u'Verified Certificate', 20, '', 'usd', None)
|
||||
mode3 = Mode(u'honor', u'Honor Code Certificate', 80, '', 'cny', None)
|
||||
set_modes = [mode1, mode2, mode3]
|
||||
for mode in set_modes:
|
||||
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency)
|
||||
@@ -98,14 +98,15 @@ class CourseModeModelTest(TestCase):
|
||||
modes = CourseMode.modes_for_course(self.course_id)
|
||||
self.assertEqual([CourseMode.DEFAULT_MODE], modes)
|
||||
|
||||
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd')
|
||||
mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None)
|
||||
self.create_mode(mode1.slug, mode1.name, mode1.min_price, mode1.suggested_prices)
|
||||
modes = CourseMode.modes_for_course(self.course_id)
|
||||
self.assertEqual([mode1], modes)
|
||||
|
||||
expired_mode.expiration_date = datetime.now(pytz.UTC) + timedelta(days=1)
|
||||
expiration_date = datetime.now(pytz.UTC) + timedelta(days=1)
|
||||
expired_mode.expiration_date = expiration_date
|
||||
expired_mode.save()
|
||||
expired_mode_value = Mode(u'verified', u'Verified Certificate', 0, '', 'usd')
|
||||
expired_mode_value = Mode(u'verified', u'Verified Certificate', 0, '', 'usd', expiration_date.date())
|
||||
modes = CourseMode.modes_for_course(self.course_id)
|
||||
self.assertEqual([expired_mode_value, mode1], modes)
|
||||
|
||||
|
||||
@@ -34,10 +34,19 @@ class ChooseModeView(View):
|
||||
@method_decorator(login_required)
|
||||
def get(self, request, course_id, error=None):
|
||||
""" Displays the course mode choice page """
|
||||
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
|
||||
return redirect(reverse('dashboard'))
|
||||
modes = CourseMode.modes_for_course_dict(course_id)
|
||||
|
||||
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_id)
|
||||
upgrade = request.GET.get('upgrade', False)
|
||||
|
||||
# verified users do not need to register or upgrade
|
||||
if enrollment_mode == 'verified':
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
# registered users who are not trying to upgrade do not need to re-register
|
||||
if enrollment_mode is not None and upgrade is False:
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
modes = CourseMode.modes_for_course_dict(course_id)
|
||||
donation_for_course = request.session.get("donation_for_course", {})
|
||||
chosen_price = donation_for_course.get(course_id, None)
|
||||
|
||||
@@ -50,6 +59,7 @@ class ChooseModeView(View):
|
||||
"course_num": course.display_number_with_default,
|
||||
"chosen_price": chosen_price,
|
||||
"error": error,
|
||||
"upgrade": upgrade,
|
||||
}
|
||||
if "verified" in modes:
|
||||
context["suggested_prices"] = [decimal.Decimal(x) for x in modes["verified"].suggested_prices.split(",")]
|
||||
@@ -70,6 +80,8 @@ class ChooseModeView(View):
|
||||
error_msg = _("Enrollment is closed")
|
||||
return self.get(request, course_id, error=error_msg)
|
||||
|
||||
upgrade = request.GET.get('upgrade', False)
|
||||
|
||||
requested_mode = self.get_requested_mode(request.POST.get("mode"))
|
||||
if requested_mode == "verified" and request.POST.get("honor-code"):
|
||||
requested_mode = "honor"
|
||||
@@ -106,13 +118,12 @@ class ChooseModeView(View):
|
||||
if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user):
|
||||
return redirect(
|
||||
reverse('verify_student_verified',
|
||||
kwargs={'course_id': course_id})
|
||||
kwargs={'course_id': course_id}) + "?upgrade={}".format(upgrade)
|
||||
)
|
||||
|
||||
return redirect(
|
||||
reverse('verify_student_show_requirements',
|
||||
kwargs={'course_id': course_id}),
|
||||
)
|
||||
kwargs={'course_id': course_id}) + "?upgrade={}".format(upgrade))
|
||||
|
||||
def get_requested_mode(self, user_choice):
|
||||
"""
|
||||
@@ -121,6 +132,7 @@ class ChooseModeView(View):
|
||||
"""
|
||||
choices = {
|
||||
"Select Audit": "audit",
|
||||
"Select Certificate": "verified"
|
||||
"Select Certificate": "verified",
|
||||
"Upgrade Your Registration": "verified"
|
||||
}
|
||||
return choices.get(user_choice)
|
||||
|
||||
@@ -2,6 +2,7 @@ from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed, CourseEnrollment,
|
||||
PendingEmailChange, UserStanding,
|
||||
)
|
||||
from course_modes.models import CourseMode
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
|
||||
@@ -36,6 +37,16 @@ class UserProfileFactory(DjangoModelFactory):
|
||||
goals = u'World domination'
|
||||
|
||||
|
||||
class CourseModeFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = CourseMode
|
||||
|
||||
course_id = None
|
||||
mode_display_name = u'Honor Code',
|
||||
mode_slug = 'honor'
|
||||
min_price = 0
|
||||
suggested_prices = ''
|
||||
currency = 'usd'
|
||||
|
||||
class RegistrationFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import logging
|
||||
import json
|
||||
import re
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
@@ -28,8 +30,8 @@ from textwrap import dedent
|
||||
|
||||
from student.models import unique_id_for_user, CourseEnrollment
|
||||
from student.views import (process_survey_link, _cert_info, password_reset, password_reset_confirm_wrapper,
|
||||
change_enrollment)
|
||||
from student.tests.factories import UserFactory
|
||||
change_enrollment, complete_course_mode_info)
|
||||
from student.tests.factories import UserFactory, CourseModeFactory
|
||||
from student.tests.test_email import mock_render_to_string
|
||||
|
||||
import shoppingcart
|
||||
@@ -216,6 +218,45 @@ class CourseEndingTest(TestCase):
|
||||
})
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class DashboardTest(TestCase):
|
||||
"""
|
||||
Tests for dashboard utility functions
|
||||
"""
|
||||
# arbitrary constant
|
||||
COURSE_SLUG = "100"
|
||||
COURSE_NAME = "test_course"
|
||||
COURSE_ORG = "EDX"
|
||||
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
|
||||
self.assertIsNotNone(self.course)
|
||||
self.user = UserFactory.create(username="jack", email="jack@fake.edx.org")
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug='honor',
|
||||
mode_display_name='Honor Code',
|
||||
)
|
||||
|
||||
def test_course_mode_info(self):
|
||||
verified_mode = CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug='verified',
|
||||
mode_display_name='Verified',
|
||||
expiration_date=(datetime.now(pytz.UTC) + timedelta(days=1)).date()
|
||||
)
|
||||
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
|
||||
course_mode_info = complete_course_mode_info(self.course.id, enrollment)
|
||||
self.assertTrue(course_mode_info['show_upsell'])
|
||||
self.assertEquals(course_mode_info['days_for_upsell'], 1)
|
||||
|
||||
verified_mode.expiration_date = datetime.now(pytz.UTC) + timedelta(days=-1)
|
||||
verified_mode.save()
|
||||
course_mode_info = complete_course_mode_info(self.course.id, enrollment)
|
||||
self.assertFalse(course_mode_info['show_upsell'])
|
||||
self.assertIsNone(course_mode_info['days_for_upsell'])
|
||||
|
||||
|
||||
class EnrollInCourseTest(TestCase):
|
||||
"""Tests enrolling and unenrolling in courses."""
|
||||
|
||||
|
||||
@@ -267,6 +267,29 @@ def register_user(request, extra_context=None):
|
||||
return render_to_response('register.html', context)
|
||||
|
||||
|
||||
def complete_course_mode_info(course_id, enrollment):
|
||||
"""
|
||||
We would like to compute some more information from the given course modes
|
||||
and the user's current enrollment
|
||||
|
||||
Returns the given information:
|
||||
- whether to show the course upsell information
|
||||
- numbers of days until they can't upsell anymore
|
||||
"""
|
||||
modes = CourseMode.modes_for_course_dict(course_id)
|
||||
mode_info = {'show_upsell': False, 'days_for_upsell': None}
|
||||
# we want to know if the user is already verified and if verified is an
|
||||
# option
|
||||
if 'verified' in modes and enrollment.mode != 'verified':
|
||||
mode_info['show_upsell'] = True
|
||||
# if there is an expiration date, find out how long from now it is
|
||||
if modes['verified'].expiration_date:
|
||||
today = datetime.datetime.now(UTC).date()
|
||||
mode_info['days_for_upsell'] = (modes['verified'].expiration_date - today).days
|
||||
|
||||
return mode_info
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def dashboard(request):
|
||||
@@ -300,6 +323,7 @@ def dashboard(request):
|
||||
show_courseware_links_for = frozenset(course.id for course, _enrollment in courses
|
||||
if has_access(request.user, course, 'load'))
|
||||
|
||||
course_modes = {course.id: complete_course_mode_info(course.id, enrollment) for course, enrollment in courses}
|
||||
cert_statuses = {course.id: cert_info(request.user, course) for course, _enrollment in courses}
|
||||
|
||||
# only show email settings for Mongo course and when bulk email is turned on
|
||||
@@ -324,6 +348,7 @@ def dashboard(request):
|
||||
'staff_access': staff_access,
|
||||
'errored_courses': errored_courses,
|
||||
'show_courseware_links_for': show_courseware_links_for,
|
||||
'all_course_modes': course_modes,
|
||||
'cert_statuses': cert_statuses,
|
||||
'show_email_settings_for': show_email_settings_for,
|
||||
}
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../main.html" />
|
||||
|
||||
<%block name="bodyclass">register verification-process step-select-track</%block>
|
||||
<%block name="title"><title>${_("Register for {} | Choose Your Track").format(course_name)}</title></%block>
|
||||
<%block name="bodyclass">register verification-process step-select-track ${'is-upgrading' if upgrade else ''}</%block>
|
||||
<%block name="title">
|
||||
<title>
|
||||
%if upgrade:
|
||||
${_("Upgrade Your Registration for {} | Choose Your Track").format(course_name)}
|
||||
%else:
|
||||
${_("Register for {} | Choose Your Track").format(course_name)}
|
||||
%endif
|
||||
</title>
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
@@ -48,7 +56,10 @@ $(document).ready(function() {
|
||||
|
||||
<div class="wrapper-register-choose wrapper-content-main">
|
||||
<article class="register-choose content-main">
|
||||
|
||||
%if not upgrade:
|
||||
<h3 class="title">${_("Select your track:")}</h3>
|
||||
%endif
|
||||
|
||||
<form class="form-register-choose" method="post" name="enrollment_mode_form" id="enrollment_mode_form">
|
||||
|
||||
@@ -57,9 +68,16 @@ $(document).ready(function() {
|
||||
<div class="wrapper-copy">
|
||||
<span class="deco-ribbon"></span>
|
||||
<h4 class="title">${_("Certificate of Achievement (ID Verified)")}</h4>
|
||||
<div class="copy">
|
||||
<p>${_("Sign up and work toward a verified Certificate of Achievement.")}</p>
|
||||
</div>
|
||||
|
||||
%if upgrade:
|
||||
<div class="copy">
|
||||
<p>${_("Upgrade and work toward a verified Certificate of Achievement.")}</p>
|
||||
</div>
|
||||
%else:
|
||||
<div class="copy">
|
||||
<p>${_("Sign up and work toward a verified Certificate of Achievement.")}</p>
|
||||
</div>
|
||||
%endif
|
||||
</div>
|
||||
|
||||
<div class="field field-certificate-contribution">
|
||||
@@ -115,16 +133,28 @@ $(document).ready(function() {
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action action-select">
|
||||
<input type="submit" name="mode" value="Select Certificate" />
|
||||
%if upgrade:
|
||||
<input type="submit" name="mode" value="Upgrade Your Registration" />
|
||||
%else:
|
||||
<input type="submit" name="mode" value="Select Certificate" />
|
||||
%endif
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="help help-register">
|
||||
<h3 class="title">${_("Verified Registration Requirements")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_("To register for a Verified Certificate of Achievement option, you will need a webcam, a credit or debit card, and an ID.")}</p>
|
||||
</div>
|
||||
|
||||
%if upgrade:
|
||||
<div class="copy">
|
||||
<p>${_("To upgrade your registration and work towards a Verified Certificate of Achievement, you will need a webcam, a credit or debit card, and an ID.")}</p>
|
||||
</div>
|
||||
%else:
|
||||
<div class="copy">
|
||||
<p>${_("To register for a Verified Certificate of Achievement option, you will need a webcam, a credit or debit card, and an ID.")}</p>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<h3 class="title">${_("What is an ID Verified Certificate?")}</h3>
|
||||
<div class="copy">
|
||||
@@ -133,25 +163,29 @@ $(document).ready(function() {
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if "audit" in modes:
|
||||
<span class="deco-divider">
|
||||
<span class="copy">${_("or")}</span>
|
||||
</span>
|
||||
<div class="register-choice register-choice-audit">
|
||||
<div class="wrapper-copy">
|
||||
<h4 class="title">${_("Audit This Course")}</h4>
|
||||
<div class="copy">
|
||||
<p>${_("Sign up to audit this course for free and track your own progress.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
%if not upgrade:
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action action-select">
|
||||
<input type="submit" name="mode" value="Select Audit" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
% endif
|
||||
% if "audit" in modes:
|
||||
<span class="deco-divider">
|
||||
<span class="copy">${_("or")}</span>
|
||||
</span>
|
||||
<div class="register-choice register-choice-audit">
|
||||
<div class="wrapper-copy">
|
||||
<h4 class="title">${_("Audit This Course")}</h4>
|
||||
<div class="copy">
|
||||
<p>${_("Sign up to audit this course for free and track your own progress.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action action-select">
|
||||
<input type="submit" name="mode" value="Select Audit" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
%endif
|
||||
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user