diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index ce15059643..8e475d1ec1 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -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 diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 651c7c51a5..7a01c30dc4 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -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) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index e247ac08a2..0993467c17 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -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) diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py index b2f4d95776..107631b17b 100644 --- a/common/djangoapps/student/tests/factories.py +++ b/common/djangoapps/student/tests/factories.py @@ -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 diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index c35ad66427..315b6e9285 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -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.""" diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 9d3d0bc63b..4ebbcff592 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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, } diff --git a/common/templates/course_modes/choose.html b/common/templates/course_modes/choose.html index 738728042b..67cc0a301d 100644 --- a/common/templates/course_modes/choose.html +++ b/common/templates/course_modes/choose.html @@ -2,8 +2,16 @@ <%! from django.core.urlresolvers import reverse %> <%inherit file="../main.html" /> -<%block name="bodyclass">register verification-process step-select-track -<%block name="title">${_("Register for {} | Choose Your Track").format(course_name)} +<%block name="bodyclass">register verification-process step-select-track ${'is-upgrading' if upgrade else ''} +<%block name="title"> + + %if upgrade: + ${_("Upgrade Your Registration for {} | Choose Your Track").format(course_name)} + %else: + ${_("Register for {} | Choose Your Track").format(course_name)} + %endif + + <%block name="js_extra"> @@ -172,7 +180,12 @@
${_("What do you do with this picture?")}
${_("We only use it to verify your identity. It is not displayed anywhere.")}
${_("What if my camera isn't working?")}
-
${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='', a_end="")}
+ + %if upgrade: +
${_("You can always continue to audit the course without verifying.")}
+ %else: +
${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='', a_end="")}
+ %endif diff --git a/lms/templates/verify_student/show_requirements.html b/lms/templates/verify_student/show_requirements.html index 432a13fc62..3280fec89a 100644 --- a/lms/templates/verify_student/show_requirements.html +++ b/lms/templates/verify_student/show_requirements.html @@ -1,8 +1,16 @@ <%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse %> <%inherit file="../main.html" /> -<%block name="bodyclass">register verification-process step-requirements -<%block name="title">${_("Register for {}").format(course_name)} +<%block name="bodyclass">register verification-process step-requirements ${'is-upgrading' if upgrade else ''} +<%block name="title"> + + %if upgrade: + ${_("Upgrade Your Registration for {}").format(course_name)} + %else: + ${_("Register for {}").format(course_name)} + %endif + + <%block name="content"> %if is_not_active: @@ -71,11 +79,19 @@
-

${_("What You Will Need to Register")}

+ %if upgrade: +

${_("What You Will Need to Upgrade")}

-
-

${_("There are three things you will need to register as an ID verified student:")}

-
+
+

${_("There are three things you will need to upgrade to being an ID verified student:")}

+
+ %else: +

${_("What You Will Need to Register")}

+ +
+

${_("There are three things you will need to register as an ID verified student:")}

+
+ %endif