Commit 26ca1f5f by Arjun Singh

Merge master

parents 3a9542eb 881404ba
<section id="filesubmission_${id}" class="filesubmission">
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files}" data-allowed_files="${allowed_files}"/><br />
<div class="grader-status file">
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
<span class="correct" id="status_${id}">Correct</span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
<span class="incorrect" id="status_${id}">Incorrect</span>
% elif state == 'queued':
<span class="processing" id="status_${id}"></span>
<span class="processing" id="status_${id}">Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif
<span style="display:none;" class="debug">(${state})</span>
<br/>
<span class="message">${msg|n}</span>
<br/>
<p class="debug">${state}</p>
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files}" data-allowed_files="${allowed_files}"/>
</div>
<div class="message">${msg|n}</div>
</section>
......@@ -7,26 +7,28 @@
<span id="answer_${id}"></span>
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'queued':
<span class="processing" id="status_${id}"></span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<br/>
<span style="display:none;" class="debug">(${state})</span>
<br/>
<span class="message">${msg|n}</span>
<br/>
<div class="grader-status">
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
% elif state == 'correct':
<span class="correct" id="status_${id}">Correct</span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span>
% elif state == 'queued':
<span class="processing" id="status_${id}">Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif
<br/>
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<p class="debug">${state}</p>
</div>
<div class="external-grader-message">
${msg|n}
</div>
<script>
// Note: We need to make the area follow the CodeMirror for this to work.
......@@ -45,12 +47,4 @@
});
});
</script>
<style type="text/css">
.CodeMirror {
border: 1px solid black;
font-size: 14px;
line-height: 18px;
resize: both;
}
</style>
</section>
......@@ -16,6 +16,7 @@ h2 {
}
}
section.problem {
@media print {
display: block;
......@@ -31,6 +32,7 @@ section.problem {
display: inline;
}
div {
p {
&.answer {
......@@ -171,8 +173,54 @@ section.problem {
top: 6px;
}
}
.grader-status {
padding: 9px;
background: #F6F6F6;
border: 1px solid #ddd;
border-top: 0;
margin-bottom: 20px;
@include clearfix;
span {
text-indent: -9999px;
overflow: hidden;
display: block;
float: left;
margin: -7px 7px 0 0;
}
p {
line-height: 20px;
text-transform: capitalize;
margin-bottom: 0;
float: left;
}
&.file {
background: #FFF;
margin-top: 20px;
padding: 20px 0 0 0;
border: {
top: 1px solid #eee;
right: 0;
bottom: 0;
left: 0;
}
p.debug {
display: none;
}
input {
float: left;
}
}
}
}
ul {
list-style: disc outside none;
margin-bottom: lh();
......@@ -246,6 +294,69 @@ section.problem {
}
code {
margin: 0 2px;
padding: 0px 5px;
white-space: nowrap;
border: 1px solid #EAEAEA;
background-color: #F8F8F8;
@include border-radius(3px);
font-size: .9em;
}
pre {
background-color: #F8F8F8;
border: 1px solid #CCC;
font-size: .9em;
line-height: 1.4;
overflow: auto;
padding: 6px 10px;
@include border-radius(3px);
> code {
margin: 0;
padding: 0;
white-space: pre;
border: none;
background: transparent;
}
}
.CodeMirror {
border: 1px solid black;
font-size: 14px;
line-height: 18px;
resize: both;
pre {
@include border-radius(0);
border-radius: 0;
border-width: 0;
margin: 0;
padding: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
white-space: pre;
word-wrap: normal;
overflow: hidden;
resize: none;
&.CodeMirror-cursor {
z-index: 10;
position: absolute;
visibility: hidden;
border-left: 1px solid black;
border-right: none;
width: 0;
}
}
}
.CodeMirror-focused pre.CodeMirror-cursor {
visibility: visible;
}
hr {
background: #ddd;
border: none;
......@@ -280,4 +391,96 @@ section.problem {
@extend .blue-button;
}
}
div.capa_alert {
padding: 8px 12px;
border: 1px solid #EBE8BF;
border-radius: 3px;
background: #FFFCDD;
font-size: 0.9em;
margin-top: 10px;
}
.hints {
border: 1px solid #ccc;
h3 {
border-bottom: 1px solid #e3e3e3;
text-shadow: 0 1px 0 #fff;
padding: 9px;
background: #eee;
font-weight: bold;
font-size: em(16);
}
div {
border-bottom: 1px solid #ddd;
&:last-child {
border-bottom: none;
}
p {
margin-bottom: 0;
}
header {
a {
display: block;
padding: 9px;
background: #F6F6F6;
@include box-shadow(inset 0 0 0 1px #fff);
}
}
section {
padding: 9px;
}
}
}
.test {
padding-top: 18px;
header {
margin-bottom: 12px;
h3 {
font-size: 0.9em;
font-weight: bold;
font-style: normal;
text-transform: uppercase;
color: #AAA;
}
}
> section {
border: 1px solid #ddd;
padding: 9px 9px 20px;
margin-bottom: 10px;
background: #FFF;
position: relative;
@include box-shadow(inset 0 0 0 1px #eee);
@include border-radius(3px);
p:last-of-type {
margin-bottom: 0;
}
.shortform {
margin-bottom: .6em;
}
a.full {
@include position(absolute, 0 0 1px 0px);
font-size: .8em;
padding: 4px;
text-align: right;
width: 100%;
display: block;
background: #F3F3F3;
@include box-sizing(border-box);
}
}
}
}
......@@ -37,7 +37,6 @@ nav.sequence-nav {
height: 44px;
margin: 0 30px;
@include linear-gradient(top, #ddd, #eee);
overflow: hidden;
@include box-shadow(0 1px 3px rgba(0, 0, 0, .1) inset);
}
......
......@@ -263,8 +263,8 @@ class @Problem
@el.find('.capa_alert').remove()
alert_elem = "<div class='capa_alert'>" + msg + "</div>"
@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) =>
......
......@@ -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.
......@@ -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)
......@@ -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():
......
......@@ -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))
......@@ -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)
......
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 = """<html name="Voltage Source Answer" >A voltage source is non-linear!
<div align="center">
<img src="/static/images/circuits/voltage-source.png"/>
\(V=V_C\)
</div>
But it is <a href="http://mathworld.wolfram.com/AffineFunction.html">affine</a>,
which means linear except for an offset.
</html>
"""
html = """<html>A voltage source is non-linear!
<div align="center">
</div>
But it is <a href="http://mathworld.wolfram.com/AffineFunction.html">affine</a>,
which means linear except for an offset.
</html>
"""
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)
......@@ -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,
......
<sequential>
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" slug="Welcome" format="Video" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Welcome"/>
<video url_name="welcome"/>
<sequential filename="System_Usage_Sequence" slug="System_Usage_Sequence" format="Lecture Sequence" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="System Usage Sequence"/>
<vertical slug="Lab0_Using_the_tools" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Lab0: Using the tools">
<html slug="html_19" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
......
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" format="Video" display_name="Welcome"/>
......@@ -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:
......
......@@ -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)
......
......@@ -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):
"""
......@@ -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,
......
......@@ -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):
......
......@@ -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,
}
}
}
......
......@@ -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
......
......@@ -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"
......
......@@ -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);
}
}
}
......
......@@ -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;
}
}
}
}
......@@ -72,30 +72,24 @@
else:
course_target = reverse('about_course', args=[course.id])
%>
<a href="${course_target}" class="cover" style="background-image: url('${course_image_url(course)}')">
<div class="shade"></div>
<div class="arrow">&#10095;</div>
</a>
<section class="info">
<hgroup>
<a href="${reverse('university_profile', args=[course.org])}" class="university">${get_course_about_section(course, 'university')}</a>
<h3><a href="${course_target}">${course.number} ${course.title}</a></h3>
</hgroup>
<section class="course-status">
<p>Class Starts - <span>${course.start_date_text}</span></div>
<a href="${course_target}">
<section class="cover" style="background-image: url('${course_image_url(course)}')">
<div class="shade"></div>
<div class="arrow">&#10095;</div>
</section>
<section class="meta">
<div class="course-work-icon"></div>
<div class="progress">
<div class="meter">
<div class="meter-fill"></div>
</div>
</div>
<div class="complete">
##<p><span class="completeness">60%</span> complete</p>
</div>
<section class="info">
<hgroup>
<h2 class="university">${get_course_about_section(course, 'university')}</h2>
<h3>${course.number} ${course.title}</h3>
</hgroup>
<section class="course-status">
<p>Class Starts - <span>${course.start_date_text}</span></p>
</section>
<p class="enter-course">View Courseware</p>
</section>
</section>
</a>
</article>
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">Unregister</a>
......
<?xml version="1.0" encoding="UTF-8"?>
<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
<XRD>
<Service priority="0">
<Type>http://specs.openid.net/auth/2.0/signon</Type>
<Type>http://openid.net/signon/1.1</Type>
<URI>${url}</URI>
</Service>
</XRD>
</xrds:XRDS>
......@@ -74,7 +74,7 @@
%if show_link:
<a href="${course_target}">
%endif
<span class="register disabled">You are registered for this course (${course.number}).</span>
<span class="register disabled">You are registered for this course (${course.number})</span> <strong>View Courseware</strong>
%if show_link:
</a>
%endif
......
......@@ -32,3 +32,27 @@
</section>
</section>
<%block name="js_extra">
<script type="text/javascript" charset="utf-8">
$(function(){
// this should be brought back into problems
$('.longform').hide();
$('.shortform').append('<a href="#" class="full">See full output</a>');
$('.full').click(function() {
$(this).parent().siblings().slideToggle();
$(this).parent().parent().toggleClass('open');
var text = $(this).text() == 'See full output' ? 'Hide output' : 'See full output';
$(this).text(text);
return false;
});
$('.collapsible section').hide()
$('.collapsible header a').click(function() {
$(this).parent().siblings().slideToggle();
$(this).parent().parent().toggleClass('open');
return false
});
});
</script>
</%block>
<%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/>
<%block name="headextra">
<style type="text/css">
.openid-login {
display: block;
position: relative;
left: 0;
margin: 100px auto;
top: 0;
z-index: 200;
}
.openid-login input[type=submit] {
white-space: normal;
height: auto !important;
}
#lean_overlay {
display: block;
position: fixed;
left: 0px;
top: 0px;
z-index: 100;
width:100%;
height:100%;
}
</style>
</%block>
<section id="login-modal" class="modal login-modal openid-login">
<div class="inner-wrapper">
<header>
<h2>Log In</h2>
<hr>
</header>
<form id="login_form" class="login_form" method="post" action="/openid/provider/login/">
%if error:
<div id="login_error" class="modal-form-error" style="display: block;">Email or password is incorrect.</div>
%endif
<label>E-mail</label>
<input type="text" name="email" placeholder="E-mail" tabindex="1" />
<label>Password</label>
<input type="password" name="password" placeholder="Password" tabindex="2" />
<div class="submit">
<input name="submit" type="submit" value="Access My Courses and Return To ${return_to}" tabindex="3" />
</div>
</form>
</div>
</section>
<div id="lean_overlay"></div>
<?xml version="1.0" encoding="UTF-8"?>
<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
<XRD>
<Service priority="0">
<Type>http://specs.openid.net/auth/2.0/server</Type>
<Type>http://openid.net/sreg/1.0</Type>
<Type>http://openid.net/srv/ax/1.0</Type>
<URI>${url}</URI>
</Service>
</XRD>
</xrds:XRDS>
......@@ -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'))
......@@ -215,9 +218,17 @@ if settings.DEBUG:
if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
urlpatterns += (
url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'),
url(r'^openid/complete/$', 'external_auth.views.edXauth_openid_login_complete', name='openid-complete'),
url(r'^openid/complete/$', 'external_auth.views.openid_login_complete', name='openid-complete'),
url(r'^openid/logo.gif$', 'django_openid_auth.views.logo', name='openid-logo'),
)
)
if settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'):
urlpatterns += (
url(r'^openid/provider/login/$', 'external_auth.views.provider_login', name='openid-provider-login'),
url(r'^openid/provider/login/(?:[\w%\. ]+)$', 'external_auth.views.provider_identity', name='openid-provider-login-identity'),
url(r'^openid/provider/identity/$', 'external_auth.views.provider_identity', name='openid-provider-identity'),
url(r'^openid/provider/xrds/$', 'external_auth.views.provider_xrds', name='openid-provider-xrds')
)
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
urlpatterns += (
......
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