Commit 9811926d by Calen Pennington

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

This commit updates lms/djangoapps/courseware.

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
......@@ -43,7 +43,7 @@ def log_in(username='robot', password='test', email='robot@edx.org', name="Robot
@world.absorb
def register_by_course_id(course_id, username='robot', password='test', is_staff=False):
def register_by_course_key(course_key, username='robot', password='test', is_staff=False):
create_user(username, password)
user = User.objects.get(username=username)
# Note: this flag makes the user global staff - that is, an edX employee - not a course staff.
......@@ -51,17 +51,17 @@ def register_by_course_id(course_id, username='robot', password='test', is_staff
if is_staff:
user.is_staff = True
user.save()
CourseEnrollment.enroll(user, course_id)
CourseEnrollment.enroll(user, course_key)
@world.absorb
def enroll_user(user, course_id):
def enroll_user(user, course_key):
# Activate user
registration = world.RegistrationFactory(user=user)
registration.register(user)
registration.activate()
# Enroll them in the course
CourseEnrollment.enroll(user, course_id)
CourseEnrollment.enroll(user, course_key)
@world.absorb
......
......@@ -21,6 +21,8 @@ from .course_helpers import *
from .ui_helpers import *
from nose.tools import assert_equals # pylint: disable=E0611
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from logging import getLogger
logger = getLogger(__name__)
......@@ -110,7 +112,8 @@ def i_am_not_logged_in(step):
@step('I am staff for course "([^"]*)"$')
def i_am_staff_for_course_by_id(step, course_id):
world.register_by_course_id(course_id, True)
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
world.register_by_course_key(course_key, True)
@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$')
......
......@@ -34,7 +34,7 @@ def debug(*args, **kwargs):
log.debug(*args, **kwargs)
def has_access(user, obj, action, course_context=None):
def has_access(user, action, obj, course_key=None):
"""
Check whether a user has the access to do action on obj. Handles any magic
switching based on various settings.
......@@ -55,7 +55,7 @@ def has_access(user, obj, action, course_context=None):
actions depend on the obj type, but include e.g. 'enroll' for courses. See the
type-specific functions below for the known actions for that type.
course_context: A course_id specifying which course run this access is for.
course_key: A course_key specifying which course run this access is for.
Required when accessing anything other than a CourseDescriptor, 'global',
or a location with category 'course'
......@@ -69,23 +69,23 @@ def has_access(user, obj, action, course_context=None):
# delegate the work to type-specific functions.
# (start with more specific types, then get more general)
if isinstance(obj, CourseDescriptor):
return _has_access_course_desc(user, obj, action)
return _has_access_course_desc(user, action, obj)
if isinstance(obj, ErrorDescriptor):
return _has_access_error_desc(user, obj, action, course_context)
return _has_access_error_desc(user, action, obj, course_key)
if isinstance(obj, XModule):
return _has_access_xmodule(user, obj, action, course_context)
return _has_access_xmodule(user, action, obj, course_key)
# NOTE: any descriptor access checkers need to go above this
if isinstance(obj, XBlock):
return _has_access_descriptor(user, obj, action, course_context)
return _has_access_descriptor(user, action, obj, course_key)
if isinstance(obj, Location):
return _has_access_location(user, obj, action, course_context)
return _has_access_location(user, action, obj, course_key)
if isinstance(obj, basestring):
return _has_access_string(user, obj, action, course_context)
return _has_access_string(user, action, obj, course_key)
# Passing an unknown object here is a coding error, so rather than
# returning a default, complain.
......@@ -94,7 +94,7 @@ def has_access(user, obj, action, course_context=None):
# ================ Implementation helpers ================================
def _has_access_course_desc(user, course, action):
def _has_access_course_desc(user, action, course):
"""
Check if user has access to a course descriptor.
......@@ -114,16 +114,19 @@ def _has_access_course_desc(user, course, action):
NOTE: this is not checking whether user is actually enrolled in the course.
"""
# delegate to generic descriptor check to check start dates
return _has_access_descriptor(user, course, 'load')
return _has_access_descriptor(user, 'load', course, course.id)
def can_load_forum():
"""
Can this user access the forums in this course?
"""
return (can_load() and \
(CourseEnrollment.is_enrolled(user, course.id) or \
_has_staff_access_to_descriptor(user, course)
))
return (
can_load() and
(
CourseEnrollment.is_enrolled(user, course.id) or
_has_staff_access_to_descriptor(user, course, course.id)
)
)
def can_enroll():
"""
......@@ -158,13 +161,16 @@ def _has_access_course_desc(user, course, action):
debug("Allow: in enrollment period")
return True
# if user is in CourseEnrollmentAllowed with right course_id then can also enroll
# if user is in CourseEnrollmentAllowed with right course key then can also enroll
# (note that course.id actually points to a CourseKey)
# (the filter call uses course_id= since that's the legacy database schema)
# (sorry that it's confusing :( )
if user is not None and user.is_authenticated() and CourseEnrollmentAllowed:
if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id):
return True
# otherwise, need staff access
return _has_staff_access_to_descriptor(user, course)
return _has_staff_access_to_descriptor(user, course, course.id)
def see_exists():
"""
......@@ -184,7 +190,7 @@ def _has_access_course_desc(user, course, action):
if course.ispublic:
debug("Allow: ACCESS_REQUIRE_STAFF_FOR_COURSE and ispublic")
return True
return _has_staff_access_to_descriptor(user, course)
return _has_staff_access_to_descriptor(user, course, course.id)
return can_enroll() or can_load()
......@@ -193,14 +199,14 @@ def _has_access_course_desc(user, course, action):
'load_forum': can_load_forum,
'enroll': can_enroll,
'see_exists': see_exists,
'staff': lambda: _has_staff_access_to_descriptor(user, course),
'instructor': lambda: _has_instructor_access_to_descriptor(user, course),
'staff': lambda: _has_staff_access_to_descriptor(user, course, course.id),
'instructor': lambda: _has_instructor_access_to_descriptor(user, course, course.id),
}
return _dispatch(checkers, action, user, course)
def _has_access_error_desc(user, descriptor, action, course_context):
def _has_access_error_desc(user, action, descriptor, course_key):
"""
Only staff should see error descriptors.
......@@ -209,7 +215,7 @@ def _has_access_error_desc(user, descriptor, action, course_context):
'staff' -- staff access to descriptor.
"""
def check_for_staff():
return _has_staff_access_to_descriptor(user, descriptor, course_context)
return _has_staff_access_to_descriptor(user, descriptor, course_key)
checkers = {
'load': check_for_staff,
......@@ -219,7 +225,7 @@ def _has_access_error_desc(user, descriptor, action, course_context):
return _dispatch(checkers, action, user, descriptor)
def _has_access_descriptor(user, descriptor, action, course_context=None):
def _has_access_descriptor(user, action, descriptor, course_key=None):
"""
Check if user has access to this descriptor.
......@@ -249,14 +255,14 @@ def _has_access_descriptor(user, descriptor, action, course_context=None):
effective_start = _adjust_start_date_for_beta_testers(
user,
descriptor,
course_context=course_context
course_key=course_key
)
if now > effective_start:
# after start date, everyone can see it
debug("Allow: now > effective start date")
return True
# otherwise, need staff access
return _has_staff_access_to_descriptor(user, descriptor, course_context)
return _has_staff_access_to_descriptor(user, descriptor, course_key)
# No start date, so can always load.
debug("Allow: no start date")
......@@ -264,13 +270,13 @@ def _has_access_descriptor(user, descriptor, action, course_context=None):
checkers = {
'load': can_load,
'staff': lambda: _has_staff_access_to_descriptor(user, descriptor, course_context)
'staff': lambda: _has_staff_access_to_descriptor(user, descriptor, course_key)
}
return _dispatch(checkers, action, user, descriptor)
def _has_access_xmodule(user, xmodule, action, course_context):
def _has_access_xmodule(user, action, xmodule, course_key):
"""
Check if user has access to this xmodule.
......@@ -278,10 +284,10 @@ def _has_access_xmodule(user, xmodule, action, course_context):
- same as the valid actions for xmodule.descriptor
"""
# Delegate to the descriptor
return has_access(user, xmodule.descriptor, action, course_context)
return has_access(user, action, xmodule.descriptor, course_key)
def _has_access_location(user, location, action, course_context):
def _has_access_location(user, action, location, course_key):
"""
Check if user has access to this location.
......@@ -295,13 +301,13 @@ def _has_access_location(user, location, action, course_context):
And in general, prefer checking access on loaded items, rather than locations.
"""
checkers = {
'staff': lambda: _has_staff_access_to_location(user, location, course_context)
'staff': lambda: _has_staff_access_to_location(user, location, course_key)
}
return _dispatch(checkers, action, user, location)
def _has_access_string(user, perm, action, course_context):
def _has_access_string(user, action, perm, course_key):
"""
Check if user has certain special access, specified as string. Valid strings:
......@@ -338,7 +344,7 @@ def _dispatch(table, action, user, obj):
debug("%s user %s, object %s, action %s",
'ALLOWED' if result else 'DENIED',
user,
obj.location.url() if isinstance(obj, XBlock) else str(obj)[:60],
obj.location.to_deprecated_string() if isinstance(obj, XBlock) else str(obj),
action)
return result
......@@ -346,7 +352,7 @@ def _dispatch(table, action, user, obj):
type(obj), action))
def _adjust_start_date_for_beta_testers(user, descriptor, course_context=None):
def _adjust_start_date_for_beta_testers(user, descriptor, course_key=None): # pylint: disable=invalid-name
"""
If user is in a beta test group, adjust the start date by the appropriate number of
days.
......@@ -373,7 +379,7 @@ def _adjust_start_date_for_beta_testers(user, descriptor, course_context=None):
# bail early if no beta testing is set up
return descriptor.start
if CourseBetaTesterRole(descriptor.location, course_context=course_context).has_user(user):
if CourseBetaTesterRole(course_key).has_user(user):
debug("Adjust start time: user in beta role for %s", descriptor)
delta = timedelta(descriptor.days_early_for_beta)
effective = descriptor.start - delta
......@@ -382,15 +388,15 @@ def _adjust_start_date_for_beta_testers(user, descriptor, course_context=None):
return descriptor.start
def _has_instructor_access_to_location(user, location, course_context=None):
return _has_access_to_location(user, location, 'instructor', course_context)
def _has_instructor_access_to_location(user, location, course_key=None): # pylint: disable=invalid-name
return _has_access_to_location(user, 'instructor', location, course_key)
def _has_staff_access_to_location(user, location, course_context=None):
return _has_access_to_location(user, location, 'staff', course_context)
def _has_staff_access_to_location(user, location, course_key=None):
return _has_access_to_location(user, 'staff', location, course_key)
def _has_access_to_location(user, location, access_level, course_context):
def _has_access_to_location(user, access_level, location, course_key):
'''
Returns True if the given user has access_level (= staff or
instructor) access to a location. For now this is equivalent to
......@@ -398,7 +404,7 @@ def _has_access_to_location(user, location, access_level, course_context):
This means that user is in the staff_* group or instructor_* group, or is an overall admin.
TODO (vshnayder): this needs to be changed to allow per-course_id permissions, not per-course
TODO (vshnayder): this needs to be changed to allow per-course_key permissions, not per-course
(e.g. staff in 2012 is different from 2013, but maybe some people always have access)
course is a string: the course field of the location being accessed.
......@@ -422,8 +428,8 @@ def _has_access_to_location(user, location, access_level, course_context):
return False
staff_access = (
CourseStaffRole(location, course_context).has_user(user) or
OrgStaffRole(location).has_user(user)
CourseStaffRole(course_key).has_user(user) or
OrgStaffRole(course_key.org).has_user(user)
)
if staff_access and access_level == 'staff':
......@@ -431,8 +437,8 @@ def _has_access_to_location(user, location, access_level, course_context):
return True
instructor_access = (
CourseInstructorRole(location, course_context).has_user(user) or
OrgInstructorRole(location).has_user(user)
CourseInstructorRole(course_key).has_user(user) or
OrgInstructorRole(course_key.org).has_user(user)
)
if instructor_access and access_level in ('staff', 'instructor'):
......@@ -443,42 +449,43 @@ def _has_access_to_location(user, location, access_level, course_context):
return False
def _has_staff_access_to_course_id(user, course_id):
"""Helper method that takes a course_id instead of a course name"""
loc = CourseDescriptor.id_to_location(course_id)
return _has_staff_access_to_location(user, loc, course_id)
# TODO Please change this function signature to _has_staff_access_to_course_key at next opportunity!
def _has_staff_access_to_course_id(user, course_key):
"""Helper method that takes a course_key instead of a course name"""
loc = CourseDescriptor.id_to_location(course_key)
return _has_staff_access_to_location(user, loc, course_key)
def _has_instructor_access_to_descriptor(user, descriptor, course_context=None):
def _has_instructor_access_to_descriptor(user, descriptor, course_key): # pylint: disable=invalid-name
"""Helper method that checks whether the user has staff access to
the course of the location.
descriptor: something that has a location attribute
"""
return _has_instructor_access_to_location(user, descriptor.location, course_context)
return _has_instructor_access_to_location(user, descriptor.location, course_key)
def _has_staff_access_to_descriptor(user, descriptor, course_context=None):
def _has_staff_access_to_descriptor(user, descriptor, course_key):
"""Helper method that checks whether the user has staff access to
the course of the location.
descriptor: something that has a location attribute
"""
return _has_staff_access_to_location(user, descriptor.location, course_context)
return _has_staff_access_to_location(user, descriptor.location, course_key)
def get_user_role(user, course_id):
def get_user_role(user, course_key):
"""
Return corresponding string if user has staff, instructor or student
course role in LMS.
"""
from courseware.courses import get_course
course = get_course(course_id)
course = get_course(course_key)
if is_masquerading_as_student(user):
return 'student'
elif has_access(user, course, 'instructor'):
elif has_access(user, 'instructor', course):
return 'instructor'
elif has_access(user, course, 'staff'):
elif has_access(user, 'staff', course):
return 'staff'
else:
return 'student'
......@@ -8,12 +8,13 @@ from django.http import Http404
from django.conf import settings
from edxmako.shortcuts import render_to_string
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location, XML_MODULESTORE_TYPE, MONGO_MODULESTORE_TYPE
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore import XML_MODULESTORE_TYPE
from xmodule.modulestore.keys import CourseKey
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from static_replace import replace_static_urls
from xmodule.modulestore import MONGO_MODULESTORE_TYPE
from courseware.access import has_access
from courseware.model_data import FieldDataCache
......@@ -49,15 +50,15 @@ def get_course(course_id, depth=0):
None means infinite depth. Default is to fetch no children.
"""
try:
course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_instance(course_id, course_loc, depth=depth)
return modulestore().get_course(course_id, depth=depth)
except (KeyError, ItemNotFoundError):
raise ValueError(u"Course not found: {0}".format(course_id))
except InvalidLocationError:
raise ValueError(u"Invalid location: {0}".format(course_id))
def get_course_by_id(course_id, depth=0):
# TODO please rename this function to get_course_by_key at next opportunity!
def get_course_by_id(course_key, depth=0):
"""
Given a course id, return the corresponding course descriptor.
......@@ -66,50 +67,55 @@ def get_course_by_id(course_id, depth=0):
depth: The number of levels of children for the modulestore to cache. None means infinite depth
"""
try:
course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_instance(course_id, course_loc, depth=depth)
course = modulestore().get_course(course_key, depth=depth)
if course:
return course
else:
raise Http404("Course not found.")
except (KeyError, ItemNotFoundError):
raise Http404("Course not found.")
except InvalidLocationError:
raise Http404("Invalid location")
def get_course_with_access(user, course_id, action, depth=0):
def get_course_with_access(user, action, course_key, depth=0):
"""
Given a course_id, look up the corresponding course descriptor,
Given a course_key, look up the corresponding course descriptor,
check that the user has the access to perform the specified action
on the course, and return the descriptor.
Raises a 404 if the course_id is invalid, or the user doesn't have access.
Raises a 404 if the course_key is invalid, or the user doesn't have access.
depth: The number of levels of children for the modulestore to cache. None means infinite depth
"""
course = get_course_by_id(course_id, depth=depth)
if not has_access(user, course, action):
assert isinstance(course_key, CourseKey)
course = get_course_by_id(course_key, depth=depth)
if not has_access(user, action, course, course_key):
# Deliberately return a non-specific error message to avoid
# leaking info about access control settings
raise Http404("Course not found.")
return course
def get_opt_course_with_access(user, course_id, action):
def get_opt_course_with_access(user, action, course_key):
"""
Same as get_course_with_access, except that if course_id is None,
Same as get_course_with_access, except that if course_key is None,
return None without performing any access checks.
"""
if course_id is None:
if course_key is None:
return None
return get_course_with_access(user, course_id, action)
return get_course_with_access(user, action, course_key)
def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
if course.static_asset_path or modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE:
""" Determine whether this is an XML or Studio-backed course, and return the appropriate course_image URL """
if course.static_asset_path or modulestore().get_modulestore_type(course.id) == XML_MODULESTORE_TYPE:
return '/static/' + (course.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg"
else:
loc = StaticContent.compute_location(course.location.org, course.location.course, course.course_image)
_path = StaticContent.get_url_path_from_location(loc)
loc = StaticContent.compute_location(course.location.course_key, course.course_image)
_path = loc.to_deprecated_string()
return _path
......@@ -158,7 +164,7 @@ def get_course_about_section(course, section_key):
# markup. This can change without effecting this interface when we find a
# good format for defining so many snippets of text/html.
# TODO: Remove number, instructors from this list
# TODO: Remove number, instructors from this list
if section_key in ['short_description', 'description', 'key_dates', 'video',
'course_staff_short', 'course_staff_extended',
'requirements', 'syllabus', 'textbook', 'faq', 'more_info',
......@@ -199,7 +205,7 @@ def get_course_about_section(course, section_key):
except ItemNotFoundError:
log.warning(
u"Missing about section {key} in course {url}".format(key=section_key, url=course.location.url())
u"Missing about section {key} in course {url}".format(key=section_key, url=course.location.to_deprecated_string())
)
return None
elif section_key == "title":
......@@ -223,14 +229,14 @@ def get_course_info_section(request, course, section_key):
- updates
- guest_updates
"""
loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key)
usage_key = course.id.make_usage_key('course_info', section_key)
# Use an empty cache
field_data_cache = FieldDataCache([], course.id, request.user)
info_module = get_module(
request.user,
request,
loc,
usage_key,
field_data_cache,
course.id,
wrap_xmodule_display=False,
......@@ -279,12 +285,12 @@ def get_course_syllabus_section(course, section_key):
return replace_static_urls(
html_file.read().decode('utf-8'),
getattr(course, 'data_dir', None),
course_id=course.location.course_id,
course_id=course.id,
static_asset_path=course.static_asset_path,
)
except ResourceNotFoundError:
log.exception(
u"Missing syllabus section {key} in course {url}".format(key=section_key, url=course.location.url())
u"Missing syllabus section {key} in course {url}".format(key=section_key, url=course.location.to_deprecated_string())
)
return "! Syllabus missing !"
......@@ -312,7 +318,7 @@ def get_courses(user, domain=None):
Returns a list of courses available, sorted by course.number
'''
courses = branding.get_visible_courses()
courses = [c for c in courses if has_access(user, c, 'see_exists')]
courses = [c for c in courses if has_access(user, 'see_exists', c)]
courses = sorted(courses, key=lambda course: course.number)
......@@ -332,15 +338,14 @@ def sort_by_announcement(courses):
return courses
def get_cms_course_link(course):
def get_cms_course_link(course, page='course'):
"""
Returns a link to course_index for editing the course in cms,
assuming that the course is actually cms-backed.
"""
locator = loc_mapper().translate_location(
course.location.course_id, course.location, False, True
)
return "//" + settings.CMS_BASE + locator.url_reverse('course/', '')
# This is fragile, but unfortunately the problem is that within the LMS we
# can't use the reverse calls from the CMS
return u"//{}/{}/{}".format(settings.CMS_BASE, page, unicode(course.id))
def get_cms_block_link(block, page):
......@@ -348,20 +353,20 @@ def get_cms_block_link(block, page):
Returns a link to block_index for editing the course in cms,
assuming that the block is actually cms-backed.
"""
locator = loc_mapper().translate_location(
block.location.course_id, block.location, False, True
)
return "//" + settings.CMS_BASE + locator.url_reverse(page, '')
# This is fragile, but unfortunately the problem is that within the LMS we
# can't use the reverse calls from the CMS
return u"//{}/{}/{}".format(settings.CMS_BASE, page, block.location)
def get_studio_url(course_id, page):
def get_studio_url(course_key, page):
"""
Get the Studio URL of the page that is passed in.
"""
course = get_course_by_id(course_id)
assert(isinstance(course_key, CourseKey))
course = get_course_by_id(course_key)
is_studio_course = course.course_edit_method == "Studio"
is_mongo_course = modulestore().get_modulestore_type(course_id) == MONGO_MODULESTORE_TYPE
is_mongo_course = modulestore().get_modulestore_type(course_key) == MONGO_MODULESTORE_TYPE
studio_link = None
if is_studio_course and is_mongo_course:
studio_link = get_cms_block_link(course, page)
studio_link = get_cms_course_link(course, page)
return studio_link
......@@ -140,7 +140,7 @@ class AnnotatableSteps(object):
def active_problem_selector(self, subselector):
return 'div[data-problem-id="{}"] {}'.format(
world.scenario_dict['PROBLEMS'][self.active_problem].location.url(),
world.scenario_dict['PROBLEMS'][self.active_problem].location.to_deprecated_string(),
subselector,
)
......
......@@ -12,6 +12,7 @@ from django.core.urlresolvers import reverse
from student.models import CourseEnrollment
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.course_module import CourseDescriptor
from courseware.courses import get_course_by_id
from xmodule import seq_module, vertical_module
......@@ -119,16 +120,19 @@ def go_into_course(step):
def course_id(course_num):
return "%s/%s/%s" % (world.scenario_dict['COURSE'].org, course_num,
world.scenario_dict['COURSE'].url_name)
return SlashSeparatedCourseKey(
world.scenario_dict['COURSE'].org,
course_num,
world.scenario_dict['COURSE'].url_name
)
def course_location(course_num):
return world.scenario_dict['COURSE'].location._replace(course=course_num)
return world.scenario_dict['COURSE'].location.replace(course=course_num)
def section_location(course_num):
return world.scenario_dict['SECTION'].location._replace(course=course_num)
return world.scenario_dict['SECTION'].location.replace(course=course_num)
def visit_scenario_item(item_key):
......@@ -140,8 +144,8 @@ def visit_scenario_item(item_key):
url = django_url(reverse(
'jump_to',
kwargs={
'course_id': world.scenario_dict['COURSE'].id,
'location': str(world.scenario_dict[item_key].location),
'course_id': world.scenario_dict['COURSE'].id.to_deprecated_string(),
'location': world.scenario_dict[item_key].location.to_deprecated_string(),
}
))
......
......@@ -46,16 +46,16 @@ class ConditionalSteps(object):
metadata = {
'xml_attributes': {
'sources': world.scenario_dict['CONDITION_SOURCE'].location.url()
condition: cond_value
}
}
metadata['xml_attributes'][condition] = cond_value
world.scenario_dict['CONDITIONAL'] = world.ItemFactory(
parent_location=world.scenario_dict['WRAPPER'].location,
category='conditional',
display_name="Test Conditional",
metadata=metadata
metadata=metadata,
sources_list=[world.scenario_dict['CONDITION_SOURCE'].location],
)
world.ItemFactory(
......
......@@ -201,26 +201,24 @@ def i_am_registered_for_the_course(coursenum, metadata, user='Instructor'):
metadata.update({'days_early_for_beta': 5, 'start': tomorrow})
create_course_for_lti(coursenum, metadata)
course_descriptor = world.scenario_dict['COURSE']
course_location = world.scenario_dict['COURSE'].location
# create beta tester
user = BetaTesterFactory(course=course_location)
user = BetaTesterFactory(course=course_descriptor.id)
normal_student = UserFactory()
instructor = InstructorFactory(course=course_location)
instructor = InstructorFactory(course=course_descriptor.id)
assert not has_access(normal_student, course_descriptor, 'load')
assert has_access(user, course_descriptor, 'load')
assert has_access(instructor, course_descriptor, 'load')
assert not has_access(normal_student, 'load', course_descriptor)
assert has_access(user, 'load', course_descriptor)
assert has_access(instructor, 'load', course_descriptor)
else:
metadata.update({'start': datetime.datetime(1970, 1, 1, tzinfo=UTC)})
create_course_for_lti(coursenum, metadata)
course_descriptor = world.scenario_dict['COURSE']
course_location = world.scenario_dict['COURSE'].location
user = InstructorFactory(course=course_location)
user = InstructorFactory(course=course_descriptor.id)
# Enroll the user in the course and log them in
if has_access(user, course_descriptor, 'load'):
world.enroll_user(user, course_id(coursenum))
if has_access(user, 'load', course_descriptor):
world.enroll_user(user, course_descriptor.id)
world.log_in(username=user.username, password='test')
......
......@@ -5,6 +5,7 @@ from lettuce import world, step
from common import course_id, course_location
from problems_setup import PROBLEM_DICT
from nose.tools import assert_in
from xmodule.modulestore.locations import SlashSeparatedCourseKey
@step(u'I am viewing a course with multiple sections')
......@@ -148,7 +149,7 @@ def create_course():
def create_user_and_visit_course():
world.register_by_course_id('edx/999/Test_Course')
world.register_by_course_key(SlashSeparatedCourseKey('edx', '999', 'Test_Course'))
world.log_in()
world.visit('/courses/edx/999/Test_Course/courseware/')
......
......@@ -10,7 +10,7 @@ logger = getLogger(__name__)
@step('I navigate to an openended question$')
def navigate_to_an_openended_question(step):
world.register_by_course_id('MITx/3.091x/2012_Fall')
world.register_by_course_key('MITx/3.091x/2012_Fall')
world.log_in(email='robot@edx.org', password='test')
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
world.browser.visit(django_url(problem))
......@@ -20,7 +20,7 @@ def navigate_to_an_openended_question(step):
@step('I navigate to an openended question as staff$')
def navigate_to_an_openended_question_as_staff(step):
world.register_by_course_id('MITx/3.091x/2012_Fall', True)
world.register_by_course_key('MITx/3.091x/2012_Fall', True)
world.log_in(email='robot@edx.org', password='test')
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
world.browser.visit(django_url(problem))
......
......@@ -7,7 +7,7 @@ from lettuce.django import django_url
@step('I register for the course "([^"]*)"$')
def i_register_for_the_course(_step, course):
url = django_url('courses/%s/about' % world.scenario_dict['COURSE'].id)
url = django_url('courses/%s/about' % world.scenario_dict['COURSE'].id.to_deprecated_string())
world.browser.visit(url)
world.css_click('section.intro a.register')
......
......@@ -195,7 +195,7 @@ def add_vertical_to_course(course_num):
def last_vertical_location(course_num):
return world.scenario_dict['LAST_VERTICAL'].location._replace(course=course_num)
return world.scenario_dict['LAST_VERTICAL'].location.replace(course=course_num)
def upload_file(filename, location):
......@@ -204,7 +204,7 @@ def upload_file(filename, location):
mime_type = "application/json"
content_location = StaticContent.compute_location(
location.org, location.course, filename
location.course_key, filename
)
content = StaticContent(content_location, filename, mime_type, f.read())
contentstore().save(content)
......
......@@ -23,6 +23,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.util.duedate import get_extended_due_date
from .models import StudentModule
from .module_render import get_module_for_descriptor
from opaque_keys import InvalidKeyError
log = logging.getLogger("edx.courseware")
......@@ -50,9 +51,9 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator):
yield next_descriptor
def answer_distributions(course_id):
def answer_distributions(course_key):
"""
Given a course_id, return answer distributions in the form of a dictionary
Given a course_key, return answer distributions in the form of a dictionary
mapping:
(problem url_name, problem display_name, problem_id) -> {dict: answer -> count}
......@@ -82,44 +83,40 @@ def answer_distributions(course_id):
# dict: { module.module_state_key : (url_name, display_name) }
state_keys_to_problem_info = {} # For caching, used by url_and_display_name
def url_and_display_name(module_state_key):
def url_and_display_name(usage_key):
"""
For a given module_state_key, return the problem's url and display_name.
For a given usage_key, return the problem's url and display_name.
Handle modulestore access and caching. This method ignores permissions.
May throw an ItemNotFoundError if there is no content that corresponds
to this module_state_key.
Raises:
InvalidKeyError: if the usage_key does not parse
ItemNotFoundError: if there is no content that corresponds
to this usage_key.
"""
problem_store = modulestore()
if module_state_key not in state_keys_to_problem_info:
problems = problem_store.get_items(module_state_key, course_id=course_id, depth=1)
if not problems:
# Likely means that the problem was deleted from the course
# after the student had answered. We log this suspicion where
# this exception is caught.
raise ItemNotFoundError(
"Answer Distribution: Module {} not found for course {}"
.format(module_state_key, course_id)
)
problem = problems[0]
if usage_key not in state_keys_to_problem_info:
problem = problem_store.get_item(usage_key)
problem_info = (problem.url_name, problem.display_name_with_default)
state_keys_to_problem_info[module_state_key] = problem_info
state_keys_to_problem_info[usage_key] = problem_info
return state_keys_to_problem_info[module_state_key]
return state_keys_to_problem_info[usage_key]
# Iterate through all problems submitted for this course in no particular
# order, and build up our answer_counts dict that we will eventually return
answer_counts = defaultdict(lambda: defaultdict(int))
for module in StudentModule.all_submitted_problems_read_only(course_id):
for module in StudentModule.all_submitted_problems_read_only(course_key):
try:
state_dict = json.loads(module.state) if module.state else {}
raw_answers = state_dict.get("student_answers", {})
except ValueError:
log.error(
"Answer Distribution: Could not parse module state for " +
"StudentModule id={}, course={}".format(module.id, course_id)
"StudentModule id={}, course={}".format(module.id, course_key)
)
continue
try:
url, display_name = url_and_display_name(module.module_id.map_into_course(course_key))
# Each problem part has an ID that is derived from the
# module.module_state_key (with some suffix appended)
for problem_part_id, raw_answer in raw_answers.items():
......@@ -128,22 +125,19 @@ def answer_distributions(course_id):
# unicode and not str -- state comes from the json decoder, and that
# always returns unicode for strings.
answer = unicode(raw_answer)
answer_counts[(url, display_name, problem_part_id)][answer] += 1
try:
url, display_name = url_and_display_name(module.module_state_key)
except ItemNotFoundError:
except (ItemNotFoundError, InvalidKeyError):
msg = "Answer Distribution: Item {} referenced in StudentModule {} " + \
"for user {} in course {} not found; " + \
"This can happen if a student answered a question that " + \
"was later deleted from the course. This answer will be " + \
"omitted from the answer distribution CSV."
log.warning(
msg.format(module.module_state_key, module.id, module.student_id, course_id)
msg.format(module.module_state_key, module.id, module.student_id, course_key)
)
continue
answer_counts[(url, display_name, problem_part_id)][answer] += 1
return answer_counts
@transaction.commit_manually
......@@ -183,7 +177,9 @@ def _grade(student, request, course, keep_raw_scores):
# Dict of item_ids -> (earned, possible) point tuples. This *only* grabs
# scores that were registered with the submissions API, which for the moment
# means only openassessment (edx-ora2)
submissions_scores = sub_api.get_scores(course.id, anonymous_id_for_user(student, course.id))
submissions_scores = sub_api.get_scores(
course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id)
)
totaled_scores = {}
# This next complicated loop is just to collect the totaled_scores, which is
......@@ -206,7 +202,7 @@ def _grade(student, request, course, keep_raw_scores):
# API. If scores exist, we have to calculate grades for this section.
if not should_grade_section:
should_grade_section = any(
descriptor.location.url() in submissions_scores
descriptor.location.to_deprecated_string() in submissions_scores
for descriptor in section['xmoduledescriptors']
)
......@@ -214,7 +210,7 @@ def _grade(student, request, course, keep_raw_scores):
with manual_transaction():
should_grade_section = StudentModule.objects.filter(
student=student,
module_state_key__in=[
module_id__in=[
descriptor.location for descriptor in section['xmoduledescriptors']
]
).exists()
......@@ -350,7 +346,7 @@ def _progress_summary(student, request, course):
# This student must not have access to the course.
return None
submissions_scores = sub_api.get_scores(course.id, anonymous_id_for_user(student, course.id))
submissions_scores = sub_api.get_scores(course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id))
chapters = []
# Don't include chapters that aren't displayable (e.g. due to error)
......@@ -427,7 +423,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, scores_cache=
if not user.is_authenticated():
return (None, None)
location_url = problem_descriptor.location.url()
location_url = problem_descriptor.location.to_deprecated_string()
if location_url in scores_cache:
return scores_cache[location_url]
......@@ -451,7 +447,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, scores_cache=
student_module = StudentModule.objects.get(
student=user,
course_id=course_id,
module_state_key=problem_descriptor.location
module_id=problem_descriptor.location
)
except StudentModule.DoesNotExist:
student_module = None
......
......@@ -73,7 +73,7 @@ def import_with_checks(course_dir, verbose=True):
return (False, None)
course = courses[0]
errors = modulestore.get_item_errors(course.location)
errors = modulestore.get_course_errors(course.id)
if len(errors) != 0:
all_ok = False
print '\n'
......
......@@ -24,18 +24,12 @@ class Command(BaseCommand):
)
def handle(self, *args, **options):
results = []
try:
name = options['modulestore']
store = modulestore(name)
except KeyError:
raise CommandError("Unknown modulestore {}".format(name))
for course in store.get_courses():
course_id = course.location.course_id
results.append(course_id)
output = '\n'.join(results) + '\n'
output = u'\n'.join(course.id.to_deprecated_string() for course in store.get_courses()) + '\n'
return output.encode('utf-8')
......@@ -25,6 +25,8 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata, compute_inherited_metadata
from xblock.fields import Scope
from opaque_keys import InvalidKeyError
from xmodule.modulestore.locations import SlashSeparatedCourseKey
FILTER_LIST = ['xml_attributes', 'checklists']
INHERITED_FILTER_LIST = ['children', 'xml_attributes', 'checklists']
......@@ -66,7 +68,11 @@ class Command(BaseCommand):
# Get the course data
course_id = args[0]
try:
course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0])
except InvalidKeyError:
raise CommandError("Invalid course_id")
course = store.get_course(course_id)
if course is None:
raise CommandError("Invalid course_id")
......@@ -90,12 +96,12 @@ def dump_module(module, destination=None, inherited=False, defaults=False):
destination = destination if destination else {}
items = own_metadata(module).iteritems()
filtered_metadata = {k: v for k, v in items if k not in FILTER_LIST}
items = own_metadata(module)
filtered_metadata = {k: v for k, v in items.iteritems() if k not in FILTER_LIST}
destination[module.location.url()] = {
destination[module.location.to_deprecated_string()] = {
'category': module.location.category,
'children': [str(child) for child in getattr(module, 'children', [])],
'children': [child.to_deprecated_string() for child in getattr(module, 'children', [])],
'metadata': filtered_metadata,
}
......@@ -116,7 +122,7 @@ def dump_module(module, destination=None, inherited=False, defaults=False):
return field.values != field.default
inherited_metadata = {field.name: field.read_json(module) for field in module.fields.values() if is_inherited(field)}
destination[module.location.url()]['inherited_metadata'] = inherited_metadata
destination[module.location.to_deprecated_string()]['inherited_metadata'] = inherited_metadata
for child in module.get_children():
dump_module(child, destination, inherited, defaults)
......
......@@ -17,6 +17,8 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_exporter import export_to_xml
from opaque_keys import InvalidKeyError
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class Command(BaseCommand):
......@@ -39,8 +41,10 @@ class Command(BaseCommand):
def _parse_arguments(self, args):
"""Parse command line arguments"""
try:
course_id = args[0]
course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0])
filename = args[1]
except InvalidKeyError:
raise CommandError("Unparsable course_id")
except IndexError:
raise CommandError("Insufficient arguments")
......@@ -54,7 +58,6 @@ class Command(BaseCommand):
def _get_results(self, filename):
"""Load results from file"""
results = None
with open(filename) as f:
results = f.read()
os.remove(filename)
......@@ -78,8 +81,8 @@ def export_course_to_directory(course_id, root_dir):
if course is None:
raise CommandError("Invalid course_id")
course_name = course.location.course_id.replace('/', '-')
export_to_xml(store, None, course.location, root_dir, course_name)
course_name = course.id.to_deprecated_string().replace('/', '-')
export_to_xml(store, None, course.id, root_dir, course_name)
course_dir = path(root_dir) / course_name
return course_dir
......
......@@ -38,7 +38,7 @@ def import_course(course_dir, verbose=True):
return None
course = courses[0]
errors = modulestore.get_item_errors(course.location)
errors = modulestore.get_course_errors(course.id)
if len(errors) != 0:
sys.stderr.write('ERRORs during import: {0}\n'.format('\n'.join(map(str_of_err, errors))))
......
......@@ -22,6 +22,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.locations import SlashSeparatedCourseKey
DATA_DIR = 'common/test/data/'
......@@ -53,7 +54,8 @@ class CommandsTestBase(TestCase):
modulestore=store)
courses = store.get_courses()
if TEST_COURSE_ID not in [c.id for c in courses]:
# NOTE: if xml store owns these, it won't import them into mongo
if SlashSeparatedCourseKey.from_deprecated_string(TEST_COURSE_ID) not in [c.id for c in courses]:
import_from_xml(store, DATA_DIR, ['toy', 'simple'])
return [course.id for course in store.get_courses()]
......@@ -70,7 +72,9 @@ class CommandsTestBase(TestCase):
output = self.call_command('dump_course_ids', **kwargs)
dumped_courses = output.decode('utf-8').strip().split('\n')
self.assertEqual(self.loaded_courses, dumped_courses)
course_ids = {course_id.to_deprecated_string() for course_id in self.loaded_courses}
dumped_ids = set(dumped_courses)
self.assertEqual(course_ids, dumped_ids)
def test_dump_course_structure(self):
args = [TEST_COURSE_ID]
......@@ -81,16 +85,15 @@ class CommandsTestBase(TestCase):
# check that all elements in the course structure have metadata,
# but not inherited metadata:
for element_name in dump:
element = dump[element_name]
for element in dump.itervalues():
self.assertIn('metadata', element)
self.assertIn('children', element)
self.assertIn('category', element)
self.assertNotIn('inherited_metadata', element)
# Check a few elements in the course dump
parent_id = 'i4x://edX/simple/chapter/Overview'
test_course_key = SlashSeparatedCourseKey.from_deprecated_string(TEST_COURSE_ID)
parent_id = test_course_key.make_usage_key('chapter', 'Overview').to_deprecated_string()
self.assertEqual(dump[parent_id]['category'], 'chapter')
self.assertEqual(len(dump[parent_id]['children']), 3)
......@@ -98,7 +101,7 @@ class CommandsTestBase(TestCase):
self.assertEqual(dump[child_id]['category'], 'videosequence')
self.assertEqual(len(dump[child_id]['children']), 2)
video_id = 'i4x://edX/simple/video/Welcome'
video_id = test_course_key.make_usage_key('video', 'Welcome').to_deprecated_string()
self.assertEqual(dump[video_id]['category'], 'video')
self.assertEqual(len(dump[video_id]['metadata']), 4)
self.assertIn('youtube_id_1_0', dump[video_id]['metadata'])
......@@ -114,8 +117,7 @@ class CommandsTestBase(TestCase):
dump = json.loads(output)
# check that all elements in the course structure have inherited metadata,
# and that it contains a particular value as well:
for element_name in dump:
element = dump[element_name]
for element in dump.itervalues():
self.assertIn('metadata', element)
self.assertIn('children', element)
self.assertIn('category', element)
......@@ -131,8 +133,7 @@ class CommandsTestBase(TestCase):
dump = json.loads(output)
# check that all elements in the course structure have inherited metadata,
# and that it contains a particular value as well:
for element_name in dump:
element = dump[element_name]
for element in dump.itervalues():
self.assertIn('metadata', element)
self.assertIn('children', element)
self.assertIn('category', element)
......@@ -158,7 +159,7 @@ class CommandsTestBase(TestCase):
self.check_export_file(tar_file)
def run_export_course(self, filename): # pylint: disable=missing-docstring
args = ['edX/simple/2012_Fall', filename]
args = [TEST_COURSE_ID, filename]
kwargs = {'modulestore': 'default'}
return self.call_command('export_course', *args, **kwargs)
......
......@@ -12,6 +12,7 @@ from .models import (
XModuleStudentInfoField
)
import logging
from xmodule.modulestore.locations import SlashSeparatedCourseKey, Location
from django.db import DatabaseError
from django.contrib.auth.models import User
......@@ -59,6 +60,8 @@ class FieldDataCache(object):
self.cache = {}
self.descriptors = descriptors
self.select_for_update = select_for_update
assert isinstance(course_id, SlashSeparatedCourseKey)
self.course_id = course_id
self.user = user
......@@ -141,8 +144,8 @@ class FieldDataCache(object):
if scope == Scope.user_state:
return self._chunked_query(
StudentModule,
'module_state_key__in',
(str(descriptor.scope_ids.usage_id) for descriptor in self.descriptors),
'module_id__in',
(descriptor.scope_ids.usage_id for descriptor in self.descriptors),
course_id=self.course_id,
student=self.user.pk,
)
......@@ -150,7 +153,7 @@ class FieldDataCache(object):
return self._chunked_query(
XModuleUserStateSummaryField,
'usage_id__in',
(str(descriptor.scope_ids.usage_id) for descriptor in self.descriptors),
(descriptor.scope_ids.usage_id for descriptor in self.descriptors),
field_name__in=set(field.name for field in fields),
)
elif scope == Scope.preferences:
......@@ -185,9 +188,9 @@ class FieldDataCache(object):
Return the key used in the FieldDataCache for the specified KeyValueStore key
"""
if key.scope == Scope.user_state:
return (key.scope, key.block_scope_id.url())
return (key.scope, key.block_scope_id)
elif key.scope == Scope.user_state_summary:
return (key.scope, key.block_scope_id.url(), key.field_name)
return (key.scope, key.block_scope_id, key.field_name)
elif key.scope == Scope.preferences:
return (key.scope, key.block_scope_id, key.field_name)
elif key.scope == Scope.user_info:
......@@ -199,9 +202,15 @@ class FieldDataCache(object):
field
"""
if scope == Scope.user_state:
return (scope, field_object.module_state_key)
assert (field_object.module_state_key.org == self.course_id.org and
field_object.module_state_key.course == self.course_id.course)
return (scope, field_object.module_state_key.map_into_course(self.course_id))
elif scope == Scope.user_state_summary:
return (scope, field_object.usage_id, field_object.field_name)
assert (field_object.usage_id.org == self.course_id.org and
field_object.usage_id.course == self.course_id.course)
return (scope, field_object.usage_id.map_into_course(self.course_id), field_object.field_name)
elif scope == Scope.preferences:
return (scope, field_object.module_type, field_object.field_name)
elif scope == Scope.user_info:
......@@ -233,10 +242,13 @@ class FieldDataCache(object):
return field_object
if key.scope == Scope.user_state:
# When we start allowing block_scope_ids to be either Locations or Locators,
# this assertion will fail. Fix the code here when that happens!
assert(isinstance(key.block_scope_id, Location))
field_object, _ = StudentModule.objects.get_or_create(
course_id=self.course_id,
student=User.objects.get(id=key.user_id),
module_state_key=key.block_scope_id.url(),
module_id=key.block_scope_id.replace(run=None),
defaults={
'state': json.dumps({}),
'module_type': key.block_scope_id.category,
......@@ -245,7 +257,7 @@ class FieldDataCache(object):
elif key.scope == Scope.user_state_summary:
field_object, _ = XModuleUserStateSummaryField.objects.get_or_create(
field_name=key.field_name,
usage_id=key.block_scope_id.url()
usage_id=key.block_scope_id
)
elif key.scope == Scope.preferences:
field_object, _ = XModuleStudentPrefsField.objects.get_or_create(
......
......@@ -18,6 +18,8 @@ from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from xmodule_django.models import CourseKeyField, LocationKeyField
class StudentModule(models.Model):
"""
......@@ -38,12 +40,31 @@ class StudentModule(models.Model):
# but for abtests and the like, this can be set to a shared value
# for many instances of the module.
# Filename for homeworks, etc.
module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
module_id = LocationKeyField(max_length=255, db_index=True, db_column='module_id')
student = models.ForeignKey(User, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
# TODO: This is a lie; course_id now represents something more like a course_key. We may
# or may not want to change references to this to something like course_key or course_key_field in
# this file. (Certain changes would require a DB migration which is probably not what we want.)
# Someone should look at this and reevaluate before the final merge into master.
course_id = CourseKeyField(max_length=255, db_index=True)
@property
def module_state_key(self):
"""
Returns a Location based on module_id and course_id
"""
return self.course_id.make_usage_key(self.module_id.category, self.module_id.name)
@module_state_key.setter
def module_state_key(self, usage_key):
"""
Set the module_id and course_id from the passed UsageKey
"""
self.course_id = usage_key.course_key
self.module_id = usage_key
class Meta:
unique_together = (('student', 'module_state_key', 'course_id'),)
unique_together = (('student', 'module_id', 'course_id'),)
## Internal state of the object
state = models.TextField(null=True, blank=True)
......@@ -110,7 +131,7 @@ class StudentModuleHistory(models.Model):
max_grade = models.FloatField(null=True, blank=True)
@receiver(post_save, sender=StudentModule)
def save_history(sender, instance, **kwargs):
def save_history(sender, instance, **kwargs): # pylint: disable=no-self-argument
if instance.module_type in StudentModuleHistory.HISTORY_SAVING_TYPES:
history_entry = StudentModuleHistory(student_module=instance,
version=None,
......@@ -133,7 +154,7 @@ class XModuleUserStateSummaryField(models.Model):
field_name = models.CharField(max_length=64, db_index=True)
# The definition id for the module
usage_id = models.CharField(max_length=255, db_index=True)
usage_id = LocationKeyField(max_length=255, db_index=True)
# The value of the field. Defaults to None dumped as json
value = models.TextField(default='null')
......@@ -221,7 +242,7 @@ class OfflineComputedGrade(models.Model):
Table of grades computed offline for a given user and course.
"""
user = models.ForeignKey(User, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
course_id = CourseKeyField(max_length=255, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
......@@ -244,10 +265,10 @@ class OfflineComputedGradeLog(models.Model):
ordering = ["-created"]
get_latest_by = "created"
course_id = models.CharField(max_length=255, db_index=True)
course_id = CourseKeyField(max_length=255, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
seconds = models.IntegerField(default=0) # seconds elapsed for computation
nstudents = models.IntegerField(default=0)
def __unicode__(self):
return "[OCGLog] %s: %s" % (self.course_id, self.created)
return "[OCGLog] %s: %s" % (self.course_id.to_deprecated_string(), self.created) # pylint: disable=no-member
......@@ -7,6 +7,7 @@ import static_replace
from functools import partial
from requests.auth import HTTPBasicAuth
from dogapi import dog_stats_api
from opaque_keys import InvalidKeyError
from django.conf import settings
from django.contrib.auth.models import User
......@@ -21,7 +22,7 @@ from courseware.access import has_access, get_user_role
from courseware.masquerade import setup_masquerade
from courseware.model_data import FieldDataCache, DjangoKeyValueStore
from lms.lib.xblock.field_data import LmsFieldData
from lms.lib.xblock.runtime import LmsModuleSystem, unquote_slashes
from lms.lib.xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes
from edxmako.shortcuts import render_to_string
from eventtracking import tracker
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
......@@ -33,7 +34,7 @@ from xblock.exceptions import NoSuchHandlerError
from xblock.django.request import django_to_webob_request, webob_to_django_response
from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore import Location
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.django import modulestore, ModuleI18nService
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.util.duedate import get_extended_due_date
......@@ -49,16 +50,20 @@ log = logging.getLogger(__name__)
if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
requests_auth = HTTPBasicAuth(*settings.XQUEUE_INTERFACE['basic_auth'])
REQUESTS_AUTH = HTTPBasicAuth(*settings.XQUEUE_INTERFACE['basic_auth'])
else:
requests_auth = None
REQUESTS_AUTH = None
xqueue_interface = XQueueInterface(
XQUEUE_INTERFACE = XQueueInterface(
settings.XQUEUE_INTERFACE['url'],
settings.XQUEUE_INTERFACE['django_auth'],
requests_auth,
REQUESTS_AUTH,
)
# TODO basically all instances of course_id in this file *should* be changed to course_key, but
# there's a couple tricky ones I'm too afraid to change before we merge the jellyfish branches.
# This should be fixed after the jellyfish merge, before merge into master.
def make_track_function(request):
'''
......@@ -127,7 +132,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
return chapters
def get_module(user, request, location, field_data_cache, course_id,
def get_module(user, request, usage_key, field_data_cache,
position=None, not_found_ok=False, wrap_xmodule_display=True,
grade_bucket_type=None, depth=0,
static_asset_path=''):
......@@ -157,9 +162,8 @@ def get_module(user, request, location, field_data_cache, course_id,
if possible. If not possible, return None.
"""
try:
location = Location(location)
descriptor = modulestore().get_instance(course_id, location, depth=depth)
return get_module_for_descriptor(user, request, descriptor, field_data_cache, course_id,
descriptor = modulestore().get_item(usage_key, depth=depth)
return get_module_for_descriptor(user, request, descriptor, field_data_cache, usage_key.course_key,
position=position,
wrap_xmodule_display=wrap_xmodule_display,
grade_bucket_type=grade_bucket_type,
......@@ -198,7 +202,7 @@ def get_module_for_descriptor(user, request, descriptor, field_data_cache, cours
See get_module() docstring for further details.
"""
# allow course staff to masquerade as student
if has_access(user, descriptor, 'staff', course_id):
if has_access(user, 'staff', descriptor, course_id):
setup_masquerade(request, True)
track_function = make_track_function(request)
......@@ -223,7 +227,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
# Do not check access when it's a noauth request.
if getattr(user, 'known', True):
# Short circuit--if the user shouldn't have access, bail without doing any work
if not has_access(user, descriptor, 'load', course_id):
if not has_access(user, 'load', descriptor, course_id):
return None
student_data = KvsFieldData(DjangoKeyValueStore(field_data_cache))
......@@ -234,9 +238,9 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
relative_xqueue_callback_url = reverse(
'xqueue_callback',
kwargs=dict(
course_id=course_id,
course_id=course_id.to_deprecated_string(),
userid=str(user.id),
mod_id=descriptor.location.url(),
mod_id=descriptor.location.to_deprecated_string(),
dispatch=dispatch
),
)
......@@ -248,7 +252,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
xqueue = {
'interface': xqueue_interface,
'interface': XQUEUE_INTERFACE,
'construct_callback': make_xqueue_callback,
'default_queuename': xqueue_default_queuename.replace(' ', '_'),
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
......@@ -312,12 +316,10 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
# Bin score into range and increment stats
score_bucket = get_score_bucket(student_module.grade, student_module.max_grade)
course_id_dict = Location.parse_course_id(course_id)
tags = [
u"org:{org}".format(**course_id_dict),
u"course:{course}".format(**course_id_dict),
u"run:{name}".format(**course_id_dict),
u"org:{}".format(course_id.org),
u"course:{}".format(course_id),
u"score_bucket:{0}".format(score_bucket)
]
......@@ -340,7 +342,11 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
# Wrap the output display in a single div to allow for the XModule
# javascript to be bound correctly
if wrap_xmodule_display is True:
block_wrappers.append(partial(wrap_xblock, 'LmsRuntime', extra_data={'course-id': course_id}))
block_wrappers.append(partial(
wrap_xblock, 'LmsRuntime',
extra_data={'course-id': course_id.to_deprecated_string()},
usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string())
))
# TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory
......@@ -366,11 +372,11 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
block_wrappers.append(partial(
replace_jump_to_id_urls,
course_id,
reverse('jump_to_id', kwargs={'course_id': course_id, 'module_id': ''}),
reverse('jump_to_id', kwargs={'course_id': course_id.to_deprecated_string(), 'module_id': ''}),
))
if settings.FEATURES.get('DISPLAY_DEBUG_INFO_TO_STAFF'):
if has_access(user, descriptor, 'staff', course_id):
if has_access(user, 'staff', descriptor, course_id):
block_wrappers.append(partial(add_staff_markup, user))
# These modules store data using the anonymous_student_id as a key.
......@@ -385,7 +391,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
if is_pure_xblock or is_lti_module:
anonymous_student_id = anonymous_id_for_user(user, course_id)
else:
anonymous_student_id = anonymous_id_for_user(user, '')
anonymous_student_id = anonymous_id_for_user(user, None)
system = LmsModuleSystem(
track_function=track_function,
......@@ -409,12 +415,12 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
),
replace_course_urls=partial(
static_replace.replace_course_urls,
course_id=course_id
course_key=course_id
),
replace_jump_to_id_urls=partial(
static_replace.replace_jump_to_id_urls,
course_id=course_id,
jump_to_id_base_url=reverse('jump_to_id', kwargs={'course_id': course_id, 'module_id': ''})
jump_to_id_base_url=reverse('jump_to_id', kwargs={'course_id': course_id.to_deprecated_string(), 'module_id': ''})
),
node_path=settings.NODE_PATH,
publish=publish,
......@@ -440,13 +446,13 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
if settings.FEATURES.get('ENABLE_PSYCHOMETRICS'):
system.set(
'psychometrics_handler', # set callback for updating PsychometricsData
make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())
make_psychometrics_data_update_handler(course_id, user, descriptor.location.to_deprecated_string())
)
system.set(u'user_is_staff', has_access(user, descriptor.location, u'staff', course_id))
system.set(u'user_is_staff', has_access(user, u'staff', descriptor.location, course_id))
# make an ErrorDescriptor -- assuming that the descriptor's system is ok
if has_access(user, descriptor.location, 'staff', course_id):
if has_access(user, u'staff', descriptor.location, course_id):
system.error_descriptor_class = ErrorDescriptor
else:
system.error_descriptor_class = NonStaffErrorDescriptor
......@@ -460,15 +466,17 @@ def find_target_student_module(request, user_id, course_id, mod_id):
"""
Retrieve target StudentModule
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
usage_key = course_id.make_usage_key_from_deprecated_string(mod_id)
user = User.objects.get(id=user_id)
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course_id,
user,
modulestore().get_instance(course_id, mod_id),
modulestore().get_item(usage_key),
depth=0,
select_for_update=True
)
instance = get_module(user, request, mod_id, field_data_cache, course_id, grade_bucket_type='xqueue')
instance = get_module(user, request, usage_key, field_data_cache, grade_bucket_type='xqueue')
if instance is None:
msg = "No module {0} for user {1}--access denied?".format(mod_id, user)
log.debug(msg)
......@@ -566,11 +574,19 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
"""
Invoke an XBlock handler, either authenticated or not.
"""
location = unquote_slashes(usage_id)
Arguments:
request (HttpRequest): the current request
course_id (str): A string of the form org/course/run
usage_id (str): A string of the form i4x://org/course/category/name@revision
handler (str): The name of the handler to invoke
suffix (str): The suffix to pass to the handler when invoked
user (User): The currently logged in user
# Check parameters and fail fast if there's a problem
if not Location.is_valid(location):
"""
try:
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
usage_key = course_id.make_usage_key_from_deprecated_string(unquote_slashes(usage_id))
except InvalidKeyError:
raise Http404("Invalid location")
# Check submitted files
......@@ -580,12 +596,12 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
return HttpResponse(json.dumps({'success': error_msg}))
try:
descriptor = modulestore().get_instance(course_id, location)
descriptor = modulestore().get_item(usage_key)
except ItemNotFoundError:
log.warn(
"Invalid location for course id {course_id}: {location}".format(
course_id=course_id,
location=location
"Invalid location for course id {course_id}: {usage_key}".format(
course_id=usage_key.course_key,
usage_key=usage_key
)
)
raise Http404
......@@ -602,11 +618,11 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
user,
descriptor
)
instance = get_module(user, request, location, field_data_cache, course_id, grade_bucket_type='ajax')
instance = get_module(user, request, usage_key, field_data_cache, grade_bucket_type='ajax')
if instance is None:
# Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to.
log.debug("No module %s for user %s -- access denied?", location, user)
log.debug("No module %s for user %s -- access denied?", usage_key, user)
raise Http404
req = django_to_webob_request(request)
......
......@@ -94,7 +94,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
#self.item_module = self.item_descriptor.xmodule_runtime.xmodule_instance
#self.item_module is None at this time
self.item_url = Location(self.item_descriptor.location).url()
self.item_url = self.item_descriptor.location.to_deprecated_string()
def setup_course(self):
self.course = CourseFactory.create(data=self.COURSE_DATA)
......@@ -139,7 +139,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
"""Return item url with dispatch."""
return reverse(
'xblock_handler',
args=(self.course.id, quote_slashes(self.item_url), 'xmodule_handler', dispatch)
args=(self.course.id.to_deprecated_string(), quote_slashes(self.item_url), 'xmodule_handler', dispatch)
)
......
......@@ -6,9 +6,6 @@ from factory.django import DjangoModelFactory
# Imported to re-export
# pylint: disable=unused-import
from student.tests.factories import UserFactory # Imported to re-export
from student.tests.factories import GroupFactory # Imported to re-export
from student.tests.factories import CourseEnrollmentAllowedFactory # Imported to re-export
from student.tests.factories import RegistrationFactory # Imported to re-export
# pylint: enable=unused-import
from student.tests.factories import UserProfileFactory as StudentUserProfileFactory
......@@ -23,10 +20,11 @@ from student.roles import (
OrgInstructorRole,
)
from xmodule.modulestore import Location
from xmodule.modulestore.locations import SlashSeparatedCourseKey
location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
course_id = SlashSeparatedCourseKey(u'edX', u'test_course', u'test')
location = partial(course_id.make_usage_key, u'problem')
class UserProfileFactory(StudentUserProfileFactory):
......@@ -41,9 +39,10 @@ class InstructorFactory(UserFactory):
last_name = "Instructor"
@factory.post_generation
# TODO Change this from course to course_key at next opportunity
def course(self, create, extracted, **kwargs):
if extracted is None:
raise ValueError("Must specify a course location for a course instructor user")
raise ValueError("Must specify a CourseKey for a course instructor user")
CourseInstructorRole(extracted).add_users(self)
......@@ -55,9 +54,10 @@ class StaffFactory(UserFactory):
last_name = "Staff"
@factory.post_generation
# TODO Change this from course to course_key at next opportunity
def course(self, create, extracted, **kwargs):
if extracted is None:
raise ValueError("Must specify a course location for a course staff user")
raise ValueError("Must specify a CourseKey for a course staff user")
CourseStaffRole(extracted).add_users(self)
......@@ -69,9 +69,10 @@ class BetaTesterFactory(UserFactory):
last_name = "Beta-Tester"
@factory.post_generation
# TODO Change this from course to course_key at next opportunity
def course(self, create, extracted, **kwargs):
if extracted is None:
raise ValueError("Must specify a course location for a beta-tester user")
raise ValueError("Must specify a CourseKey for a beta-tester user")
CourseBetaTesterRole(extracted).add_users(self)
......@@ -83,10 +84,11 @@ class OrgStaffFactory(UserFactory):
last_name = "Org-Staff"
@factory.post_generation
# TODO Change this from course to course_key at next opportunity
def course(self, create, extracted, **kwargs):
if extracted is None:
raise ValueError("Must specify a course location for an org-staff user")
OrgStaffRole(extracted).add_users(self)
raise ValueError("Must specify a CourseKey for an org-staff user")
OrgStaffRole(extracted.org).add_users(self)
class OrgInstructorFactory(UserFactory):
......@@ -97,10 +99,11 @@ class OrgInstructorFactory(UserFactory):
last_name = "Org-Instructor"
@factory.post_generation
# TODO Change this from course to course_key at next opportunity
def course(self, create, extracted, **kwargs):
if extracted is None:
raise ValueError("Must specify a course location for an org-instructor user")
OrgInstructorRole(extracted).add_users(self)
raise ValueError("Must specify a CourseKey for an org-instructor user")
OrgInstructorRole(extracted.org).add_users(self)
class GlobalStaffFactory(UserFactory):
......@@ -119,7 +122,7 @@ class StudentModuleFactory(DjangoModelFactory):
module_type = "problem"
student = factory.SubFactory(UserFactory)
course_id = "MITx/999/Robot_Super_Course"
course_id = SlashSeparatedCourseKey("MITx", "999", "Robot_Super_Course")
state = None
grade = None
max_grade = None
......@@ -131,7 +134,7 @@ class UserStateSummaryFactory(DjangoModelFactory):
field_name = 'existing_field'
value = json.dumps('old_value')
usage_id = location('usage_id').url()
usage_id = location('usage_id')
class StudentPrefsFactory(DjangoModelFactory):
......
......@@ -130,7 +130,7 @@ class LoginEnrollmentTestCase(TestCase):
"""
resp = self.client.post(reverse('change_enrollment'), {
'enrollment_action': 'enroll',
'course_id': course.id,
'course_id': course.id.to_deprecated_string(),
})
result = resp.status_code == 200
if verify:
......@@ -142,5 +142,7 @@ class LoginEnrollmentTestCase(TestCase):
Unenroll the currently logged-in user, and check that it worked.
`course` is an instance of CourseDescriptor.
"""
check_for_post_code(self, 200, reverse('change_enrollment'), {'enrollment_action': 'unenroll',
'course_id': course.id})
check_for_post_code(self, 200, reverse('change_enrollment'), {
'enrollment_action': 'unenroll',
'course_id': course.id.to_deprecated_string()
})
......@@ -9,6 +9,7 @@ from .helpers import LoginEnrollmentTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.locations import SlashSeparatedCourseKey
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
......@@ -22,13 +23,13 @@ class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
def test_logged_in(self):
self.setup_user()
url = reverse('about_course', args=[self.course.id])
url = reverse('about_course', args=[self.course.id.to_deprecated_string()])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
def test_anonymous_user(self):
url = reverse('about_course', args=[self.course.id])
url = reverse('about_course', args=[self.course.id.to_deprecated_string()])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
......@@ -39,7 +40,7 @@ class AboutTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
# The following XML test course (which lives at common/test/data/2014)
# is closed; we're testing that an about page still appears when
# the course is already closed
xml_course_id = 'edX/detached_pages/2014'
xml_course_id = SlashSeparatedCourseKey('edX', 'detached_pages', '2014')
# this text appears in that course's about page
# common/test/data/2014/about/overview.html
......@@ -48,14 +49,14 @@ class AboutTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
@mock.patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_logged_in_xml(self):
self.setup_user()
url = reverse('about_course', args=[self.xml_course_id])
url = reverse('about_course', args=[self.xml_course_id.to_deprecated_string()])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn(self.xml_data, resp.content)
@mock.patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_anonymous_user_xml(self):
url = reverse('about_course', args=[self.xml_course_id])
url = reverse('about_course', args=[self.xml_course_id.to_deprecated_string()])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn(self.xml_data, resp.content)
......@@ -82,7 +83,7 @@ class AboutWithCappedEnrollmentsTestCase(LoginEnrollmentTestCase, ModuleStoreTes
This test will make sure that enrollment caps are enforced
"""
self.setup_user()
url = reverse('about_course', args=[self.course.id])
url = reverse('about_course', args=[self.course.id.to_deprecated_string()])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn('<a href="#" class="register">', resp.content)
......
......@@ -6,11 +6,12 @@ from mock import Mock
from django.test import TestCase
from django.test.utils import override_settings
from courseware.tests.factories import UserFactory, CourseEnrollmentAllowedFactory, StaffFactory, InstructorFactory
from student.tests.factories import AnonymousUserFactory
from courseware.tests.factories import UserFactory, StaffFactory, InstructorFactory
from student.tests.factories import AnonymousUserFactory, CourseEnrollmentAllowedFactory
from xmodule.modulestore import Location
from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE
import pytz
from xmodule.modulestore.locations import SlashSeparatedCourseKey
# pylint: disable=protected-access
......@@ -21,129 +22,161 @@ class AccessTestCase(TestCase):
"""
def setUp(self):
self.course = Location('i4x://edX/toy/course/2012_Fall')
course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
self.course = course_key.make_usage_key('course', course_key.run)
self.anonymous_user = AnonymousUserFactory()
self.student = UserFactory()
self.global_staff = UserFactory(is_staff=True)
self.course_staff = StaffFactory(course=self.course)
self.course_instructor = InstructorFactory(course=self.course)
# TODO please change the StaffFactory and InstructorFactory parameters ASAP!
self.course_staff = StaffFactory(course=self.course.course_key)
self.course_instructor = InstructorFactory(course=self.course.course_key)
def test__has_access_to_location(self):
self.assertFalse(access._has_access_to_location(None, self.course, 'staff', None))
self.assertFalse(access._has_access_to_location(self.anonymous_user, self.course, 'staff', None))
self.assertFalse(access._has_access_to_location(self.anonymous_user, self.course, 'instructor', None))
self.assertTrue(access._has_access_to_location(self.global_staff, self.course, 'staff', None))
self.assertTrue(access._has_access_to_location(self.global_staff, self.course, 'instructor', None))
self.assertFalse(access._has_access_to_location(
None, 'staff', self.course, self.course.course_key
))
self.assertFalse(access._has_access_to_location(
self.anonymous_user, 'staff', self.course, self.course.course_key
))
self.assertFalse(access._has_access_to_location(
self.anonymous_user, 'instructor', self.course, self.course.course_key
))
self.assertTrue(access._has_access_to_location(
self.global_staff, 'staff', self.course, self.course.course_key
))
self.assertTrue(access._has_access_to_location(
self.global_staff, 'instructor', self.course, self.course.course_key
))
# A user has staff access if they are in the staff group
self.assertTrue(access._has_access_to_location(self.course_staff, self.course, 'staff', None))
self.assertFalse(access._has_access_to_location(self.course_staff, self.course, 'instructor', None))
self.assertTrue(access._has_access_to_location(
self.course_staff, 'staff', self.course, self.course.course_key
))
self.assertFalse(access._has_access_to_location(
self.course_staff, 'instructor', self.course, self.course.course_key
))
# A user has staff and instructor access if they are in the instructor group
self.assertTrue(access._has_access_to_location(self.course_instructor, self.course, 'staff', None))
self.assertTrue(access._has_access_to_location(self.course_instructor, self.course, 'instructor', None))
self.assertTrue(access._has_access_to_location(
self.course_instructor, 'staff', self.course, self.course.course_key
))
self.assertTrue(access._has_access_to_location(
self.course_instructor, 'instructor', self.course, self.course.course_key
))
# A user does not have staff or instructor access if they are
# not in either the staff or the the instructor group
self.assertFalse(access._has_access_to_location(self.student, self.course, 'staff', None))
self.assertFalse(access._has_access_to_location(self.student, self.course, 'instructor', None))
self.assertFalse(access._has_access_to_location(
self.student, 'staff', self.course, self.course.course_key
))
self.assertFalse(access._has_access_to_location(
self.student, 'instructor', self.course, self.course.course_key
))
def test__has_access_string(self):
u = Mock(is_staff=True)
self.assertFalse(access._has_access_string(u, 'not_global', 'staff', None))
user = Mock(is_staff=True)
self.assertFalse(access._has_access_string(user, 'staff', 'not_global', self.course.course_key))
u._has_global_staff_access.return_value = True
self.assertTrue(access._has_access_string(u, 'global', 'staff', None))
user._has_global_staff_access.return_value = True
self.assertTrue(access._has_access_string(user, 'staff', 'global', self.course.course_key))
self.assertRaises(ValueError, access._has_access_string, u, 'global', 'not_staff', None)
self.assertRaises(ValueError, access._has_access_string, user, 'not_staff', 'global', self.course.course_key)
def test__has_access_descriptor(self):
# TODO: override DISABLE_START_DATES and test the start date branch of the method
u = Mock()
d = Mock()
d.start = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1) # make sure the start time is in the past
user = Mock()
date = Mock()
date.start = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1) # make sure the start time is in the past
# Always returns true because DISABLE_START_DATES is set in test.py
self.assertTrue(access._has_access_descriptor(u, d, 'load'))
self.assertRaises(ValueError, access._has_access_descriptor, u, d, 'not_load_or_staff')
self.assertTrue(access._has_access_descriptor(user, 'load', date))
with self.assertRaises(ValueError):
access._has_access_descriptor(user, 'not_load_or_staff', date)
def test__has_access_course_desc_can_enroll(self):
u = Mock()
user = Mock()
yesterday = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1)
tomorrow = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1)
c = Mock(enrollment_start=yesterday, enrollment_end=tomorrow, enrollment_domain='')
course = Mock(enrollment_start=yesterday, enrollment_end=tomorrow, enrollment_domain='')
# User can enroll if it is between the start and end dates
self.assertTrue(access._has_access_course_desc(u, c, 'enroll'))
self.assertTrue(access._has_access_course_desc(user, 'enroll', course))
# User can enroll if authenticated and specifically allowed for that course
# even outside the open enrollment period
u = Mock(email='test@edx.org', is_staff=False)
u.is_authenticated.return_value = True
user = Mock(email='test@edx.org', is_staff=False)
user.is_authenticated.return_value = True
c = Mock(enrollment_start=tomorrow, enrollment_end=tomorrow, id='edX/test/2012_Fall', enrollment_domain='')
course = Mock(
enrollment_start=tomorrow, enrollment_end=tomorrow,
id=SlashSeparatedCourseKey('edX', 'test', '2012_Fall'), enrollment_domain=''
)
allowed = CourseEnrollmentAllowedFactory(email=u.email, course_id=c.id)
CourseEnrollmentAllowedFactory(email=user.email, course_id=course.id)
self.assertTrue(access._has_access_course_desc(u, c, 'enroll'))
self.assertTrue(access._has_access_course_desc(user, 'enroll', course))
# Staff can always enroll even outside the open enrollment period
u = Mock(email='test@edx.org', is_staff=True)
u.is_authenticated.return_value = True
user = Mock(email='test@edx.org', is_staff=True)
user.is_authenticated.return_value = True
c = Mock(enrollment_start=tomorrow, enrollment_end=tomorrow, id='edX/test/Whenever', enrollment_domain='')
self.assertTrue(access._has_access_course_desc(u, c, 'enroll'))
course = Mock(
enrollment_start=tomorrow, enrollment_end=tomorrow,
id=SlashSeparatedCourseKey('edX', 'test', 'Whenever'), enrollment_domain='',
)
self.assertTrue(access._has_access_course_desc(user, 'enroll', course))
# TODO:
# Non-staff cannot enroll outside the open enrollment period if not specifically allowed
def test__user_passed_as_none(self):
"""Ensure has_access handles a user being passed as null"""
access.has_access(None, 'global', 'staff', None)
access.has_access(None, 'staff', 'global', None)
class UserRoleTestCase(TestCase):
"""
Tests for user roles.
"""
def setUp(self):
self.course = Location('i4x://edX/toy/course/2012_Fall')
self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
self.anonymous_user = AnonymousUserFactory()
self.student = UserFactory()
self.global_staff = UserFactory(is_staff=True)
self.course_staff = StaffFactory(course=self.course)
self.course_instructor = InstructorFactory(course=self.course)
self.course_staff = StaffFactory(course=self.course_key)
self.course_instructor = InstructorFactory(course=self.course_key)
def test_user_role_staff(self):
"""Ensure that user role is student for staff masqueraded as student."""
self.assertEqual(
'staff',
access.get_user_role(self.course_staff, self.course.course_id)
access.get_user_role(self.course_staff, self.course_key)
)
# Masquerade staff
self.course_staff.masquerade_as_student = True
self.assertEqual(
'student',
access.get_user_role(self.course_staff, self.course.course_id)
access.get_user_role(self.course_staff, self.course_key)
)
def test_user_role_instructor(self):
"""Ensure that user role is student for instructor masqueraded as student."""
self.assertEqual(
'instructor',
access.get_user_role(self.course_instructor, self.course.course_id)
access.get_user_role(self.course_instructor, self.course_key)
)
# Masquerade instructor
self.course_instructor.masquerade_as_student = True
self.assertEqual(
'student',
access.get_user_role(self.course_instructor, self.course.course_id)
access.get_user_role(self.course_instructor, self.course_key)
)
def test_user_role_anonymous(self):
"""Ensure that user role is student for anonymous user."""
self.assertEqual(
'student',
access.get_user_role(self.anonymous_user, self.course.course_id)
access.get_user_role(self.anonymous_user, self.course_key)
)
......@@ -22,13 +22,13 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
def test_logged_in(self):
self.setup_user()
url = reverse('info', args=[self.course.id])
url = reverse('info', args=[self.course.id.to_deprecated_string()])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
def test_anonymous_user(self):
url = reverse('info', args=[self.course.id])
url = reverse('info', args=[self.course.id.to_deprecated_string()])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertNotIn("OOGIE BLOOGIE", resp.content)
......
......@@ -4,7 +4,6 @@ Tests for course access
"""
import mock
from django.http import Http404
from django.test.utils import override_settings
from student.tests.factories import UserFactory
from xmodule.modulestore.django import get_default_store_name_for_current_request
......@@ -14,16 +13,12 @@ from xmodule.tests.xml import factories as xml
from xmodule.tests.xml import XModuleXmlImportTest
from courseware.courses import (
get_course_by_id,
get_course,
get_cms_course_link,
get_cms_block_link,
course_image_url,
get_course_info_section,
get_course_about_section
get_course_by_id, get_cms_course_link, course_image_url,
get_course_info_section, get_course_about_section, get_course
)
from courseware.tests.helpers import get_request_for_user
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE, TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.locations import SlashSeparatedCourseKey
CMS_BASE_TEST = 'testcms'
......@@ -34,25 +29,29 @@ class CoursesTest(ModuleStoreTestCase):
def test_get_course_by_id_invalid_chars(self):
"""
Test that `get_course_by_id` throws a 404, rather than
an exception, when faced with unexpected characters
(such as unicode characters, and symbols such as = and ' ')
Test that `get_course` throws a 404, rather than an exception,
when faced with unexpected characters (such as unicode characters,
and symbols such as = and ' ')
"""
with self.assertRaises(Http404):
get_course_by_id('MITx/foobar/statistics=introduction')
get_course_by_id('MITx/foobar/business and management')
get_course_by_id('MITx/foobar/NiñøJoséMaríáßç')
get_course_by_id(SlashSeparatedCourseKey('MITx', 'foobar', 'business and management'))
with self.assertRaises(Http404):
get_course_by_id(SlashSeparatedCourseKey('MITx', 'foobar' 'statistics=introduction'))
with self.assertRaises(Http404):
get_course_by_id(SlashSeparatedCourseKey('MITx', 'foobar', 'NiñøJoséMaríáßç'))
def test_get_course_invalid_chars(self):
"""
Test that `get_course` throws a ValueError, rather than
a 404, when faced with unexpected characters
(such as unicode characters, and symbols such as = and ' ')
Test that `get_course` throws a ValueError, rather than a 404,
when faced with unexpected characters (such as unicode characters,
and symbols such as = and ' ')
"""
with self.assertRaises(ValueError):
get_course('MITx/foobar/statistics=introduction')
get_course('MITx/foobar/business and management')
get_course('MITx/foobar/NiñøJoséMaríáßç')
get_course(SlashSeparatedCourseKey('MITx', 'foobar', 'business and management'))
with self.assertRaises(ValueError):
get_course(SlashSeparatedCourseKey('MITx', 'foobar', 'statistics=introduction'))
with self.assertRaises(ValueError):
get_course(SlashSeparatedCourseKey('MITx', 'foobar', 'NiñøJoséMaríáßç'))
@override_settings(
MODULESTORE=TEST_DATA_MONGO_MODULESTORE, CMS_BASE=CMS_BASE_TEST
......@@ -67,6 +66,7 @@ class CoursesTest(ModuleStoreTestCase):
org='org', number='num', display_name='name'
)
cms_url = u"//{}/course/slashes:org+num+name".format(CMS_BASE_TEST)
self.assertEqual(cms_url, get_cms_course_link(self.course))
self.assertEqual(cms_url, get_cms_block_link(self.course, 'course'))
......@@ -146,10 +146,11 @@ class XmlCourseImageTestCase(XModuleXmlImportTest):
class CoursesRenderTest(ModuleStoreTestCase):
"""Test methods related to rendering courses content."""
toy_course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
def test_get_course_info_section_render(self):
course = get_course_by_id('edX/toy/2012_Fall')
course = get_course_by_id(self.toy_course_key)
request = get_request_for_user(UserFactory.create())
# Test render works okay
......@@ -167,7 +168,7 @@ class CoursesRenderTest(ModuleStoreTestCase):
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
@mock.patch('courseware.courses.get_request_for_thread')
def test_get_course_about_section_render(self, mock_get_request):
course = get_course_by_id('edX/toy/2012_Fall')
course = get_course_by_id(self.toy_course_key)
request = get_request_for_user(UserFactory.create())
mock_get_request.return_value = request
......
......@@ -2,7 +2,7 @@ from django.test import TestCase
from django.test.utils import override_settings
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from modulestore_config import TEST_DATA_DRAFT_MONGO_MODULESTORE
......@@ -13,8 +13,7 @@ class TestDraftModuleStore(TestCase):
store = modulestore()
# fix was to allow get_items() to take the course_id parameter
store.get_items(Location(None, None, 'vertical', None, None),
course_id='abc', depth=0)
store.get_items(SlashSeparatedCourseKey('a', 'b', 'c'), category='vertical')
# test success is just getting through the above statement.
# The bug was that 'course_id' argument was
......
......@@ -9,6 +9,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from courseware.grades import grade, iterate_grades_for
......@@ -62,7 +63,7 @@ class TestGradeIteration(ModuleStoreTestCase):
should be raised. This is a horrible crossing of abstraction boundaries
and should be fixed, but for now we're just testing the behavior. :-("""
with self.assertRaises(Http404):
gradeset_results = iterate_grades_for("I/dont/exist", [])
gradeset_results = iterate_grades_for(SlashSeparatedCourseKey("I", "dont", "exist"), [])
gradeset_results.next()
def test_all_empty_grades(self):
......
......@@ -27,7 +27,8 @@ class TestLTI(BaseTestXmodule):
mocked_signature_after_sign = u'my_signature%3D'
mocked_decoded_signature = u'my_signature='
context_id = self.item_descriptor.course_id
# TODO this course_id is actually a course_key; please change this ASAP!
context_id = self.item_descriptor.course_id.to_deprecated_string()
user_id = unicode(self.item_descriptor.xmodule_runtime.anonymous_student_id)
hostname = self.item_descriptor.xmodule_runtime.hostname
resource_link_id = unicode(urllib.quote('{}-{}'.format(hostname, self.item_descriptor.location.html_id())))
......@@ -38,10 +39,6 @@ class TestLTI(BaseTestXmodule):
user_id=user_id
)
lis_outcome_service_url = 'https://{host}{path}'.format(
host=hostname,
path=self.item_descriptor.xmodule_runtime.handler_url(self.item_descriptor, 'grade_handler', thirdparty=True).rstrip('/?')
)
self.correct_headers = {
u'user_id': user_id,
u'oauth_callback': u'about:blank',
......
......@@ -12,14 +12,14 @@ import json
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from courseware.tests.factories import StaffFactory
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from student.roles import CourseStaffRole
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from lms.lib.xblock.runtime import quote_slashes
from xmodule.modulestore.locations import SlashSeparatedCourseKey
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
......@@ -33,26 +33,19 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
# Clear out the modulestores, causing them to reload
clear_existing_modulestores()
self.graded_course = modulestore().get_course("edX/graded/2012_Fall")
self.graded_course = modulestore().get_course(SlashSeparatedCourseKey("edX", "graded", "2012_Fall"))
# Create staff account
self.instructor = 'view2@test.com'
self.password = 'foo'
self.create_account('u2', self.instructor, self.password)
self.activate_user(self.instructor)
def make_instructor(course):
CourseStaffRole(course.location).add_users(User.objects.get(email=self.instructor))
make_instructor(self.graded_course)
self.staff = StaffFactory(course=self.graded_course.id)
self.logout()
self.login(self.instructor, self.password)
# self.staff.password is the sha hash but login takes the plain text
self.login(self.staff.email, 'test')
self.enroll(self.graded_course)
def get_cw_section(self):
url = reverse('courseware_section',
kwargs={'course_id': self.graded_course.id,
kwargs={'course_id': self.graded_course.id.to_deprecated_string(),
'chapter': 'GradedChapter',
'section': 'Homework1'})
......@@ -64,7 +57,7 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
def test_staff_debug_for_staff(self):
resp = self.get_cw_section()
sdebug = 'Staff Debug Info'
print resp.content
self.assertTrue(sdebug in resp.content)
def toggle_masquerade(self):
......@@ -88,11 +81,11 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
def get_problem(self):
pun = 'H1P1'
problem_location = "i4x://edX/graded/problem/%s" % pun
problem_location = self.graded_course.id.make_usage_key("problem", pun)
modx_url = reverse('xblock_handler',
kwargs={'course_id': self.graded_course.id,
'usage_id': quote_slashes(problem_location),
kwargs={'course_id': self.graded_course.id.to_deprecated_string(),
'usage_id': quote_slashes(problem_location.to_deprecated_string()),
'handler': 'xmodule_handler',
'suffix': 'problem_get'})
......
......@@ -11,12 +11,11 @@ from courseware.models import StudentModule, XModuleUserStateSummaryField
from courseware.models import XModuleStudentInfoField, XModuleStudentPrefsField
from student.tests.factories import UserFactory
from courseware.tests.factories import StudentModuleFactory as cmfStudentModuleFactory
from courseware.tests.factories import StudentModuleFactory as cmfStudentModuleFactory, location, course_id
from courseware.tests.factories import UserStateSummaryFactory
from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory
from xblock.fields import Scope, BlockScope, ScopeIds
from xmodule.modulestore import Location
from django.test import TestCase
from django.db import DatabaseError
from xblock.core import KeyValueMultiSaveError
......@@ -37,9 +36,6 @@ def mock_descriptor(fields=[]):
descriptor.module_class.__name__ = 'MockProblemModule'
return descriptor
location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
course_id = 'edX/test_course/test'
# The user ids here are 1 because we make a student in the setUp functions, and
# they get an id of 1. There's an assertion in setUp to ensure that assumption
# is still true.
......@@ -51,7 +47,7 @@ user_info_key = partial(DjangoKeyValueStore.Key, Scope.user_info, 1, None)
class StudentModuleFactory(cmfStudentModuleFactory):
module_state_key = location('usage_id').url()
module_state_key = location('usage_id')
course_id = course_id
......@@ -204,7 +200,7 @@ class TestMissingStudentModule(TestCase):
student_module = StudentModule.objects.all()[0]
self.assertEquals({'a_field': 'a_value'}, json.loads(student_module.state))
self.assertEquals(self.user, student_module.student)
self.assertEquals(location('usage_id').url(), student_module.module_state_key)
self.assertEquals(location('usage_id'), student_module.module_state_key)
self.assertEquals(course_id, student_module.course_id)
def test_delete_field_from_missing_student_module(self):
......@@ -317,12 +313,12 @@ class StorageTestBase(object):
self.assertEquals(exception.saved_field_names[0], 'existing_field')
class TestContentStorage(StorageTestBase, TestCase):
"""Tests for ContentStorage"""
class TestUserStateSummaryStorage(StorageTestBase, TestCase):
"""Tests for UserStateSummaryStorage"""
factory = UserStateSummaryFactory
scope = Scope.user_state_summary
key_factory = user_state_summary_key
storage_class = XModuleUserStateSummaryField
storage_class = factory.FACTORY_FOR
class TestStudentPrefsStorage(OtherUserFailureTestMixin, StorageTestBase, TestCase):
......
......@@ -18,11 +18,11 @@ from xblock.field_data import FieldData
from xblock.runtime import Runtime
from xblock.fields import ScopeIds
from xmodule.lti_module import LTIDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from courseware import module_render as render
from courseware.courses import get_course_with_access, course_image_url, get_course_info_section
......@@ -43,9 +43,9 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
Tests of courseware.module_render
"""
def setUp(self):
self.location = Location('i4x', 'edX', 'toy', 'chapter', 'Overview')
self.course_id = 'edX/toy/2012_Fall'
self.toy_course = modulestore().get_course(self.course_id)
self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
self.location = self.course_key.make_usage_key('chapter', 'Overview')
self.toy_course = modulestore().get_course(self.course_key)
self.mock_user = UserFactory()
self.mock_user.id = 1
self.request_factory = RequestFactory()
......@@ -56,7 +56,7 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.dispatch = 'score_update'
# Construct a 'standard' xqueue_callback url
self.callback_url = reverse('xqueue_callback', kwargs=dict(course_id=self.course_id,
self.callback_url = reverse('xqueue_callback', kwargs=dict(course_id=self.course_key.to_deprecated_string(),
userid=str(self.mock_user.id),
mod_id=self.mock_module.id,
dispatch=self.dispatch))
......@@ -76,17 +76,17 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
mock_request = MagicMock()
mock_request.user = self.mock_user
course = get_course_with_access(self.mock_user, self.course_id, 'load')
course = get_course_with_access(self.mock_user, 'load', self.course_key)
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course_id, self.mock_user, course, depth=2)
self.course_key, self.mock_user, course, depth=2)
module = render.get_module(
self.mock_user,
mock_request,
Location('i4x', 'edX', 'toy', 'html', 'toyjumpto'),
self.course_key.make_usage_key('html', 'toyjumpto'),
field_data_cache,
self.course_id
self.course_key
)
# get the rendered HTML output which should have the rewritten link
......@@ -94,7 +94,7 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
# See if the url got rewritten to the target link
# note if the URL mapping changes then this assertion will break
self.assertIn('/courses/' + self.course_id + '/jump_to_id/vertical_test', html)
self.assertIn('/courses/' + self.course_key.to_deprecated_string() + '/jump_to_id/vertical_test', html)
def test_xqueue_callback_success(self):
......@@ -113,7 +113,7 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
get_fake_module.return_value = self.mock_module
# call xqueue_callback with our mocked information
request = self.request_factory.post(self.callback_url, data)
render.xqueue_callback(request, self.course_id, self.mock_user.id, self.mock_module.id, self.dispatch)
render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch)
# Verify that handle ajax is called with the correct data
request.POST['queuekey'] = fake_key
......@@ -130,12 +130,12 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
# Test with missing xqueue data
with self.assertRaises(Http404):
request = self.request_factory.post(self.callback_url, {})
render.xqueue_callback(request, self.course_id, self.mock_user.id, self.mock_module.id, self.dispatch)
render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch)
# Test with missing xqueue_header
with self.assertRaises(Http404):
request = self.request_factory.post(self.callback_url, data)
render.xqueue_callback(request, self.course_id, self.mock_user.id, self.mock_module.id, self.dispatch)
render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch)
def test_get_score_bucket(self):
self.assertEquals(render.get_score_bucket(0, 10), 'incorrect')
......@@ -149,8 +149,8 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
dispatch_url = reverse(
'xblock_handler',
args=[
'edX/toy/2012_Fall',
quote_slashes('i4x://edX/toy/videosequence/Toy_Videos'),
self.course_key.to_deprecated_string(),
quote_slashes(self.course_key.make_usage_key('videosequence', 'Toy_Videos').to_deprecated_string()),
'xmodule_handler',
'goto_position'
]
......@@ -166,9 +166,9 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
def setUp(self):
self.location = Location('i4x', 'edX', 'toy', 'chapter', 'Overview')
self.course_id = 'edX/toy/2012_Fall'
self.toy_course = modulestore().get_course(self.course_id)
self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
self.location = self.course_key.make_usage_key('chapter', 'Overview')
self.toy_course = modulestore().get_course(self.course_key)
self.mock_user = UserFactory()
self.mock_user.id = 1
self.request_factory = RequestFactory()
......@@ -179,10 +179,14 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.dispatch = 'score_update'
# Construct a 'standard' xqueue_callback url
self.callback_url = reverse('xqueue_callback', kwargs=dict(course_id=self.course_id,
userid=str(self.mock_user.id),
mod_id=self.mock_module.id,
dispatch=self.dispatch))
self.callback_url = reverse(
'xqueue_callback', kwargs={
'course_id': self.course_key.to_deprecated_string(),
'userid': str(self.mock_user.id),
'mod_id': self.mock_module.id,
'dispatch': self.dispatch
}
)
def _mock_file(self, name='file', size=10):
"""Create a mock file object for testing uploads"""
......@@ -201,7 +205,7 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
with self.assertRaises(Http404):
render.handle_xblock_callback(
request,
'dummy/course/id',
self.course_key.to_deprecated_string(),
'invalid Location',
'dummy_handler'
'dummy_dispatch'
......@@ -216,8 +220,8 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertEquals(
render.handle_xblock_callback(
request,
'dummy/course/id',
quote_slashes(str(self.location)),
self.course_key.to_deprecated_string(),
quote_slashes(self.location.to_deprecated_string()),
'dummy_handler'
).content,
json.dumps({
......@@ -236,8 +240,8 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertEquals(
render.handle_xblock_callback(
request,
'dummy/course/id',
quote_slashes(str(self.location)),
self.course_key.to_deprecated_string(),
quote_slashes(self.location.to_deprecated_string()),
'dummy_handler'
).content,
json.dumps({
......@@ -251,8 +255,8 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
request.user = self.mock_user
response = render.handle_xblock_callback(
request,
self.course_id,
quote_slashes(str(self.location)),
self.course_key.to_deprecated_string(),
quote_slashes(self.location.to_deprecated_string()),
'xmodule_handler',
'goto_position',
)
......@@ -265,7 +269,7 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
render.handle_xblock_callback(
request,
'bad_course_id',
quote_slashes(str(self.location)),
quote_slashes(self.location.to_deprecated_string()),
'xmodule_handler',
'goto_position',
)
......@@ -276,8 +280,8 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
with self.assertRaises(Http404):
render.handle_xblock_callback(
request,
self.course_id,
quote_slashes(str(Location('i4x', 'edX', 'toy', 'chapter', 'bad_location'))),
self.course_key.to_deprecated_string(),
quote_slashes(self.course_key.make_usage_key('chapter', 'bad_location').to_deprecated_string()),
'xmodule_handler',
'goto_position',
)
......@@ -288,8 +292,8 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
with self.assertRaises(Http404):
render.handle_xblock_callback(
request,
self.course_id,
quote_slashes(str(self.location)),
self.course_key.to_deprecated_string(),
quote_slashes(self.location.to_deprecated_string()),
'xmodule_handler',
'bad_dispatch',
)
......@@ -300,8 +304,8 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
with self.assertRaises(Http404):
render.handle_xblock_callback(
request,
self.course_id,
quote_slashes(str(self.location)),
self.course_key.to_deprecated_string(),
quote_slashes(self.location.to_deprecated_string()),
'bad_handler',
'bad_dispatch',
)
......@@ -313,13 +317,13 @@ class TestTOC(TestCase):
def setUp(self):
# Toy courses should be loaded
self.course_name = 'edX/toy/2012_Fall'
self.toy_course = modulestore().get_course(self.course_name)
self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
self.toy_course = modulestore().get_course(self.course_key)
self.portal_user = UserFactory()
def test_toc_toy_from_chapter(self):
chapter = 'Overview'
chapter_url = '%s/%s/%s' % ('/courses', self.course_name, chapter)
chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter)
factory = RequestFactory()
request = factory.get(chapter_url)
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
......@@ -346,7 +350,7 @@ class TestTOC(TestCase):
def test_toc_toy_from_section(self):
chapter = 'Overview'
chapter_url = '%s/%s/%s' % ('/courses', self.course_name, chapter)
chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter)
section = 'Welcome'
factory = RequestFactory()
request = factory.get(chapter_url)
......@@ -506,7 +510,7 @@ class TestHtmlModifiers(ModuleStoreTestCase):
self.assertIn(
'/courses/{course_id}/bar/content'.format(
course_id=self.course.id
course_id=self.course.id.to_deprecated_string()
),
result_fragment.content
)
......@@ -558,11 +562,11 @@ class ViewInStudioTest(ModuleStoreTestCase):
Define the XML backed course to use.
Toy courses are already loaded in XML and mixed modulestores.
"""
course_id = 'edX/toy/2012_Fall'
location = Location('i4x', 'edX', 'toy', 'chapter', 'Overview')
descriptor = modulestore().get_instance(course_id, location)
course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
location = course_key.make_usage_key('chapter', 'Overview')
descriptor = modulestore().get_item(location)
self._get_module(course_id, descriptor, location)
self._get_module(course_key, descriptor, location)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
......@@ -722,7 +726,7 @@ class TestStaffDebugInfo(ModuleStoreTestCase):
StudentModuleFactory.create(
course_id=self.course.id,
module_state_key=self.location,
module_id=self.location,
student=UserFactory(),
grade=1,
max_grade=1,
......@@ -761,7 +765,7 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
@patch('courseware.module_render.has_access', Mock(return_value=True))
def _get_anonymous_id(self, course_id, xblock_class):
location = Location('dummy_org', 'dummy_course', 'dummy_category', 'dummy_name')
location = course_id.make_usage_key('dummy_category', 'dummy_name')
descriptor = Mock(
spec=xblock_class,
_field_data=Mock(spec=FieldData),
......@@ -796,7 +800,7 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
# This value is set by observation, so that later changes to the student
# id computation don't break old data
'5afe5d9bb03796557ee2614f5c9611fb',
self._get_anonymous_id(course_id, descriptor_class)
self._get_anonymous_id(SlashSeparatedCourseKey.from_deprecated_string(course_id), descriptor_class)
)
@data(*PER_COURSE_ANONYMIZED_DESCRIPTORS)
......@@ -805,14 +809,14 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
# This value is set by observation, so that later changes to the student
# id computation don't break old data
'e3b0b940318df9c14be59acb08e78af5',
self._get_anonymous_id('MITx/6.00x/2012_Fall', descriptor_class)
self._get_anonymous_id(SlashSeparatedCourseKey('MITx', '6.00x', '2012_Fall'), descriptor_class)
)
self.assertEquals(
# This value is set by observation, so that later changes to the student
# id computation don't break old data
'f82b5416c9f54b5ce33989511bb5ef2e',
self._get_anonymous_id('MITx/6.00x/2013_Spring', descriptor_class)
self._get_anonymous_id(SlashSeparatedCourseKey('MITx', '6.00x', '2013_Spring'), descriptor_class)
)
......@@ -858,8 +862,8 @@ class TestModuleTrackingContext(ModuleStoreTestCase):
render.handle_xblock_callback(
self.request,
self.course.id,
quote_slashes(str(descriptor.location)),
self.course.id.to_deprecated_string(),
quote_slashes(descriptor.location.to_deprecated_string()),
'xmodule_handler',
'problem_check',
)
......
......@@ -75,10 +75,10 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.enroll(self.test_course, True)
resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id}))
kwargs={'course_id': self.course.id.to_deprecated_string()}))
self.assertRedirects(resp, reverse(
'courseware_section', kwargs={'course_id': self.course.id,
'courseware_section', kwargs={'course_id': self.course.id.to_deprecated_string(),
'chapter': 'Overview',
'section': 'Welcome'}))
......@@ -92,16 +92,22 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.enroll(self.course, True)
self.enroll(self.test_course, True)
self.client.get(reverse('courseware_section', kwargs={'course_id': self.course.id,
self.client.get(reverse('courseware_section', kwargs={
'course_id': self.course.id.to_deprecated_string(),
'chapter': 'Overview',
'section': 'Welcome'}))
'section': 'Welcome',
}))
resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id}))
kwargs={'course_id': self.course.id.to_deprecated_string()}))
self.assertRedirects(resp, reverse('courseware_chapter',
kwargs={'course_id': self.course.id,
'chapter': 'Overview'}))
self.assertRedirects(resp, reverse(
'courseware_chapter',
kwargs={
'course_id': self.course.id.to_deprecated_string(),
'chapter': 'Overview'
}
))
def test_accordion_state(self):
"""
......@@ -113,15 +119,19 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.enroll(self.test_course, True)
# Now we directly navigate to a section in a chapter other than 'Overview'.
check_for_get_code(self, 200, reverse('courseware_section',
kwargs={'course_id': self.course.id,
check_for_get_code(self, 200, reverse(
'courseware_section',
kwargs={
'course_id': self.course.id.to_deprecated_string(),
'chapter': 'factory_chapter',
'section': 'factory_section'}))
'section': 'factory_section'
}
))
# And now hitting the courseware tab should redirect to 'factory_chapter'
resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id}))
kwargs={'course_id': self.course.id.to_deprecated_string()}))
self.assertRedirects(resp, reverse('courseware_chapter',
kwargs={'course_id': self.course.id,
kwargs={'course_id': self.course.id.to_deprecated_string(),
'chapter': 'factory_chapter'}))
......@@ -113,11 +113,10 @@ class SplitTestBase(ModuleStoreTestCase):
resp = self.client.get(reverse(
'courseware_section',
kwargs={'course_id': self.course.id,
kwargs={'course_id': self.course.id.to_deprecated_string(),
'chapter': self.chapter.url_name,
'section': self.sequential.url_name}
))
content = resp.content
# Assert we see the proper icon in the top display
......@@ -176,15 +175,15 @@ class TestVertSplitTestVert(SplitTestBase):
display_name="Split test vertical",
)
# pylint: disable=protected-access
c0_url = self.course.location._replace(category="vertical", name="split_test_cond0")
c1_url = self.course.location._replace(category="vertical", name="split_test_cond1")
c0_url = self.course.id.make_usage_key("vertical", "split_test_cond0")
c1_url = self.course.id.make_usage_key("vertical", "split_test_cond1")
split_test = ItemFactory.create(
parent_location=vert1.location,
category="split_test",
display_name="Split test",
user_partition_id='0',
group_id_to_child={"0": c0_url.url(), "1": c1_url.url()},
group_id_to_child={"0": c0_url, "1": c1_url},
)
cond0vert = ItemFactory.create(
......@@ -242,15 +241,15 @@ class TestSplitTestVert(SplitTestBase):
# split_test cond 0 = vert <- {video, problem}
# split_test cond 1 = vert <- {video, html}
# pylint: disable=protected-access
c0_url = self.course.location._replace(category="vertical", name="split_test_cond0")
c1_url = self.course.location._replace(category="vertical", name="split_test_cond1")
c0_url = self.course.id.make_usage_key("vertical", "split_test_cond0")
c1_url = self.course.id.make_usage_key("vertical", "split_test_cond1")
split_test = ItemFactory.create(
parent_location=self.sequential.location,
category="split_test",
display_name="Split test",
user_partition_id='0',
group_id_to_child={"0": c0_url.url(), "1": c1_url.url()},
group_id_to_child={"0": c0_url, "1": c1_url},
)
cond0vert = ItemFactory.create(
......
......@@ -46,6 +46,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
def setUp(self):
super(TestSubmittingProblems, self).setUp()
# Create course
self.course = CourseFactory.create(display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
assert self.course, "Couldn't load course %r" % self.COURSE_NAME
......@@ -63,14 +64,14 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Re-fetch the course from the database so that the object being dealt with has everything added to it.
"""
self.course = modulestore().get_instance(self.course.id, self.course.location)
self.course = modulestore().get_course(self.course.id)
def problem_location(self, problem_url_name):
"""
Returns the url of the problem given the problem's name
"""
return "i4x://" + self.course.org + "/{}/problem/{}".format(self.COURSE_SLUG, problem_url_name)
return self.course.id.make_usage_key('problem', problem_url_name)
def modx_url(self, problem_location, dispatch):
"""
......@@ -84,8 +85,8 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
return reverse(
'xblock_handler',
kwargs={
'course_id': self.course.id,
'usage_id': quote_slashes(problem_location),
'course_id': self.course.id.to_deprecated_string(),
'usage_id': quote_slashes(problem_location.to_deprecated_string()),
'handler': 'xmodule_handler',
'suffix': dispatch,
}
......@@ -247,7 +248,7 @@ class TestCourseGrader(TestSubmittingProblems):
"""
fake_request = self.factory.get(
reverse('progress', kwargs={'course_id': self.course.id})
reverse('progress', kwargs={'course_id': self.course.id.to_deprecated_string()})
)
return grades.grade(self.student_user, fake_request, self.course)
......@@ -265,7 +266,7 @@ class TestCourseGrader(TestSubmittingProblems):
"""
fake_request = self.factory.get(
reverse('progress', kwargs={'course_id': self.course.id})
reverse('progress', kwargs={'course_id': self.course.id.to_deprecated_string()})
)
progress_summary = grades.progress_summary(
......@@ -493,7 +494,7 @@ class TestCourseGrader(TestSubmittingProblems):
# score read from StudentModule and our student gets an A instead.
with patch('submissions.api.get_scores') as mock_get_scores:
mock_get_scores.return_value = {
self.problem_location('p3'): (1, 1)
self.problem_location('p3').to_deprecated_string(): (1, 1)
}
self.check_grade_percent(1.0)
self.assertEqual(self.get_grade_summary()['grade'], 'A')
......@@ -509,12 +510,14 @@ class TestCourseGrader(TestSubmittingProblems):
with patch('submissions.api.get_scores') as mock_get_scores:
mock_get_scores.return_value = {
self.problem_location('p3'): (1, 1)
self.problem_location('p3').to_deprecated_string(): (1, 1)
}
self.get_grade_summary()
# Verify that the submissions API was sent an anonymized student ID
mock_get_scores.assert_called_with(self.course.id, '99ac6730dc5f900d69fd735975243b31')
mock_get_scores.assert_called_with(
self.course.id.to_deprecated_string(), '99ac6730dc5f900d69fd735975243b31'
)
def test_weighted_homework(self):
"""
......@@ -631,7 +634,7 @@ class ProblemWithUploadedFilesTest(TestSubmittingProblems):
self.addCleanup(fileobj.close)
self.problem_setup("the_problem", filenames)
with patch('courseware.module_render.xqueue_interface.session') as mock_session:
with patch('courseware.module_render.XQUEUE_INTERFACE.session') as mock_session:
resp = self.submit_question_answer("the_problem", {'2_1': fileobjs})
self.assertEqual(resp.status_code, 200)
......@@ -946,7 +949,7 @@ class TestAnswerDistributions(TestSubmittingProblems):
user2 = UserFactory.create()
problems = StudentModule.objects.filter(
course_id=self.course.id,
student_id=self.student_user.id
student=self.student_user
)
for problem in problems:
problem.student_id = user2.id
......@@ -981,7 +984,7 @@ class TestAnswerDistributions(TestSubmittingProblems):
# Now fetch the state entry for that problem.
student_module = StudentModule.objects.get(
course_id=self.course.id,
student_id=self.student_user.id
student=self.student_user
)
for val in ('Correct', True, False, 0, 0.0, 1, 1.0, None):
state = json.loads(student_module.state)
......@@ -1008,9 +1011,11 @@ class TestAnswerDistributions(TestSubmittingProblems):
# to a non-existent problem.
student_module = StudentModule.objects.get(
course_id=self.course.id,
student_id=self.student_user.id
student=self.student_user
)
student_module.module_state_key = student_module.module_state_key.replace(
name=student_module.module_state_key.name + "_fake"
)
student_module.module_state_key += "_fake"
student_module.save()
# It should be empty (ignored)
......@@ -1027,7 +1032,7 @@ class TestAnswerDistributions(TestSubmittingProblems):
# Now fetch the StudentModule entry for p1 so we can corrupt its state
prb1 = StudentModule.objects.get(
course_id=self.course.id,
student_id=self.student_user.id
student=self.student_user
)
# Submit p2
......
......@@ -15,6 +15,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from courseware.tests.helpers import get_request_for_user, LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.locations import SlashSeparatedCourseKey
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
......@@ -27,29 +28,30 @@ class StaticTabDateTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
category="static_tab", parent_location=self.course.location,
data="OOGIE BLOOGIE", display_name="new_tab"
)
self.toy_course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
def test_logged_in(self):
self.setup_user()
url = reverse('static_tab', args=[self.course.id, 'new_tab'])
url = reverse('static_tab', args=[self.course.id.to_deprecated_string(), 'new_tab'])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
def test_anonymous_user(self):
url = reverse('static_tab', args=[self.course.id, 'new_tab'])
url = reverse('static_tab', args=[self.course.id.to_deprecated_string(), 'new_tab'])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
def test_get_static_tab_contents(self):
course = get_course_by_id('edX/toy/2012_Fall')
course = get_course_by_id(self.toy_course_key)
request = get_request_for_user(UserFactory.create())
tab = CourseTabList.get_tab_by_slug(course.tabs, 'resources')
# Test render works okay
tab_content = get_static_tab_contents(request, course, tab)
self.assertIn('edX/toy/2012_Fall', tab_content)
self.assertIn(self.toy_course_key.to_deprecated_string(), tab_content)
self.assertIn('static_tab', tab_content)
# Test when render raises an exception
......@@ -66,7 +68,7 @@ class StaticTabDateTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
# The following XML test course (which lives at common/test/data/2014)
# is closed; we're testing that tabs still appear when
# the course is already closed
xml_course_id = 'edX/detached_pages/2014'
xml_course_key = SlashSeparatedCourseKey('edX', 'detached_pages', '2014')
# this text appears in the test course's tab
# common/test/data/2014/tabs/8e4cce2b4aaf4ba28b1220804619e41f.html
......@@ -76,14 +78,14 @@ class StaticTabDateTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_logged_in_xml(self):
self.setup_user()
url = reverse('static_tab', args=[self.xml_course_id, self.xml_url])
url = reverse('static_tab', args=[self.xml_course_key.to_deprecated_string(), self.xml_url])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn(self.xml_data, resp.content)
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_anonymous_user_xml(self):
url = reverse('static_tab', args=[self.xml_course_id, self.xml_url])
url = reverse('static_tab', args=[self.xml_course_key.to_deprecated_string(), self.xml_url])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn(self.xml_data, resp.content)
......
......@@ -10,7 +10,6 @@ from datetime import timedelta
from webob import Request
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import Location
from xmodule.contentstore.django import contentstore
from . import BaseTestXmodule
from .test_video_xml import SOURCE_XML
......@@ -21,6 +20,7 @@ from xmodule.video_module.transcripts_utils import (
TranscriptException,
TranscriptsGenerationException,
)
from xmodule.modulestore.mongo.base import MongoModuleStore
SRT_content = textwrap.dedent("""
0
......@@ -46,7 +46,7 @@ def _check_asset(location, asset_name):
Check that asset with asset_name exists in assets.
"""
content_location = StaticContent.compute_location(
location.org, location.course, asset_name
location.course_key, asset_name
)
try:
contentstore().find(content_location)
......@@ -61,16 +61,12 @@ def _clear_assets(location):
"""
store = contentstore()
content_location = StaticContent.compute_location(
location.org, location.course, location.name
)
assets, __ = store.get_all_content_for_course(content_location)
assets, __ = store.get_all_content_for_course(location.course_key)
for asset in assets:
asset_location = Location(asset["_id"])
asset_location = MongoModuleStore._location_from_id(asset["_id"], location.course_key.run)
del_cached_content(asset_location)
id = StaticContent.get_id_from_location(asset_location)
store.delete(id)
mongo_id = StaticContent.get_id_from_location(asset_location)
store.delete(mongo_id)
def _get_subs_id(filename):
......@@ -97,7 +93,7 @@ def _upload_sjson_file(subs_file, location, default_filename='subs_{}.srt.sjson'
def _upload_file(subs_file, location, filename):
mime_type = subs_file.content_type
content_location = StaticContent.compute_location(
location.org, location.course, filename
location.course_key, filename
)
content = StaticContent(content_location, filename, mime_type, subs_file.read())
contentstore().save(content)
......
......@@ -14,6 +14,7 @@ from xmodule.video_module import create_youtube_string
from xmodule.tests import get_test_descriptor_system
from xmodule.modulestore import Location
from xmodule.video_module import VideoDescriptor
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from . import BaseTestXmodule
from .test_video_xml import SOURCE_XML
......@@ -511,10 +512,11 @@ class VideoDescriptorTest(unittest.TestCase):
def setUp(self):
system = get_test_descriptor_system()
location = Location('i4x://org/course/video/name')
course_key = SlashSeparatedCourseKey('org', 'course', 'run')
usage_key = course_key.make_usage_key('video', 'name')
self.descriptor = system.construct_xblock_from_class(
VideoDescriptor,
scope_ids=ScopeIds(None, None, location, location),
scope_ids=ScopeIds(None, None, usage_key, usage_key),
field_data=DictFieldData({}),
)
self.descriptor.runtime.handler_url = MagicMock()
......
......@@ -48,7 +48,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
Returns a list URLs corresponding to section in the passed in course.
"""
return [reverse(name, kwargs={'course_id': course.id})
return [reverse(name, kwargs={'course_id': course.id.to_deprecated_string()})
for name in names]
def _check_non_staff_light(self, course):
......@@ -57,7 +57,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
`course` is an instance of CourseDescriptor.
"""
urls = [reverse('about_course', kwargs={'course_id': course.id}), reverse('courses')]
urls = [reverse('about_course', kwargs={'course_id': course.id.to_deprecated_string()}),
reverse('courses')]
for url in urls:
check_for_get_code(self, 200, url)
......@@ -69,7 +70,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
names = ['courseware', 'instructor_dashboard', 'progress']
urls = self._reverse_urls(names, course)
urls.extend([
reverse('book', kwargs={'course_id': course.id,
reverse('book', kwargs={'course_id': course.id.to_deprecated_string(),
'book_index': index})
for index, book in enumerate(course.textbooks)
])
......@@ -83,7 +84,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
names = ['about_course', 'instructor_dashboard', 'progress']
urls = self._reverse_urls(names, course)
urls.extend([
reverse('book', kwargs={'course_id': course.id,
reverse('book', kwargs={'course_id': course.id.to_deprecated_string(),
'book_index': index})
for index in xrange(len(course.textbooks))
])
......@@ -97,7 +98,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
# to make access checking smarter and understand both the effective
# user (the student), and the requesting user (the prof)
url = reverse('student_progress',
kwargs={'course_id': course.id,
kwargs={'course_id': course.id.to_deprecated_string(),
'student_id': self.enrolled_user.id})
check_for_get_code(self, 404, url)
......@@ -137,12 +138,11 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
CourseEnrollmentFactory(user=self.enrolled_user, course_id=self.course.id)
CourseEnrollmentFactory(user=self.enrolled_user, course_id=self.test_course.id)
self.staff_user = StaffFactory(course=self.course.location)
self.staff_user = StaffFactory(course=self.course.id)
self.instructor_user = InstructorFactory(
course=self.course.location)
self.org_staff_user = OrgStaffFactory(course=self.course.location)
self.org_instructor_user = OrgInstructorFactory(
course=self.course.location)
course=self.course.id)
self.org_staff_user = OrgStaffFactory(course=self.course.id)
self.org_instructor_user = OrgInstructorFactory(course=self.course.id)
def test_redirection_unenrolled(self):
"""
......@@ -151,10 +151,10 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
self.login(self.unenrolled_user)
response = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id}))
kwargs={'course_id': self.course.id.to_deprecated_string()}))
self.assertRedirects(response,
reverse('about_course',
args=[self.course.id]))
args=[self.course.id.to_deprecated_string()]))
def test_redirection_enrolled(self):
"""
......@@ -164,11 +164,11 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.login(self.enrolled_user)
response = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id}))
kwargs={'course_id': self.course.id.to_deprecated_string()}))
self.assertRedirects(response,
reverse('courseware_section',
kwargs={'course_id': self.course.id,
kwargs={'course_id': self.course.id.to_deprecated_string(),
'chapter': 'Overview',
'section': 'Welcome'}))
......@@ -179,8 +179,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
self.login(self.enrolled_user)
urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}),
reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})]
urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}),
reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})]
# Shouldn't be able to get to the instructor pages
for url in urls:
......@@ -194,10 +194,10 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.login(self.staff_user)
# Now should be able to get to self.course, but not self.test_course
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
check_for_get_code(self, 200, url)
url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})
url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})
check_for_get_code(self, 404, url)
def test_instructor_course_access(self):
......@@ -208,10 +208,10 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.login(self.instructor_user)
# Now should be able to get to self.course, but not self.test_course
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
check_for_get_code(self, 200, url)
url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})
url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})
check_for_get_code(self, 404, url)
def test_org_staff_access(self):
......@@ -220,13 +220,13 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
and student profile pages for course in their org.
"""
self.login(self.org_staff_user)
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
check_for_get_code(self, 200, url)
url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})
url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})
check_for_get_code(self, 200, url)
url = reverse('instructor_dashboard', kwargs={'course_id': self.other_org_course.id})
url = reverse('instructor_dashboard', kwargs={'course_id': self.other_org_course.id.to_deprecated_string()})
check_for_get_code(self, 404, url)
def test_org_instructor_access(self):
......@@ -235,13 +235,13 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
and student profile pages for course in their org.
"""
self.login(self.org_instructor_user)
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
check_for_get_code(self, 200, url)
url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})
url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})
check_for_get_code(self, 200, url)
url = reverse('instructor_dashboard', kwargs={'course_id': self.other_org_course.id})
url = reverse('instructor_dashboard', kwargs={'course_id': self.other_org_course.id.to_deprecated_string()})
check_for_get_code(self, 404, url)
def test_global_staff_access(self):
......@@ -251,8 +251,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.login(self.global_staff_user)
# and now should be able to load both
urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}),
reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})]
urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}),
reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})]
for url in urls:
check_for_get_code(self, 200, url)
......@@ -374,7 +374,7 @@ class TestBetatesterAccess(ModuleStoreTestCase):
self.content = ItemFactory(parent=self.course)
self.normal_student = UserFactory()
self.beta_tester = BetaTesterFactory(course=self.course.location)
self.beta_tester = BetaTesterFactory(course=self.course.id)
@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_course_beta_period(self):
......@@ -384,10 +384,10 @@ class TestBetatesterAccess(ModuleStoreTestCase):
self.assertFalse(self.course.has_started())
# student user shouldn't see it
self.assertFalse(has_access(self.normal_student, self.course, 'load'))
self.assertFalse(has_access(self.normal_student, 'load', self.course))
# now the student should see it
self.assertTrue(has_access(self.beta_tester, self.course, 'load'))
self.assertTrue(has_access(self.beta_tester, 'load', self.course))
@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_content_beta_period(self):
......@@ -395,7 +395,7 @@ class TestBetatesterAccess(ModuleStoreTestCase):
Check that beta-test access works for content.
"""
# student user shouldn't see it
self.assertFalse(has_access(self.normal_student, self.content, 'load', self.course.id))
self.assertFalse(has_access(self.normal_student, 'load', self.content, self.course.id))
# now the student should see it
self.assertTrue(has_access(self.beta_tester, self.content, 'load', self.course.id))
self.assertTrue(has_access(self.beta_tester, 'load', self.content, self.course.id))
......@@ -24,6 +24,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from student.tests.factories import UserFactory
import courseware.views as views
......@@ -42,31 +43,32 @@ class TestJumpTo(TestCase):
def setUp(self):
# Use toy course from XML
self.course_name = 'edX/toy/2012_Fall'
self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
def test_jumpto_invalid_location(self):
location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None)
jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', self.course_name, location)
location = self.course_key.make_usage_key(None, 'NoSuchPlace')
# This is fragile, but unfortunately the problem is that within the LMS we
# can't use the reverse calls from the CMS
jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string())
response = self.client.get(jumpto_url)
self.assertEqual(response.status_code, 404)
def test_jumpto_from_chapter(self):
location = Location('i4x', 'edX', 'toy', 'chapter', 'Overview')
jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', self.course_name, location)
location = self.course_key.make_usage_key('chapter', 'Overview')
jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string())
expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
response = self.client.get(jumpto_url)
self.assertRedirects(response, expected, status_code=302, target_status_code=302)
def test_jumpto_id(self):
location = Location('i4x', 'edX', 'toy', 'chapter', 'Overview')
jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', self.course_name, location.name)
jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', self.course_key.to_deprecated_string(), 'Overview')
expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
response = self.client.get(jumpto_url)
self.assertRedirects(response, expected, status_code=302, target_status_code=302)
def test_jumpto_id_invalid_location(self):
location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None)
jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', self.course_name, location.name)
location = Location('edX', 'toy', 'NoSuchPlace', None, None, None)
jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string())
response = self.client.get(jumpto_url)
self.assertEqual(response.status_code, 404)
......@@ -80,15 +82,15 @@ class ViewsTestCase(TestCase):
self.user = User.objects.create(username='dummy', password='123456',
email='test@mit.edu')
self.date = datetime(2013, 1, 22, tzinfo=UTC)
self.course_id = 'edX/toy/2012_Fall'
self.enrollment = CourseEnrollment.enroll(self.user, self.course_id)
self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
self.enrollment = CourseEnrollment.enroll(self.user, self.course_key)
self.enrollment.created = self.date
self.enrollment.save()
self.location = ['tag', 'org', 'course', 'category', 'name']
self.request_factory = RequestFactory()
chapter = 'Overview'
self.chapter_url = '%s/%s/%s' % ('/courses', self.course_id, chapter)
self.chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter)
@unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings")
@patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})
......@@ -96,22 +98,22 @@ class ViewsTestCase(TestCase):
in_cart_span = '<span class="add-to-cart">'
# don't mock this course due to shopping cart existence checking
course = CourseFactory.create(org="new", number="unenrolled", display_name="course")
request = self.request_factory.get(reverse('about_course', args=[course.id]))
request = self.request_factory.get(reverse('about_course', args=[course.id.to_deprecated_string()]))
request.user = AnonymousUser()
response = views.course_about(request, course.id)
response = views.course_about(request, course.id.to_deprecated_string())
self.assertEqual(response.status_code, 200)
self.assertNotIn(in_cart_span, response.content)
# authenticated user with nothing in cart
request.user = self.user
response = views.course_about(request, course.id)
response = views.course_about(request, course.id.to_deprecated_string())
self.assertEqual(response.status_code, 200)
self.assertNotIn(in_cart_span, response.content)
# now add the course to the cart
cart = shoppingcart.models.Order.get_cart_for_user(self.user)
shoppingcart.models.PaidCourseRegistration.add_to_order(cart, course.id)
response = views.course_about(request, course.id)
response = views.course_about(request, course.id.to_deprecated_string())
self.assertEqual(response.status_code, 200)
self.assertIn(in_cart_span, response.content)
......@@ -146,15 +148,15 @@ class ViewsTestCase(TestCase):
mock_user.is_authenticated.return_value = False
self.assertFalse(views.registered_for_course('dummy', mock_user))
mock_course = MagicMock()
mock_course.id = self.course_id
mock_course.id = self.course_key
self.assertTrue(views.registered_for_course(mock_course, self.user))
def test_jump_to_invalid(self):
# TODO add a test for invalid location
# TODO add a test for no data *
request = self.request_factory.get(self.chapter_url)
self.assertRaisesRegexp(Http404, 'Invalid location', views.jump_to,
self.assertRaisesRegexp(Http404, 'Invalid course_key or usage_key', views.jump_to,
request, 'bar', ())
self.assertRaisesRegexp(Http404, 'No data*', views.jump_to, request,
'dummy', self.location)
def test_no_end_on_about_page(self):
# Toy course has no course end date or about/end_date blob
......@@ -170,6 +172,13 @@ class ViewsTestCase(TestCase):
self.verify_end_date("edX/test_about_blob_end_date/2012_Fall", "Learning never ends")
def verify_end_date(self, course_id, expected_end_text=None):
"""
Visits the about page for `course_id` and tests that both the text "Classes End", as well
as the specified `expected_end_text`, is present on the page.
If `expected_end_text` is None, verifies that the about page *does not* contain the text
"Classes End".
"""
request = self.request_factory.get("foo")
request.user = self.user
......@@ -214,7 +223,7 @@ class ViewsTestCase(TestCase):
def test_course_mktg_register(self):
admin = AdminFactory()
self.client.login(username=admin.username, password='test')
url = reverse('mktg_about_course', kwargs={'course_id': self.course_id})
url = reverse('mktg_about_course', kwargs={'course_id': self.course_key.to_deprecated_string()})
response = self.client.get(url)
self.assertIn('Register for', response.content)
self.assertNotIn('and choose your student track', response.content)
......@@ -223,12 +232,12 @@ class ViewsTestCase(TestCase):
admin = AdminFactory()
CourseMode.objects.get_or_create(mode_slug='honor',
mode_display_name='Honor Code Certificate',
course_id=self.course_id)
course_id=self.course_key)
CourseMode.objects.get_or_create(mode_slug='verified',
mode_display_name='Verified Certificate',
course_id=self.course_id)
course_id=self.course_key)
self.client.login(username=admin.username, password='test')
url = reverse('mktg_about_course', kwargs={'course_id': self.course_id})
url = reverse('mktg_about_course', kwargs={'course_id': self.course_key.to_deprecated_string()})
response = self.client.get(url)
self.assertIn('Register for', response.content)
self.assertIn('and choose your student track', response.content)
......@@ -243,7 +252,7 @@ class ViewsTestCase(TestCase):
# try it with an existing user and a malicious location
url = reverse('submission_history', kwargs={
'course_id': self.course_id,
'course_id': self.course_key.to_deprecated_string(),
'student_username': 'dummy',
'location': '<script>alert("hello");</script>'
})
......@@ -252,13 +261,14 @@ class ViewsTestCase(TestCase):
# try it with a malicious user and a non-existent location
url = reverse('submission_history', kwargs={
'course_id': self.course_id,
'course_id': self.course_key.to_deprecated_string(),
'student_username': '<script>alert("hello");</script>',
'location': 'dummy'
})
response = self.client.get(url)
self.assertFalse('<script>' in response.content)
# setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE, TIME_ZONE_DISPLAYED_FOR_DEADLINES="UTC")
class BaseDueDateTests(ModuleStoreTestCase):
......@@ -283,7 +293,7 @@ class BaseDueDateTests(ModuleStoreTestCase):
vertical = ItemFactory(category='vertical', parent_location=section.location)
ItemFactory(category='problem', parent_location=vertical.location)
course = modulestore().get_instance(course.id, course.location) # pylint: disable=no-member
course = modulestore().get_course(course.id) # pylint: disable=no-member
self.assertIsNotNone(course.get_children()[0].get_children()[0].due)
return course
......@@ -356,7 +366,7 @@ class TestProgressDueDate(BaseDueDateTests):
def get_text(self, course):
""" Returns the HTML for the progress page """
return views.progress(self.request, course.id, self.user.id).content
return views.progress(self.request, course.id.to_deprecated_string(), self.user.id).content
class TestAccordionDueDate(BaseDueDateTests):
......@@ -368,7 +378,7 @@ class TestAccordionDueDate(BaseDueDateTests):
def get_text(self, course):
""" Returns the HTML for the accordion """
return views.render_accordion(
self.request, course, course.get_children()[0].id, None, None
self.request, course, course.get_children()[0].scope_ids.usage_id.to_deprecated_string(), None, None
)
......@@ -392,14 +402,14 @@ class StartDateTests(ModuleStoreTestCase):
:param course_kwargs: All kwargs are passed to through to the :class:`CourseFactory`
"""
course = CourseFactory(start=datetime(2013, 9, 16, 7, 17, 28))
course = modulestore().get_instance(course.id, course.location) # pylint: disable=no-member
course = modulestore().get_course(course.id) # pylint: disable=no-member
return course
def get_about_text(self, course_id):
def get_about_text(self, course_key):
"""
Get the text of the /about page for the course.
"""
text = views.course_about(self.request, course_id).content
text = views.course_about(self.request, course_key.to_deprecated_string()).content
return text
@patch('util.date_utils.pgettext', fake_pgettext(translations={
......@@ -421,7 +431,7 @@ class StartDateTests(ModuleStoreTestCase):
"SHORT_DATE_FORMAT": "%Y-%b-%d",
}))
def test_format_localized_in_xml_course(self):
text = self.get_about_text('edX/toy/TT_2012_Fall')
text = self.get_about_text(SlashSeparatedCourseKey('edX', 'toy', 'TT_2012_Fall'))
# The start date is set in common/test/data/two_toys/policies/TT_2012_Fall/policy.json
self.assertIn("2015-JULY-17", text)
......@@ -441,7 +451,7 @@ class ProgressPageTests(ModuleStoreTestCase):
MakoMiddleware().process_request(self.request)
course = CourseFactory(start=datetime(2013, 9, 16, 7, 17, 28))
self.course = modulestore().get_instance(course.id, course.location) # pylint: disable=no-member
self.course = modulestore().get_course(course.id) # pylint: disable=no-member
self.chapter = ItemFactory(category='chapter', parent_location=self.course.location) # pylint: disable=no-member
self.section = ItemFactory(category='sequential', parent_location=self.chapter.location)
......@@ -450,6 +460,5 @@ class ProgressPageTests(ModuleStoreTestCase):
def test_pure_ungraded_xblock(self):
ItemFactory(category='acid', parent_location=self.vertical.location)
resp = views.progress(self.request, self.course.id)
resp = views.progress(self.request, self.course.id.to_deprecated_string())
self.assertEquals(resp.status_code, 200)
......@@ -11,7 +11,7 @@ from textwrap import dedent
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -48,6 +48,7 @@ class PageLoaderTestCase(LoginEnrollmentTestCase):
Base class that adds a function to load all pages in a modulestore.
"""
# TODO once everything is merged can someone please check whether this function takes a course_id or course_key
def check_all_pages_load(self, course_id):
"""
Assert that all pages in the course load correctly.
......@@ -61,18 +62,7 @@ class PageLoaderTestCase(LoginEnrollmentTestCase):
self.enroll(course, True)
# Search for items in the course
# None is treated as a wildcard
course_loc = course.location
location_query = Location(
course_loc.tag, course_loc.org,
course_loc.course, None, None, None
)
items = store.get_items(
location_query,
course_id=course_id,
depth=2
)
items = store.get_items(course_id)
if len(items) < 1:
self.fail('Could not retrieve any items from course')
......@@ -82,22 +72,22 @@ class PageLoaderTestCase(LoginEnrollmentTestCase):
if descriptor.location.category == 'about':
self._assert_loads('about_course',
{'course_id': course_id},
{'course_id': course_id.to_deprecated_string()},
descriptor)
elif descriptor.location.category == 'static_tab':
kwargs = {'course_id': course_id,
kwargs = {'course_id': course_id.to_deprecated_string(),
'tab_slug': descriptor.location.name}
self._assert_loads('static_tab', kwargs, descriptor)
elif descriptor.location.category == 'course_info':
self._assert_loads('info', {'course_id': course_id},
self._assert_loads('info', {'course_id': course_id.to_deprecated_string()},
descriptor)
else:
kwargs = {'course_id': course_id,
'location': descriptor.location.url()}
kwargs = {'course_id': course_id.to_deprecated_string(),
'location': descriptor.location.to_deprecated_string()}
self._assert_loads('jump_to', kwargs, descriptor,
expect_redirect=True,
......@@ -118,7 +108,7 @@ class PageLoaderTestCase(LoginEnrollmentTestCase):
if response.status_code != 200:
self.fail('Status %d for page %s' %
(response.status_code, descriptor.location.url()))
(response.status_code, descriptor.location))
if expect_redirect:
self.assertEqual(response.redirect_chain[0][1], 302)
......@@ -142,7 +132,7 @@ class TestXmlCoursesLoad(ModuleStoreTestCase, PageLoaderTestCase):
# Load one of the XML based courses
# Our test mapping rules allow the MixedModuleStore
# to load this course from XML, not Mongo.
self.check_all_pages_load('edX/toy/2012_Fall')
self.check_all_pages_load(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
# Importing XML courses isn't possible with MixedModuleStore,
......@@ -169,7 +159,7 @@ class TestMongoCoursesLoad(ModuleStoreTestCase, PageLoaderTestCase):
</table_of_contents>
""").strip()
location = Location(['i4x', 'edX', 'toy', 'course', '2012_Fall', None])
location = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall').make_usage_key('course', '2012_Fall')
course = self.store.get_item(location)
self.assertGreater(len(course.textbooks), 0)
......@@ -180,8 +170,7 @@ class TestDraftModuleStore(ModuleStoreTestCase):
store = modulestore()
# fix was to allow get_items() to take the course_id parameter
store.get_items(Location(None, None, 'vertical', None, None),
course_id='abc', depth=0)
store.get_items(SlashSeparatedCourseKey('abc', 'def', 'ghi'), category='vertical')
# test success is just getting through the above statement.
# The bug was that 'course_id' argument was
......
......@@ -37,15 +37,15 @@ from student.models import UserTestGroup, CourseEnrollment
from student.views import course_from_id, single_course_reverification_info
from util.cache import cache, cache_if_anonymous
from xblock.fragment import Fragment
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
from xmodule.modulestore.search import path_to_location
from xmodule.course_module import CourseDescriptor
from xmodule.tabs import CourseTabList, StaffGradingTab, PeerGradingTab, OpenEndedGradingTab
import shoppingcart
from opaque_keys import InvalidKeyError
from microsite_configuration import microsite
from xmodule.modulestore.locations import SlashSeparatedCourseKey
log = logging.getLogger("edx.courseware")
......@@ -106,7 +106,7 @@ def render_accordion(request, course, chapter, section, field_data_cache):
context = dict([
('toc', toc),
('course_id', course.id),
('course_id', course.id.to_deprecated_string()),
('csrf', csrf(request)['csrf_token']),
('due_date_display_format', course.due_date_display_format)
] + template_imports.items())
......@@ -152,7 +152,7 @@ def redirect_to_course_position(course_module):
the first child.
"""
urlargs = {'course_id': course_module.id}
urlargs = {'course_id': course_module.id.to_deprecated_string()}
chapter = get_current_child(course_module)
if chapter is None:
# oops. Something bad has happened.
......@@ -176,7 +176,7 @@ def save_child_position(seq_module, child_name):
child_name: url_name of the child
"""
for position, c in enumerate(seq_module.get_display_items(), start=1):
if c.url_name == child_name:
if c.location.name == child_name:
# Only save if position changed
if position != seq_module.position:
seq_module.position = position
......@@ -241,29 +241,30 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = User.objects.prefetch_related("groups").get(id=request.user.id)
request.user = user # keep just one instance of User
course = get_course_with_access(user, course_id, 'load', depth=2)
staff_access = has_access(user, course, 'staff')
course = get_course_with_access(user, 'load', course_key, depth=2)
staff_access = has_access(user, 'staff', course)
registered = registered_for_course(course, user)
if not registered:
# TODO (vshnayder): do course instructors need to be registered to see course?
log.debug(u'User %s tried to view course %s but is not enrolled', user, course.location.url())
return redirect(reverse('about_course', args=[course.id]))
log.debug(u'User %s tried to view course %s but is not enrolled', user, course.location.to_deprecated_string())
return redirect(reverse('about_course', args=[course_key.to_deprecated_string()]))
masq = setup_masquerade(request, staff_access)
try:
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id, user, course, depth=2)
course_key, user, course, depth=2)
course_module = get_module_for_descriptor(user, request, course, field_data_cache, course.id)
course_module = get_module_for_descriptor(user, request, course, field_data_cache, course_key)
if course_module is None:
log.warning(u'If you see this, something went wrong: if we got this'
u' far, should have gotten a course module for this user')
return redirect(reverse('about_course', args=[course.id]))
return redirect(reverse('about_course', args=[course_key.to_deprecated_string()]))
studio_url = get_studio_url(course_id, 'course')
studio_url = get_studio_url(course_key, 'course')
if chapter is None:
return redirect_to_course_position(course_module)
......@@ -279,7 +280,7 @@ def index(request, course_id, chapter=None, section=None,
'studio_url': studio_url,
'masquerade': masq,
'xqa_server': settings.FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
'reverifications': fetch_reverify_banner_info(request, course_id),
'reverifications': fetch_reverify_banner_info(request, course_key),
}
# Only show the chat if it's enabled by the course and in the
......@@ -294,44 +295,44 @@ def index(request, course_id, chapter=None, section=None,
context['show_chat'] = show_chat
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter)
if chapter_descriptor is not None:
save_child_position(course_module, chapter)
else:
raise Http404('No chapter descriptor found with name {}'.format(chapter))
chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
chapter_module = course_module.get_child_by(lambda m: m.location.name == chapter)
if chapter_module is None:
# User may be trying to access a chapter that isn't live yet
if masq == 'student': # if staff is masquerading as student be kinder, don't 404
log.debug('staff masq as student: no chapter %s' % chapter)
return redirect(reverse('courseware', args=[course.id]))
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
raise Http404
if section is not None:
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section)
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.location.name == section)
if section_descriptor is None:
# Specifically asked-for section doesn't exist
if masq == 'student': # if staff is masquerading as student be kinder, don't 404
log.debug('staff masq as student: no section %s' % section)
return redirect(reverse('courseware', args=[course.id]))
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
raise Http404
# cdodge: this looks silly, but let's refetch the section_descriptor with depth=None
# which will prefetch the children more efficiently than doing a recursive load
section_descriptor = modulestore().get_instance(course.id, section_descriptor.location, depth=None)
section_descriptor = modulestore().get_item(section_descriptor.location, depth=None)
# Load all descendants of the section, because we're going to display its
# html, which in general will need all of its children
section_field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course_id, user, section_descriptor, depth=None)
course_key, user, section_descriptor, depth=None)
section_module = get_module_for_descriptor(
request.user,
request,
section_descriptor,
section_field_data_cache,
course_id,
course_key,
position
)
......@@ -346,14 +347,16 @@ def index(request, course_id, chapter=None, section=None,
context['section_title'] = section_descriptor.display_name_with_default
else:
# section is none, so display a message
studio_url = get_studio_url(course_id, 'course')
studio_url = get_studio_url(course_key, 'course')
prev_section = get_current_child(chapter_module)
if prev_section is None:
# Something went wrong -- perhaps this chapter has no sections visible to the user
raise Http404
prev_section_url = reverse('courseware_section', kwargs={'course_id': course_id,
prev_section_url = reverse('courseware_section', kwargs={
'course_id': course_key.to_deprecated_string(),
'chapter': chapter_descriptor.url_name,
'section': prev_section.url_name})
'section': prev_section.url_name
})
context['fragment'] = Fragment(content=render_to_string(
'courseware/welcome-back.html',
{
......@@ -404,13 +407,8 @@ def jump_to_id(request, course_id, module_id):
This entry point allows for a shorter version of a jump to where just the id of the element is
passed in. This assumes that id is unique within the course_id namespace
"""
course_location = CourseDescriptor.id_to_location(course_id)
items = modulestore().get_items(
Location('i4x', course_location.org, course_location.course, None, module_id),
course_id=course_id
)
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
items = modulestore().get_items(course_key, name=module_id)
if len(items) == 0:
raise Http404(
......@@ -420,10 +418,10 @@ def jump_to_id(request, course_id, module_id):
if len(items) > 1:
log.warning(
u"Multiple items found with id: {0} in course_id: {1}. Referer: {2}. Using first: {3}".format(
module_id, course_id, request.META.get("HTTP_REFERER", ""), items[0].location.url()
module_id, course_id, request.META.get("HTTP_REFERER", ""), items[0].location.to_deprecated_string()
))
return jump_to(request, course_id, items[0].location.url())
return jump_to(request, course_id, items[0].location.to_deprecated_string())
@ensure_csrf_cookie
......@@ -436,31 +434,29 @@ def jump_to(request, course_id, location):
Otherwise, delegates to the index view to figure out whether this user
has access, and what they should see.
"""
# Complain if the location isn't valid
try:
location = Location(location)
except InvalidLocationError:
raise Http404("Invalid location")
# Complain if there's not data for this location
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
usage_key = course_key.make_usage_key_from_deprecated_string(location)
except InvalidKeyError:
raise Http404(u"Invalid course_key or usage_key")
try:
(course_id, chapter, section, position) = path_to_location(modulestore(), course_id, location)
(course_key, chapter, section, position) = path_to_location(modulestore(), usage_key)
except ItemNotFoundError:
raise Http404(u"No data at this location: {0}".format(location))
raise Http404(u"No data at this location: {0}".format(usage_key))
except NoPathToItem:
raise Http404(u"This location is not in any class: {0}".format(location))
raise Http404(u"This location is not in any class: {0}".format(usage_key))
# choose the appropriate view (and provide the necessary args) based on the
# args provided by the redirect.
# Rely on index to do all error handling and access control.
if chapter is None:
return redirect('courseware', course_id=course_id)
return redirect('courseware', course_id=course_key.to_deprecated_string())
elif section is None:
return redirect('courseware_chapter', course_id=course_id, chapter=chapter)
return redirect('courseware_chapter', course_id=course_key.to_deprecated_string(), chapter=chapter)
elif position is None:
return redirect('courseware_section', course_id=course_id, chapter=chapter, section=section)
return redirect('courseware_section', course_id=course_key.to_deprecated_string(), chapter=chapter, section=section)
else:
return redirect('courseware_position', course_id=course_id, chapter=chapter, section=section, position=position)
return redirect('courseware_position', course_id=course_key.to_deprecated_string(), chapter=chapter, section=section, position=position)
@ensure_csrf_cookie
......@@ -470,15 +466,16 @@ def course_info(request, course_id):
Assumes the course_id is in a valid format.
"""
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course = get_course_with_access(request.user, 'load', course_key)
staff_access = has_access(request.user, 'staff', course)
masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page
studio_url = get_studio_url(course_id, 'course_info')
reverifications = fetch_reverify_banner_info(request, course_id)
reverifications = fetch_reverify_banner_info(request, course_key)
studio_url = get_studio_url(course_key, 'course_info')
context = {
'request': request,
'course_id': course_id,
'course_id': course_key.to_deprecated_string(),
'cache': None,
'course': course,
'staff_access': staff_access,
......@@ -497,7 +494,8 @@ def static_tab(request, course_id, tab_slug):
Assumes the course_id is in a valid format.
"""
course = get_course_with_access(request.user, course_id, 'load')
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course = get_course_with_access(request.user, 'load', course_key)
tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
if tab is None:
......@@ -527,8 +525,9 @@ def syllabus(request, course_id):
Assumes the course_id is in a valid format.
"""
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course = get_course_with_access(request.user, 'load', course_key)
staff_access = has_access(request.user, 'staff', course)
return render_to_response('courseware/syllabus.html', {
'course': course,
......@@ -562,18 +561,18 @@ def course_about(request, course_id):
settings.FEATURES.get('ENABLE_MKTG_SITE', False)
):
raise Http404
course = get_course_with_access(request.user, course_id, 'see_exists')
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course = get_course_with_access(request.user, 'see_exists', course_key)
registered = registered_for_course(course, request.user)
staff_access = has_access(request.user, course, 'staff')
studio_url = get_studio_url(course_id, 'settings/details')
staff_access = has_access(request.user, 'staff', course)
studio_url = get_studio_url(course_key, 'settings/details')
if has_access(request.user, course, 'load'):
course_target = reverse('info', args=[course.id])
if has_access(request.user, 'load', course):
course_target = reverse('info', args=[course.id.to_deprecated_string()])
else:
course_target = reverse('about_course', args=[course.id])
course_target = reverse('about_course', args=[course.id.to_deprecated_string()])
show_courseware_link = (has_access(request.user, course, 'load') or
show_courseware_link = (has_access(request.user, 'load', course) or
settings.FEATURES.get('ENABLE_LMS_MIGRATION'))
# Note: this is a flow for payment for course registration, not the Verified Certificate flow.
......@@ -582,14 +581,14 @@ def course_about(request, course_id):
reg_then_add_to_cart_link = ""
if (settings.FEATURES.get('ENABLE_SHOPPING_CART') and
settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION')):
registration_price = CourseMode.min_course_price_for_currency(course_id,
registration_price = CourseMode.min_course_price_for_currency(course_key,
settings.PAID_COURSE_REGISTRATION_CURRENCY[0])
if request.user.is_authenticated():
cart = shoppingcart.models.Order.get_cart_for_user(request.user)
in_cart = shoppingcart.models.PaidCourseRegistration.contained_in_order(cart, course_id)
in_cart = shoppingcart.models.PaidCourseRegistration.contained_in_order(cart, course_key)
reg_then_add_to_cart_link = "{reg_url}?course_id={course_id}&enrollment_action=add_to_cart".format(
reg_url=reverse('register_user'), course_id=course.id)
reg_url=reverse('register_user'), course_id=course.id.to_deprecated_string())
# see if we have already filled up all allowed enrollments
is_course_full = CourseEnrollment.is_course_full(course)
......@@ -615,25 +614,26 @@ def mktg_course_about(request, course_id):
This is the button that gets put into an iframe on the Drupal site
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
try:
course = get_course_with_access(request.user, course_id, 'see_exists')
course = get_course_with_access(request.user, 'see_exists', course_key)
except (ValueError, Http404) as e:
# if a course does not exist yet, display a coming
# soon button
return render_to_response(
'courseware/mktg_coming_soon.html', {'course_id': course_id}
'courseware/mktg_coming_soon.html', {'course_id': course_key.to_deprecated_string()}
)
registered = registered_for_course(course, request.user)
if has_access(request.user, course, 'load'):
course_target = reverse('info', args=[course.id])
if has_access(request.user, 'load', course):
course_target = reverse('info', args=[course.id.to_deprecated_string()])
else:
course_target = reverse('about_course', args=[course.id])
course_target = reverse('about_course', args=[course.id.to_deprecated_string()])
allow_registration = has_access(request.user, course, 'enroll')
allow_registration = has_access(request.user, 'enroll', course)
show_courseware_link = (has_access(request.user, course, 'load') or
show_courseware_link = (has_access(request.user, 'load', course) or
settings.FEATURES.get('ENABLE_LMS_MIGRATION'))
course_modes = CourseMode.modes_for_course(course.id)
......@@ -656,10 +656,10 @@ def progress(request, course_id, student_id=None):
there are unanticipated errors.
"""
with grades.manual_transaction():
return _progress(request, course_id, student_id)
return _progress(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), student_id)
def _progress(request, course_id, student_id):
def _progress(request, course_key, student_id):
"""
Unwrapped version of "progress".
......@@ -667,8 +667,8 @@ def _progress(request, course_id, student_id):
Course staff are allowed to see the progress of students in their class.
"""
course = get_course_with_access(request.user, course_id, 'load', depth=None)
staff_access = has_access(request.user, course, 'staff')
course = get_course_with_access(request.user, 'load', course_key, depth=None)
staff_access = has_access(request.user, 'staff', course)
if student_id is None or student_id == request.user.id:
# always allowed to see your own profile
......@@ -687,7 +687,7 @@ def _progress(request, course_id, student_id):
student = User.objects.prefetch_related("groups").get(id=student.id)
courseware_summary = grades.progress_summary(student, request, course)
studio_url = get_studio_url(course_id, 'settings/grading')
studio_url = get_studio_url(course_key, 'settings/grading')
grade_summary = grades.grade(student, request, course)
if courseware_summary is None:
......@@ -701,7 +701,7 @@ def _progress(request, course_id, student_id):
'grade_summary': grade_summary,
'staff_access': staff_access,
'student': student,
'reverifications': fetch_reverify_banner_info(request, course_id)
'reverifications': fetch_reverify_banner_info(request, course_key)
}
with grades.manual_transaction():
......@@ -710,7 +710,7 @@ def _progress(request, course_id, student_id):
return response
def fetch_reverify_banner_info(request, course_id):
def fetch_reverify_banner_info(request, course_key):
"""
Fetches needed context variable to display reverification banner in courseware
"""
......@@ -718,8 +718,8 @@ def fetch_reverify_banner_info(request, course_id):
user = request.user
if not user.id:
return reverifications
enrollment = CourseEnrollment.get_or_create_enrollment(request.user, course_id)
course = course_from_id(course_id)
enrollment = CourseEnrollment.get_or_create_enrollment(request.user, course_key)
course = course_from_id(course_key)
info = single_course_reverification_info(user, course, enrollment)
if info:
reverifications[info.status].append(info)
......@@ -733,9 +733,18 @@ def submission_history(request, course_id, student_username, location):
Right now this only works for problems because that's all
StudentModuleHistory records.
"""
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
except (InvalidKeyError, AssertionError):
return HttpResponse(escape(_(u'Invalid course id.')))
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
try:
usage_key = course_key.make_usage_key_from_deprecated_string(location)
except (InvalidKeyError, AssertionError):
return HttpResponse(escape(_(u'Invalid location.')))
course = get_course_with_access(request.user, 'load', course_key)
staff_access = has_access(request.user, 'staff', course)
# Permission Denied if they don't have staff access and are trying to see
# somebody else's submission history.
......@@ -745,8 +754,8 @@ def submission_history(request, course_id, student_username, location):
try:
student = User.objects.get(username=student_username)
student_module = StudentModule.objects.get(
course_id=course_id,
module_state_key=location,
course_id=course_key,
module_id=usage_key,
student_id=student.id
)
except User.DoesNotExist:
......@@ -756,7 +765,6 @@ def submission_history(request, course_id, student_username, location):
username=student_username,
location=location
)))
history_entries = StudentModuleHistory.objects.filter(
student_module=student_module
).order_by('-id')
......@@ -772,7 +780,7 @@ def submission_history(request, course_id, student_username, location):
'history_entries': history_entries,
'username': student.username,
'location': location,
'course_id': course_id
'course_id': course_key.to_deprecated_string()
}
return render_to_response('courseware/submission_history.html', context)
......@@ -801,15 +809,12 @@ def get_static_tab_contents(request, course, tab):
"""
Returns the contents for the given static tab
"""
loc = Location(
course.location.tag,
course.location.org,
course.location.course,
loc = course.id.make_usage_key(
tab.type,
tab.url_slug,
)
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id, request.user, modulestore().get_instance(course.id, loc), depth=0
course.id, request.user, modulestore().get_item(loc), depth=0
)
tab_module = get_module(
request.user, request, loc, field_data_cache, course.id, static_asset_path=course.static_asset_path
......
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