Commit 021ccde1 by Victor Shnayder

Make jump_to work with the xml modulestore

* it now works in the context of a specific course_id
* add tracking of parent locations to xml modulestore
* adjust lots of tests, including some refactoring
* NOT working yet: jumping to the right position in a sequence.
parent 22aa325d
...@@ -6,15 +6,14 @@ from .exceptions import (ItemNotFoundError, NoPathToItem) ...@@ -6,15 +6,14 @@ from .exceptions import (ItemNotFoundError, NoPathToItem)
from . import ModuleStore, Location from . import ModuleStore, Location
def path_to_location(modulestore, location, course_name=None): def path_to_location(modulestore, course_id, location):
''' '''
Try to find a course_id/chapter/section[/position] path to location in Try to find a course_id/chapter/section[/position] path to location in
modulestore. The courseware insists that the first level in the course is modulestore. The courseware insists that the first level in the course is
chapter, but any kind of module can be a "section". chapter, but any kind of module can be a "section".
location: something that can be passed to Location location: something that can be passed to Location
course_name: [optional]. If not None, restrict search to paths course_id: Search for paths in this course.
in that course.
raise ItemNotFoundError if the location doesn't exist. raise ItemNotFoundError if the location doesn't exist.
...@@ -41,7 +40,7 @@ def path_to_location(modulestore, location, course_name=None): ...@@ -41,7 +40,7 @@ def path_to_location(modulestore, location, course_name=None):
xs = xs[1] xs = xs[1]
return p return p
def find_path_to_course(location, course_name=None): def find_path_to_course():
'''Find a path up the location graph to a node with the '''Find a path up the location graph to a node with the
specified category. specified category.
...@@ -69,7 +68,8 @@ def path_to_location(modulestore, location, course_name=None): ...@@ -69,7 +68,8 @@ def path_to_location(modulestore, location, course_name=None):
# print 'Processing loc={0}, path={1}'.format(loc, path) # print 'Processing loc={0}, path={1}'.format(loc, path)
if loc.category == "course": if loc.category == "course":
if course_name is None or course_name == loc.name: # confirm that this is the right course
if course_id == CourseDescriptor.location_to_id(loc):
# Found it! # Found it!
path = (loc, path) path = (loc, path)
return flatten(path) return flatten(path)
...@@ -81,7 +81,7 @@ def path_to_location(modulestore, location, course_name=None): ...@@ -81,7 +81,7 @@ def path_to_location(modulestore, location, course_name=None):
# If we're here, there is no path # If we're here, there is no path
return None return None
path = find_path_to_course(location, course_name) path = find_path_to_course()
if path is None: if path is None:
raise(NoPathToItem(location)) raise(NoPathToItem(location))
......
from path import path
# from ~/mitx_all/mitx/common/lib/xmodule/xmodule/modulestore/tests/
# to ~/mitx_all/mitx/common/test
TEST_DIR = path(__file__).abspath().dirname()
for i in range(5):
TEST_DIR = TEST_DIR.dirname()
TEST_DIR = TEST_DIR / 'test'
DATA_DIR = TEST_DIR / 'data'
from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
from xmodule.modulestore.search import path_to_location
def check_path_to_location(modulestore):
'''Make sure that path_to_location works: should be passed a modulestore
with the toy and simple courses loaded.'''
should_work = (
("i4x://edX/toy/video/Welcome",
("edX/toy/2012_Fall", "Overview", "Welcome", None)),
("i4x://edX/toy/chapter/Overview",
("edX/toy/2012_Fall", "Overview", None, None)),
)
course_id = "edX/toy/2012_Fall"
for location, expected in should_work:
assert_equals(path_to_location(modulestore, course_id, location), expected)
not_found = (
"i4x://edX/toy/video/WelcomeX", "i4x://edX/toy/course/NotHome"
)
for location in not_found:
assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location)
# Since our test files are valid, there shouldn't be any
# elements with no path to them. But we can look for them in
# another course.
no_path = (
"i4x://edX/simple/video/Lost_Video",
)
for location in no_path:
assert_raises(NoPathToItem, path_to_location, modulestore, course_id, location)
import pymongo import pymongo
from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup
from path import path
from pprint import pprint from pprint import pprint
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.search import path_to_location
# from ~/mitx_all/mitx/common/lib/xmodule/xmodule/modulestore/tests/ from .test_modulestore import check_path_to_location
# to ~/mitx_all/mitx/common/test from . import DATA_DIR
TEST_DIR = path(__file__).abspath().dirname()
for i in range(5):
TEST_DIR = TEST_DIR.dirname()
TEST_DIR = TEST_DIR / 'test'
DATA_DIR = TEST_DIR / 'data'
HOST = 'localhost' HOST = 'localhost'
...@@ -110,27 +101,5 @@ class TestMongoModuleStore(object): ...@@ -110,27 +101,5 @@ class TestMongoModuleStore(object):
def test_path_to_location(self): def test_path_to_location(self):
'''Make sure that path_to_location works''' '''Make sure that path_to_location works'''
should_work = ( check_path_to_location(self.store)
("i4x://edX/toy/video/Welcome",
("edX/toy/2012_Fall", "Overview", "Welcome", None)),
("i4x://edX/toy/chapter/Overview",
("edX/toy/2012_Fall", "Overview", None, None)),
)
for location, expected in should_work:
assert_equals(path_to_location(self.store, location), expected)
not_found = (
"i4x://edX/toy/video/WelcomeX", "i4x://edX/toy/course/NotHome"
)
for location in not_found:
assert_raises(ItemNotFoundError, path_to_location, self.store, location)
# Since our test files are valid, there shouldn't be any
# elements with no path to them. But we can look for them in
# another course.
no_path = (
"i4x://edX/simple/video/Lost_Video",
)
for location in no_path:
assert_raises(NoPathToItem, path_to_location, self.store, location, "toy")
from xmodule.modulestore import Location
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.xml_importer import import_from_xml
from .test_modulestore import check_path_to_location
from . import DATA_DIR
class TestXMLModuleStore(object):
def test_path_to_location(self):
"""Make sure that path_to_location works properly"""
print "Starting import"
modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy', 'simple'])
print "finished import"
check_path_to_location(modulestore)
...@@ -37,7 +37,7 @@ def clean_out_mako_templating(xml_string): ...@@ -37,7 +37,7 @@ def clean_out_mako_templating(xml_string):
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
def __init__(self, xmlstore, course_id, course_dir, def __init__(self, xmlstore, course_id, course_dir,
policy, error_tracker, **kwargs): policy, error_tracker, parent_tracker, **kwargs):
""" """
A class that handles loading from xml. Does some munging to ensure that A class that handles loading from xml. Does some munging to ensure that
all elements have unique slugs. all elements have unique slugs.
...@@ -134,8 +134,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -134,8 +134,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
xmlstore.modules[course_id][descriptor.location] = descriptor xmlstore.modules[course_id][descriptor.location] = descriptor
if xmlstore.eager: for child in descriptor.get_children():
descriptor.get_children() parent_tracker.add_parent(child.location, descriptor.location)
return descriptor return descriptor
render_template = lambda: '' render_template = lambda: ''
...@@ -151,6 +151,46 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -151,6 +151,46 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
error_tracker, process_xml, policy, **kwargs) error_tracker, process_xml, policy, **kwargs)
class ParentTracker(object):
"""A simple class to factor out the logic for tracking location parent pointers."""
def __init__(self):
"""
Init
"""
# location -> set(parents). Not using defaultdict because we care about the empty case.
self._parents = dict()
def add_parent(self, child, parent):
"""
Add a parent of child location to the set of parents. Duplicate calls have no effect.
child and parent must be something that can be passed to Location.
"""
child = Location(child)
parent = Location(parent)
s = self._parents.setdefault(child, set())
s.add(parent)
def is_known(self, child):
"""
returns True iff child has some parents.
"""
child = Location(child)
return child in self._parents
def make_known(self, location):
"""Tell the parent tracker about an object, without registering any
parents for it. Used for the top level course descriptor locations."""
self._parents.setdefault(location, set())
def parents(self, child):
"""
Return a list of the parents of this child. If not is_known(child), will throw a KeyError
"""
child = Location(child)
return list(self._parents[child])
class XMLModuleStore(ModuleStoreBase): class XMLModuleStore(ModuleStoreBase):
""" """
An XML backed ModuleStore An XML backed ModuleStore
...@@ -191,6 +231,8 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -191,6 +231,8 @@ class XMLModuleStore(ModuleStoreBase):
#log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager, self.data_dir)) #log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager, self.data_dir))
#log.debug('default_class = %s' % self.default_class) #log.debug('default_class = %s' % self.default_class)
self.parent_tracker = ParentTracker()
# If we are specifically asked for missing courses, that should # If we are specifically asked for missing courses, that should
# be an error. If we are asked for "all" courses, find the ones # be an error. If we are asked for "all" courses, find the ones
# that have a course.xml # that have a course.xml
...@@ -221,6 +263,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -221,6 +263,7 @@ class XMLModuleStore(ModuleStoreBase):
if course_descriptor is not None: if course_descriptor is not None:
self.courses[course_dir] = course_descriptor self.courses[course_dir] = course_descriptor
self._location_errors[course_descriptor.location] = errorlog self._location_errors[course_descriptor.location] = errorlog
self.parent_tracker.make_known(course_descriptor.location)
else: else:
# Didn't load course. Instead, save the errors elsewhere. # Didn't load course. Instead, save the errors elsewhere.
self.errored_courses[course_dir] = errorlog self.errored_courses[course_dir] = errorlog
...@@ -339,7 +382,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -339,7 +382,7 @@ class XMLModuleStore(ModuleStoreBase):
course_id = CourseDescriptor.make_id(org, course, url_name) course_id = CourseDescriptor.make_id(org, course, url_name)
system = ImportSystem(self, course_id, course_dir, policy, tracker) system = ImportSystem(self, course_id, course_dir, policy, tracker, self.parent_tracker)
course_descriptor = system.process_xml(etree.tostring(course_data)) course_descriptor = system.process_xml(etree.tostring(course_data))
...@@ -450,3 +493,19 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -450,3 +493,19 @@ class XMLModuleStore(ModuleStoreBase):
metadata: A nested dictionary of module metadata metadata: A nested dictionary of module metadata
""" """
raise NotImplementedError("XMLModuleStores are read-only") raise NotImplementedError("XMLModuleStores are read-only")
def get_parent_locations(self, location):
'''Find all locations that are the parents of this location. Needed
for path_to_location().
If there is no data at location in this modulestore, raise
ItemNotFoundError.
returns an iterable of things that can be passed to Location. This may
be empty if there are no parents.
'''
location = Location.ensure_fully_specified(location)
if not self.parent_tracker.is_known(location):
raise ItemNotFoundError(location)
return self.parent_tracker.parents(location)
...@@ -544,7 +544,13 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -544,7 +544,13 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
# Put import here to avoid circular import errors # Put import here to avoid circular import errors
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
msg = "Error loading from xml." msg = "Error loading from xml."
log.warning(msg + " " + str(err)) log.warning(msg + " " + str(err)[:200])
# Normally, we don't want lots of exception traces in our logs from common
# content problems. But if you're debugging the xml loading code itself,
# uncomment the next line.
# log.exception(msg)
system.error_tracker(msg) system.error_tracker(msg)
err_msg = msg + "\n" + exc_info_to_str(sys.exc_info()) err_msg = msg + "\n" + exc_info_to_str(sys.exc_info())
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course, descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course,
......
...@@ -204,6 +204,7 @@ class PageLoader(ActivateLoginTestCase): ...@@ -204,6 +204,7 @@ class PageLoader(ActivateLoginTestCase):
self.assertEqual(len(courses), 1) self.assertEqual(len(courses), 1)
course = courses[0] course = courses[0]
self.enroll(course) self.enroll(course)
course_id = course.id
n = 0 n = 0
num_bad = 0 num_bad = 0
...@@ -214,7 +215,8 @@ class PageLoader(ActivateLoginTestCase): ...@@ -214,7 +215,8 @@ class PageLoader(ActivateLoginTestCase):
print "Checking ", descriptor.location.url() print "Checking ", descriptor.location.url()
#print descriptor.__class__, descriptor.location #print descriptor.__class__, descriptor.location
resp = self.client.get(reverse('jump_to', resp = self.client.get(reverse('jump_to',
kwargs={'location': descriptor.location.url()})) kwargs={'course_id': course_id,
'location': descriptor.location.url()}))
msg = str(resp.status_code) msg = str(resp.status_code)
if resp.status_code != 200: if resp.status_code != 200:
......
...@@ -194,7 +194,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -194,7 +194,7 @@ def index(request, course_id, chapter=None, section=None,
@ensure_csrf_cookie @ensure_csrf_cookie
def jump_to(request, location): def jump_to(request, course_id, location):
''' '''
Show the page that contains a specific location. Show the page that contains a specific location.
...@@ -211,7 +211,7 @@ def jump_to(request, location): ...@@ -211,7 +211,7 @@ def jump_to(request, location):
# Complain if there's not data for this location # Complain if there's not data for this location
try: try:
(course_id, chapter, section, position) = path_to_location(modulestore(), location) (course_id, chapter, section, position) = path_to_location(modulestore(), course_id, location)
except ItemNotFoundError: except ItemNotFoundError:
raise Http404("No data at this location: {0}".format(location)) raise Http404("No data at this location: {0}".format(location))
except NoPathToItem: except NoPathToItem:
......
...@@ -98,8 +98,9 @@ if settings.COURSEWARE_ENABLED: ...@@ -98,8 +98,9 @@ if settings.COURSEWARE_ENABLED:
urlpatterns += ( urlpatterns += (
# Hook django-masquerade, allowing staff to view site as other users # Hook django-masquerade, allowing staff to view site as other users
url(r'^masquerade/', include('masquerade.urls')), url(r'^masquerade/', include('masquerade.urls')),
url(r'^jump_to/(?P<location>.*)$', 'courseware.views.jump_to', name="jump_to"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/jump_to/(?P<location>.*)$',
'courseware.views.jump_to', name="jump_to"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<location>.*?)/(?P<dispatch>[^/]*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.modx_dispatch', 'courseware.module_render.modx_dispatch',
name='modx_dispatch'), name='modx_dispatch'),
......
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