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

This commit updates common/djangoapps.

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

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

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

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

View File

@@ -32,30 +32,30 @@ def local_random():
return _local_random
def is_course_cohorted(course_id):
def is_course_cohorted(course_key):
"""
Given a course id, return a boolean for whether or not the course is
Given a course key, return a boolean for whether or not the course is
cohorted.
Raises:
Http404 if the course doesn't exist.
"""
return courses.get_course_by_id(course_id).is_cohorted
return courses.get_course_by_id(course_key).is_cohorted
def get_cohort_id(user, course_id):
def get_cohort_id(user, course_key):
"""
Given a course id and a user, return the id of the cohort that user is
Given a course key and a user, return the id of the cohort that user is
assigned to in that course. If they don't have a cohort, return None.
"""
cohort = get_cohort(user, course_id)
cohort = get_cohort(user, course_key)
return None if cohort is None else cohort.id
def is_commentable_cohorted(course_id, commentable_id):
def is_commentable_cohorted(course_key, commentable_id):
"""
Args:
course_id: string
course_key: CourseKey
commentable_id: string
Returns:
@@ -64,7 +64,7 @@ def is_commentable_cohorted(course_id, commentable_id):
Raises:
Http404 if the course doesn't exist.
"""
course = courses.get_course_by_id(course_id)
course = courses.get_course_by_id(course_key)
if not course.is_cohorted:
# this is the easy case :)
@@ -77,18 +77,18 @@ def is_commentable_cohorted(course_id, commentable_id):
# inline discussions are cohorted by default
ans = True
log.debug(u"is_commentable_cohorted({0}, {1}) = {2}".format(course_id,
commentable_id,
ans))
log.debug(u"is_commentable_cohorted({0}, {1}) = {2}".format(
course_key, commentable_id, ans
))
return ans
def get_cohorted_commentables(course_id):
def get_cohorted_commentables(course_key):
"""
Given a course_id return a list of strings representing cohorted commentables
Given a course_key return a list of strings representing cohorted commentables
"""
course = courses.get_course_by_id(course_id)
course = courses.get_course_by_id(course_key)
if not course.is_cohorted:
# this is the easy case :)
@@ -99,34 +99,34 @@ def get_cohorted_commentables(course_id):
return ans
def get_cohort(user, course_id):
def get_cohort(user, course_key):
"""
Given a django User and a course_id, return the user's cohort in that
Given a django User and a CourseKey, return the user's cohort in that
cohort.
Arguments:
user: a Django User object.
course_id: string in the format 'org/course/run'
course_key: CourseKey
Returns:
A CourseUserGroup object if the course is cohorted and the User has a
cohort, else None.
Raises:
ValueError if the course_id doesn't exist.
ValueError if the CourseKey doesn't exist.
"""
# First check whether the course is cohorted (users shouldn't be in a cohort
# in non-cohorted courses, but settings can change after course starts)
try:
course = courses.get_course_by_id(course_id)
course = courses.get_course_by_id(course_key)
except Http404:
raise ValueError("Invalid course_id")
raise ValueError("Invalid course_key")
if not course.is_cohorted:
return None
try:
return CourseUserGroup.objects.get(course_id=course_id,
return CourseUserGroup.objects.get(course_id=course_key,
group_type=CourseUserGroup.COHORT,
users__id=user.id)
except CourseUserGroup.DoesNotExist:
@@ -142,72 +142,81 @@ def get_cohort(user, course_id):
# Nowhere to put user
log.warning("Course %s is auto-cohorted, but there are no"
" auto_cohort_groups specified",
course_id)
course_key)
return None
# Put user in a random group, creating it if needed
group_name = local_random().choice(choices)
group, created = CourseUserGroup.objects.get_or_create(
course_id=course_id,
course_id=course_key,
group_type=CourseUserGroup.COHORT,
name=group_name)
name=group_name
)
user.course_groups.add(group)
return group
def get_course_cohorts(course_id):
def get_course_cohorts(course_key):
"""
Get a list of all the cohorts in the given course.
Arguments:
course_id: string in the format 'org/course/run'
course_key: CourseKey
Returns:
A list of CourseUserGroup objects. Empty if there are no cohorts. Does
not check whether the course is cohorted.
"""
return list(CourseUserGroup.objects.filter(course_id=course_id,
group_type=CourseUserGroup.COHORT))
return list(CourseUserGroup.objects.filter(
course_id=course_key,
group_type=CourseUserGroup.COHORT
))
### Helpers for cohort management views
def get_cohort_by_name(course_id, name):
def get_cohort_by_name(course_key, name):
"""
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
it isn't present.
"""
return CourseUserGroup.objects.get(course_id=course_id,
group_type=CourseUserGroup.COHORT,
name=name)
return CourseUserGroup.objects.get(
course_id=course_key,
group_type=CourseUserGroup.COHORT,
name=name
)
def get_cohort_by_id(course_id, cohort_id):
def get_cohort_by_id(course_key, cohort_id):
"""
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
it isn't present. Uses the course_id for extra validation...
it isn't present. Uses the course_key for extra validation...
"""
return CourseUserGroup.objects.get(course_id=course_id,
group_type=CourseUserGroup.COHORT,
id=cohort_id)
return CourseUserGroup.objects.get(
course_id=course_key,
group_type=CourseUserGroup.COHORT,
id=cohort_id
)
def add_cohort(course_id, name):
def add_cohort(course_key, name):
"""
Add a cohort to a course. Raises ValueError if a cohort of the same name already
exists.
"""
log.debug("Adding cohort %s to %s", name, course_id)
if CourseUserGroup.objects.filter(course_id=course_id,
log.debug("Adding cohort %s to %s", name, course_key)
if CourseUserGroup.objects.filter(course_id=course_key,
group_type=CourseUserGroup.COHORT,
name=name).exists():
raise ValueError("Can't create two cohorts with the same name")
return CourseUserGroup.objects.create(course_id=course_id,
group_type=CourseUserGroup.COHORT,
name=name)
return CourseUserGroup.objects.create(
course_id=course_key,
group_type=CourseUserGroup.COHORT,
name=name
)
class CohortConflict(Exception):
@@ -237,9 +246,10 @@ def add_user_to_cohort(cohort, username_or_email):
previous_cohort = None
course_cohorts = CourseUserGroup.objects.filter(
course_id=cohort.course_id,
course_id=cohort.course_key,
users__id=user.id,
group_type=CourseUserGroup.COHORT)
group_type=CourseUserGroup.COHORT
)
if course_cohorts.exists():
if course_cohorts[0] == cohort:
raise ValueError("User {0} already present in cohort {1}".format(
@@ -253,21 +263,21 @@ def add_user_to_cohort(cohort, username_or_email):
return (user, previous_cohort)
def get_course_cohort_names(course_id):
def get_course_cohort_names(course_key):
"""
Return a list of the cohort names in a course.
"""
return [c.name for c in get_course_cohorts(course_id)]
return [c.name for c in get_course_cohorts(course_key)]
def delete_empty_cohort(course_id, name):
def delete_empty_cohort(course_key, name):
"""
Remove an empty cohort. Raise ValueError if cohort is not empty.
"""
cohort = get_cohort_by_name(course_id, name)
cohort = get_cohort_by_name(course_key, name)
if cohort.users.exists():
raise ValueError(
"Can't delete non-empty cohort {0} in course {1}".format(
name, course_id))
name, course_key))
cohort.delete()

