Skip to content
Snippets Groups Projects
Commit e2bfcf2a authored by Calen Pennington's avatar Calen Pennington
Browse files

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
No related merge requests found
Showing
with 247 additions and 207 deletions
...@@ -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)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment