Commit e2bfcf2a by Calen Pennington

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]
parent 7852906c
......@@ -4,10 +4,12 @@ from student.models import CourseEnrollment
from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG
from xmodule.modulestore import InvalidLocationError
from xmodule.modulestore import InvalidLocationError, InvalidKeyError
from cache_toolbox.core import get_cached_content, set_cached_content
from xmodule.exceptions import NotFoundError
# TODO: Soon as we have a reasonable way to serialize/deserialize AssetKeys, we need
# to change this file so instead of using course_id_partial, we're just using asset keys
class StaticContentServer(object):
def process_request(self, request):
......@@ -15,7 +17,7 @@ class StaticContentServer(object):
if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'):
try:
loc = StaticContent.get_location_from_path(request.path)
except InvalidLocationError:
except (InvalidLocationError, InvalidKeyError):
# return a 'Bad Request' to browser as we have a malformed Location
response = HttpResponse()
response.status_code = 400
......@@ -47,9 +49,9 @@ class StaticContentServer(object):
if getattr(content, "locked", False):
if not hasattr(request, "user") or not request.user.is_authenticated():
return HttpResponseForbidden('Unauthorized')
course_partial_id = "/".join([loc.org, loc.course])
if not request.user.is_staff and not CourseEnrollment.is_enrolled_by_partial(
request.user, course_partial_id):
request.user, loc.course_key
):
return HttpResponseForbidden('Unauthorized')
# convert over the DB persistent last modified timestamp to a HTTP compatible
......
......@@ -15,9 +15,9 @@ from django.test.utils import override_settings
from student.models import CourseEnrollment
from xmodule.contentstore.django import contentstore, _CONTENTSTORE
from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.django_utils import (studio_store_config,
ModuleStoreTestCase)
from xmodule.modulestore.xml_importer import import_from_xml
......@@ -47,18 +47,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client()
self.contentstore = contentstore()
# A locked asset
self.loc_locked = Location('c4x', 'edX', 'toy', 'asset', 'sample_static.txt')
self.url_locked = StaticContent.get_url_path_from_location(self.loc_locked)
# An unlocked asset
self.loc_unlocked = Location('c4x', 'edX', 'toy', 'asset', 'another_static.txt')
self.url_unlocked = StaticContent.get_url_path_from_location(self.loc_unlocked)
self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
import_from_xml(modulestore('direct'), 'common/test/data/', ['toy'],
static_content_store=self.contentstore, verbose=True)
self.contentstore.set_attr(self.loc_locked, 'locked', True)
# A locked asset
self.locked_asset = self.course_key.make_asset_key('asset', 'sample_static.txt')
self.url_locked = self.locked_asset.to_deprecated_string()
# An unlocked asset
self.unlocked_asset = self.course_key.make_asset_key('asset', 'another_static.txt')
self.url_unlocked = self.unlocked_asset.to_deprecated_string()
self.contentstore.set_attr(self.locked_asset, 'locked', True)
# Create user
self.usr = 'testuser'
......@@ -114,10 +116,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
Test that locked assets behave appropriately in case user is logged in
and registered for the course.
"""
# pylint: disable=E1101
course_id = "/".join([self.loc_locked.org, self.loc_locked.course, '2012_Fall'])
CourseEnrollment.enroll(self.user, course_id)
self.assertTrue(CourseEnrollment.is_enrolled(self.user, course_id))
CourseEnrollment.enroll(self.user, self.course_key)
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key))
self.client.login(username=self.usr, password=self.pwd)
resp = self.client.get(self.url_locked)
......@@ -127,9 +127,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
"""
Test that locked assets behave appropriately in case user is staff.
"""
# pylint: disable=E1101
course_id = "/".join([self.loc_locked.org, self.loc_locked.course, '2012_Fall'])
self.client.login(username=self.staff_usr, password=self.staff_pwd)
resp = self.client.get(self.url_locked)
self.assertEqual(resp.status_code, 200) # pylint: disable=E1103
......
......@@ -32,30 +32,30 @@ def local_random():
return _local_random
def is_course_cohorted(course_id):
def is_course_cohorted(course_key):
"""
Given a course id, return a boolean for whether or not the course is
Given a course key, return a boolean for whether or not the course is
cohorted.
Raises:
Http404 if the course doesn't exist.
"""
return courses.get_course_by_id(course_id).is_cohorted
return courses.get_course_by_id(course_key).is_cohorted
def get_cohort_id(user, course_id):
def get_cohort_id(user, course_key):
"""
Given a course id and a user, return the id of the cohort that user is
Given a course key and a user, return the id of the cohort that user is
assigned to in that course. If they don't have a cohort, return None.
"""
cohort = get_cohort(user, course_id)
cohort = get_cohort(user, course_key)
return None if cohort is None else cohort.id
def is_commentable_cohorted(course_id, commentable_id):
def is_commentable_cohorted(course_key, commentable_id):
"""
Args:
course_id: string
course_key: CourseKey
commentable_id: string
Returns:
......@@ -64,7 +64,7 @@ def is_commentable_cohorted(course_id, commentable_id):
Raises:
Http404 if the course doesn't exist.
"""
course = courses.get_course_by_id(course_id)
course = courses.get_course_by_id(course_key)
if not course.is_cohorted:
# this is the easy case :)
......@@ -77,18 +77,18 @@ def is_commentable_cohorted(course_id, commentable_id):
# inline discussions are cohorted by default
ans = True
log.debug(u"is_commentable_cohorted({0}, {1}) = {2}".format(course_id,
commentable_id,
ans))
log.debug(u"is_commentable_cohorted({0}, {1}) = {2}".format(
course_key, commentable_id, ans
))
return ans
def get_cohorted_commentables(course_id):
def get_cohorted_commentables(course_key):
"""
Given a course_id return a list of strings representing cohorted commentables
Given a course_key return a list of strings representing cohorted commentables
"""
course = courses.get_course_by_id(course_id)
course = courses.get_course_by_id(course_key)
if not course.is_cohorted:
# this is the easy case :)
......@@ -99,34 +99,34 @@ def get_cohorted_commentables(course_id):
return ans
def get_cohort(user, course_id):
def get_cohort(user, course_key):
"""
Given a django User and a course_id, return the user's cohort in that
Given a django User and a CourseKey, return the user's cohort in that
cohort.
Arguments:
user: a Django User object.
course_id: string in the format 'org/course/run'
course_key: CourseKey
Returns:
A CourseUserGroup object if the course is cohorted and the User has a
cohort, else None.
Raises:
ValueError if the course_id doesn't exist.
ValueError if the CourseKey doesn't exist.
"""
# First check whether the course is cohorted (users shouldn't be in a cohort
# in non-cohorted courses, but settings can change after course starts)
try:
course = courses.get_course_by_id(course_id)
course = courses.get_course_by_id(course_key)
except Http404:
raise ValueError("Invalid course_id")
raise ValueError("Invalid course_key")
if not course.is_cohorted:
return None
try:
return CourseUserGroup.objects.get(course_id=course_id,
return CourseUserGroup.objects.get(course_id=course_key,
group_type=CourseUserGroup.COHORT,
users__id=user.id)
except CourseUserGroup.DoesNotExist:
......@@ -142,72 +142,81 @@ def get_cohort(user, course_id):
# Nowhere to put user
log.warning("Course %s is auto-cohorted, but there are no"
" auto_cohort_groups specified",
course_id)
course_key)
return None
# Put user in a random group, creating it if needed
group_name = local_random().choice(choices)
group, created = CourseUserGroup.objects.get_or_create(
course_id=course_id,
course_id=course_key,
group_type=CourseUserGroup.COHORT,
name=group_name)
name=group_name
)
user.course_groups.add(group)
return group
def get_course_cohorts(course_id):
def get_course_cohorts(course_key):
"""
Get a list of all the cohorts in the given course.
Arguments:
course_id: string in the format 'org/course/run'
course_key: CourseKey
Returns:
A list of CourseUserGroup objects. Empty if there are no cohorts. Does
not check whether the course is cohorted.
"""
return list(CourseUserGroup.objects.filter(course_id=course_id,
group_type=CourseUserGroup.COHORT))
return list(CourseUserGroup.objects.filter(
course_id=course_key,
group_type=CourseUserGroup.COHORT
))
### Helpers for cohort management views
def get_cohort_by_name(course_id, name):
def get_cohort_by_name(course_key, name):
"""
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
it isn't present.
"""
return CourseUserGroup.objects.get(course_id=course_id,
group_type=CourseUserGroup.COHORT,
name=name)
return CourseUserGroup.objects.get(
course_id=course_key,
group_type=CourseUserGroup.COHORT,
name=name
)
def get_cohort_by_id(course_id, cohort_id):
def get_cohort_by_id(course_key, cohort_id):
"""
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
it isn't present. Uses the course_id for extra validation...
it isn't present. Uses the course_key for extra validation...
"""
return CourseUserGroup.objects.get(course_id=course_id,
group_type=CourseUserGroup.COHORT,
id=cohort_id)
return CourseUserGroup.objects.get(
course_id=course_key,
group_type=CourseUserGroup.COHORT,
id=cohort_id
)
def add_cohort(course_id, name):
def add_cohort(course_key, name):
"""
Add a cohort to a course. Raises ValueError if a cohort of the same name already
exists.
"""
log.debug("Adding cohort %s to %s", name, course_id)
if CourseUserGroup.objects.filter(course_id=course_id,
log.debug("Adding cohort %s to %s", name, course_key)
if CourseUserGroup.objects.filter(course_id=course_key,
group_type=CourseUserGroup.COHORT,
name=name).exists():
raise ValueError("Can't create two cohorts with the same name")
return CourseUserGroup.objects.create(course_id=course_id,
group_type=CourseUserGroup.COHORT,
name=name)
return CourseUserGroup.objects.create(
course_id=course_key,
group_type=CourseUserGroup.COHORT,
name=name
)
class CohortConflict(Exception):
......@@ -237,9 +246,10 @@ def add_user_to_cohort(cohort, username_or_email):
previous_cohort = None
course_cohorts = CourseUserGroup.objects.filter(
course_id=cohort.course_id,
course_id=cohort.course_key,
users__id=user.id,
group_type=CourseUserGroup.COHORT)
group_type=CourseUserGroup.COHORT
)
if course_cohorts.exists():
if course_cohorts[0] == cohort:
raise ValueError("User {0} already present in cohort {1}".format(
......@@ -253,21 +263,21 @@ def add_user_to_cohort(cohort, username_or_email):
return (user, previous_cohort)
def get_course_cohort_names(course_id):
def get_course_cohort_names(course_key):
"""
Return a list of the cohort names in a course.
"""
return [c.name for c in get_course_cohorts(course_id)]
return [c.name for c in get_course_cohorts(course_key)]
def delete_empty_cohort(course_id, name):
def delete_empty_cohort(course_key, name):
"""
Remove an empty cohort. Raise ValueError if cohort is not empty.
"""
cohort = get_cohort_by_name(course_id, name)
cohort = get_cohort_by_name(course_key, name)
if cohort.users.exists():
raise ValueError(
"Can't delete non-empty cohort {0} in course {1}".format(
name, course_id))
name, course_key))
cohort.delete()
......@@ -2,6 +2,7 @@ import logging
from django.contrib.auth.models import User
from django.db import models
from xmodule_django.models import CourseKeyField
log = logging.getLogger(__name__)
......@@ -23,7 +24,8 @@ class CourseUserGroup(models.Model):
# Note: groups associated with particular runs of a course. E.g. Fall 2012 and Spring
# 2013 versions of 6.00x will have separate groups.
course_id = models.CharField(max_length=255, db_index=True,
# TODO change field name to course_key
course_id = CourseKeyField(max_length=255, db_index=True,
help_text="Which course is this group associated with?")
# For now, only have group type 'cohort', but adding a type field to support
......
......@@ -9,6 +9,7 @@ from course_groups.cohorts import (get_cohort, get_course_cohorts,
is_commentable_cohorted, get_cohort_by_name)
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.django_utils import mixed_store_config
......@@ -84,13 +85,14 @@ class TestCohorts(django.test.TestCase):
Make sure that course is reloaded every time--clear out the modulestore.
"""
clear_existing_modulestores()
self.toy_course_key = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
def test_get_cohort(self):
"""
Make sure get_cohort() does the right thing when the course is cohorted
"""
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertEqual(course.id, "edX/toy/2012_Fall")
course = modulestore().get_course(self.toy_course_key)
self.assertEqual(course.id, self.toy_course_key)
self.assertFalse(course.is_cohorted)
user = User.objects.create(username="test", email="a@b.com")
......@@ -120,8 +122,7 @@ class TestCohorts(django.test.TestCase):
"""
Make sure get_cohort() does the right thing when the course is auto_cohorted
"""
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertEqual(course.id, "edX/toy/2012_Fall")
course = modulestore().get_course(self.toy_course_key)
self.assertFalse(course.is_cohorted)
user1 = User.objects.create(username="test", email="a@b.com")
......@@ -168,8 +169,7 @@ class TestCohorts(django.test.TestCase):
"""
Make sure get_cohort() randomizes properly.
"""
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertEqual(course.id, "edX/toy/2012_Fall")
course = modulestore().get_course(self.toy_course_key)
self.assertFalse(course.is_cohorted)
groups = ["group_{0}".format(n) for n in range(5)]
......@@ -194,26 +194,26 @@ class TestCohorts(django.test.TestCase):
self.assertLess(num_users, 50)
def test_get_course_cohorts(self):
course1_id = 'a/b/c'
course2_id = 'e/f/g'
course1_key = SlashSeparatedCourseKey('a', 'b', 'c')
course2_key = SlashSeparatedCourseKey('e', 'f', 'g')
# add some cohorts to course 1
cohort = CourseUserGroup.objects.create(name="TestCohort",
course_id=course1_id,
course_id=course1_key,
group_type=CourseUserGroup.COHORT)
cohort = CourseUserGroup.objects.create(name="TestCohort2",
course_id=course1_id,
course_id=course1_key,
group_type=CourseUserGroup.COHORT)
# second course should have no cohorts
self.assertEqual(get_course_cohorts(course2_id), [])
self.assertEqual(get_course_cohorts(course2_key), [])
cohorts = sorted([c.name for c in get_course_cohorts(course1_id)])
cohorts = sorted([c.name for c in get_course_cohorts(course1_key)])
self.assertEqual(cohorts, ['TestCohort', 'TestCohort2'])
def test_is_commentable_cohorted(self):
course = modulestore().get_course("edX/toy/2012_Fall")
course = modulestore().get_course(self.toy_course_key)
self.assertFalse(course.is_cohorted)
def to_id(name):
......
......@@ -33,25 +33,25 @@ def split_by_comma_and_whitespace(s):
@ensure_csrf_cookie
def list_cohorts(request, course_id):
def list_cohorts(request, course_key):
"""
Return json dump of dict:
{'success': True,
'cohorts': [{'name': name, 'id': id}, ...]}
"""
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
all_cohorts = [{'name': c.name, 'id': c.id}
for c in cohorts.get_course_cohorts(course_id)]
for c in cohorts.get_course_cohorts(course_key)]
return json_http_response({'success': True,
'cohorts': all_cohorts})
'cohorts': all_cohorts})
@ensure_csrf_cookie
@require_POST
def add_cohort(request, course_id):
def add_cohort(request, course_key):
"""
Return json of dict:
{'success': True,
......@@ -63,7 +63,7 @@ def add_cohort(request, course_id):
{'success': False,
'msg': error_msg} if there's an error
"""
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
name = request.POST.get("name")
if not name:
......@@ -71,7 +71,7 @@ def add_cohort(request, course_id):
'msg': "No name specified"})
try:
cohort = cohorts.add_cohort(course_id, name)
cohort = cohorts.add_cohort(course_key, name)
except ValueError as err:
return json_http_response({'success': False,
'msg': str(err)})
......@@ -84,7 +84,7 @@ def add_cohort(request, course_id):
@ensure_csrf_cookie
def users_in_cohort(request, course_id, cohort_id):
def users_in_cohort(request, course_key, cohort_id):
"""
Return users in the cohort. Show up to 100 per page, and page
using the 'page' GET attribute in the call. Format:
......@@ -97,11 +97,11 @@ def users_in_cohort(request, course_id, cohort_id):
'users': [{'username': ..., 'email': ..., 'name': ...}]
}
"""
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
# this will error if called with a non-int cohort_id. That's ok--it
# shoudn't happen for valid clients.
cohort = cohorts.get_cohort_by_id(course_id, int(cohort_id))
cohort = cohorts.get_cohort_by_id(course_key, int(cohort_id))
paginator = Paginator(cohort.users.all(), 100)
page = request.GET.get('page')
......@@ -119,17 +119,17 @@ def users_in_cohort(request, course_id, cohort_id):
user_info = [{'username': u.username,
'email': u.email,
'name': '{0} {1}'.format(u.first_name, u.last_name)}
for u in users]
for u in users]
return json_http_response({'success': True,
'page': page,
'num_pages': paginator.num_pages,
'users': user_info})
'page': page,
'num_pages': paginator.num_pages,
'users': user_info})
@ensure_csrf_cookie
@require_POST
def add_users_to_cohort(request, course_id, cohort_id):
def add_users_to_cohort(request, course_key, cohort_id):
"""
Return json dict of:
......@@ -144,9 +144,9 @@ def add_users_to_cohort(request, course_id, cohort_id):
'present': [str1, str2, ...], # already there
'unknown': [str1, str2, ...]}
"""
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
cohort = cohorts.get_cohort_by_id(course_id, cohort_id)
cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
users = request.POST.get('users', '')
added = []
......@@ -175,15 +175,15 @@ def add_users_to_cohort(request, course_id, cohort_id):
unknown.append(username_or_email)
return json_http_response({'success': True,
'added': added,
'changed': changed,
'present': present,
'unknown': unknown})
'added': added,
'changed': changed,
'present': present,
'unknown': unknown})
@ensure_csrf_cookie
@require_POST
def remove_user_from_cohort(request, course_id, cohort_id):
def remove_user_from_cohort(request, course_key, cohort_id):
"""
Expects 'username': username in POST data.
......@@ -193,14 +193,14 @@ def remove_user_from_cohort(request, course_id, cohort_id):
{'success': False,
'msg': error_msg}
"""
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
username = request.POST.get('username')
if username is None:
return json_http_response({'success': False,
'msg': 'No username specified'})
'msg': 'No username specified'})
cohort = cohorts.get_cohort_by_id(course_id, cohort_id)
cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
try:
user = User.objects.get(username=username)
cohort.users.remove(user)
......@@ -208,16 +208,18 @@ def remove_user_from_cohort(request, course_id, cohort_id):
except User.DoesNotExist:
log.debug('no user')
return json_http_response({'success': False,
'msg': "No user '{0}'".format(username)})
'msg': "No user '{0}'".format(username)})
def debug_cohort_mgmt(request, course_id):
def debug_cohort_mgmt(request, course_key):
"""
Debugging view for dev.
"""
# add staff check to make sure it's safe if it's accidentally deployed.
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
context = {'cohorts_ajax_url': reverse('cohorts',
kwargs={'course_id': course_id})}
context = {'cohorts_ajax_url': reverse(
'cohorts',
kwargs={'course_id': course_key.to_deprecated_string()}
)}
return render_to_response('/course_groups/debug.html', context)
......@@ -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
......
......@@ -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)
......@@ -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):
"""
......
......@@ -9,7 +9,8 @@ from django.utils.translation import ugettext_noop
from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
FORUM_ROLE_ADMINISTRATOR = ugettext_noop('Administrator')
FORUM_ROLE_MODERATOR = ugettext_noop('Moderator')
......@@ -48,16 +49,20 @@ def assign_default_role(course_id, user):
class Role(models.Model):
objects = NoneToEmptyManager()
name = models.CharField(max_length=30, null=False, blank=False)
users = models.ManyToManyField(User, related_name="roles")
course_id = models.CharField(max_length=255, blank=True, db_index=True)
course_id = CourseKeyField(max_length=255, blank=True, db_index=True)
class Meta:
# use existing table that was originally created from django_comment_client app
db_table = 'django_comment_client_role'
def __unicode__(self):
return self.name + " for " + (self.course_id if self.course_id else "all courses")
# pylint: disable=no-member
return self.name + " for " + (self.course_id.to_deprecated_string() if self.course_id else "all courses")
def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing,
# since it's one-off and doesn't handle inheritance later
......@@ -71,8 +76,9 @@ class Role(models.Model):
self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
def has_permission(self, permission):
course_loc = CourseDescriptor.id_to_location(self.course_id)
course = modulestore().get_instance(self.course_id, course_loc)
course = modulestore().get_course(self.course_id)
if course is None:
raise ItemNotFoundError(self.course_id)
if self.name == FORUM_ROLE_STUDENT and \
(permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
(not course.forum_posts_allowed):
......
from django.test import TestCase
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from django_comment_common.models import Role
from student.models import CourseEnrollment, User
......@@ -21,13 +22,13 @@ class RoleAssignmentTest(TestCase):
"hacky",
"hacky@fake.edx.org"
)
self.course_id = "edX/Fake101/2012"
CourseEnrollment.enroll(self.staff_user, self.course_id)
CourseEnrollment.enroll(self.student_user, self.course_id)
self.course_key = SlashSeparatedCourseKey("edX", "Fake101", "2012")
CourseEnrollment.enroll(self.staff_user, self.course_key)
CourseEnrollment.enroll(self.student_user, self.course_key)
def test_enrollment_auto_role_creation(self):
student_role = Role.objects.get(
course_id=self.course_id,
course_id=self.course_key,
name="Student"
)
......
......@@ -10,27 +10,27 @@ _MODERATOR_ROLE_PERMISSIONS = ["edit_content", "delete_thread", "openclose_threa
_ADMINISTRATOR_ROLE_PERMISSIONS = ["manage_moderator"]
def _save_forum_role(course_id, name):
def _save_forum_role(course_key, name):
"""
Save and Update 'course_id' for all roles which are already created to keep course_id same
as actual passed course id
Save and Update 'course_key' for all roles which are already created to keep course_id same
as actual passed course key
"""
role, created = Role.objects.get_or_create(name=name, course_id=course_id)
role, created = Role.objects.get_or_create(name=name, course_id=course_key)
if created is False:
role.course_id = course_id
role.course_id = course_key
role.save()
return role
def seed_permissions_roles(course_id):
def seed_permissions_roles(course_key):
"""
Create and assign permissions for forum roles
"""
administrator_role = _save_forum_role(course_id, "Administrator")
moderator_role = _save_forum_role(course_id, "Moderator")
community_ta_role = _save_forum_role(course_id, "Community TA")
student_role = _save_forum_role(course_id, "Student")
administrator_role = _save_forum_role(course_key, "Administrator")
moderator_role = _save_forum_role(course_key, "Moderator")
community_ta_role = _save_forum_role(course_key, "Community TA")
student_role = _save_forum_role(course_key, "Student")
for per in _STUDENT_ROLE_PERMISSIONS:
student_role.add_permission(per)
......
......@@ -10,6 +10,8 @@ from embargo.fixtures.country_codes import COUNTRY_CODES
import socket
from xmodule.modulestore.django import modulestore
from opaque_keys import InvalidKeyError
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class EmbargoedCourseForm(forms.ModelForm): # pylint: disable=incomplete-protocol
......@@ -20,19 +22,29 @@ class EmbargoedCourseForm(forms.ModelForm): # pylint: disable=incomplete-protoc
def clean_course_id(self):
"""Validate the course id"""
course_id = self.cleaned_data["course_id"]
cleaned_id = self.cleaned_data["course_id"]
try:
course_id = SlashSeparatedCourseKey.from_deprecated_string(cleaned_id)
except InvalidKeyError:
msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(cleaned_id)
msg += 'Please recheck that you have supplied a valid course id.'
raise forms.ValidationError(msg)
# Try to get the course. If this returns None, it's not a real course
try:
course = modulestore().get_course(course_id)
except ValueError:
msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(course_id)
msg += u' --- Entered course id was: "{0}". '.format(course_id.to_deprecated_string())
msg += 'Please recheck that you have supplied a valid course id.'
raise forms.ValidationError(msg)
if not course:
msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(course_id)
msg += u' --- Entered course id was: "{0}". '.format(course_id.to_deprecated_string())
msg += 'Please recheck that you have supplied a valid course id.'
raise forms.ValidationError(msg)
......
......@@ -13,14 +13,17 @@ file and check it in at the same time as your model changes. To do that,
from django.db import models
from config_models.models import ConfigurationModel
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
class EmbargoedCourse(models.Model):
"""
Enable course embargo on a course-by-course basis.
"""
objects = NoneToEmptyManager()
# The course to embargo
course_id = models.CharField(max_length=255, db_index=True, unique=True)
course_id = CourseKeyField(max_length=255, db_index=True, unique=True)
# Whether or not to embargo
embargoed = models.BooleanField(default=False)
......@@ -42,7 +45,8 @@ class EmbargoedCourse(models.Model):
not_em = "Not "
if self.embargoed:
not_em = ""
return u"Course '{}' is {}Embargoed".format(self.course_id, not_em)
# pylint: disable=no-member
return u"Course '{}' is {}Embargoed".format(self.course_id.to_deprecated_string(), not_em)
class EmbargoedState(ConfigurationModel):
......
......@@ -22,8 +22,8 @@ class EmbargoCourseFormTest(ModuleStoreTestCase):
def setUp(self):
self.course = CourseFactory.create()
self.true_form_data = {'course_id': self.course.id, 'embargoed': True}
self.false_form_data = {'course_id': self.course.id, 'embargoed': False}
self.true_form_data = {'course_id': self.course.id.to_deprecated_string(), 'embargoed': True}
self.false_form_data = {'course_id': self.course.id.to_deprecated_string(), 'embargoed': False}
def test_embargo_course(self):
self.assertFalse(EmbargoedCourse.is_embargoed(self.course.id))
......@@ -62,7 +62,7 @@ class EmbargoCourseFormTest(ModuleStoreTestCase):
def test_form_typo(self):
# Munge course id
bad_id = self.course.id + '_typo'
bad_id = self.course.id.to_deprecated_string() + '_typo'
form_data = {'course_id': bad_id, 'embargoed': True}
form = EmbargoedCourseForm(data=form_data)
......@@ -79,7 +79,7 @@ class EmbargoCourseFormTest(ModuleStoreTestCase):
def test_invalid_location(self):
# Munge course id
bad_id = self.course.id.split('/')[-1]
bad_id = self.course.id.to_deprecated_string().split('/')[-1]
form_data = {'course_id': bad_id, 'embargoed': True}
form = EmbargoedCourseForm(data=form_data)
......
......@@ -32,8 +32,8 @@ class EmbargoMiddlewareTests(TestCase):
self.embargo_course.save()
self.regular_course = CourseFactory.create(org="Regular")
self.regular_course.save()
self.embargoed_page = '/courses/' + self.embargo_course.id + '/info'
self.regular_page = '/courses/' + self.regular_course.id + '/info'
self.embargoed_page = '/courses/' + self.embargo_course.id.to_deprecated_string() + '/info'
self.regular_page = '/courses/' + self.regular_course.id.to_deprecated_string() + '/info'
EmbargoedCourse(course_id=self.embargo_course.id, embargoed=True).save()
EmbargoedState(
embargoed_countries="cu, ir, Sy, SD",
......
"""Test of models for embargo middleware app"""
from django.test import TestCase
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
class EmbargoModelsTest(TestCase):
"""Test each of the 3 models in embargo.models"""
def test_course_embargo(self):
course_id = 'abc/123/doremi'
course_id = SlashSeparatedCourseKey('abc', '123', 'doremi')
# Test that course is not authorized by default
self.assertFalse(EmbargoedCourse.is_embargoed(course_id))
......
......@@ -19,6 +19,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import editable_modulestore
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from external_auth.models import ExternalAuthMap
from external_auth.views import shib_login, course_specific_login, course_specific_register, _flatten_to_ascii
......@@ -340,8 +341,8 @@ class ShibSPTest(ModuleStoreTestCase):
'?course_id=MITx/999/course/Robot_Super_Course' +
'&enrollment_action=enroll')
login_response = course_specific_login(login_request, 'MITx/999/Robot_Super_Course')
reg_response = course_specific_register(login_request, 'MITx/999/Robot_Super_Course')
login_response = course_specific_login(login_request, SlashSeparatedCourseKey('MITx', '999', 'Robot_Super_Course'))
reg_response = course_specific_register(login_request, SlashSeparatedCourseKey('MITx', '999', 'Robot_Super_Course'))
if "shib" in domain:
self.assertIsInstance(login_response, HttpResponseRedirect)
......@@ -375,8 +376,8 @@ class ShibSPTest(ModuleStoreTestCase):
'?course_id=DNE/DNE/DNE/Robot_Super_Course' +
'&enrollment_action=enroll')
login_response = course_specific_login(login_request, 'DNE/DNE/DNE')
reg_response = course_specific_register(login_request, 'DNE/DNE/DNE')
login_response = course_specific_login(login_request, SlashSeparatedCourseKey('DNE', 'DNE', 'DNE'))
reg_response = course_specific_register(login_request, SlashSeparatedCourseKey('DNE', 'DNE', 'DNE'))
self.assertIsInstance(login_response, HttpResponseRedirect)
self.assertEqual(login_response['Location'],
......@@ -436,7 +437,7 @@ class ShibSPTest(ModuleStoreTestCase):
for student in [shib_student, other_ext_student, int_student]:
request = self.request_factory.post('/change_enrollment')
request.POST.update({'enrollment_action': 'enroll',
'course_id': course.id})
'course_id': course.id.to_deprecated_string()})
request.user = student
response = change_enrollment(request)
# If course is not limited or student has correct shib extauth then enrollment should be allowed
......@@ -476,7 +477,7 @@ class ShibSPTest(ModuleStoreTestCase):
self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
self.client.logout()
request_kwargs = {'path': '/shib-login/',
'data': {'enrollment_action': 'enroll', 'course_id': course.id, 'next': '/testredirect'},
'data': {'enrollment_action': 'enroll', 'course_id': course.id.to_deprecated_string(), 'next': '/testredirect'},
'follow': False,
'REMOTE_USER': 'testuser@stanford.edu',
'Shib-Identity-Provider': 'https://idp.stanford.edu/'}
......
......@@ -3,8 +3,6 @@ Provides unit tests for SSL based authentication portions
of the external_auth app.
"""
import logging
import StringIO
import unittest
from django.conf import settings
......@@ -22,7 +20,7 @@ from edxmako.middleware import MakoMiddleware
from external_auth.models import ExternalAuthMap
import external_auth.views
from student.tests.factories import UserFactory
from xmodule.modulestore.exceptions import InsufficientSpecificationError
from opaque_keys import InvalidKeyError
FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy()
FEATURES_WITH_SSL_AUTH['AUTH_USE_CERTIFICATES'] = True
......@@ -193,18 +191,23 @@ class SSLClientTest(TestCase):
This tests to make sure when immediate signup is on that
the user doesn't get presented with the registration page.
"""
# Expect a NotImplementError from course page as we don't have anything else built
with self.assertRaisesRegexp(InsufficientSpecificationError,
'Must provide one of url, version_guid, package_id'):
# Expect an InvalidKeyError from course page as we don't have anything else built
with self.assertRaisesRegexp(
InvalidKeyError,
"<class 'xmodule.modulestore.keys.CourseKey'>: None"
):
self.client.get(
reverse('signup'), follow=True,
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
)
# assert that we are logged in
self.assertIn(SESSION_KEY, self.client.session)
# Now that we are logged in, make sure we don't see the registration page
with self.assertRaisesRegexp(InsufficientSpecificationError,
'Must provide one of url, version_guid, package_id'):
with self.assertRaisesRegexp(
InvalidKeyError,
"<class 'xmodule.modulestore.keys.CourseKey'>: None"
):
self.client.get(reverse('signup'), follow=True)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
......@@ -228,7 +231,6 @@ class SSLClientTest(TestCase):
self.assertIn(reverse('dashboard'), response['location'])
self.assertIn(SESSION_KEY, self.client.session)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
def test_ssl_bad_eamap(self):
......
......@@ -576,9 +576,8 @@ def course_specific_login(request, course_id):
Dispatcher function for selecting the specific login method
required by the course
"""
try:
course = course_from_id(course_id)
except ItemNotFoundError:
course = student.views.course_from_id(course_id)
if not course:
# couldn't find the course, will just return vanilla signin page
return _redirect_with_get_querydict('signin_user', request.GET)
......@@ -595,9 +594,9 @@ def course_specific_register(request, course_id):
Dispatcher function for selecting the specific registration method
required by the course
"""
try:
course = course_from_id(course_id)
except ItemNotFoundError:
course = student.views.course_from_id(course_id)
if not course:
# couldn't find the course, will just return vanilla registration page
return _redirect_with_get_querydict('register_user', request.GET)
......@@ -934,9 +933,3 @@ def provider_xrds(request):
# custom XRDS header necessary for discovery process
response['X-XRDS-Location'] = get_xrds_url('xrds', request)
return response
def course_from_id(course_id):
"""Return the CourseDescriptor corresponding to this course_id"""
course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_instance(course_id, course_loc)
......@@ -13,6 +13,6 @@ def heartbeat(request):
"""
output = {
'date': datetime.now(UTC).isoformat(),
'courses': [course.location.url() for course in modulestore().get_courses()],
'courses': [course.location.to_deprecated_string() for course in modulestore().get_courses()],
}
return HttpResponse(json.dumps(output, indent=4))
......@@ -7,6 +7,7 @@ import pytz
from django.core.exceptions import ValidationError
from django.db import models
from util.validate_on_save import ValidateOnSaveMixin
from xmodule_django.models import CourseKeyField
class MidcourseReverificationWindow(ValidateOnSaveMixin, models.Model):
......@@ -17,7 +18,7 @@ class MidcourseReverificationWindow(ValidateOnSaveMixin, models.Model):
overlapping time ranges. This is enforced by this class's clean() method.
"""
# the course that this window is attached to
course_id = models.CharField(max_length=255, db_index=True)
course_id = CourseKeyField(max_length=255, db_index=True)
start_date = models.DateTimeField(default=None, null=True, blank=True)
end_date = models.DateTimeField(default=None, null=True, blank=True)
......
......@@ -5,6 +5,7 @@ from reverification.models import MidcourseReverificationWindow
from factory.django import DjangoModelFactory
import pytz
from datetime import timedelta, datetime
from xmodule.modulestore.locations import SlashSeparatedCourseKey
# Factories don't have __init__ methods, and are self documenting
......@@ -13,7 +14,7 @@ class MidcourseReverificationWindowFactory(DjangoModelFactory):
""" Creates a generic MidcourseReverificationWindow. """
FACTORY_FOR = MidcourseReverificationWindow
course_id = u'MITx/999/Robot_Super_Course'
course_id = SlashSeparatedCourseKey.from_deprecated_string(u'MITx/999/Robot_Super_Course')
# By default this factory creates a window that is currently open
start_date = datetime.now(pytz.UTC) - timedelta(days=100)
end_date = datetime.now(pytz.UTC) + timedelta(days=100)
......@@ -72,7 +72,7 @@ def replace_jump_to_id_urls(text, course_id, jump_to_id_base_url):
return re.sub(_url_replace_regex('/jump_to_id/'), replace_jump_to_id_url, text)
def replace_course_urls(text, course_id):
def replace_course_urls(text, course_key):
"""
Replace /course/$stuff urls with /courses/$course_id/$stuff urls
......@@ -82,6 +82,8 @@ def replace_course_urls(text, course_id):
returns: text with the links replaced
"""
course_id = course_key.to_deprecated_string()
def replace_course_url(match):
quote = match.group('quote')
rest = match.group('rest')
......
......@@ -4,12 +4,13 @@ from nose.tools import assert_equals, assert_true, assert_false # pylint: disab
from static_replace import (replace_static_urls, replace_course_urls,
_url_replace_regex)
from mock import patch, Mock
from xmodule.modulestore import Location
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.xml import XMLModuleStore
DATA_DIRECTORY = 'data_dir'
COURSE_ID = 'org/course/run'
COURSE_KEY = SlashSeparatedCourseKey('org', 'course', 'run')
STATIC_SOURCE = '"/static/file.png"'
......@@ -21,8 +22,8 @@ def test_multi_replace():
replace_static_urls(replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY), DATA_DIRECTORY)
)
assert_equals(
replace_course_urls(course_source, COURSE_ID),
replace_course_urls(replace_course_urls(course_source, COURSE_ID), COURSE_ID)
replace_course_urls(course_source, COURSE_KEY),
replace_course_urls(replace_course_urls(course_source, COURSE_KEY), COURSE_KEY)
)
......@@ -59,10 +60,10 @@ def test_mongo_filestore(mock_modulestore, mock_static_content):
# Namespace => content url
assert_equals(
'"' + mock_static_content.convert_legacy_static_url_with_course_id.return_value + '"',
replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY, course_id=COURSE_ID)
replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY, course_id=COURSE_KEY)
)
mock_static_content.convert_legacy_static_url_with_course_id.assert_called_once_with('file.png', COURSE_ID)
mock_static_content.convert_legacy_static_url_with_course_id.assert_called_once_with('file.png', COURSE_KEY)
@patch('static_replace.settings')
......@@ -101,7 +102,7 @@ def test_static_url_with_query(mock_modulestore, mock_storage):
pre_text = 'EMBED src ="/static/LAlec04_controller.swf?csConfigFile=/c4x/org/course/asset/LAlec04_config.xml"'
post_text = 'EMBED src ="/c4x/org/course/asset/LAlec04_controller.swf?csConfigFile=/c4x/org/course/asset/LAlec04_config.xml"'
assert_equals(post_text, replace_static_urls(pre_text, DATA_DIRECTORY, COURSE_ID))
assert_equals(post_text, replace_static_urls(pre_text, DATA_DIRECTORY, COURSE_KEY))
def test_regex():
......
......@@ -35,7 +35,7 @@ def has_access(user, role):
return True
# if not, then check inferred permissions
if (isinstance(role, (CourseStaffRole, CourseBetaTesterRole)) and
CourseInstructorRole(role.location).has_user(user)):
CourseInstructorRole(role.course_key).has_user(user)):
return True
return False
......@@ -81,6 +81,6 @@ def _check_caller_authority(caller, role):
if isinstance(role, (GlobalStaff, CourseCreatorRole)):
raise PermissionDenied
elif isinstance(role, CourseRole): # instructors can change the roles w/in their course
if not has_access(caller, CourseInstructorRole(role.location)):
if not has_access(caller, CourseInstructorRole(role.course_key)):
raise PermissionDenied
......@@ -59,7 +59,7 @@ class Command(BaseCommand):
for student in students:
csv_writer.writerow((
student.id,
anonymous_id_for_user(student, ''),
anonymous_id_for_user(student, None),
anonymous_id_for_user(student, course_id)
))
except IOError:
......
......@@ -5,6 +5,9 @@ from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from django.utils import translation
from opaque_keys import InvalidKeyError
from xmodule.modulestore.keys import CourseKey
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from student.models import CourseEnrollment, Registration, create_comments_service_user
from student.views import _do_create_account, AccountValidationError
from track.management.tracked_command import TrackedCommand
......@@ -68,6 +71,15 @@ class Command(TrackedCommand):
if not name:
name = options['email'].split('@')[0]
# parse out the course into a coursekey
if options['course']:
try:
course = CourseKey.from_string(options['course'])
# if it's not a new-style course key, parse it from an old-style
# course key
except InvalidKeyError:
course = SlashSeparatedCourseKey.from_deprecated_string(options['course'])
post_data = {
'username': username,
'email': options['email'],
......@@ -93,5 +105,5 @@ class Command(TrackedCommand):
print e.message
user = User.objects.get(email=options['email'])
if options['course']:
CourseEnrollment.enroll(user, options['course'], mode=options['mode'])
CourseEnrollment.enroll(user, course, mode=options['mode'])
translation.deactivate()
......@@ -5,12 +5,12 @@ import mock
from django.test import TestCase
from django.contrib.auth.models import User, AnonymousUser
from xmodule.modulestore import Location
from django.core.exceptions import PermissionDenied
from student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole
from student.tests.factories import AdminFactory
from student.auth import has_access, add_users, remove_users
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class CreatorGroupTest(TestCase):
......@@ -137,54 +137,54 @@ class CourseGroupTest(TestCase):
self.global_admin = AdminFactory()
self.creator = User.objects.create_user('testcreator', 'testcreator+courses@edx.org', 'foo')
self.staff = User.objects.create_user('teststaff', 'teststaff+courses@edx.org', 'foo')
self.location = Location('i4x', 'mitX', '101', 'course', 'test')
self.course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
def test_add_user_to_course_group(self):
"""
Tests adding user to course group (happy path).
"""
# Create groups for a new course (and assign instructor role to the creator).
self.assertFalse(has_access(self.creator, CourseInstructorRole(self.location)))
add_users(self.global_admin, CourseInstructorRole(self.location), self.creator)
add_users(self.global_admin, CourseStaffRole(self.location), self.creator)
self.assertTrue(has_access(self.creator, CourseInstructorRole(self.location)))
self.assertFalse(has_access(self.creator, CourseInstructorRole(self.course_key)))
add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator)
add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator)
self.assertTrue(has_access(self.creator, CourseInstructorRole(self.course_key)))
# Add another user to the staff role.
self.assertFalse(has_access(self.staff, CourseStaffRole(self.location)))
add_users(self.creator, CourseStaffRole(self.location), self.staff)
self.assertTrue(has_access(self.staff, CourseStaffRole(self.location)))
self.assertFalse(has_access(self.staff, CourseStaffRole(self.course_key)))
add_users(self.creator, CourseStaffRole(self.course_key), self.staff)
self.assertTrue(has_access(self.staff, CourseStaffRole(self.course_key)))
def test_add_user_to_course_group_permission_denied(self):
"""
Verifies PermissionDenied if caller of add_user_to_course_group is not instructor role.
"""
add_users(self.global_admin, CourseInstructorRole(self.location), self.creator)
add_users(self.global_admin, CourseStaffRole(self.location), self.creator)
add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator)
add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator)
with self.assertRaises(PermissionDenied):
add_users(self.staff, CourseStaffRole(self.location), self.staff)
add_users(self.staff, CourseStaffRole(self.course_key), self.staff)
def test_remove_user_from_course_group(self):
"""
Tests removing user from course group (happy path).
"""
add_users(self.global_admin, CourseInstructorRole(self.location), self.creator)
add_users(self.global_admin, CourseStaffRole(self.location), self.creator)
add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator)
add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator)
add_users(self.creator, CourseStaffRole(self.location), self.staff)
self.assertTrue(has_access(self.staff, CourseStaffRole(self.location)))
add_users(self.creator, CourseStaffRole(self.course_key), self.staff)
self.assertTrue(has_access(self.staff, CourseStaffRole(self.course_key)))
remove_users(self.creator, CourseStaffRole(self.location), self.staff)
self.assertFalse(has_access(self.staff, CourseStaffRole(self.location)))
remove_users(self.creator, CourseStaffRole(self.course_key), self.staff)
self.assertFalse(has_access(self.staff, CourseStaffRole(self.course_key)))
remove_users(self.creator, CourseInstructorRole(self.location), self.creator)
self.assertFalse(has_access(self.creator, CourseInstructorRole(self.location)))
remove_users(self.creator, CourseInstructorRole(self.course_key), self.creator)
self.assertFalse(has_access(self.creator, CourseInstructorRole(self.course_key)))
def test_remove_user_from_course_group_permission_denied(self):
"""
Verifies PermissionDenied if caller of remove_user_from_course_group is not instructor role.
"""
add_users(self.global_admin, CourseInstructorRole(self.location), self.creator)
add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator)
another_staff = User.objects.create_user('another', 'teststaff+anothercourses@edx.org', 'foo')
add_users(self.global_admin, CourseStaffRole(self.location), self.creator, self.staff, another_staff)
add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator, self.staff, another_staff)
with self.assertRaises(PermissionDenied):
remove_users(self.staff, CourseStaffRole(self.location), another_staff)
remove_users(self.staff, CourseStaffRole(self.course_key), another_staff)
......@@ -6,6 +6,7 @@ from django_comment_common.models import (
from django_comment_common.utils import seed_permissions_roles
from student.models import CourseEnrollment, UserProfile
from util.testing import UrlResetMixin
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from mock import patch
......@@ -23,6 +24,8 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
super(AutoAuthEnabledTestCase, self).setUp()
self.url = '/auto_auth'
self.client = Client()
self.course_id = 'edX/Test101/2014_Spring'
self.course_key = SlashSeparatedCourseKey.from_deprecated_string(self.course_id)
def test_create_user(self):
"""
......@@ -83,43 +86,39 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
def test_course_enrollment(self):
# Create a user and enroll in a course
course_id = "edX/Test101/2014_Spring"
self._auto_auth(username='test', course_id=course_id)
self._auto_auth(username='test', course_id=self.course_id)
# Check that a course enrollment was created for the user
self.assertEqual(CourseEnrollment.objects.count(), 1)
enrollment = CourseEnrollment.objects.get(course_id=course_id)
enrollment = CourseEnrollment.objects.get(course_id=self.course_key)
self.assertEqual(enrollment.user.username, "test")
def test_double_enrollment(self):
# Create a user and enroll in a course
course_id = "edX/Test101/2014_Spring"
self._auto_auth(username='test', course_id=course_id)
self._auto_auth(username='test', course_id=self.course_id)
# Make the same call again, re-enrolling the student in the same course
self._auto_auth(username='test', course_id=course_id)
self._auto_auth(username='test', course_id=self.course_id)
# Check that only one course enrollment was created for the user
self.assertEqual(CourseEnrollment.objects.count(), 1)
enrollment = CourseEnrollment.objects.get(course_id=course_id)
enrollment = CourseEnrollment.objects.get(course_id=self.course_key)
self.assertEqual(enrollment.user.username, "test")
def test_set_roles(self):
course_id = "edX/Test101/2014_Spring"
seed_permissions_roles(course_id)
course_roles = dict((r.name, r) for r in Role.objects.filter(course_id=course_id))
seed_permissions_roles(self.course_key)
course_roles = dict((r.name, r) for r in Role.objects.filter(course_id=self.course_key))
self.assertEqual(len(course_roles), 4) # sanity check
# Student role is assigned by default on course enrollment.
self._auto_auth(username='a_student', course_id=course_id)
self._auto_auth(username='a_student', course_id=self.course_id)
user = User.objects.get(username='a_student')
user_roles = user.roles.all()
self.assertEqual(len(user_roles), 1)
self.assertEqual(user_roles[0], course_roles[FORUM_ROLE_STUDENT])
self._auto_auth(username='a_moderator', course_id=course_id, roles='Moderator')
self._auto_auth(username='a_moderator', course_id=self.course_id, roles='Moderator')
user = User.objects.get(username='a_moderator')
user_roles = user.roles.all()
self.assertEqual(
......@@ -128,7 +127,7 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
course_roles[FORUM_ROLE_MODERATOR]]))
# check multiple roles work.
self._auto_auth(username='an_admin', course_id=course_id,
self._auto_auth(username='an_admin', course_id=self.course_id,
roles='{},{}'.format(FORUM_ROLE_MODERATOR, FORUM_ROLE_ADMINISTRATOR))
user = User.objects.get(username='an_admin')
user_roles = user.roles.all()
......
......@@ -15,6 +15,7 @@ from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from bulk_email.models import CourseAuthorization
......@@ -100,7 +101,10 @@ class TestStudentDashboardEmailViewXMLBacked(ModuleStoreTestCase):
# Create student account
student = UserFactory.create()
CourseEnrollmentFactory.create(user=student, course_id=self.course_name)
CourseEnrollmentFactory.create(
user=student,
course_id=SlashSeparatedCourseKey.from_deprecated_string(self.course_name)
)
self.client.login(username=student.username, password="test")
try:
......
......@@ -20,6 +20,7 @@ from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import editable_modulestore
from external_auth.models import ExternalAuthMap
from xmodule.modulestore.locations import SlashSeparatedCourseKey
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
......@@ -275,7 +276,10 @@ class UtilFnTest(TestCase):
COURSE_ID = u'org/num/run' # pylint: disable=C0103
COURSE_URL = u'/courses/{}/otherstuff'.format(COURSE_ID) # pylint: disable=C0103
NON_COURSE_URL = u'/blahblah' # pylint: disable=C0103
self.assertEqual(_parse_course_id_from_string(COURSE_URL), COURSE_ID)
self.assertEqual(
_parse_course_id_from_string(COURSE_URL),
SlashSeparatedCourseKey.from_deprecated_string(COURSE_ID)
)
self.assertIsNone(_parse_course_id_from_string(NON_COURSE_URL))
......@@ -320,7 +324,7 @@ class ExternalAuthShibTest(ModuleStoreTestCase):
"""
Tests the _get_course_enrollment_domain utility function
"""
self.assertIsNone(_get_course_enrollment_domain("I/DONT/EXIST"))
self.assertIsNone(_get_course_enrollment_domain(SlashSeparatedCourseKey("I", "DONT", "EXIST")))
self.assertIsNone(_get_course_enrollment_domain(self.course.id))
self.assertEqual(self.shib_course.enrollment_domain, _get_course_enrollment_domain(self.shib_course.id))
......@@ -340,7 +344,7 @@ class ExternalAuthShibTest(ModuleStoreTestCase):
Tests the redirects when visiting course-specific URL with @login_required.
Should vary by course depending on its enrollment_domain
"""
TARGET_URL = reverse('courseware', args=[self.course.id]) # pylint: disable=C0103
TARGET_URL = reverse('courseware', args=[self.course.id.to_deprecated_string()]) # pylint: disable=C0103
noshib_response = self.client.get(TARGET_URL, follow=True)
self.assertEqual(noshib_response.redirect_chain[-1],
('http://testserver/accounts/login?next={url}'.format(url=TARGET_URL), 302))
......@@ -348,7 +352,7 @@ class ExternalAuthShibTest(ModuleStoreTestCase):
.format(platform_name=settings.PLATFORM_NAME)))
self.assertEqual(noshib_response.status_code, 200)
TARGET_URL_SHIB = reverse('courseware', args=[self.shib_course.id]) # pylint: disable=C0103
TARGET_URL_SHIB = reverse('courseware', args=[self.shib_course.id.to_deprecated_string()]) # pylint: disable=C0103
shib_response = self.client.get(**{'path': TARGET_URL_SHIB,
'follow': True,
'REMOTE_USER': self.extauth.external_id,
......
......@@ -53,7 +53,7 @@ class UserStandingTest(TestCase):
try:
self.some_url = reverse('dashboard')
except NoReverseMatch:
self.some_url = '/course'
self.some_url = '/course/'
# since it's only possible to disable accounts from lms, we're going
# to skip tests for cms
......
......@@ -55,6 +55,7 @@ from dark_lang.models import DarkLangConfig
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore import XML_MODULESTORE_TYPE, Location
from collections import namedtuple
......@@ -132,8 +133,7 @@ def index(request, extra_context={}, user=AnonymousUser()):
def course_from_id(course_id):
"""Return the CourseDescriptor corresponding to this course_id"""
course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_instance(course_id, course_loc)
return modulestore().get_course(course_id)
day_pattern = re.compile(r'\s\d+,\s')
multimonth_pattern = re.compile(r'\s?\-\s?\S+\s')
......@@ -269,8 +269,8 @@ def get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set):
a student's dashboard.
"""
for enrollment in CourseEnrollment.enrollments_for_user(user):
try:
course = course_from_id(enrollment.course_id)
course = course_from_id(enrollment.course_id)
if course:
# if we are in a Microsite, then filter out anything that is not
# attributed (by ORG) to that Microsite
......@@ -282,7 +282,7 @@ def get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set):
continue
yield (course, enrollment)
except ItemNotFoundError:
else:
log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id))
......@@ -478,13 +478,13 @@ def dashboard(request):
# Global staff can see what courses errored on their dashboard
staff_access = False
errored_courses = {}
if has_access(user, 'global', 'staff'):
if has_access(user, 'staff', 'global'):
# Show any courses that errored on load
staff_access = True
errored_courses = modulestore().get_errored_courses()
show_courseware_links_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
if has_access(request.user, course, 'load'))
if has_access(request.user, 'load', course))
course_modes = {course.id: complete_course_mode_info(course.id, enrollment) for course, enrollment in course_enrollment_pairs}
cert_statuses = {course.id: cert_info(request.user, course) for course, _enrollment in course_enrollment_pairs}
......@@ -617,10 +617,11 @@ def change_enrollment(request):
user = request.user
action = request.POST.get("enrollment_action")
course_id = request.POST.get("course_id")
if course_id is None:
if 'course_id' not in request.POST:
return HttpResponseBadRequest(_("Course id not specified"))
course_id = SlashSeparatedCourseKey.from_deprecated_string(request.POST.get("course_id"))
if not user.is_authenticated():
return HttpResponseForbidden()
......@@ -634,7 +635,7 @@ def change_enrollment(request):
.format(user.username, course_id))
return HttpResponseBadRequest(_("Course id is invalid"))
if not has_access(user, course, 'enroll'):
if not has_access(user, 'enroll', course):
return HttpResponseBadRequest(_("Enrollment is closed"))
# see if we have already filled up all allowed enrollments
......@@ -648,7 +649,7 @@ def change_enrollment(request):
available_modes = CourseMode.modes_for_course(course_id)
if len(available_modes) > 1:
return HttpResponse(
reverse("course_modes_choose", kwargs={'course_id': course_id})
reverse("course_modes_choose", kwargs={'course_id': course_id.to_deprecated_string()})
)
current_mode = available_modes[0]
......@@ -664,7 +665,7 @@ def change_enrollment(request):
# the user to the shopping cart page always, where they can reasonably discern the status of their cart,
# whether things got added, etc
shoppingcart.views.add_course_to_cart(request, course_id)
shoppingcart.views.add_course_to_cart(request, course_id.to_deprecated_string())
return HttpResponse(
reverse("shoppingcart.views.show_cart")
)
......@@ -686,7 +687,7 @@ def _parse_course_id_from_string(input_str):
"""
m_obj = re.match(r'^/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)', input_str)
if m_obj:
return m_obj.group('course_id')
return SlashSeparatedCourseKey.from_deprecated_string(m_obj.group('course_id'))
return None
......@@ -696,12 +697,12 @@ def _get_course_enrollment_domain(course_id):
@param course_id:
@return:
"""
try:
course = course_from_id(course_id)
return course.enrollment_domain
except ItemNotFoundError:
course = course_from_id(course_id)
if course is None:
return None
return course.enrollment_domain
@ensure_csrf_cookie
def accounts_login(request):
......@@ -1378,6 +1379,9 @@ def auto_auth(request):
full_name = request.GET.get('full_name', username)
is_staff = request.GET.get('staff', None)
course_id = request.GET.get('course_id', None)
course_key = None
if course_id:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
role_names = [v.strip() for v in request.GET.get('roles', '').split(',') if v.strip()]
# Get or create the user object
......@@ -1413,12 +1417,12 @@ def auto_auth(request):
reg.save()
# Enroll the user in a course
if course_id is not None:
CourseEnrollment.enroll(user, course_id)
if course_key is not None:
CourseEnrollment.enroll(user, course_key)
# Apply the roles
for role_name in role_names:
role = Role.objects.get(name=role_name, course_id=course_id)
role = Role.objects.get(name=role_name, course_id=course_key)
user.roles.add(role)
# Log in as the user
......@@ -1865,15 +1869,16 @@ def change_email_settings(request):
user = request.user
course_id = request.POST.get("course_id")
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
receive_emails = request.POST.get("receive_emails")
if receive_emails:
optout_object = Optout.objects.filter(user=user, course_id=course_id)
optout_object = Optout.objects.filter(user=user, course_id=course_key)
if optout_object:
optout_object.delete()
log.info(u"User {0} ({1}) opted in to receive emails from course {2}".format(user.username, user.email, course_id))
track.views.server_track(request, "change-email-settings", {"receive_emails": "yes", "course": course_id}, page='dashboard')
else:
Optout.objects.get_or_create(user=user, course_id=course_id)
Optout.objects.get_or_create(user=user, course_id=course_key)
log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(user.username, user.email, course_id))
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
......@@ -1889,7 +1894,7 @@ def token(request):
the token was issued. This will be stored with the user along with
the id for identification purposes in the backend.
'''
course_id = request.GET.get("course_id")
course_id = SlashSeparatedCourseKey.from_deprecated_string(request.GET.get("course_id"))
course = course_from_id(course_id)
dtnow = datetime.datetime.now()
dtutcnow = datetime.datetime.utcnow()
......
"""Generates common contexts"""
import logging
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from opaque_keys import InvalidKeyError
from util.request import COURSE_REGEX
log = logging.getLogger(__name__)
......@@ -9,15 +10,24 @@ log = logging.getLogger(__name__)
def course_context_from_url(url):
"""
Extracts the course_id from the given `url` and passes it on to
Extracts the course_context from the given `url` and passes it on to
`course_context_from_course_id()`.
"""
url = url or ''
match = COURSE_REGEX.match(url)
course_id = ''
course_id = None
if match:
course_id = match.group('course_id') or ''
course_id_string = match.group('course_id')
try:
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id_string)
except InvalidKeyError:
log.warning(
'unable to parse course_id "{course_id}"'.format(
course_id=course_id_string
),
exc_info=True
)
return course_context_from_course_id(course_id)
......@@ -34,23 +44,12 @@ def course_context_from_course_id(course_id):
}
"""
course_id = course_id or ''
context = {
'course_id': course_id,
'org_id': ''
if course_id is None:
return {'course_id': '', 'org_id': ''}
# TODO: Make this accept any CourseKey, and serialize it using .to_string
assert(isinstance(course_id, SlashSeparatedCourseKey))
return {
'course_id': course_id.to_deprecated_string(),
'org_id': course_id.org,
}
if course_id:
try:
location = CourseDescriptor.id_to_location(course_id)
context['org_id'] = location.org
except ValueError:
log.warning(
'Unable to parse course_id "{course_id}"'.format(
course_id=course_id
),
exc_info=True
)
return context
......@@ -5,6 +5,7 @@ Adds user's tags to tracking event context.
from track.contexts import COURSE_REGEX
from eventtracking import tracker
from user_api.models import UserCourseTag
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class UserTagsEventContextMiddleware(object):
......@@ -19,6 +20,7 @@ class UserTagsEventContextMiddleware(object):
course_id = None
if match:
course_id = match.group('course_id')
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
context = {}
......@@ -29,7 +31,7 @@ class UserTagsEventContextMiddleware(object):
context['course_user_tags'] = dict(
UserCourseTag.objects.filter(
user=request.user.pk,
course_id=course_id
course_id=course_key,
).values_list('key', 'value')
)
else:
......
......@@ -2,6 +2,8 @@ from django.contrib.auth.models import User
from django.core.validators import RegexValidator
from django.db import models
from xmodule_django.models import CourseKeyField
class UserPreference(models.Model):
"""A user's preference, stored as generic text to be processed by client"""
......@@ -44,7 +46,7 @@ class UserCourseTag(models.Model):
"""
user = models.ForeignKey(User, db_index=True, related_name="+")
key = models.CharField(max_length=255, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
course_id = CourseKeyField(max_length=255, db_index=True)
value = models.TextField()
class Meta: # pylint: disable=missing-docstring
......
......@@ -3,6 +3,7 @@ from factory.django import DjangoModelFactory
from factory import SubFactory
from student.tests.factories import UserFactory
from user_api.models import UserPreference, UserCourseTag
from xmodule.modulestore.locations import SlashSeparatedCourseKey
# Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232, C0111
......@@ -18,6 +19,6 @@ class UserCourseTagFactory(DjangoModelFactory):
FACTORY_FOR = UserCourseTag
user = SubFactory(UserFactory)
course_id = 'org/course/run'
course_id = SlashSeparatedCourseKey('org', 'course', 'run')
key = None
value = None
......@@ -5,6 +5,7 @@ from django.test import TestCase
from student.tests.factories import UserFactory
from user_api import user_service
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class TestUserService(TestCase):
......@@ -13,7 +14,7 @@ class TestUserService(TestCase):
"""
def setUp(self):
self.user = UserFactory.create()
self.course_id = 'test_org/test_course_number/test_run'
self.course_id = SlashSeparatedCourseKey('test_org', 'test_course_number', 'test_run')
self.test_key = 'test_key'
def test_get_set_course_tag(self):
......
......@@ -3,6 +3,7 @@ import re
from django.conf import settings
from microsite_configuration import microsite
from xmodule.modulestore.locations import SlashSeparatedCourseKey
COURSE_REGEX = re.compile(r'^.*?/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)')
......@@ -26,11 +27,17 @@ def course_id_from_url(url):
"""
Extracts the course_id from the given `url`.
"""
url = url or ''
if not url:
return None
match = COURSE_REGEX.match(url)
course_id = ''
if match:
course_id = match.group('course_id') or ''
return course_id
if match is None:
return None
course_id = match.group('course_id')
if course_id is None:
return None
return SlashSeparatedCourseKey.from_deprecated_string(course_id)
......@@ -18,7 +18,7 @@ from xmodule.vertical_module import VerticalModule
from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule
from lms.lib.xblock.runtime import quote_slashes
from xmodule.modulestore import MONGO_MODULESTORE_TYPE
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore.django import modulestore
log = logging.getLogger(__name__)
......@@ -33,7 +33,7 @@ def wrap_fragment(fragment, new_content):
return wrapper_frag
def wrap_xblock(runtime_class, block, view, frag, context, display_name_only=False, extra_data=None): # pylint: disable=unused-argument
def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer, display_name_only=False, extra_data=None): # pylint: disable=unused-argument
"""
Wraps the results of rendering an XBlock view in a standard <section> with identifying
data so that the appropriate javascript module can be loaded onto it.
......@@ -43,6 +43,8 @@ def wrap_xblock(runtime_class, block, view, frag, context, display_name_only=Fal
:param view: The name of the view that rendered the fragment being wrapped
:param frag: The :class:`Fragment` to be wrapped
:param context: The context passed to the view being rendered
:param usage_id_serializer: A function to serialize the block's usage_id for use by the
front-end Javascript Runtime.
:param display_name_only: If true, don't render the fragment content at all.
Instead, just render the `display_name` of `block`
:param extra_data: A dictionary with extra data values to be set on the wrapper
......@@ -74,13 +76,14 @@ def wrap_xblock(runtime_class, block, view, frag, context, display_name_only=Fal
data['runtime-class'] = runtime_class
data['runtime-version'] = frag.js_init_version
data['block-type'] = block.scope_ids.block_type
data['usage-id'] = quote_slashes(unicode(block.scope_ids.usage_id))
data['usage-id'] = usage_id_serializer(block.scope_ids.usage_id)
template_context = {
'content': block.display_name if display_name_only else frag.content,
'classes': css_classes,
'display_name': block.display_name_with_default,
'data_attributes': u' '.join(u'data-{}="{}"'.format(key, value) for key, value in data.items()),
'data_attributes': u' '.join(u'data-{}="{}"'.format(key, value)
for key, value in data.iteritems()),
}
return wrap_fragment(frag, render_to_string('xblock_wrapper.html', template_context))
......@@ -145,7 +148,7 @@ def grade_histogram(module_id):
WHERE courseware_studentmodule.module_id=%s
GROUP BY courseware_studentmodule.grade"""
# Passing module_id this way prevents sql-injection.
cursor.execute(q, [module_id])
cursor.execute(q, [module_id.to_deprecated_string()])
grades = list(cursor.fetchall())
grades.sort(key=lambda x: x[0]) # Add ORDER BY to sql query?
......@@ -167,14 +170,14 @@ def add_staff_markup(user, block, view, frag, context): # pylint: disable=unuse
# TODO: make this more general, eg use an XModule attribute instead
if isinstance(block, VerticalModule):
# check that the course is a mongo backed Studio course before doing work
is_mongo_course = modulestore().get_modulestore_type(block.course_id) == MONGO_MODULESTORE_TYPE
is_mongo_course = modulestore().get_modulestore_type(block.location.course_key) == MONGO_MODULESTORE_TYPE
is_studio_course = block.course_edit_method == "Studio"
if is_studio_course and is_mongo_course:
# get relative url/location of unit in Studio
locator = loc_mapper().translate_location(block.course_id, block.location, False, True)
# build edit link to unit in CMS
edit_link = "//" + settings.CMS_BASE + locator.url_reverse('unit', '')
# build edit link to unit in CMS. Can't use reverse here as lms doesn't load cms's urls.py
# reverse for contentstore.views.unit_handler
edit_link = "//" + settings.CMS_BASE + '/unit/' + unicode(block.location)
# return edit link in rendered HTML for display
return wrap_fragment(frag, render_to_string("edit_unit_link.html", {'frag_content': frag.content, 'edit_link': edit_link}))
else:
......@@ -183,7 +186,7 @@ def add_staff_markup(user, block, view, frag, context): # pylint: disable=unuse
if isinstance(block, SequenceModule):
return frag
block_id = block.id
block_id = block.location
if block.has_score and settings.FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
histogram = grade_histogram(block_id)
render_histogram = len(histogram) > 0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment