"
@el.find('.action').after(alert_elem)
- @el.find('.capa_alert').animate(opacity: 0, 500).animate(opacity: 1, 500)
-
+ @el.find('.capa_alert').css(opacity: 0).animate(opacity: 1, 700)
+
save: =>
Logger.log 'problem_save', @answers
$.postWithPrefix "#{@url}/problem_save", @answers, (response) =>
diff --git a/common/lib/xmodule/xmodule/modulestore/search.py b/common/lib/xmodule/xmodule/modulestore/search.py
index baf3d46b57..f9901e8bfe 100644
--- a/common/lib/xmodule/xmodule/modulestore/search.py
+++ b/common/lib/xmodule/xmodule/modulestore/search.py
@@ -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)
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/__init__.py b/common/lib/xmodule/xmodule/modulestore/tests/__init__.py
new file mode 100644
index 0000000000..126f0136e2
--- /dev/null
+++ b/common/lib/xmodule/xmodule/modulestore/tests/__init__.py
@@ -0,0 +1,12 @@
+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'
+
+
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py
new file mode 100644
index 0000000000..c1d1d50a53
--- /dev/null
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py
@@ -0,0 +1,34 @@
+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)
+
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
index 746240e763..4c593e391e 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
@@ -1,23 +1,14 @@
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)
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_xml.py b/common/lib/xmodule/xmodule/modulestore/tests/test_xml.py
new file mode 100644
index 0000000000..c4446bebb5
--- /dev/null
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_xml.py
@@ -0,0 +1,16 @@
+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)
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 92eca8f5e6..23a5473292 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -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.
@@ -79,11 +79,12 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
del attr[key]
break
- def fallback_name():
+ def fallback_name(orig_name=None):
"""Return the fallback name for this module. This is a function instead of a variable
because we want it to be lazy."""
- # use the hash of the content--the first 12 bytes should be plenty.
- return tag + "_" + hashlib.sha1(xml).hexdigest()[:12]
+ # append the hash of the content--the first 12 bytes should be plenty.
+ orig_name = "_" + orig_name if orig_name is not None else ""
+ return tag + orig_name + "_" + hashlib.sha1(xml).hexdigest()[:12]
# Fallback if there was nothing we could use:
if url_name is None or url_name == "":
@@ -93,8 +94,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
need_uniq_names = ('problem', 'sequence', 'video', 'course', 'chapter')
if tag in need_uniq_names:
- error_tracker("ERROR: no name of any kind specified for {tag}. Student "
- "state won't work right. Problem xml: '{xml}...'".format(tag=tag, xml=xml[:100]))
+ error_tracker("PROBLEM: no name of any kind specified for {tag}. Student "
+ "state will not be properly tracked for this module. Problem xml:"
+ " '{xml}...'".format(tag=tag, xml=xml[:100]))
else:
# TODO (vshnayder): We may want to enable this once course repos are cleaned up.
# (or we may want to give up on the requirement for non-state-relevant issues...)
@@ -103,13 +105,20 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# Make sure everything is unique
if url_name in self.used_names[tag]:
- msg = ("Non-unique url_name in xml. This may break content. url_name={0}. Content={1}"
- .format(url_name, xml[:100]))
- error_tracker("ERROR: " + msg)
+ msg = ("Non-unique url_name in xml. This may break state tracking for content."
+ " url_name={0}. Content={1}".format(url_name, xml[:100]))
+ error_tracker("PROBLEM: " + msg)
log.warning(msg)
# Just set name to fallback_name--if there are multiple things with the same fallback name,
# they are actually identical, so it's fragile, but not immediately broken.
- url_name = fallback_name()
+
+ # TODO (vshnayder): if the tag is a pointer tag, this will
+ # break the content because we won't have the right link.
+ # That's also a legitimate attempt to reuse the same content
+ # from multiple places. Once we actually allow that, we'll
+ # need to update this to complain about non-unique names for
+ # definitions, but allow multiple uses.
+ url_name = fallback_name(url_name)
self.used_names[tag].add(url_name)
xml_data.set('url_name', url_name)
@@ -134,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: ''
@@ -151,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
@@ -165,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
@@ -186,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
@@ -221,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
@@ -339,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))
@@ -450,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)
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
index 89f94d8cdb..be0bdc24c2 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
@@ -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():
diff --git a/common/lib/xmodule/xmodule/stringify.py b/common/lib/xmodule/xmodule/stringify.py
index dad964140f..1e3fa91210 100644
--- a/common/lib/xmodule/xmodule/stringify.py
+++ b/common/lib/xmodule/xmodule/stringify.py
@@ -12,9 +12,17 @@ def stringify_children(node):
fixed from
http://stackoverflow.com/questions/4624062/get-all-text-inside-a-tag-in-lxml
'''
- parts = ([node.text] +
- list(chain(*([etree.tostring(c), c.tail]
- for c in node.getchildren())
- )))
+ # Useful things to know:
+
+ # node.tostring() -- generates xml for the node, including start
+ # and end tags. We'll use this for the children.
+ # node.text -- the text after the end of a start tag to the start
+ # of the first child
+ # node.tail -- the text after the end this tag to the start of the
+ # next element.
+ parts = [node.text]
+ for c in node.getchildren():
+ parts.append(etree.tostring(c, with_tail=True))
+
# filter removes possible Nones in texts and tails
return ''.join(filter(None, parts))
diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py
index 2520d95937..826e6c9d5a 100644
--- a/common/lib/xmodule/xmodule/tests/test_export.py
+++ b/common/lib/xmodule/xmodule/tests/test_export.py
@@ -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)
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index a369850209..e81d82bf9e 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -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)
diff --git a/common/lib/xmodule/xmodule/tests/test_stringify.py b/common/lib/xmodule/xmodule/tests/test_stringify.py
index 1c6ee855f3..29e99bef56 100644
--- a/common/lib/xmodule/xmodule/tests/test_stringify.py
+++ b/common/lib/xmodule/xmodule/tests/test_stringify.py
@@ -1,4 +1,4 @@
-from nose.tools import assert_equals
+from nose.tools import assert_equals, assert_true, assert_false
from lxml import etree
from xmodule.stringify import stringify_children
@@ -8,3 +8,32 @@ def test_stringify():
xml = etree.fromstring(html)
out = stringify_children(xml)
assert_equals(out, text)
+
+def test_stringify_again():
+ html = """A voltage source is non-linear!
+
+
+ \(V=V_C\)
+
+ But it is affine,
+ which means linear except for an offset.
+
+"""
+
+ html = """A voltage source is non-linear!
+
+
+
+ But it is affine,
+ which means linear except for an offset.
+
+ """
+ xml = etree.fromstring(html)
+ out = stringify_children(xml)
+
+ print "output:"
+ print out
+
+ # Tracking strange content repeating bug
+ # Should appear once
+ assert_equals(out.count("But it is "), 1)
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 0dc16bd976..82f623e977 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -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,
diff --git a/common/test/data/full/chapter/Overview.xml b/common/test/data/full/chapter/Overview.xml
index 89917d20da..a11a11a1e0 100644
--- a/common/test/data/full/chapter/Overview.xml
+++ b/common/test/data/full/chapter/Overview.xml
@@ -1,5 +1,5 @@
-
+
See the Lab Introduction or Interactive Lab Usage Handout for information on how to do the lab
diff --git a/common/test/data/full/video/welcome.xml b/common/test/data/full/video/welcome.xml
new file mode 100644
index 0000000000..762bbeeaf1
--- /dev/null
+++ b/common/test/data/full/video/welcome.xml
@@ -0,0 +1 @@
+
diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py
index e32eb92138..9fe912e947 100644
--- a/lms/djangoapps/branding/views.py
+++ b/lms/djangoapps/branding/views.py
@@ -20,8 +20,8 @@ def index(request):
return redirect(reverse('dashboard'))
if settings.MITX_FEATURES.get('AUTH_USE_MIT_CERTIFICATES'):
- from external_auth.views import edXauth_ssl_login
- return edXauth_ssl_login(request)
+ from external_auth.views import ssl_login
+ return ssl_login(request)
university = branding.get_university(request.META.get('HTTP_HOST'))
if university is None:
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py
index dbe4ff376d..91c769f90a 100644
--- a/lms/djangoapps/courseware/access.py
+++ b/lms/djangoapps/courseware/access.py
@@ -321,7 +321,7 @@ def _has_staff_access_to_location(user, location):
return True
# If not global staff, is the user in the Auth group for this class?
- user_groups = [x[1] for x in user.groups.values_list()]
+ user_groups = [g.name for g in user.groups.all()]
staff_group = _course_staff_group_name(location)
if staff_group in user_groups:
debug("Allow: user in group %s", staff_group)
diff --git a/lms/djangoapps/courseware/management/commands/clean_xml.py b/lms/djangoapps/courseware/management/commands/clean_xml.py
index 9fd52178a2..425dd156c1 100644
--- a/lms/djangoapps/courseware/management/commands/clean_xml.py
+++ b/lms/djangoapps/courseware/management/commands/clean_xml.py
@@ -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):
diff --git a/lms/djangoapps/courseware/management/commands/metadata_to_json.py b/lms/djangoapps/courseware/management/commands/metadata_to_json.py
index dcbcdc0df3..8ef0dee7b3 100644
--- a/lms/djangoapps/courseware/management/commands/metadata_to_json.py
+++ b/lms/djangoapps/courseware/management/commands/metadata_to_json.py
@@ -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):
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 82ec3cadeb..ee29491d27 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -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)
diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py
index 88e0f53f70..56a2384bc5 100644
--- a/lms/djangoapps/courseware/tests/tests.py
+++ b/lms/djangoapps/courseware/tests/tests.py
@@ -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:
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 92f6716320..6122ebd333 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -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):
"""
@@ -328,6 +329,10 @@ def progress(request, course_id, student_id=None):
# NOTE: To make sure impersonation by instructor works, use
# student instead of request.user in the rest of the function.
+ # The pre-fetching of groups is done to make auth checks not require an
+ # additional DB lookup (this kills the Progress page in particular).
+ student = User.objects.prefetch_related("groups").get(id=student.id)
+
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course_id, student, course)
course_module = get_module(student, request, course.location,
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index fd9e53b3b8..79911a38bc 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -149,8 +149,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):
diff --git a/lms/envs/common.py b/lms/envs/common.py
index ce08bf9666..5cd28d24d9 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -77,7 +77,7 @@ MITX_FEATURES = {
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False,
'AUTH_USE_MIT_CERTIFICATES' : False,
-
+ 'AUTH_USE_OPENID_PROVIDER': False,
}
# Used for A/B testing
@@ -120,6 +120,10 @@ node_paths = [COMMON_ROOT / "static/js/vendor",
]
NODE_PATH = ':'.join(node_paths)
+
+############################ OpenID Provider ##################################
+OPENID_PROVIDER_TRUSTED_ROOTS = ['cs50.net', '*.cs50.net']
+
################################## MITXWEB #####################################
# This is where we stick our compiled template files. Most of the app uses Mako
# templates
@@ -219,7 +223,6 @@ MODULESTORE = {
'OPTIONS': {
'data_dir': DATA_DIR,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
- 'eager': True,
}
}
}
diff --git a/lms/envs/dev.py b/lms/envs/dev.py
index 5da84f59f0..974b8c9fd6 100644
--- a/lms/envs/dev.py
+++ b/lms/envs/dev.py
@@ -105,6 +105,7 @@ LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1']
################################ OpenID Auth #################################
MITX_FEATURES['AUTH_USE_OPENID'] = True
+MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
MITX_FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True
INSTALLED_APPS += ('external_auth',)
@@ -115,6 +116,8 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True
OPENID_SSO_SERVER_URL = 'https://www.google.com/accounts/o8/id' # TODO: accept more endpoints
OPENID_USE_AS_ADMIN_LOGIN = False
+OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
+
################################ MIT Certificates SSL Auth #################################
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
diff --git a/lms/envs/test.py b/lms/envs/test.py
index c164889d79..7cab4cb52c 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -123,6 +123,11 @@ CACHES = {
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
+################################## OPENID ######################################
+MITX_FEATURES['AUTH_USE_OPENID'] = True
+MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
+OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
+
############################ FILE UPLOADS (ASKBOT) #############################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = TEST_ROOT / "uploads"
diff --git a/lms/static/sass/multicourse/_course_about.scss b/lms/static/sass/multicourse/_course_about.scss
index 92d19dff86..3f2813ba0e 100644
--- a/lms/static/sass/multicourse/_course_about.scss
+++ b/lms/static/sass/multicourse/_course_about.scss
@@ -115,6 +115,30 @@
}
}
+ a {
+ &:hover, &:visited {
+ text-decoration: none;
+ }
+ }
+
+ strong {
+ @include button(shiny, $blue);
+ @include box-sizing(border-box);
+ @include border-radius(3px);
+ display: block;
+ float: left;
+ font: normal 1.2rem/1.6rem $sans-serif;
+ letter-spacing: 1px;
+ padding: 10px 0px;
+ text-transform: uppercase;
+ text-align: center;
+ width: flex-grid(3, 8);
+
+ &:hover {
+ color: rgb(255,255,255);
+ }
+ }
+
span.register {
background: lighten($blue, 20%);
border: 1px solid $blue;
@@ -125,7 +149,10 @@
padding: 10px 0px 8px;
text-transform: uppercase;
text-align: center;
- width: flex-grid(12);
+ float: left;
+ margin: 1px flex-gutter(8) 0 0;
+ @include transition();
+ width: flex-grid(5, 8);
}
}
}
diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss
index f37c772aef..53418bc0dd 100644
--- a/lms/static/sass/multicourse/_dashboard.scss
+++ b/lms/static/sass/multicourse/_dashboard.scss
@@ -261,7 +261,7 @@
padding: 12px 0px;
width: 100%;
- a.university {
+ .university {
background: rgba(255,255,255, 1);
border: 1px solid rgb(180,180,180);
@include border-radius(3px);
@@ -269,17 +269,14 @@
color: $lighter-base-font-color;
display: block;
font-style: italic;
+ font-family: $sans-serif;
+ font-size: 16px;
font-weight: 800;
@include inline-block;
margin-right: 10px;
+ margin-bottom: 0;
padding: 5px 10px;
-
float: left;
-
- &:hover {
- color: $blue;
- text-decoration: none;
- }
}
h3 {
@@ -306,8 +303,12 @@
background: $yellow;
border: 1px solid rgb(200,200,200);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
- margin-top: 16px;
+ margin-top: 17px;
+ margin-right: flex-gutter();
padding: 5px;
+ width: flex-grid(8);
+ float: left;
+ @include box-sizing(border-box);
p {
color: $lighter-base-font-color;
@@ -317,93 +318,46 @@
}
}
- .meta {
- @include clearfix;
- margin-top: 22px;
- position: relative;
- @include transition(opacity, 0.15s, linear);
- width: 100%;
-
-
- .course-work-icon {
- @include background-image(url('../images/portal-icons/pencil-icon.png'));
- background-size: cover;
- float: left;
- height: 22px;
- opacity: 0.7;
- width: 22px;
- }
-
- .complete {
- float: right;
-
- p {
- color: $lighter-base-font-color;
- font-style: italic;
- @include inline-block;
- text-align: right;
- text-shadow: 0 1px rgba(255,255,255, 0.6);
-
- .completeness {
- color: $base-font-color;
- font-weight: 700;
- margin-right: 5px;
- }
- }
- }
-
- .progress {
- @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
- left: 35px;
- position: absolute;
- right: 130px;
-
- .meter {
- background: rgb(245,245,245);
- border: 1px solid rgb(160,160,160);
- @include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15));
- @include box-sizing(border-box);
- @include border-radius(4px);
- height: 22px;
- margin: 0 auto;
- padding: 2px;
- width: 100%;
-
- .meter-fill {
- background: $blue;
- @include background-image(linear-gradient(-45deg, rgba(255,255,255, 0.15) 25%,
- transparent 25%,
- transparent 50%,
- rgba(255,255,255, 0.15) 50%,
- rgba(255,255,255, 0.15) 75%,
- transparent 75%));
- background-size: 40px 40px;
- background-repeat: repeat-x;
- border: 1px solid rgb(115,115,115);
- @include border-radius(4px);
- @include box-sizing(border-box);
- content: "";
- display: block;
- height: 100%;
- width: 60%;
- }
- }
- }
+ .enter-course {
+ @include button(shiny, $blue);
+ @include box-sizing(border-box);
+ @include border-radius(3px);
+ display: block;
+ float: left;
+ font: normal 1rem/1.6rem $sans-serif;
+ letter-spacing: 1px;
+ padding: 6px 0px;
+ text-transform: uppercase;
+ text-align: center;
+ margin-top: 16px;
+ width: flex-grid(4);
}
}
- &:hover {
+ > a:hover {
.cover {
.shade {
background: rgba(255,255,255, 0.1);
@include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%,
- rgba(0,0,0, 0.3) 100%));
+ rgba(0,0,0, 0.3) 100%));
}
.arrow {
opacity: 1;
}
}
+
+ .info {
+ background: darken(rgb(250,250,250), 5%);
+ @include background-image(linear-gradient(-90deg, darken(rgb(253,253,253), 3%), darken(rgb(240,240,240), 5%)));
+ border-color: darken(rgb(190,190,190), 10%);
+
+ .course-status {
+ background: darken($yellow, 3%);
+ border-color: darken(rgb(200,200,200), 3%);
+ @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
+ }
+ }
}
}
@@ -420,5 +374,6 @@
color: #333;
}
}
+
}
}
diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html
index 770e849841..0f9c26611b 100644
--- a/lms/templates/dashboard.html
+++ b/lms/templates/dashboard.html
@@ -72,30 +72,24 @@
else:
course_target = reverse('about_course', args=[course.id])
%>
-
-
-