Commit 3893a9ae by Calen Pennington

Merge pull request #4258 from cpennington/studio-deprecated-urls

Make Studio able to handle deprecated key formats in urls
parents 0765fbb4 1c882da3
...@@ -131,7 +131,6 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -131,7 +131,6 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
descriptor = store.get_items(course.id, category='vertical',) descriptor = store.get_items(course.id, category='vertical',)
resp = self.client.get_html(get_url('unit_handler', descriptor[0].location)) resp = self.client.get_html(get_url('unit_handler', descriptor[0].location))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
for expected in expected_types: for expected in expected_types:
self.assertIn(expected, resp.content) self.assertIn(expected, resp.content)
...@@ -157,7 +156,6 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -157,7 +156,6 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
resp = self.client.get_html(get_url('unit_handler', usage_key)) resp = self.client.get_html(get_url('unit_handler', usage_key))
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
_test_no_locations(self, resp, status_code=400)
def check_edit_unit(self, test_course_name): def check_edit_unit(self, test_course_name):
_, course_items = import_from_xml(modulestore(), self.user.id, 'common/test/data/', [test_course_name]) _, course_items = import_from_xml(modulestore(), self.user.id, 'common/test/data/', [test_course_name])
...@@ -364,8 +362,6 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -364,8 +362,6 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
get_url('xblock_view_handler', usage_key, kwargs={'view_name': 'container_preview'}) get_url('xblock_view_handler', usage_key, kwargs={'view_name': 'container_preview'})
) )
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# TODO: uncomment when preview no longer has locations being returned.
# _test_no_locations(self, resp)
# These are the data-ids of the xblocks contained in the vertical. # These are the data-ids of the xblocks contained in the vertical.
self.assertContains(resp, 'edX+toy+2012_Fall+video+sample_video') self.assertContains(resp, 'edX+toy+2012_Fall+video+sample_video')
...@@ -534,7 +530,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -534,7 +530,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
url = reverse_course_url( url = reverse_course_url(
'assets_handler', 'assets_handler',
course.id, course.id,
kwargs={'asset_key_string': course.id.make_asset_key('asset', 'sample_static.txt')} kwargs={'asset_key_string': unicode(course.id.make_asset_key('asset', 'sample_static.txt'))}
) )
resp = self.client.delete(url) resp = self.client.delete(url)
self.assertEqual(resp.status_code, 204) self.assertEqual(resp.status_code, 204)
...@@ -761,7 +757,6 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -761,7 +757,6 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
def test_bad_contentstore_request(self): def test_bad_contentstore_request(self):
resp = self.client.get_html('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png') resp = self.client.get_html('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png')
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
_test_no_locations(self, resp, 400)
def test_rewrite_nonportable_links_on_import(self): def test_rewrite_nonportable_links_on_import(self):
module_store = modulestore() module_store = modulestore()
...@@ -1196,7 +1191,6 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -1196,7 +1191,6 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
for descriptor in items: for descriptor in items:
resp = self.client.get_html(get_url('unit_handler', descriptor.location)) resp = self.client.get_html(get_url('unit_handler', descriptor.location))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
class ContentStoreTest(ContentStoreTestCase): class ContentStoreTest(ContentStoreTestCase):
...@@ -1475,7 +1469,6 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1475,7 +1469,6 @@ class ContentStoreTest(ContentStoreTestCase):
status_code=200, status_code=200,
html=True html=True
) )
_test_no_locations(self, resp)
def test_course_factory(self): def test_course_factory(self):
"""Test that the course factory works correctly.""" """Test that the course factory works correctly."""
...@@ -1498,7 +1491,6 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1498,7 +1491,6 @@ class ContentStoreTest(ContentStoreTestCase):
status_code=200, status_code=200,
html=True html=True
) )
_test_no_locations(self, resp)
def test_course_overview_view_with_course(self): def test_course_overview_view_with_course(self):
"""Test viewing the course overview page with an existing course""" """Test viewing the course overview page with an existing course"""
...@@ -1522,7 +1514,6 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1522,7 +1514,6 @@ class ContentStoreTest(ContentStoreTestCase):
} }
resp = self.client.ajax_post(reverse_url('xblock_handler'), section_data) resp = self.client.ajax_post(reverse_url('xblock_handler'), section_data)
_test_no_locations(self, resp, html=False)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
data = parse_json(resp) data = parse_json(resp)
...@@ -1563,7 +1554,6 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1563,7 +1554,6 @@ class ContentStoreTest(ContentStoreTestCase):
get_url(handler, course_key, 'course_key_string') get_url(handler, course_key, 'course_key_string')
) )
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
_, course_items = import_from_xml(modulestore(), self.user.id, 'common/test/data/', ['simple']) _, course_items = import_from_xml(modulestore(), self.user.id, 'common/test/data/', ['simple'])
course_key = course_items[0].id course_key = course_items[0].id
...@@ -1589,20 +1579,17 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1589,20 +1579,17 @@ class ContentStoreTest(ContentStoreTestCase):
subsection_key = course_key.make_usage_key('sequential', 'test_sequence') subsection_key = course_key.make_usage_key('sequential', 'test_sequence')
resp = self.client.get_html(get_url('subsection_handler', subsection_key)) resp = self.client.get_html(get_url('subsection_handler', subsection_key))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
# go look at the Edit page # go look at the Edit page
unit_key = course_key.make_usage_key('vertical', 'test_vertical') unit_key = course_key.make_usage_key('vertical', 'test_vertical')
resp = self.client.get_html(get_url('unit_handler', unit_key)) resp = self.client.get_html(get_url('unit_handler', unit_key))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
def delete_item(category, name): def delete_item(category, name):
""" Helper method for testing the deletion of an xblock item. """ """ Helper method for testing the deletion of an xblock item. """
item_key = course_key.make_usage_key(category, name) item_key = course_key.make_usage_key(category, name)
resp = self.client.delete(get_url('xblock_handler', item_key)) resp = self.client.delete(get_url('xblock_handler', item_key))
self.assertEqual(resp.status_code, 204) self.assertEqual(resp.status_code, 204)
_test_no_locations(self, resp, status_code=204, html=False)
# delete a component # delete a component
delete_item(category='html', name='test_html') delete_item(category='html', name='test_html')
...@@ -1805,7 +1792,6 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1805,7 +1792,6 @@ class ContentStoreTest(ContentStoreTestCase):
Show the course overview page. Show the course overview page.
""" """
resp = self.client.get_html(get_url('course_handler', course_key, 'course_key_string')) resp = self.client.get_html(get_url('course_handler', course_key, 'course_key_string'))
_test_no_locations(self, resp)
return resp return resp
def test_wiki_slug(self): def test_wiki_slug(self):
...@@ -1887,7 +1873,6 @@ class EntryPageTestCase(TestCase): ...@@ -1887,7 +1873,6 @@ class EntryPageTestCase(TestCase):
def _test_page(self, page, status_code=200): def _test_page(self, page, status_code=200):
resp = self.client.get_html(page) resp = self.client.get_html(page)
self.assertEqual(resp.status_code, status_code) self.assertEqual(resp.status_code, status_code)
_test_no_locations(self, resp, status_code)
def test_how_it_works(self): def test_how_it_works(self):
self._test_page("/howitworks") self._test_page("/howitworks")
...@@ -1925,19 +1910,3 @@ def _course_factory_create_course(): ...@@ -1925,19 +1910,3 @@ def _course_factory_create_course():
def _get_course_id(course_data): def _get_course_id(course_data):
"""Returns the course ID (org/number/run).""" """Returns the course ID (org/number/run)."""
return SlashSeparatedCourseKey(course_data['org'], course_data['number'], course_data['run']) return SlashSeparatedCourseKey(course_data['org'], course_data['number'], course_data['run'])
def _test_no_locations(test, resp, status_code=200, html=True):
"""
Verifies that "i4x", which appears in old locations, but not
new locators, does not appear in the HTML response output.
Used to verify that database refactoring is complete.
"""
test.assertNotContains(resp, 'i4x', status_code=status_code, html=html)
if html:
# For HTML pages, it is nice to call the method with html=True because
# it checks that the HTML properly parses. However, it won't find i4x usages
# in JavaScript blocks.
content = resp.content
hits = len(re.findall(r"(?<!jump_to/)i4x://", content))
test.assertEqual(hits, 0, "i4x found outside of LMS jump-to links")
...@@ -189,7 +189,7 @@ class ContentStoreImportTest(ModuleStoreTestCase): ...@@ -189,7 +189,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
target_course_id.make_usage_key('conditional', 'condone') target_course_id.make_usage_key('conditional', 'condone')
) )
self.assertIsNotNone(conditional_module) self.assertIsNotNone(conditional_module)
different_course_id = SlashSeparatedCourseKey('edX', 'different_course', 'copy_run') different_course_id = SlashSeparatedCourseKey('edX', 'different_course', None)
self.assertListEqual( self.assertListEqual(
[ [
target_course_id.make_usage_key('problem', 'choiceprob'), target_course_id.make_usage_key('problem', 'choiceprob'),
......
...@@ -61,7 +61,7 @@ else: ...@@ -61,7 +61,7 @@ else:
# XBlocks from pmitros repos are prototypes. They should not be used # XBlocks from pmitros repos are prototypes. They should not be used
# except for edX Learning Sciences experiments on edge.edx.org without # except for edX Learning Sciences experiments on edge.edx.org without
# further work to make them robust, maintainable, finalize data formats, # further work to make them robust, maintainable, finalize data formats,
# etc. # etc.
'concept', # Concept mapper. See https://github.com/pmitros/ConceptXBlock 'concept', # Concept mapper. See https://github.com/pmitros/ConceptXBlock
'done', # Lets students mark things as done. See https://github.com/pmitros/DoneXBlock 'done', # Lets students mark things as done. See https://github.com/pmitros/DoneXBlock
'audio', # Embed an audio file. See https://github.com/pmitros/AudioXBlock 'audio', # Embed an audio file. See https://github.com/pmitros/AudioXBlock
...@@ -97,7 +97,7 @@ def subsection_handler(request, usage_key_string): ...@@ -97,7 +97,7 @@ def subsection_handler(request, usage_key_string):
except ItemNotFoundError: except ItemNotFoundError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
preview_link = get_lms_link_for_item(usage_key, preview=True) preview_link = get_lms_link_for_item(item.location, preview=True)
# make sure that location references a 'sequential', otherwise return # make sure that location references a 'sequential', otherwise return
# BadRequest # BadRequest
...@@ -134,9 +134,9 @@ def subsection_handler(request, usage_key_string): ...@@ -134,9 +134,9 @@ def subsection_handler(request, usage_key_string):
'new_unit_category': 'vertical', 'new_unit_category': 'vertical',
'lms_link': lms_link, 'lms_link': lms_link,
'preview_link': preview_link, 'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(usage_key.course_key).graders), 'course_graders': json.dumps(CourseGradingModel.fetch(item.location.course_key).graders),
'parent_item': parent, 'parent_item': parent,
'locator': usage_key, 'locator': item.location,
'policy_metadata': policy_metadata, 'policy_metadata': policy_metadata,
'subsection_units': subsection_units, 'subsection_units': subsection_units,
'can_view_live': can_view_live 'can_view_live': can_view_live
...@@ -211,7 +211,7 @@ def unit_handler(request, usage_key_string): ...@@ -211,7 +211,7 @@ def unit_handler(request, usage_key_string):
return render_to_response('unit.html', { return render_to_response('unit.html', {
'context_course': course, 'context_course': course,
'unit': item, 'unit': item,
'unit_usage_key': usage_key, 'unit_usage_key': item.location,
'child_usage_keys': [block.scope_ids.usage_id for block in xblocks], 'child_usage_keys': [block.scope_ids.usage_id for block in xblocks],
'component_templates': json.dumps(component_templates), 'component_templates': json.dumps(component_templates),
'draft_preview_link': preview_lms_link, 'draft_preview_link': preview_lms_link,
...@@ -267,7 +267,7 @@ def container_handler(request, usage_key_string): ...@@ -267,7 +267,7 @@ def container_handler(request, usage_key_string):
'context_course': course, # Needed only for display of menus at top of page. 'context_course': course, # Needed only for display of menus at top of page.
'xblock': xblock, 'xblock': xblock,
'unit_publish_state': unit_publish_state, 'unit_publish_state': unit_publish_state,
'xblock_locator': usage_key, 'xblock_locator': xblock.location,
'unit': None if not ancestor_xblocks else ancestor_xblocks[0], 'unit': None if not ancestor_xblocks else ancestor_xblocks[0],
'ancestor_xblocks': ancestor_xblocks, 'ancestor_xblocks': ancestor_xblocks,
'component_templates': json.dumps(component_templates), 'component_templates': json.dumps(component_templates),
...@@ -415,7 +415,7 @@ def _get_item_in_course(request, usage_key): ...@@ -415,7 +415,7 @@ def _get_item_in_course(request, usage_key):
course = modulestore().get_course(course_key) course = modulestore().get_course(course_key)
item = modulestore().get_item(usage_key, depth=1) item = modulestore().get_item(usage_key, depth=1)
lms_link = get_lms_link_for_item(usage_key) lms_link = get_lms_link_for_item(item.location)
return course, item, lms_link return course, item, lms_link
......
...@@ -144,7 +144,7 @@ def xblock_handler(request, usage_key_string): ...@@ -144,7 +144,7 @@ def xblock_handler(request, usage_key_string):
request.user, request.user,
) )
return JsonResponse({"locator": unicode(dest_usage_key)}) return JsonResponse({"locator": unicode(dest_usage_key), "courseKey": unicode(dest_usage_key.course_key)})
else: else:
return _create_item(request) return _create_item(request)
else: else:
...@@ -403,7 +403,7 @@ def _create_item(request): ...@@ -403,7 +403,7 @@ def _create_item(request):
if display_name is not None: if display_name is not None:
metadata['display_name'] = display_name metadata['display_name'] = display_name
store.create_and_save_xmodule( created_block = store.create_and_save_xmodule(
dest_usage_key, dest_usage_key,
request.user.id, request.user.id,
definition_data=data, definition_data=data,
...@@ -426,10 +426,10 @@ def _create_item(request): ...@@ -426,10 +426,10 @@ def _create_item(request):
# TODO replace w/ nicer accessor # TODO replace w/ nicer accessor
if not 'detached' in parent.runtime.load_block_type(category)._class_tags: if not 'detached' in parent.runtime.load_block_type(category)._class_tags:
parent.children.append(dest_usage_key) parent.children.append(created_block.location)
store.update_item(parent, request.user.id) store.update_item(parent, request.user.id)
return JsonResponse({"locator": unicode(dest_usage_key), "courseKey": unicode(dest_usage_key.course_key)}) return JsonResponse({"locator": unicode(created_block.location), "courseKey": unicode(created_block.location.course_key)})
def _duplicate_item(parent_usage_key, duplicate_source_usage_key, display_name=None, user=None): def _duplicate_item(parent_usage_key, duplicate_source_usage_key, display_name=None, user=None):
...@@ -439,8 +439,8 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, display_name=N ...@@ -439,8 +439,8 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, display_name=N
store = modulestore() store = modulestore()
source_item = store.get_item(duplicate_source_usage_key) source_item = store.get_item(duplicate_source_usage_key)
# Change the blockID to be unique. # Change the blockID to be unique.
dest_usage_key = duplicate_source_usage_key.replace(name=uuid4().hex) dest_usage_key = source_item.location.replace(name=uuid4().hex)
category = dest_usage_key.category category = dest_usage_key.block_type
# Update the display name to indicate this is a duplicate (unless display name provided). # Update the display name to indicate this is a duplicate (unless display name provided).
duplicate_metadata = own_metadata(source_item) duplicate_metadata = own_metadata(source_item)
...@@ -465,7 +465,7 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, display_name=N ...@@ -465,7 +465,7 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, display_name=N
if source_item.has_children: if source_item.has_children:
dest_module.children = [] dest_module.children = []
for child in source_item.children: for child in source_item.children:
dupe = _duplicate_item(dest_usage_key, child, user=user) dupe = _duplicate_item(dest_module.location, child, user=user)
dest_module.children.append(dupe) dest_module.children.append(dupe)
store.update_item(dest_module, user.id if user else None) store.update_item(dest_module, user.id if user else None)
...@@ -473,14 +473,14 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, display_name=N ...@@ -473,14 +473,14 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, display_name=N
parent = store.get_item(parent_usage_key) parent = store.get_item(parent_usage_key)
# If source was already a child of the parent, add duplicate immediately afterward. # If source was already a child of the parent, add duplicate immediately afterward.
# Otherwise, add child to end. # Otherwise, add child to end.
if duplicate_source_usage_key in parent.children: if source_item.location in parent.children:
source_index = parent.children.index(duplicate_source_usage_key) source_index = parent.children.index(source_item.location)
parent.children.insert(source_index + 1, dest_usage_key) parent.children.insert(source_index + 1, dest_module.location)
else: else:
parent.children.append(dest_usage_key) parent.children.append(dest_module.location)
store.update_item(parent, user.id if user else None) store.update_item(parent, user.id if user else None)
return dest_usage_key return dest_module.location
def _delete_item(usage_key, user): def _delete_item(usage_key, user):
...@@ -553,12 +553,12 @@ def _get_module_info(usage_key, user, rewrite_static_links=True): ...@@ -553,12 +553,12 @@ def _get_module_info(usage_key, user, rewrite_static_links=True):
data = replace_static_urls( data = replace_static_urls(
data, data,
None, None,
course_id=usage_key.course_key course_id=module.location.course_key
) )
# Note that children aren't being returned until we have a use case. # Note that children aren't being returned until we have a use case.
return { return {
'id': unicode(usage_key), 'id': unicode(module.location),
'data': data, 'data': data,
'metadata': own_metadata(module) 'metadata': own_metadata(module)
} }
...@@ -4,6 +4,7 @@ Unit tests for getting the list of courses and the course outline. ...@@ -4,6 +4,7 @@ Unit tests for getting the list of courses and the course outline.
import json import json
import lxml import lxml
from cms.urls import COURSE_KEY_PATTERN
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from contentstore.utils import reverse_course_url from contentstore.utils import reverse_course_url
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
...@@ -38,7 +39,7 @@ class TestCourseIndex(CourseTestCase): ...@@ -38,7 +39,7 @@ class TestCourseIndex(CourseTestCase):
for link in course_link_eles: for link in course_link_eles:
self.assertRegexpMatches( self.assertRegexpMatches(
link.get("href"), link.get("href"),
'course/slashes:{0}'.format(Locator.ALLOWED_ID_CHARS) 'course/{}'.format(COURSE_KEY_PATTERN)
) )
# now test that url # now test that url
outline_response = authed_client.get(link.get("href"), {}, HTTP_ACCEPT='text/html') outline_response = authed_client.get(link.get("href"), {}, HTTP_ACCEPT='text/html')
......
...@@ -53,7 +53,10 @@ class ItemTest(CourseTestCase): ...@@ -53,7 +53,10 @@ class ItemTest(CourseTestCase):
""" """
parsed = json.loads(response.content) parsed = json.loads(response.content)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
return UsageKey.from_string(parsed['locator']) key = UsageKey.from_string(parsed['locator'])
if key.course_key.run is None:
key = key.map_into_course(CourseKey.from_string(parsed['courseKey']))
return key
def create_xblock(self, parent_usage_key=None, display_name=None, category=None, boilerplate=None): def create_xblock(self, parent_usage_key=None, display_name=None, category=None, boilerplate=None):
data = { data = {
......
...@@ -5,6 +5,10 @@ from django.conf.urls import patterns, include, url ...@@ -5,6 +5,10 @@ from django.conf.urls import patterns, include, url
from ratelimitbackend import admin from ratelimitbackend import admin
admin.autodiscover() admin.autodiscover()
COURSE_KEY_PATTERN = r'(?P<course_key_string>(?:[^/]+/[^/]+/[^/]+)|(?:[^/]+))'
USAGE_KEY_PATTERN = r'(?P<usage_key_string>(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+))'
ASSET_KEY_PATTERN = r'(?P<asset_key_string>(?:/?c4x(:/)?/[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+))'
urlpatterns = patterns('', # nopep8 urlpatterns = patterns('', # nopep8
url(r'^transcripts/upload$', 'contentstore.views.upload_transcripts', name='upload_transcripts'), url(r'^transcripts/upload$', 'contentstore.views.upload_transcripts', name='upload_transcripts'),
...@@ -66,30 +70,30 @@ urlpatterns += patterns( ...@@ -66,30 +70,30 @@ urlpatterns += patterns(
url(r'^signin$', 'login_page', name='login'), url(r'^signin$', 'login_page', name='login'),
url(r'^request_course_creator$', 'request_course_creator'), url(r'^request_course_creator$', 'request_course_creator'),
url(r'^course_team/(?P<course_key_string>[^/]+)/(?P<email>.+)?$', 'course_team_handler'), url(r'^course_team/{}/(?P<email>.+)?$'.format(COURSE_KEY_PATTERN), 'course_team_handler'),
url(r'^course_info/(?P<course_key_string>[^/]+)$', 'course_info_handler'), url(r'^course_info/{}$'.format(COURSE_KEY_PATTERN), 'course_info_handler'),
url( url(
r'^course_info_update/(?P<course_key_string>[^/]+)/(?P<provided_id>\d+)?$', r'^course_info_update/{}/(?P<provided_id>\d+)?$'.format(COURSE_KEY_PATTERN),
'course_info_update_handler' 'course_info_update_handler'
), ),
url(r'^course/(?P<course_key_string>[^/]+)?$', 'course_handler', name='course_handler'), url(r'^course/{}?$'.format(COURSE_KEY_PATTERN), 'course_handler', name='course_handler'),
url(r'^subsection/(?P<usage_key_string>[^/]+)$', 'subsection_handler'), url(r'^subsection/{}$'.format(USAGE_KEY_PATTERN), 'subsection_handler'),
url(r'^unit/(?P<usage_key_string>[^/]+)$', 'unit_handler'), url(r'^unit/{}$'.format(USAGE_KEY_PATTERN), 'unit_handler'),
url(r'^container/(?P<usage_key_string>[^/]+)$', 'container_handler'), url(r'^container/{}$'.format(USAGE_KEY_PATTERN), 'container_handler'),
url(r'^checklists/(?P<course_key_string>[^/]+)/(?P<checklist_index>\d+)?$', 'checklists_handler'), url(r'^checklists/{}/(?P<checklist_index>\d+)?$'.format(COURSE_KEY_PATTERN), 'checklists_handler'),
url(r'^orphan/(?P<course_key_string>[^/]+)$', 'orphan_handler'), url(r'^orphan/{}$'.format(COURSE_KEY_PATTERN), 'orphan_handler'),
url(r'^assets/(?P<course_key_string>[^/]+)/(?P<asset_key_string>.+)?$', 'assets_handler'), url(r'^assets/{}/{}?$'.format(COURSE_KEY_PATTERN, ASSET_KEY_PATTERN), 'assets_handler'),
url(r'^import/(?P<course_key_string>[^/]+)$', 'import_handler'), url(r'^import/{}$'.format(COURSE_KEY_PATTERN), 'import_handler'),
url(r'^import_status/(?P<course_key_string>[^/]+)/(?P<filename>.+)$', 'import_status_handler'), url(r'^import_status/{}/(?P<filename>.+)$'.format(COURSE_KEY_PATTERN), 'import_status_handler'),
url(r'^export/(?P<course_key_string>[^/]+)$', 'export_handler'), url(r'^export/{}$'.format(COURSE_KEY_PATTERN), 'export_handler'),
url(r'^xblock/(?P<usage_key_string>[^/]+)/(?P<view_name>[^/]+)$', 'xblock_view_handler'), url(r'^xblock/{}/(?P<view_name>[^/]+)$'.format(USAGE_KEY_PATTERN), 'xblock_view_handler'),
url(r'^xblock/(?P<usage_key_string>[^/]+)?$', 'xblock_handler'), url(r'^xblock/{}?$'.format(USAGE_KEY_PATTERN), 'xblock_handler'),
url(r'^tabs/(?P<course_key_string>[^/]+)$', 'tabs_handler'), url(r'^tabs/{}$'.format(COURSE_KEY_PATTERN), 'tabs_handler'),
url(r'^settings/details/(?P<course_key_string>[^/]+)$', 'settings_handler'), url(r'^settings/details/{}$'.format(COURSE_KEY_PATTERN), 'settings_handler'),
url(r'^settings/grading/(?P<course_key_string>[^/]+)(/)?(?P<grader_index>\d+)?$', 'grading_handler'), url(r'^settings/grading/{}(/)?(?P<grader_index>\d+)?$'.format(COURSE_KEY_PATTERN), 'grading_handler'),
url(r'^settings/advanced/(?P<course_key_string>[^/]+)$', 'advanced_settings_handler'), url(r'^settings/advanced/{}$'.format(COURSE_KEY_PATTERN), 'advanced_settings_handler'),
url(r'^textbooks/(?P<course_key_string>[^/]+)$', 'textbooks_list_handler'), url(r'^textbooks/{}$'.format(COURSE_KEY_PATTERN), 'textbooks_list_handler'),
url(r'^textbooks/(?P<course_key_string>[^/]+)/(?P<textbook_id>\d[^/]*)$', 'textbooks_detail_handler'), url(r'^textbooks/{}/(?P<textbook_id>\d[^/]*)$'.format(COURSE_KEY_PATTERN), 'textbooks_detail_handler'),
) )
js_info_dict = { js_info_dict = {
...@@ -105,7 +109,7 @@ urlpatterns += patterns('', ...@@ -105,7 +109,7 @@ urlpatterns += patterns('',
if settings.FEATURES.get('ENABLE_EXPORT_GIT'): if settings.FEATURES.get('ENABLE_EXPORT_GIT'):
urlpatterns += (url(r'^export_git/(?P<course_key_string>[^/]+)$', urlpatterns += (url(r'^export_git/{}$'.format(COURSE_KEY_PATTERN),
'contentstore.views.export_git', name='export_git'),) 'contentstore.views.export_git', name='export_git'),)
if settings.FEATURES.get('ENABLE_SERVICE_STATUS'): if settings.FEATURES.get('ENABLE_SERVICE_STATUS'):
......
...@@ -197,7 +197,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -197,7 +197,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
del metadata[old_name] del metadata[old_name]
children = [ children = [
location.course_key.make_usage_key_from_deprecated_string(childloc) self._convert_reference_to_key(childloc)
for childloc in definition.get('children', []) for childloc in definition.get('children', [])
] ]
data = definition.get('data', {}) data = definition.get('data', {})
...@@ -254,6 +254,13 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -254,6 +254,13 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
error_msg=exc_info_to_str(sys.exc_info()) error_msg=exc_info_to_str(sys.exc_info())
) )
def _convert_reference_to_key(self, ref_string):
"""
Convert a single serialized UsageKey string in a ReferenceField into a UsageKey.
"""
key = Location.from_deprecated_string(ref_string)
return key.replace(run=self.modulestore._fill_in_run(key.course_key).run)
def _convert_reference_fields_to_keys(self, class_, course_key, jsonfields): def _convert_reference_fields_to_keys(self, class_, course_key, jsonfields):
""" """
Find all fields of type reference and convert the payload into UsageKeys Find all fields of type reference and convert the payload into UsageKeys
...@@ -267,15 +274,15 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -267,15 +274,15 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
if field is None: if field is None:
continue continue
elif isinstance(field, Reference): elif isinstance(field, Reference):
jsonfields[field_name] = course_key.make_usage_key_from_deprecated_string(value) jsonfields[field_name] = self._convert_reference_to_key(value)
elif isinstance(field, ReferenceList): elif isinstance(field, ReferenceList):
jsonfields[field_name] = [ jsonfields[field_name] = [
course_key.make_usage_key_from_deprecated_string(ele) for ele in value self._convert_reference_to_key(ele) for ele in value
] ]
elif isinstance(field, ReferenceValueDict): elif isinstance(field, ReferenceValueDict):
for key, subvalue in value.iteritems(): for key, subvalue in value.iteritems():
assert isinstance(subvalue, basestring) assert isinstance(subvalue, basestring)
value[key] = course_key.make_usage_key_from_deprecated_string(subvalue) value[key] = self._convert_reference_to_key(subvalue)
return jsonfields return jsonfields
...@@ -378,6 +385,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -378,6 +385,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
# performance optimization to prevent updating the meta-data inheritance tree during # performance optimization to prevent updating the meta-data inheritance tree during
# bulk write operations # bulk write operations
self.ignore_write_events_on_courses = set() self.ignore_write_events_on_courses = set()
self._course_run_cache = {}
def begin_bulk_write_operation_on_course(self, course_id): def begin_bulk_write_operation_on_course(self, course_id):
""" """
...@@ -394,6 +402,27 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -394,6 +402,27 @@ class MongoModuleStore(ModuleStoreWriteBase):
self.ignore_write_events_on_courses.remove(course_id) self.ignore_write_events_on_courses.remove(course_id)
self.refresh_cached_metadata_inheritance_tree(course_id) self.refresh_cached_metadata_inheritance_tree(course_id)
def _fill_in_run(self, course_key):
if course_key.run is not None:
return course_key
cache_key = (course_key.org, course_key.course)
if cache_key not in self._course_run_cache:
matching_courses = list(self.collection.find(SON([
('_id.tag', 'i4x'),
('_id.org', course_key.org),
('_id.course', course_key.course),
('_id.category', 'course'),
])).limit(1))
if not matching_courses:
return course_key
self._course_run_cache[cache_key] = matching_courses[0]['_id']['name']
return course_key.replace(run=self._course_run_cache[cache_key])
def _compute_metadata_inheritance_tree(self, course_id): def _compute_metadata_inheritance_tree(self, course_id):
''' '''
TODO (cdodge) This method can be deleted when the 'split module store' work has been completed TODO (cdodge) This method can be deleted when the 'split module store' work has been completed
...@@ -401,6 +430,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -401,6 +430,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
# get all collections in the course, this query should not return any leaf nodes # get all collections in the course, this query should not return any leaf nodes
# note this is a bit ugly as when we add new categories of containers, we have to add it here # note this is a bit ugly as when we add new categories of containers, we have to add it here
course_id = self._fill_in_run(course_id)
block_types_with_children = set( block_types_with_children = set(
name for name, class_ in XBlock.load_classes() if getattr(class_, 'has_children', False) name for name, class_ in XBlock.load_classes() if getattr(class_, 'has_children', False)
) )
...@@ -476,6 +506,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -476,6 +506,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
''' '''
tree = {} tree = {}
course_id = self._fill_in_run(course_id)
if not force_refresh: if not force_refresh:
# see if we are first in the request cache (if present) # see if we are first in the request cache (if present)
if self.request_cache is not None and course_id in self.request_cache.data.get('metadata_inheritance', {}): if self.request_cache is not None and course_id in self.request_cache.data.get('metadata_inheritance', {}):
...@@ -554,6 +585,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -554,6 +585,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
data = {} data = {}
to_process = list(items) to_process = list(items)
course_key = self._fill_in_run(course_key)
while to_process and depth is None or depth >= 0: while to_process and depth is None or depth >= 0:
children = [] children = []
for item in to_process: for item in to_process:
...@@ -581,6 +613,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -581,6 +613,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
""" """
Load an XModuleDescriptor from item, using the children stored in data_cache Load an XModuleDescriptor from item, using the children stored in data_cache
""" """
course_key = self._fill_in_run(course_key)
location = Location._from_deprecated_son(item['location'], course_key.run) location = Location._from_deprecated_son(item['location'], course_key.run)
data_dir = getattr(item, 'data_dir', location.course) data_dir = getattr(item, 'data_dir', location.course)
root = self.fs_root / data_dir root = self.fs_root / data_dir
...@@ -617,6 +650,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -617,6 +650,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
Load a list of xmodules from the data in items, with children cached up Load a list of xmodules from the data in items, with children cached up
to specified depth to specified depth
""" """
course_key = self._fill_in_run(course_key)
data_cache = self._cache_children(course_key, items, depth) data_cache = self._cache_children(course_key, items, depth)
# if we are loading a course object, if we're not prefetching children (depth != 0) then don't # if we are loading a course object, if we're not prefetching children (depth != 0) then don't
...@@ -669,6 +703,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -669,6 +703,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
Get the course with the given courseid (org/course/run) Get the course with the given courseid (org/course/run)
""" """
assert(isinstance(course_key, SlashSeparatedCourseKey)) assert(isinstance(course_key, SlashSeparatedCourseKey))
course_key = self._fill_in_run(course_key)
location = course_key.make_usage_key('course', course_key.run) location = course_key.make_usage_key('course', course_key.run)
try: try:
return self.get_item(location, depth=depth) return self.get_item(location, depth=depth)
...@@ -685,6 +720,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -685,6 +720,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
otherwise, do a case sensitive search otherwise, do a case sensitive search
""" """
assert(isinstance(course_key, SlashSeparatedCourseKey)) assert(isinstance(course_key, SlashSeparatedCourseKey))
course_key = self._fill_in_run(course_key)
location = course_key.make_usage_key('course', course_key.run) location = course_key.make_usage_key('course', course_key.run)
if ignore_case: if ignore_case:
course_query = location.to_deprecated_son('_id.') course_query = location.to_deprecated_son('_id.')
...@@ -873,6 +909,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -873,6 +909,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
:param runtime: if you already have an xblock from the course, the xblock.runtime value :param runtime: if you already have an xblock from the course, the xblock.runtime value
:param fields: a dictionary of field names and values for the new xmodule :param fields: a dictionary of field names and values for the new xmodule
""" """
location = location.replace(run=self._fill_in_run(location.course_key).run)
# differs from split mongo in that I believe most of this logic should be above the persistence # differs from split mongo in that I believe most of this logic should be above the persistence
# layer but added it here to enable quick conversion. I'll need to reconcile these. # layer but added it here to enable quick conversion. I'll need to reconcile these.
if metadata is None: if metadata is None:
...@@ -1073,6 +1110,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -1073,6 +1110,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
""" """
Return an array of all of the locations (deprecated string format) for orphans in the course. Return an array of all of the locations (deprecated string format) for orphans in the course.
""" """
course_key = self._fill_in_run(course_key)
detached_categories = [name for name, __ in XBlock.load_tagged_classes("detached")] detached_categories = [name for name, __ in XBlock.load_tagged_classes("detached")]
query = self._course_key_to_son(course_key) query = self._course_key_to_son(course_key)
query['_id.category'] = {'$nin': detached_categories} query['_id.category'] = {'$nin': detached_categories}
......
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