View File

@@ -2,6 +2,7 @@ import logging
from django.contrib.auth.models import User
from django.db import models
from xmodule_django.models import CourseKeyField
log = logging.getLogger(__name__)
@@ -23,7 +24,8 @@ class CourseUserGroup(models.Model):
# Note: groups associated with particular runs of a course. E.g. Fall 2012 and Spring
# 2013 versions of 6.00x will have separate groups.
course_id = models.CharField(max_length=255, db_index=True,
# TODO change field name to course_key
course_id = CourseKeyField(max_length=255, db_index=True,
help_text="Which course is this group associated with?")
# For now, only have group type 'cohort', but adding a type field to support

View File

@@ -9,6 +9,7 @@ from course_groups.cohorts import (get_cohort, get_course_cohorts,
is_commentable_cohorted, get_cohort_by_name)
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.django_utils import mixed_store_config
@@ -84,13 +85,14 @@ class TestCohorts(django.test.TestCase):
Make sure that course is reloaded every time--clear out the modulestore.
"""
clear_existing_modulestores()
self.toy_course_key = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
def test_get_cohort(self):
"""
Make sure get_cohort() does the right thing when the course is cohorted
"""
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertEqual(course.id, "edX/toy/2012_Fall")
course = modulestore().get_course(self.toy_course_key)
self.assertEqual(course.id, self.toy_course_key)
self.assertFalse(course.is_cohorted)
user = User.objects.create(username="test", email="a@b.com")
@@ -120,8 +122,7 @@ class TestCohorts(django.test.TestCase):
"""
Make sure get_cohort() does the right thing when the course is auto_cohorted
"""
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertEqual(course.id, "edX/toy/2012_Fall")
course = modulestore().get_course(self.toy_course_key)
self.assertFalse(course.is_cohorted)
user1 = User.objects.create(username="test", email="a@b.com")
@@ -168,8 +169,7 @@ class TestCohorts(django.test.TestCase):
"""
Make sure get_cohort() randomizes properly.
"""
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertEqual(course.id, "edX/toy/2012_Fall")
course = modulestore().get_course(self.toy_course_key)
self.assertFalse(course.is_cohorted)
groups = ["group_{0}".format(n) for n in range(5)]
@@ -194,26 +194,26 @@ class TestCohorts(django.test.TestCase):
self.assertLess(num_users, 50)
def test_get_course_cohorts(self):
course1_id = 'a/b/c'
course2_id = 'e/f/g'
course1_key = SlashSeparatedCourseKey('a', 'b', 'c')
course2_key = SlashSeparatedCourseKey('e', 'f', 'g')
# add some cohorts to course 1
cohort = CourseUserGroup.objects.create(name="TestCohort",
course_id=course1_id,
course_id=course1_key,
group_type=CourseUserGroup.COHORT)
cohort = CourseUserGroup.objects.create(name="TestCohort2",
course_id=course1_id,
course_id=course1_key,
group_type=CourseUserGroup.COHORT)
# second course should have no cohorts
self.assertEqual(get_course_cohorts(course2_id), [])
self.assertEqual(get_course_cohorts(course2_key), [])
cohorts = sorted([c.name for c in get_course_cohorts(course1_id)])
cohorts = sorted([c.name for c in get_course_cohorts(course1_key)])
self.assertEqual(cohorts, ['TestCohort', 'TestCohort2'])
def test_is_commentable_cohorted(self):
course = modulestore().get_course("edX/toy/2012_Fall")
course = modulestore().get_course(self.toy_course_key)
self.assertFalse(course.is_cohorted)
def to_id(name):

View File

@@ -33,25 +33,25 @@ def split_by_comma_and_whitespace(s):
@ensure_csrf_cookie
def list_cohorts(request, course_id):
def list_cohorts(request, course_key):
"""
Return json dump of dict:
{'success': True,
'cohorts': [{'name': name, 'id': id}, ...]}
"""
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
all_cohorts = [{'name': c.name, 'id': c.id}
for c in cohorts.get_course_cohorts(course_id)]
for c in cohorts.get_course_cohorts(course_key)]
return json_http_response({'success': True,
'cohorts': all_cohorts})
'cohorts': all_cohorts})
@ensure_csrf_cookie
@require_POST
def add_cohort(request, course_id):
def add_cohort(request, course_key):
"""
Return json of dict:
{'success': True,
@@ -63,7 +63,7 @@ def add_cohort(request, course_id):
{'success': False,
'msg': error_msg} if there's an error
"""
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
name = request.POST.get("name")
if not name:
@@ -71,7 +71,7 @@ def add_cohort(request, course_id):
'msg': "No name specified"})
try:
cohort = cohorts.add_cohort(course_id, name)
cohort = cohorts.add_cohort(course_key, name)
except ValueError as err:
return json_http_response({'success': False,
'msg': str(err)})
@@ -84,7 +84,7 @@ def add_cohort(request, course_id):
@ensure_csrf_cookie
def users_in_cohort(request, course_id, cohort_id):
def users_in_cohort(request, course_key, cohort_id):
"""
Return users in the cohort. Show up to 100 per page, and page
using the 'page' GET attribute in the call. Format:
@@ -97,11 +97,11 @@ def users_in_cohort(request, course_id, cohort_id):
'users': [{'username': ..., 'email': ..., 'name': ...}]
}
"""
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
# this will error if called with a non-int cohort_id. That's ok--it
# shoudn't happen for valid clients.
cohort = cohorts.get_cohort_by_id(course_id, int(cohort_id))
cohort = cohorts.get_cohort_by_id(course_key, int(cohort_id))
paginator = Paginator(cohort.users.all(), 100)
page = request.GET.get('page')
@@ -119,17 +119,17 @@ def users_in_cohort(request, course_id, cohort_id):
user_info = [{'username': u.username,
'email': u.email,
'name': '{0} {1}'.format(u.first_name, u.last_name)}
for u in users]
for u in users]
return json_http_response({'success': True,
'page': page,
'num_pages': paginator.num_pages,
'users': user_info})
'page': page,
'num_pages': paginator.num_pages,
'users': user_info})
@ensure_csrf_cookie
@require_POST
def add_users_to_cohort(request, course_id, cohort_id):
def add_users_to_cohort(request, course_key, cohort_id):
"""
Return json dict of:
@@ -144,9 +144,9 @@ def add_users_to_cohort(request, course_id, cohort_id):
'present': [str1, str2, ...], # already there
'unknown': [str1, str2, ...]}
"""
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
cohort = cohorts.get_cohort_by_id(course_id, cohort_id)
cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
users = request.POST.get('users', '')
added = []
@@ -175,15 +175,15 @@ def add_users_to_cohort(request, course_id, cohort_id):
unknown.append(username_or_email)
return json_http_response({'success': True,
'added': added,
'changed': changed,
'present': present,
'unknown': unknown})
'added': added,
'changed': changed,
'present': present,
'unknown': unknown})
@ensure_csrf_cookie
@require_POST
def remove_user_from_cohort(request, course_id, cohort_id):
def remove_user_from_cohort(request, course_key, cohort_id):
"""
Expects 'username': username in POST data.
@@ -193,14 +193,14 @@ def remove_user_from_cohort(request, course_id, cohort_id):
{'success': False,
'msg': error_msg}
"""
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
username = request.POST.get('username')
if username is None:
return json_http_response({'success': False,
'msg': 'No username specified'})
'msg': 'No username specified'})
cohort = cohorts.get_cohort_by_id(course_id, cohort_id)
cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
try:
user = User.objects.get(username=username)
cohort.users.remove(user)
@@ -208,16 +208,18 @@ def remove_user_from_cohort(request, course_id, cohort_id):
except User.DoesNotExist:
log.debug('no user')
return json_http_response({'success': False,
'msg': "No user '{0}'".format(username)})
'msg': "No user '{0}'".format(username)})
def debug_cohort_mgmt(request, course_id):
def debug_cohort_mgmt(request, course_key):
"""
Debugging view for dev.
"""
# add staff check to make sure it's safe if it's accidentally deployed.
get_course_with_access(request.user, course_id, 'staff')
get_course_with_access(request.user, 'staff', course_key)
context = {'cohorts_ajax_url': reverse('cohorts',
kwargs={'course_id': course_id})}
context = {'cohorts_ajax_url': reverse(
'cohorts',
kwargs={'course_id': course_key.to_deprecated_string()}
)}
return render_to_response('/course_groups/debug.html', context)