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>
-<%block name="title">
${_("Register for {} | Choose Your Track").format(course_name)}%block>
+<%block name="bodyclass">register verification-process step-select-track ${'is-upgrading' if upgrade else ''}%block>
+<%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>
<%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>
-<%block name="title">${_("Register for {}").format(course_name)}%block>
+<%block name="bodyclass">register verification-process step-requirements ${'is-upgrading' if upgrade else ''}%block>
+<%block name="title">
+
+ %if upgrade:
+ ${_("Upgrade Your Registration for {}").format(course_name)}
+ %else:
+ ${_("Register for {}").format(course_name)}
+ %endif
+
+%block>
<%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
%if is_not_active:
@@ -149,11 +165,16 @@