Merge pull request #1483 from edx/diana/certs-upgrade-path

Verified Certificate Upsell
This commit is contained in:
Diana Huang
2013-10-28 14:28:50 -07:00
20 changed files with 649 additions and 122 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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."""

View File

@@ -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,
}

View File

@@ -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>