From eb5989aa992b646e5335f43d0cd4aa0001218255 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 19 Jul 2012 19:22:03 -0400
Subject: [PATCH] Ready to implement path_to_location
* Clean up test data for simple, toy courses
* clean up test_mongo.py
* write initial test for path_to_location
* hook up view to use path_to_location
Next: actually implement it :)
---
.../xmodule/xmodule/modulestore/__init__.py | 23 +++++
.../xmodule/xmodule/modulestore/exceptions.py | 3 +
.../lib/xmodule/xmodule/modulestore/mongo.py | 24 +++++-
.../xmodule/modulestore/tests/test_mongo.py | 84 ++++++++++++-------
common/test/data/simple/course.xml | 24 ++++++
common/test/data/simple/html/toylab.html | 3 +
.../data/simple/problems/L1_Problem_1.xml | 43 ++++++++++
.../test/data/simple/problems/ps01-simple.xml | 62 ++++++++++++++
common/test/data/toy/course.xml | 4 +-
lms/djangoapps/courseware/views.py | 11 ++-
10 files changed, 244 insertions(+), 37 deletions(-)
create mode 100644 common/test/data/simple/course.xml
create mode 100644 common/test/data/simple/html/toylab.html
create mode 100644 common/test/data/simple/problems/L1_Problem_1.xml
create mode 100644 common/test/data/simple/problems/ps01-simple.xml
diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py
index 77dfacf372..279782b61a 100644
--- a/common/lib/xmodule/xmodule/modulestore/__init__.py
+++ b/common/lib/xmodule/xmodule/modulestore/__init__.py
@@ -253,3 +253,26 @@ class ModuleStore(object):
in this modulestore.
'''
raise NotImplementedError
+
+ def path_to_location(self, location, course=None, chapter=None, section=None):
+ '''
+ Try to find a course/chapter/section[/position] path to this location.
+
+ raise ItemNotFoundError if the location doesn't exist.
+
+ If course, chapter, section are not None, restrict search to paths with those
+ components as specified.
+
+ raise NoPathToItem if the location exists, but isn't accessible via
+ a path that matches the course/chapter/section restrictions.
+
+ In general, 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 a sequence, position should be the position of this location
+ in that sequence. Otherwise, position should be None.
+ '''
+ raise NotImplementedError
+
diff --git a/common/lib/xmodule/xmodule/modulestore/exceptions.py b/common/lib/xmodule/xmodule/modulestore/exceptions.py
index a6dc99883f..d860a1d263 100644
--- a/common/lib/xmodule/xmodule/modulestore/exceptions.py
+++ b/common/lib/xmodule/xmodule/modulestore/exceptions.py
@@ -13,3 +13,6 @@ class InsufficientSpecificationError(Exception):
class InvalidLocationError(Exception):
pass
+
+class NoPathToItem(Exception):
+ pass
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index 95c882824b..d2b68d03ef 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -251,7 +251,7 @@ class MongoModuleStore(ModuleStore):
def update_metadata(self, location, metadata):
"""
- Set the children for the item specified by the location to
+ Set the metadata for the item specified by the location to
metadata
location: Something that can be passed to Location
@@ -264,3 +264,25 @@ class MongoModuleStore(ModuleStore):
{'_id': Location(location).dict()},
{'$set': {'metadata': metadata}}
)
+
+ def path_to_location(self, location, course=None):
+ '''
+ Try to find a course/chapter/section[/position] path to this location.
+
+ 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, chapter, section, position).
+
+ If the section a sequence, position should be the position of this location
+ in that sequence. Otherwise, position should be None.
+ '''
+ raise NotImplementedError
+
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
index 55e573677f..493b8a385d 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
@@ -4,7 +4,7 @@ from nose.tools import assert_equals, assert_raises, assert_not_equals, with_set
from path import path
from xmodule.modulestore import Location
-from xmodule.modulestore.exceptions import InvalidLocationError
+from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.xml_importer import import_from_xml
@@ -26,36 +26,60 @@ FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing loa
DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
-connection = None
+class TestMongoModuleStore(object):
-def setup():
- global connection
- connection = pymongo.connection.Connection(HOST, PORT)
+ @classmethod
+ def setupClass(cls):
+ cls.connection = pymongo.connection.Connection(HOST, PORT)
+ cls.connection.drop_database(DB)
+
+ @classmethod
+ def teardownClass(cls):
+ pass
+
+ def setUp(self):
+ # connect to the db
+ self.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)
+ self.connection = TestMongoModuleStore.connection
+ def tearDown(self):
+ # Destroy the test db.
+ self.connection.drop_database(DB)
+ self.store = None
-
-def setup_func():
- # connect to the db
- global store
- store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, default_class=DEFAULT_CLASS)
- print 'data_dir: {0}'.format(DATA_DIR)
- import_from_xml(store, DATA_DIR)
-
-def teardown_func():
- global store
- store = None
- # Destroy the test db.
- connection.drop_database(DB)
-
-
-@with_setup(setup_func, teardown_func)
-def test_init():
- '''Just make sure the db loads'''
- pass
-
-@with_setup(setup_func, teardown_func)
-def test_get_courses():
- '''Make sure the course objects loaded properly'''
- courses = store.get_courses()
- print courses
+ def test_init(self):
+ '''Just make sure the db loads'''
+ ids = list(self.connection[DB][COLLECTION].find({}, {'_id': True}))
+ print len(ids)
+ def test_get_courses(self):
+ '''Make sure the course objects loaded properly'''
+ courses = self.store.get_courses()
+ assert_equals(len(courses), 2)
+ courses.sort(key=lambda c: c.id)
+ assert_equals(courses[0].id, 'edX/simple/2012_Fall')
+ assert_equals(courses[1].id, 'edX/toy/2012_Fall')
+
+ def Xtest_path_to_location(self):
+ '''Make sure that path_to_location works'''
+ should_work = (
+ ("i4x://edX/toy/video/Welcome", ("toy", "Overview", None, None)),
+ )
+ for location, expected in should_work:
+ assert_equals(self.store.path_to_location(location), expected)
+
+ not_found = (
+ "i4x://edX/toy/video/WelcomeX",
+ )
+ for location in not_found:
+ assert_raises(ItemNotFoundError, self.store.path_to_location, location)
+
+ no_path = (
+ "i4x://edX/toy/video/Lost_Video",
+ )
+ for location in not_found:
+ assert_raises(ItemNotFoundError, self.store.path_to_location, location)
+
diff --git a/common/test/data/simple/course.xml b/common/test/data/simple/course.xml
new file mode 100644
index 0000000000..59a0a0c5bc
--- /dev/null
+++ b/common/test/data/simple/course.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/common/test/data/simple/html/toylab.html b/common/test/data/simple/html/toylab.html
new file mode 100644
index 0000000000..81df84bd63
--- /dev/null
+++ b/common/test/data/simple/html/toylab.html
@@ -0,0 +1,3 @@
+Lab 2A: Superposition Experiment
+
+Isn't the toy course great?
diff --git a/common/test/data/simple/problems/L1_Problem_1.xml b/common/test/data/simple/problems/L1_Problem_1.xml
new file mode 100644
index 0000000000..2ba0617904
--- /dev/null
+++ b/common/test/data/simple/problems/L1_Problem_1.xml
@@ -0,0 +1,43 @@
+
+
+
+
Finger Exercise 1
+
+
+Here are two definitions:
+
+ -
+
+Declarative knowledge refers to statements of fact.
+
+ -
+
+Imperative knowledge refers to 'how to' methods.
+
+
+
+Which of the following choices is correct?
+
+ -
+
+Statement 1 is true, Statement 2 is false
+
+ -
+
+Statement 1 is false, Statement 2 is true
+
+ -
+
+Statement 1 and Statement 2 are both false
+
+ -
+
+Statement 1 and Statement 2 are both true
+
+
+
+
+
+
+
+
diff --git a/common/test/data/simple/problems/ps01-simple.xml b/common/test/data/simple/problems/ps01-simple.xml
new file mode 100644
index 0000000000..e70d8f2c8d
--- /dev/null
+++ b/common/test/data/simple/problems/ps01-simple.xml
@@ -0,0 +1,62 @@
+
+Paying Off Credit Card Debt
+ Each month, a credit
+ card statement will come with the option for you to pay a
+ minimum amount of your charge, usually 2% of the balance due.
+ However, the credit card company earns money by charging
+ interest on the balance that you don't pay. So even if you
+ pay credit card payments on time, interest is still accruing
+ on the outstanding balance.
+Say you've made a
+ $5,000 purchase on a credit card with 18% annual interest
+ rate and 2% minimum monthly payment rate. After a year, how
+ much is the remaining balance? Use the following
+ equations.
+
+Minimum monthly payment
+= (Minimum monthly payment rate) x (Balance)
+ (Minimum monthly payment gets split into interest paid and
+ principal paid)
+Interest Paid = (Annual interest rate) / (12
+ months) x (Balance)
+Principal paid = (Minimum monthly payment) -
+ (Interest paid)
+Remaining balance = Balance - (Principal
+ paid)
+
+For month 1, compute the minimum monthly payment by taking 2% of the balance.
+
+Minimum monthly payment
+= .02 x $5000 = $100
+We can't simply deduct this from the balance because
+ there is compounding interest. Of this $100 monthly
+ payment, compute how much will go to paying off interest
+ and how much will go to paying off the principal. Remember
+ that it's the annual interest rate that is given, so we
+ need to divide it by 12 to get the monthly interest
+ rate.
+Interest paid = .18/12 x $5000 =
+ $75
+Principal paid = $100 - $75 = $25
+The remaining balance at the end of the first month will
+ be the principal paid this month subtracted from the
+ balance at the start of the month.
+Remaining balance = $5000 - $25 =
+ $4975
+
+For month 2, we
+ repeat the same steps.
+
+Minimum monthly payment
+= .02 x $4975 = $99.50
+Interest Paid = .18/12 x $4975 =
+ $74.63
+Principal Paid = $99.50 - $74.63 =
+ $24.87
+Remaining Balance = $4975 - $24.87 =
+ $4950.13
+
+After 12 months, the
+ total amount paid is $1167.55, leaving an outstanding balance
+ of $4708.10. Pretty depressing!
+
diff --git a/common/test/data/toy/course.xml b/common/test/data/toy/course.xml
index 391eb68e2b..1c080bfe18 100644
--- a/common/test/data/toy/course.xml
+++ b/common/test/data/toy/course.xml
@@ -1,9 +1,9 @@
-
+
-
+
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index ca9aa417d1..7cfe2f5365 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -21,7 +21,7 @@ from models import StudentModuleCache
from student.models import UserProfile
from multicourse import multicourse_settings
from xmodule.modulestore import Location
-from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError
+from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
@@ -228,11 +228,14 @@ def jump_to(request, location):
# Complain if there's not data for this location
try:
- item = modulestore().get_item(location)
+ (course, chapter, section, position) = modulestore().path_to_location(location)
except ItemNotFoundError:
raise Http404("No data at this location: {0}".format(location))
-
- return HttpResponse("O hai")
+ except NoPathToItem:
+ raise Http404("This location is not in any class: {0}".format(location))
+
+
+ return index(course, chapter, section, position)
@ensure_csrf_cookie
def course_info(request, course_id):