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/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index f80184832f..b7a48fb82a 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -4,6 +4,7 @@ some xmodules by conditions. import json import logging +from lazy import lazy from lxml import etree from pkg_resources import resource_string @@ -97,10 +98,12 @@ class ConditionalModule(ConditionalFields, XModule): return xml_value, attr_name raise Exception('Error in conditional module: unknown condition "%s"' % xml_attr) - def is_condition_satisfied(self): - self.required_modules = [self.system.get_module(descriptor) for - descriptor in self.descriptor.get_required_module_descriptors()] + @lazy + def required_modules(self): + return [self.system.get_module(descriptor) for + descriptor in self.descriptor.get_required_module_descriptors()] + def is_condition_satisfied(self): xml_value, attr_name = self._get_condition() if xml_value and self.required_modules: diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py index a9eda48299..7fa30b4885 100644 --- a/common/lib/xmodule/xmodule/lti_module.py +++ b/common/lib/xmodule/xmodule/lti_module.py @@ -181,7 +181,7 @@ class LTIModule(LTIFields, XModule): ] # Obtains client_key and client_secret credentials from current course: - course_id = self.runtime.course_id + course_id = self.course_id course_location = CourseDescriptor.id_to_location(course_id) course = self.descriptor.runtime.modulestore.get_item(course_location) client_key = client_secret = '' diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index 6da50a7544..7c056d611c 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -519,7 +519,7 @@ class PeerGradingModule(PeerGradingFields, XModule): error_text = "" problem_list = [] try: - problem_list_json = self.peer_gs.get_problem_list(self.system.course_id, self.system.anonymous_student_id) + problem_list_json = self.peer_gs.get_problem_list(self.course_id, self.system.anonymous_student_id) problem_list_dict = problem_list_json success = problem_list_dict['success'] if 'error' in problem_list_dict: @@ -569,7 +569,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ajax_url = self.ajax_url html = self.system.render_template('peer_grading/peer_grading.html', { - 'course_id': self.system.course_id, + 'course_id': self.course_id, 'ajax_url': ajax_url, 'success': success, 'problem_list': good_problem_list, @@ -603,7 +603,7 @@ class PeerGradingModule(PeerGradingFields, XModule): html = self.system.render_template('peer_grading/peer_grading_problem.html', { 'view_html': '', 'problem_location': problem_location, - 'course_id': self.system.course_id, + 'course_id': self.course_id, 'ajax_url': ajax_url, # Checked above 'staff_access': False, diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 5a739e5fa4..b1e5f59bb9 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -133,6 +133,10 @@ class XModuleMixin(XBlockMixin): default=None ) + @property + def course_id(self): + return self.runtime.course_id + @property def id(self): return self.location.url() @@ -743,6 +747,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): self.xmodule_runtime.xmodule_instance = descriptor._xmodule # pylint: disable=protected-access return self.xmodule_runtime.xmodule_instance + course_id = module_attr('course_id') displayable_items = module_attr('displayable_items') get_display_items = module_attr('get_display_items') get_icon_class = module_attr('get_icon_class') 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