Make course ids and usage ids opaque to LMS and Studio [partial commit]

This commit updates common/djangoapps.

These keys are now objects with a limited interface, and the particular
internal representation is managed by the data storage layer (the
modulestore).

For the LMS, there should be no outward-facing changes to the system.
The keys are, for now, a change to internal representation only. For
Studio, the new serialized form of the keys is used in urls, to allow
for further migration in the future.

Co-Author: Andy Armstrong <andya@edx.org>
Co-Author: Christina Roberts <christina@edx.org>
Co-Author: David Baumgold <db@edx.org>
Co-Author: Diana Huang <dkh@edx.org>
Co-Author: Don Mitchell <dmitchell@edx.org>
Co-Author: Julia Hansbrough <julia@edx.org>
Co-Author: Nimisha Asthagiri <nasthagiri@edx.org>
Co-Author: Sarina Canelake <sarina@edx.org>

[LMS-2370]
This commit is contained in:
Calen Pennington
2014-04-30 10:17:43 -04:00
parent 7852906ce0
commit e2bfcf2a36
42 changed files with 603 additions and 330 deletions

View File

@@ -9,6 +9,8 @@ from collections import namedtuple
from django.utils.translation import ugettext as _
from django.db.models import Q
from xmodule_django.models import CourseKeyField
Mode = namedtuple('Mode', ['slug', 'name', 'min_price', 'suggested_prices', 'currency', 'expiration_datetime'])
class CourseMode(models.Model):
@@ -17,7 +19,7 @@ class CourseMode(models.Model):
"""
# the course that this mode is attached to
course_id = models.CharField(max_length=255, db_index=True)
course_id = CourseKeyField(max_length=255, db_index=True)
# the reference to this mode that can be used by Enrollments to generate
# similar behavior for the same slug across courses

View File

@@ -8,6 +8,7 @@ Replace this with more appropriate tests for your application.
from datetime import datetime, timedelta
import pytz
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from django.test import TestCase
from course_modes.models import CourseMode, Mode
@@ -18,7 +19,7 @@ class CourseModeModelTest(TestCase):
"""
def setUp(self):
self.course_id = 'TestCourse'
self.course_key = SlashSeparatedCourseKey('Test', 'TestCourse', 'TestCourseRun')
CourseMode.objects.all().delete()
def create_mode(self, mode_slug, mode_name, min_price=0, suggested_prices='', currency='usd'):
@@ -26,7 +27,7 @@ class CourseModeModelTest(TestCase):
Create a new course mode
"""
return CourseMode.objects.get_or_create(
course_id=self.course_id,
course_id=self.course_key,
mode_display_name=mode_name,
mode_slug=mode_slug,
min_price=min_price,
@@ -39,7 +40,7 @@ class CourseModeModelTest(TestCase):
If we can't find any modes, we should get back the default mode
"""
# shouldn't be able to find a corresponding course
modes = CourseMode.modes_for_course(self.course_id)
modes = CourseMode.modes_for_course(self.course_key)
self.assertEqual([CourseMode.DEFAULT_MODE], modes)
def test_nodes_for_course_single(self):
@@ -48,13 +49,13 @@ class CourseModeModelTest(TestCase):
"""
self.create_mode('verified', 'Verified Certificate')
modes = CourseMode.modes_for_course(self.course_id)
modes = CourseMode.modes_for_course(self.course_key)
mode = Mode(u'verified', u'Verified Certificate', 0, '', 'usd', None)
self.assertEqual([mode], modes)
modes_dict = CourseMode.modes_for_course_dict(self.course_id)
modes_dict = CourseMode.modes_for_course_dict(self.course_key)
self.assertEqual(modes_dict['verified'], mode)
self.assertEqual(CourseMode.mode_for_course(self.course_id, 'verified'),
self.assertEqual(CourseMode.mode_for_course(self.course_key, 'verified'),
mode)
def test_modes_for_course_multiple(self):
@@ -67,18 +68,18 @@ class CourseModeModelTest(TestCase):
for mode in set_modes:
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices)
modes = CourseMode.modes_for_course(self.course_id)
modes = CourseMode.modes_for_course(self.course_key)
self.assertEqual(modes, set_modes)
self.assertEqual(mode1, CourseMode.mode_for_course(self.course_id, u'honor'))
self.assertEqual(mode2, CourseMode.mode_for_course(self.course_id, u'verified'))
self.assertIsNone(CourseMode.mode_for_course(self.course_id, 'DNE'))
self.assertEqual(mode1, CourseMode.mode_for_course(self.course_key, u'honor'))
self.assertEqual(mode2, CourseMode.mode_for_course(self.course_key, u'verified'))
self.assertIsNone(CourseMode.mode_for_course(self.course_key, 'DNE'))
def test_min_course_price_for_currency(self):
"""
Get the min course price for a course according to currency
"""
# no modes, should get 0
self.assertEqual(0, CourseMode.min_course_price_for_currency(self.course_id, 'usd'))
self.assertEqual(0, CourseMode.min_course_price_for_currency(self.course_key, 'usd'))
# create some modes
mode1 = Mode(u'honor', u'Honor Code Certificate', 10, '', 'usd', None)
@@ -88,27 +89,27 @@ class CourseModeModelTest(TestCase):
for mode in set_modes:
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency)
self.assertEqual(10, CourseMode.min_course_price_for_currency(self.course_id, 'usd'))
self.assertEqual(80, CourseMode.min_course_price_for_currency(self.course_id, 'cny'))
self.assertEqual(10, CourseMode.min_course_price_for_currency(self.course_key, 'usd'))
self.assertEqual(80, CourseMode.min_course_price_for_currency(self.course_key, 'cny'))
def test_modes_for_course_expired(self):
expired_mode, _status = self.create_mode('verified', 'Verified Certificate')
expired_mode.expiration_datetime = datetime.now(pytz.UTC) + timedelta(days=-1)
expired_mode.save()
modes = CourseMode.modes_for_course(self.course_id)
modes = CourseMode.modes_for_course(self.course_key)
self.assertEqual([CourseMode.DEFAULT_MODE], modes)
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)
modes = CourseMode.modes_for_course(self.course_key)
self.assertEqual([mode1], modes)
expiration_datetime = datetime.now(pytz.UTC) + timedelta(days=1)
expired_mode.expiration_datetime = expiration_datetime
expired_mode.save()
expired_mode_value = Mode(u'verified', u'Verified Certificate', 0, '', 'usd', expiration_datetime)
modes = CourseMode.modes_for_course(self.course_id)
modes = CourseMode.modes_for_course(self.course_key)
self.assertEqual([expired_mode_value, mode1], modes)
modes = CourseMode.modes_for_course('second_test_course')
modes = CourseMode.modes_for_course(SlashSeparatedCourseKey('TestOrg', 'TestCourse', 'TestRun'))
self.assertEqual([CourseMode.DEFAULT_MODE], modes)

View File

@@ -20,6 +20,7 @@ from courseware.access import has_access
from student.models import CourseEnrollment
from student.views import course_from_id
from verify_student.models import SoftwareSecurePhotoVerification
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class ChooseModeView(View):
@@ -35,7 +36,9 @@ class ChooseModeView(View):
def get(self, request, course_id, error=None):
""" Displays the course mode choice page """
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_id)
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_key)
upgrade = request.GET.get('upgrade', False)
request.session['attempting_upgrade'] = upgrade
@@ -47,13 +50,13 @@ class ChooseModeView(View):
if enrollment_mode is not None and upgrade is False:
return redirect(reverse('dashboard'))
modes = CourseMode.modes_for_course_dict(course_id)
modes = CourseMode.modes_for_course_dict(course_key)
donation_for_course = request.session.get("donation_for_course", {})
chosen_price = donation_for_course.get(course_id, None)
chosen_price = donation_for_course.get(course_key, None)
course = course_from_id(course_id)
course = course_from_id(course_key)
context = {
"course_id": course_id,
"course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}),
"modes": modes,
"course_name": course.display_name_with_default,
"course_org": course.display_org_with_default,
@@ -72,25 +75,26 @@ class ChooseModeView(View):
@method_decorator(login_required)
def post(self, request, course_id):
""" Takes the form submission from the page and parses it """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = request.user
# This is a bit redundant with logic in student.views.change_enrollement,
# but I don't really have the time to refactor it more nicely and test.
course = course_from_id(course_id)
if not has_access(user, course, 'enroll'):
course = course_from_id(course_key)
if not has_access(user, 'enroll', course):
error_msg = _("Enrollment is closed")
return self.get(request, course_id, error=error_msg)
return self.get(request, course_key, error=error_msg)
upgrade = request.GET.get('upgrade', False)
requested_mode = self.get_requested_mode(request.POST)
allowed_modes = CourseMode.modes_for_course_dict(course_id)
allowed_modes = CourseMode.modes_for_course_dict(course_key)
if requested_mode not in allowed_modes:
return HttpResponseBadRequest(_("Enrollment mode not supported"))
if requested_mode in ("audit", "honor"):
CourseEnrollment.enroll(user, course_id, requested_mode)
CourseEnrollment.enroll(user, course_key, requested_mode)
return redirect('dashboard')
mode_info = allowed_modes[requested_mode]
@@ -104,25 +108,25 @@ class ChooseModeView(View):
amount_value = decimal.Decimal(amount).quantize(decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN)
except decimal.InvalidOperation:
error_msg = _("Invalid amount selected.")
return self.get(request, course_id, error=error_msg)
return self.get(request, course_key, error=error_msg)
# Check for minimum pricing
if amount_value < mode_info.min_price:
error_msg = _("No selected price or selected price is too low.")
return self.get(request, course_id, error=error_msg)
return self.get(request, course_key, error=error_msg)
donation_for_course = request.session.get("donation_for_course", {})
donation_for_course[course_id] = amount_value
donation_for_course[course_key] = amount_value
request.session["donation_for_course"] = donation_for_course
if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user):
return redirect(
reverse('verify_student_verified',
kwargs={'course_id': course_id}) + "?upgrade={}".format(upgrade)
kwargs={'course_id': course_key.to_deprecated_string()}) + "?upgrade={}".format(upgrade)
)
return redirect(
reverse('verify_student_show_requirements',
kwargs={'course_id': course_id}) + "?upgrade={}".format(upgrade))
kwargs={'course_id': course_key.to_deprecated_string()}) + "?upgrade={}".format(upgrade))
def get_requested_mode(self, request_dict):
"""