Commit 982026bb by Victor Shnayder

Add jump_to functionality to lms on mongo

* More tests for mongo modulestore, including tweaks to test files
* add location_to_id method to CourseDescriptors
* Implement path_to_location method in mongo.py
  - TODO: does not handle position in sequences yet.
* fix bug in jump_to view.
parent 10246bfa
......@@ -11,17 +11,30 @@ log = logging.getLogger(__name__)
class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule
@classmethod
def id_to_location(cls, course_id):
@staticmethod
def id_to_location(course_id):
'''Convert the given course_id (org/course/name) to a location object.
Throws ValueError if course_id is of the wrong format.
'''
org, course, name = course_id.split('/')
return Location('i4x', org, course, 'course', name)
@staticmethod
def location_to_id(location):
'''Convert a location of a course to a course_id. If location category
is not "course", raise a ValueError.
location: something that can be passed to Location
'''
loc = Location(location)
if loc.category != "course":
raise ValueError("{0} is not a course location".format(loc))
return "/".join([loc.org, loc.course, loc.name])
@property
def id(self):
return "/".join([self.location.org, self.location.course, self.location.name])
return self.location_to_id(self.location)
@property
def title(self):
......
......@@ -8,16 +8,18 @@ from itertools import repeat
from importlib import import_module
from xmodule.x_module import XModuleDescriptor
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.course_module import CourseDescriptor
from mitxmako.shortcuts import render_to_string
from . import ModuleStore, Location
from .exceptions import ItemNotFoundError, InsufficientSpecificationError
from .exceptions import ItemNotFoundError, InsufficientSpecificationError, NoPathToItem
# TODO (cpennington): This code currently operates under the assumption that
# there is only one revision for each item. Once we start versioning inside the CMS,
# that assumption will have to change
class CachingDescriptorSystem(MakoDescriptorSystem):
"""
A system that has a cache of module json that it will use to load modules
......@@ -152,6 +154,14 @@ class MongoModuleStore(ModuleStore):
course_filter = Location("i4x", category="course")
return self.get_items(course_filter)
def _find_one(self, location):
'''Look for a given location in the collection.
If revision isn't specified, returns the latest.'''
return self.collection.find_one(
location_to_query(location),
sort=[('revision', pymongo.ASCENDING)],
)
def get_item(self, location, depth=0):
"""
Returns an XModuleDescriptor instance for the item at location.
......@@ -175,10 +185,7 @@ class MongoModuleStore(ModuleStore):
if key != 'revision' and val is None:
raise InsufficientSpecificationError(location)
item = self.collection.find_one(
location_to_query(location),
sort=[('revision', pymongo.ASCENDING)],
)
item = self._find_one(location)
if item is None:
raise ItemNotFoundError(location)
return self._load_items([item], depth)[0]
......@@ -250,24 +257,101 @@ class MongoModuleStore(ModuleStore):
{'$set': {'metadata': metadata}}
)
def path_to_location(self, location, course=None):
def get_parent_locations(self, location):
'''Find all locations that are the parents of this location.
Mostly intended for use in path_to_location, but exposed for testing
and possible other usefulness.
returns an iterable of things that can be passed to Location.
'''
Try to find a course/chapter/section[/position] path to this location.
location = Location(location)
items = self.collection.find({'definition.children': str(location)},
{'_id': True})
return [i['_id'] for i in items]
def path_to_location(self, location, course_name=None):
'''
Try to find a course_id/chapter/section[/position] path to this location.
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.
raise ItemNotFoundError if the location doesn't exist.
If course is not None, restrict search to paths in that course.
raise NoPathToItem if the location exists, but isn't accessible via
a chapter/section path in the course(s) being searched.
In general, a location may be accessible via many paths. This method may
return any valid path.
Return a tuple (course_id, chapter, section, position) suitable for the
courseware index view.
Return a tuple (course, chapter, section, position).
A location may be accessible via many paths. This method may
return any valid path.
If the section a sequence, position should be the position of this location
in that sequence. Otherwise, position should be None.
If the section is a sequence, position will be the position
of this location in that sequence. Otherwise, position will
be None. TODO (vshnayder): Not true yet.
'''
raise NotImplementedError
# Check that location is present at all
if self._find_one(location) is None:
raise ItemNotFoundError(location)
def flatten(xs):
'''Convert lisp-style (a, (b, (c, ()))) lists into a python list.
Not a general flatten function. '''
p = []
while xs != ():
p.append(xs[0])
xs = xs[1]
return p
def find_path_to_course(location, course_name=None):
'''Find a path up the location graph to a node with the
specified category. If no path exists, return None. If a
path exists, return it as a list with target location
first, and the starting location last.
'''
# Standard DFS
# To keep track of where we came from, the work queue has
# tuples (location, path-so-far). To avoid lots of
# copying, the path-so-far is stored as a lisp-style
# list--nested hd::tl tuples, and flattened at the end.
queue = [(location, ())]
while len(queue) > 0:
(loc, path) = queue.pop() # Takes from the end
loc = Location(loc)
print 'Processing loc={0}, path={1}'.format(loc, path)
if loc.category == "course":
if course_name is None or course_name == loc.name:
# Found it!
path = (loc, path)
return flatten(path)
# otherwise, add parent locations at the end
newpath = (loc, path)
parents = self.get_parent_locations(loc)
queue.extend(zip(parents, repeat(newpath)))
# If we're here, there is no path
return None
path = find_path_to_course(location, course_name)
if path is None:
raise(NoPathToItem(location))
n = len(path)
course_id = CourseDescriptor.location_to_id(path[0])
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...
position = None
return (course_id, chapter, section, position)
......@@ -2,9 +2,10 @@ 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
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.xml_importer import import_from_xml
......@@ -33,28 +34,44 @@ class TestMongoModuleStore(object):
cls.connection = pymongo.connection.Connection(HOST, PORT)
cls.connection.drop_database(DB)
# NOTE: Creating a single db for all the tests to save time. This
# is ok only as long as none of the tests modify the db.
# If (when!) that changes, need to either reload the db, or load
# once and copy over to a tmp db for each test.
cls.store = cls.initdb()
@classmethod
def teardownClass(cls):
pass
def setUp(self):
@staticmethod
def initdb():
# connect to the db
self.store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, default_class=DEFAULT_CLASS)
store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, default_class=DEFAULT_CLASS)
# Explicitly list the courses to load (don't want the big one)
courses = ['toy', 'simple']
import_from_xml(self.store, DATA_DIR, courses)
import_from_xml(store, DATA_DIR, courses)
return store
@staticmethod
def destroy_db(connection):
# Destroy the test db.
connection.drop_database(DB)
def setUp(self):
# make a copy for convenience
self.connection = TestMongoModuleStore.connection
def tearDown(self):
# Destroy the test db.
self.connection.drop_database(DB)
self.store = None
pass
def test_init(self):
'''Just make sure the db loads'''
'''Make sure the db loads, and print all the locations in the db.
Call this directly from failing tests to see what's loaded'''
ids = list(self.connection[DB][COLLECTION].find({}, {'_id': True}))
print len(ids)
pprint([Location(i['_id']).url() for i in ids])
def test_get_courses(self):
'''Make sure the course objects loaded properly'''
courses = self.store.get_courses()
......@@ -63,10 +80,41 @@ class TestMongoModuleStore(object):
assert_equals(courses[0].id, 'edX/simple/2012_Fall')
assert_equals(courses[1].id, 'edX/toy/2012_Fall')
def Xtest_path_to_location(self):
def test_loads(self):
assert_not_equals(
self.store.get_item("i4x://edX/toy/course/2012_Fall"),
None)
assert_not_equals(
self.store.get_item("i4x://edX/simple/course/2012_Fall"),
None)
assert_not_equals(
self.store.get_item("i4x://edX/toy/video/Welcome"),
None)
def test_find_one(self):
assert_not_equals(
self.store._find_one(Location("i4x://edX/toy/course/2012_Fall")),
None)
assert_not_equals(
self.store._find_one(Location("i4x://edX/simple/course/2012_Fall")),
None)
assert_not_equals(
self.store._find_one(Location("i4x://edX/toy/video/Welcome")),
None)
def test_path_to_location(self):
'''Make sure that path_to_location works'''
should_work = (
("i4x://edX/toy/video/Welcome", ("toy", "Overview", None, None)),
("i4x://edX/toy/video/Welcome",
("edX/toy/2012_Fall", "Overview", "Welcome", None)),
("i4x://edX/toy/html/toylab",
("edX/toy/2012_Fall", "Overview", "Toy_Videos", None)),
)
for location, expected in should_work:
assert_equals(self.store.path_to_location(location), expected)
......@@ -76,10 +124,13 @@ class TestMongoModuleStore(object):
)
for location in not_found:
assert_raises(ItemNotFoundError, self.store.path_to_location, 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/toy/video/Lost_Video",
"i4x://edX/simple/video/Lost_Video",
)
for location in not_found:
assert_raises(ItemNotFoundError, self.store.path_to_location, location)
for location in no_path:
assert_raises(NoPathToItem, self.store.path_to_location, location, "toy")
......@@ -18,7 +18,7 @@
<problem type="lecture" showanswer="attempted" rerandomize="true" title="A simple coding problem" name="Simple coding problem" filename="ps01-simple"/>
</sequential>
</section>
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/>
</chapter>
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/>
</course>
<course name="Toy Course" org="edX" course="toy" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall">
<chapter name="Overview">
<video name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
<videosequence format="Lecture Sequence" name="System Usage Sequence">
<html id="toylab" filename="toylab"/>
<video name="S0V1: Video Resources" youtube="1.0:1bK-WdDi6Qw"/>
<videosequence format="Lecture Sequence" name="Toy Videos">
<html name="toylab" filename="toylab"/>
<video name="Video Resources" youtube="1.0:1bK-WdDi6Qw"/>
</videosequence>
<video name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
</chapter>
</course>
......@@ -201,7 +201,7 @@ def index(request, course_id=None, chapter=None, section=None,
result = render_to_response('courseware.html', context)
return result
@ensure_csrf_cookie
def jump_to(request, location):
'''
Show the page that contains a specific location.
......@@ -221,20 +221,20 @@ def jump_to(request, location):
# Complain if there's not data for this location
try:
(course, chapter, section, position) = modulestore().path_to_location(location)
(course_id, chapter, section, position) = modulestore().path_to_location(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))
return index(course, chapter, section, position)
return index(request, course_id, chapter, section, position)
@ensure_csrf_cookie
def course_info(request, course_id):
'''
Display the course's info.html, or 404 if there is no such course.
Assumes the course_id is in a valid format.
'''
csrf_token = csrf(request)['csrf_token']
......
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