Commit 168f76ce 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 eb5989aa
......@@ -28,17 +28,30 @@ class CourseDescriptor(SequenceDescriptor):
def has_started(self):
return time.gmtime() > self.start
@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 start_date_text(self):
......
......@@ -9,16 +9,18 @@ from importlib import import_module
from xmodule.errorhandlers import strict_error_handler
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
......@@ -167,6 +169,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.
......@@ -190,10 +200,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]
......@@ -265,24 +272,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]
raise ItemNotFoundError if the location doesn't exist.
If course is not None, restrict search to paths in that course.
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.
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 a tuple (course_id, chapter, section, position) suitable for the
courseware index view.
A location may be accessible via many paths. This method may
return any valid path.
Return a tuple (course, chapter, section, position).
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.
'''
# Check that location is present at all
if self._find_one(location) is None:
raise ItemNotFoundError(location)
If the section a sequence, position should be the position of this location
in that sequence. Otherwise, position should be None.
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.
'''
raise NotImplementedError
# 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,27 +34,43 @@ 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'''
......@@ -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)
......@@ -77,9 +125,12 @@ 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>
</chapter>
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/>
</chapter>
</course>
<course name="Toy Course" org="edX" course="toy" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall" start="2015-07-17T12:00">
<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>
......@@ -208,7 +208,7 @@ def index(request, course_id, 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.
......@@ -228,17 +228,22 @@ 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.
'''
course = check_course(course_id)
return render_to_response('info.html', {'course': course})
......
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