Commit 69ea7aa8 by David Ormsbee

Merge pull request #597 from MITx/feature/victor/nested-links

Feature/victor/nested links
parents 10a80ddf b5843654
......@@ -6,15 +6,14 @@ from .exceptions import (ItemNotFoundError, NoPathToItem)
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
modulestore. The courseware insists that the first level in the course is
chapter, but any kind of module can be a "section".
location: something that can be passed to Location
course_name: [optional]. If not None, restrict search to paths
in that course.
course_id: Search for paths in this course.
raise ItemNotFoundError if the location doesn't exist.
......@@ -27,7 +26,7 @@ def path_to_location(modulestore, location, course_name=None):
A location may be accessible via many paths. This method may
return any valid path.
If the section is a sequence, position will be the position
If the section is a sequential or vertical, position will be the position
of this location in that sequence. Otherwise, position will
be None. TODO (vshnayder): Not true yet.
'''
......@@ -41,7 +40,7 @@ def path_to_location(modulestore, location, course_name=None):
xs = xs[1]
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
specified category.
......@@ -69,7 +68,8 @@ def path_to_location(modulestore, location, course_name=None):
# print 'Processing loc={0}, path={1}'.format(loc, path)
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!
path = (loc, path)
return flatten(path)
......@@ -81,17 +81,34 @@ def path_to_location(modulestore, location, course_name=None):
# If we're here, there is no path
return None
path = find_path_to_course(location, course_name)
path = find_path_to_course()
if path is None:
raise(NoPathToItem(location))
raise NoPathToItem(location)
n = len(path)
course_id = CourseDescriptor.location_to_id(path[0])
# pull out the location names
chapter = path[1].name if n > 1 else None
section = path[2].name if n > 2 else None
# TODO (vshnayder): not handling position at all yet...
# Figure out the position
position = None
# This block of code will find the position of a module within a nested tree
# of modules. If a problem is on tab 2 of a sequence that's on tab 3 of a
# sequence, the resulting position is 3_2. However, no positional modules
# (e.g. sequential and videosequence) currently deal with this form of
# representing nested positions. This needs to happen before jumping to a
# module nested in more than one positional module will work.
if n > 3:
position_list = []
for path_index in range(2, n-1):
category = path[path_index].category
if category == 'sequential' or category == 'videosequence':
section_desc = modulestore.get_instance(course_id, path[path_index])
child_locs = [c.location for c in section_desc.get_children()]
# positions are 1-indexed, and should be strings to be consistent with
# url parsing.
position_list.append(str(child_locs.index(path[path_index+1]) + 1))
position = "_".join(position_list)
return (course_id, chapter, section, position)
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
from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup
from path import path
from pprint import pprint
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.mongo import MongoModuleStore
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/
# 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 .test_modulestore import check_path_to_location
from . import DATA_DIR
HOST = 'localhost'
......@@ -110,27 +101,5 @@ class TestMongoModuleStore(object):
def test_path_to_location(self):
'''Make sure that path_to_location works'''
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)),
)
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")
check_path_to_location(self.store)
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, course_dirs=['toy', 'simple'])
print "finished import"
check_path_to_location(modulestore)
......@@ -37,7 +37,7 @@ def clean_out_mako_templating(xml_string):
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
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
all elements have unique slugs.
......@@ -143,8 +143,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
xmlstore.modules[course_id][descriptor.location] = descriptor
if xmlstore.eager:
descriptor.get_children()
for child in descriptor.get_children():
parent_tracker.add_parent(child.location, descriptor.location)
return descriptor
render_template = lambda: ''
......@@ -160,12 +160,51 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
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):
"""
An XML backed ModuleStore
"""
def __init__(self, data_dir, default_class=None, eager=False,
course_dirs=None):
def __init__(self, data_dir, default_class=None, course_dirs=None):
"""
Initialize an XMLModuleStore from data_dir
......@@ -174,15 +213,11 @@ class XMLModuleStore(ModuleStoreBase):
default_class: dot-separated string defining the default descriptor
class to use if none is specified in entry_points
eager: If true, load the modules children immediately to force the
entire course tree to be parsed
course_dirs: If specified, the list of course_dirs to load. Otherwise,
load all course dirs
"""
ModuleStoreBase.__init__(self)
self.eager = eager
self.data_dir = path(data_dir)
self.modules = defaultdict(dict) # course_id -> dict(location -> XModuleDescriptor)
self.courses = {} # course_dir -> XModuleDescriptor for the course
......@@ -195,10 +230,7 @@ class XMLModuleStore(ModuleStoreBase):
class_ = getattr(import_module(module_path), class_name)
self.default_class = class_
# TODO (cpennington): We need a better way of selecting specific sets of
# debug messages to enable. These were drowning out important messages
#log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager, self.data_dir))
#log.debug('default_class = %s' % self.default_class)
self.parent_tracker = ParentTracker()
# If we are specifically asked for missing courses, that should
# be an error. If we are asked for "all" courses, find the ones
......@@ -230,6 +262,7 @@ class XMLModuleStore(ModuleStoreBase):
if course_descriptor is not None:
self.courses[course_dir] = course_descriptor
self._location_errors[course_descriptor.location] = errorlog
self.parent_tracker.make_known(course_descriptor.location)
else:
# Didn't load course. Instead, save the errors elsewhere.
self.errored_courses[course_dir] = errorlog
......@@ -348,7 +381,7 @@ class XMLModuleStore(ModuleStoreBase):
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))
......@@ -459,3 +492,19 @@ class XMLModuleStore(ModuleStoreBase):
metadata: A nested dictionary of module metadata
"""
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)
......@@ -6,7 +6,7 @@ from .exceptions import DuplicateItemError
log = logging.getLogger(__name__)
def import_from_xml(store, data_dir, course_dirs=None, eager=True,
def import_from_xml(store, data_dir, course_dirs=None,
default_class='xmodule.raw_module.RawDescriptor'):
"""
Import the specified xml data_dir into the "store" modulestore,
......@@ -19,7 +19,6 @@ def import_from_xml(store, data_dir, course_dirs=None, eager=True,
module_store = XMLModuleStore(
data_dir,
default_class=default_class,
eager=eager,
course_dirs=course_dirs
)
for course_id in module_store.modules.keys():
......
......@@ -49,7 +49,7 @@ class RoundTripTestCase(unittest.TestCase):
copytree(data_dir / course_dir, root_dir / course_dir)
print "Starting import"
initial_import = XMLModuleStore(root_dir, eager=True, course_dirs=[course_dir])
initial_import = XMLModuleStore(root_dir, course_dirs=[course_dir])
courses = initial_import.get_courses()
self.assertEquals(len(courses), 1)
......@@ -66,7 +66,7 @@ class RoundTripTestCase(unittest.TestCase):
course_xml.write(xml)
print "Starting second import"
second_import = XMLModuleStore(root_dir, eager=True, course_dirs=[course_dir])
second_import = XMLModuleStore(root_dir, course_dirs=[course_dir])
courses2 = second_import.get_courses()
self.assertEquals(len(courses2), 1)
......
......@@ -193,7 +193,7 @@ class ImportTestCase(unittest.TestCase):
"""Make sure that metadata is inherited properly"""
print "Starting import"
initial_import = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy'])
initial_import = XMLModuleStore(DATA_DIR, course_dirs=['toy'])
courses = initial_import.get_courses()
self.assertEquals(len(courses), 1)
......@@ -216,7 +216,7 @@ class ImportTestCase(unittest.TestCase):
def get_course(name):
print "Importing {0}".format(name)
modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=[name])
modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
courses = modulestore.get_courses()
self.assertEquals(len(courses), 1)
return courses[0]
......@@ -245,7 +245,7 @@ class ImportTestCase(unittest.TestCase):
happen--locations should uniquely name definitions. But in
our imperfect XML world, it can (and likely will) happen."""
modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy', 'two_toys'])
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy', 'two_toys'])
toy_id = "edX/toy/2012_Fall"
two_toy_id = "edX/toy/TT_2012_Fall"
......@@ -261,7 +261,7 @@ class ImportTestCase(unittest.TestCase):
"""Ensure that colons in url_names convert to file paths properly"""
print "Starting import"
modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy'])
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy'])
courses = modulestore.get_courses()
self.assertEquals(len(courses), 1)
......
......@@ -544,7 +544,13 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
# Put import here to avoid circular import errors
from xmodule.error_module import ErrorDescriptor
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)
err_msg = msg + "\n" + exc_info_to_str(sys.exc_info())
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course,
......
......@@ -57,7 +57,6 @@ def import_with_checks(course_dir, verbose=True):
# module.
modulestore = XMLModuleStore(data_dir,
default_class=None,
eager=True,
course_dirs=course_dirs)
def str_of_err(tpl):
......
......@@ -23,7 +23,6 @@ def import_course(course_dir, verbose=True):
# module.
modulestore = XMLModuleStore(data_dir,
default_class=None,
eager=True,
course_dirs=course_dirs)
def str_of_err(tpl):
......
......@@ -195,7 +195,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
descriptor.category,
shared_state_key)
instance_state = instance_module.state if instance_module is not None else None
shared_state = shared_module.state if shared_module is not None else None
......@@ -254,7 +253,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
# by the replace_static_urls code below
replace_urls=replace_urls,
node_path=settings.NODE_PATH,
anonymous_student_id=anonymous_student_id
anonymous_student_id=anonymous_student_id,
)
# pass position specified in URL to module through ModuleSystem
system.set('position', position)
......
......@@ -68,7 +68,6 @@ def xml_store_config(data_dir):
'OPTIONS': {
'data_dir': data_dir,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
'eager': True,
}
}
}
......@@ -204,7 +203,8 @@ class PageLoader(ActivateLoginTestCase):
self.assertEqual(len(courses), 1)
course = courses[0]
self.enroll(course)
course_id = course.id
n = 0
num_bad = 0
all_ok = True
......@@ -214,7 +214,8 @@ class PageLoader(ActivateLoginTestCase):
print "Checking ", descriptor.location.url()
#print descriptor.__class__, descriptor.location
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)
if resp.status_code != 200:
......
......@@ -5,8 +5,6 @@ import itertools
from functools import partial
from functools import partial
from django.conf import settings
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
......@@ -152,7 +150,7 @@ def index(request, course_id, chapter=None, section=None,
course_id, request.user, section_descriptor)
module = get_module(request.user, request,
section_descriptor.location,
student_module_cache, course_id)
student_module_cache, course_id, position)
if module is None:
# User is probably being clever and trying to access something
# they don't have access to.
......@@ -196,7 +194,7 @@ def index(request, course_id, chapter=None, section=None,
@ensure_csrf_cookie
def jump_to(request, location):
def jump_to(request, course_id, location):
'''
Show the page that contains a specific location.
......@@ -213,15 +211,18 @@ def jump_to(request, location):
# Complain if there's not data for this location
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:
raise Http404("No data at this location: {0}".format(location))
except NoPathToItem:
raise Http404("This location is not in any class: {0}".format(location))
# Rely on index to do all error handling and access control.
return index(request, course_id, chapter, section, position)
return redirect('courseware_position',
course_id=course_id,
chapter=chapter,
section=section,
position=position)
@ensure_csrf_cookie
def course_info(request, course_id):
"""
......
......@@ -43,7 +43,8 @@ def get_full_modules():
class_path = settings.MODULESTORE['default']['ENGINE']
module_path, _, class_name = class_path.rpartition('.')
class_ = getattr(import_module(module_path), class_name)
modulestore = class_(**dict(settings.MODULESTORE['default']['OPTIONS'].items() + [('eager', True)]))
# TODO (vshnayder): wth is this doing???
modulestore = class_(**dict(settings.MODULESTORE['default']['OPTIONS'].items()))
_FULLMODULES = modulestore.modules
return _FULLMODULES
......@@ -76,7 +77,7 @@ def initialize_discussion_info(request, course):
_is_course_discussion = lambda x: x[0].dict()['category'] == 'discussion' \
and x[0].dict()['course'] == course_name
_get_module_descriptor = operator.itemgetter(1)
def _get_module(module_descriptor):
......@@ -135,8 +136,8 @@ class HtmlResponse(HttpResponse):
def __init__(self, html=''):
super(HtmlResponse, self).__init__(html, content_type='text/plain')
class ViewNameMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs):
class ViewNameMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs):
request.view_name = view_func.__name__
class QueryCountDebugMiddleware(object):
......
......@@ -223,7 +223,6 @@ MODULESTORE = {
'OPTIONS': {
'data_dir': DATA_DIR,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
'eager': True,
}
}
}
......
......@@ -98,8 +98,9 @@ if settings.COURSEWARE_ENABLED:
urlpatterns += (
# Hook django-masquerade, allowing staff to view site as other users
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>[^/]*)$',
'courseware.module_render.modx_dispatch',
name='modx_dispatch'),
......@@ -142,6 +143,8 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.index', name="courseware_chapter"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$',
'courseware.views.index', name="courseware_section"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/(?P<position>[^/]*)/?$',
'courseware.views.index', name="courseware_position"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/progress$',
'courseware.views.progress', name="progress"),
# Takes optional student_id for instructor use--shows profile as that student sees it.
......@@ -164,7 +167,7 @@ if settings.COURSEWARE_ENABLED:
if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
urlpatterns += (
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/news$',
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/news$',
'courseware.views.news', name="news"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/',
include('django_comment_client.urls'))
......
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