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