Commit 26ca1f5f by Arjun Singh

Merge master

parents 3a9542eb 881404ba
<section id="filesubmission_${id}" class="filesubmission"> <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': % 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': % elif state == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}">Correct</span>
% elif state == 'incorrect': % elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}">Incorrect</span>
% elif state == 'queued': % 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> <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
<span style="display:none;" class="debug">(${state})</span> <p class="debug">${state}</p>
<br/>
<span class="message">${msg|n}</span> <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>
<div class="message">${msg|n}</div>
</section> </section>
...@@ -7,26 +7,28 @@ ...@@ -7,26 +7,28 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if state == 'unsubmitted': <div class="grader-status">
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> % if state == 'unsubmitted':
% elif state == 'correct': <span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
<span class="correct" id="status_${id}"></span> % elif state == 'correct':
% elif state == 'incorrect': <span class="correct" id="status_${id}">Correct</span>
<span class="incorrect" id="status_${id}"></span> % elif state == 'incorrect':
% elif state == 'queued': <span class="incorrect" id="status_${id}">Incorrect</span>
<span class="processing" id="status_${id}"></span> % elif state == 'queued':
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span> <span class="processing" id="status_${id}">Queued</span>
% endif <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% if hidden: % endif
<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/>
<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> <script>
// Note: We need to make the area follow the CodeMirror for this to work. // Note: We need to make the area follow the CodeMirror for this to work.
...@@ -45,12 +47,4 @@ ...@@ -45,12 +47,4 @@
}); });
}); });
</script> </script>
<style type="text/css">
.CodeMirror {
border: 1px solid black;
font-size: 14px;
line-height: 18px;
resize: both;
}
</style>
</section> </section>
...@@ -16,6 +16,7 @@ h2 { ...@@ -16,6 +16,7 @@ h2 {
} }
} }
section.problem { section.problem {
@media print { @media print {
display: block; display: block;
...@@ -31,6 +32,7 @@ section.problem { ...@@ -31,6 +32,7 @@ section.problem {
display: inline; display: inline;
} }
div { div {
p { p {
&.answer { &.answer {
...@@ -171,8 +173,54 @@ section.problem { ...@@ -171,8 +173,54 @@ section.problem {
top: 6px; 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 { ul {
list-style: disc outside none; list-style: disc outside none;
margin-bottom: lh(); margin-bottom: lh();
...@@ -246,6 +294,69 @@ section.problem { ...@@ -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 { hr {
background: #ddd; background: #ddd;
border: none; border: none;
...@@ -280,4 +391,96 @@ section.problem { ...@@ -280,4 +391,96 @@ section.problem {
@extend .blue-button; @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 { ...@@ -37,7 +37,6 @@ nav.sequence-nav {
height: 44px; height: 44px;
margin: 0 30px; margin: 0 30px;
@include linear-gradient(top, #ddd, #eee); @include linear-gradient(top, #ddd, #eee);
overflow: hidden;
@include box-shadow(0 1px 3px rgba(0, 0, 0, .1) inset); @include box-shadow(0 1px 3px rgba(0, 0, 0, .1) inset);
} }
......
...@@ -263,8 +263,8 @@ class @Problem ...@@ -263,8 +263,8 @@ class @Problem
@el.find('.capa_alert').remove() @el.find('.capa_alert').remove()
alert_elem = "<div class='capa_alert'>" + msg + "</div>" alert_elem = "<div class='capa_alert'>" + msg + "</div>"
@el.find('.action').after(alert_elem) @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: => save: =>
Logger.log 'problem_save', @answers Logger.log 'problem_save', @answers
$.postWithPrefix "#{@url}/problem_save", @answers, (response) => $.postWithPrefix "#{@url}/problem_save", @answers, (response) =>
......
...@@ -6,15 +6,14 @@ from .exceptions import (ItemNotFoundError, NoPathToItem) ...@@ -6,15 +6,14 @@ from .exceptions import (ItemNotFoundError, NoPathToItem)
from . import ModuleStore, Location 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 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 modulestore. The courseware insists that the first level in the course is
chapter, but any kind of module can be a "section". chapter, but any kind of module can be a "section".
location: something that can be passed to Location location: something that can be passed to Location
course_name: [optional]. If not None, restrict search to paths course_id: Search for paths in this course.
in that course.
raise ItemNotFoundError if the location doesn't exist. raise ItemNotFoundError if the location doesn't exist.
...@@ -27,7 +26,7 @@ def path_to_location(modulestore, location, course_name=None): ...@@ -27,7 +26,7 @@ def path_to_location(modulestore, location, course_name=None):
A location may be accessible via many paths. This method may A location may be accessible via many paths. This method may
return any valid path. 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 of this location in that sequence. Otherwise, position will
be None. TODO (vshnayder): Not true yet. be None. TODO (vshnayder): Not true yet.
''' '''
...@@ -41,7 +40,7 @@ def path_to_location(modulestore, location, course_name=None): ...@@ -41,7 +40,7 @@ def path_to_location(modulestore, location, course_name=None):
xs = xs[1] xs = xs[1]
return p 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 '''Find a path up the location graph to a node with the
specified category. specified category.
...@@ -69,7 +68,8 @@ def path_to_location(modulestore, location, course_name=None): ...@@ -69,7 +68,8 @@ def path_to_location(modulestore, location, course_name=None):
# print 'Processing loc={0}, path={1}'.format(loc, path) # print 'Processing loc={0}, path={1}'.format(loc, path)
if loc.category == "course": 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! # Found it!
path = (loc, path) path = (loc, path)
return flatten(path) return flatten(path)
...@@ -81,17 +81,34 @@ def path_to_location(modulestore, location, course_name=None): ...@@ -81,17 +81,34 @@ def path_to_location(modulestore, location, course_name=None):
# If we're here, there is no path # If we're here, there is no path
return None return None
path = find_path_to_course(location, course_name) path = find_path_to_course()
if path is None: if path is None:
raise(NoPathToItem(location)) raise NoPathToItem(location)
n = len(path) n = len(path)
course_id = CourseDescriptor.location_to_id(path[0]) course_id = CourseDescriptor.location_to_id(path[0])
# pull out the location names # pull out the location names
chapter = path[1].name if n > 1 else None chapter = path[1].name if n > 1 else None
section = path[2].name if n > 2 else None section = path[2].name if n > 2 else None
# Figure out the position
# TODO (vshnayder): not handling position at all yet...
position = None 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) 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 import pymongo
from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup
from path import path
from pprint import pprint from pprint import pprint
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.xml_importer import import_from_xml 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/ from .test_modulestore import check_path_to_location
# to ~/mitx_all/mitx/common/test from . import DATA_DIR
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'
HOST = 'localhost' HOST = 'localhost'
...@@ -110,27 +101,5 @@ class TestMongoModuleStore(object): ...@@ -110,27 +101,5 @@ class TestMongoModuleStore(object):
def test_path_to_location(self): def test_path_to_location(self):
'''Make sure that path_to_location works''' '''Make sure that path_to_location works'''
should_work = ( check_path_to_location(self.store)
("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")
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): ...@@ -37,7 +37,7 @@ def clean_out_mako_templating(xml_string):
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
def __init__(self, xmlstore, course_id, course_dir, 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 A class that handles loading from xml. Does some munging to ensure that
all elements have unique slugs. all elements have unique slugs.
...@@ -79,11 +79,12 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -79,11 +79,12 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
del attr[key] del attr[key]
break 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 """Return the fallback name for this module. This is a function instead of a variable
because we want it to be lazy.""" because we want it to be lazy."""
# use the hash of the content--the first 12 bytes should be plenty. # append the hash of the content--the first 12 bytes should be plenty.
return tag + "_" + hashlib.sha1(xml).hexdigest()[:12] 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: # Fallback if there was nothing we could use:
if url_name is None or url_name == "": if url_name is None or url_name == "":
...@@ -93,8 +94,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -93,8 +94,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
need_uniq_names = ('problem', 'sequence', 'video', 'course', 'chapter') need_uniq_names = ('problem', 'sequence', 'video', 'course', 'chapter')
if tag in need_uniq_names: if tag in need_uniq_names:
error_tracker("ERROR: no name of any kind specified for {tag}. Student " error_tracker("PROBLEM: no name of any kind specified for {tag}. Student "
"state won't work right. Problem xml: '{xml}...'".format(tag=tag, xml=xml[:100])) "state will not be properly tracked for this module. Problem xml:"
" '{xml}...'".format(tag=tag, xml=xml[:100]))
else: else:
# TODO (vshnayder): We may want to enable this once course repos are cleaned up. # 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...) # (or we may want to give up on the requirement for non-state-relevant issues...)
...@@ -103,13 +105,20 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -103,13 +105,20 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# Make sure everything is unique # Make sure everything is unique
if url_name in self.used_names[tag]: if url_name in self.used_names[tag]:
msg = ("Non-unique url_name in xml. This may break content. url_name={0}. Content={1}" msg = ("Non-unique url_name in xml. This may break state tracking for content."
.format(url_name, xml[:100])) " url_name={0}. Content={1}".format(url_name, xml[:100]))
error_tracker("ERROR: " + msg) error_tracker("PROBLEM: " + msg)
log.warning(msg) log.warning(msg)
# Just set name to fallback_name--if there are multiple things with the same fallback name, # 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. # 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) self.used_names[tag].add(url_name)
xml_data.set('url_name', url_name) xml_data.set('url_name', url_name)
...@@ -134,8 +143,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -134,8 +143,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
xmlstore.modules[course_id][descriptor.location] = descriptor xmlstore.modules[course_id][descriptor.location] = descriptor
if xmlstore.eager: for child in descriptor.get_children():
descriptor.get_children() parent_tracker.add_parent(child.location, descriptor.location)
return descriptor return descriptor
render_template = lambda: '' render_template = lambda: ''
...@@ -151,12 +160,51 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -151,12 +160,51 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
error_tracker, process_xml, policy, **kwargs) 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): class XMLModuleStore(ModuleStoreBase):
""" """
An XML backed ModuleStore An XML backed ModuleStore
""" """
def __init__(self, data_dir, default_class=None, eager=False, def __init__(self, data_dir, default_class=None, course_dirs=None):
course_dirs=None):
""" """
Initialize an XMLModuleStore from data_dir Initialize an XMLModuleStore from data_dir
...@@ -165,15 +213,11 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -165,15 +213,11 @@ class XMLModuleStore(ModuleStoreBase):
default_class: dot-separated string defining the default descriptor default_class: dot-separated string defining the default descriptor
class to use if none is specified in entry_points 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, course_dirs: If specified, the list of course_dirs to load. Otherwise,
load all course dirs load all course dirs
""" """
ModuleStoreBase.__init__(self) ModuleStoreBase.__init__(self)
self.eager = eager
self.data_dir = path(data_dir) self.data_dir = path(data_dir)
self.modules = defaultdict(dict) # course_id -> dict(location -> XModuleDescriptor) self.modules = defaultdict(dict) # course_id -> dict(location -> XModuleDescriptor)
self.courses = {} # course_dir -> XModuleDescriptor for the course self.courses = {} # course_dir -> XModuleDescriptor for the course
...@@ -186,10 +230,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -186,10 +230,7 @@ class XMLModuleStore(ModuleStoreBase):
class_ = getattr(import_module(module_path), class_name) class_ = getattr(import_module(module_path), class_name)
self.default_class = class_ self.default_class = class_
# TODO (cpennington): We need a better way of selecting specific sets of self.parent_tracker = ParentTracker()
# 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)
# If we are specifically asked for missing courses, that should # If we are specifically asked for missing courses, that should
# be an error. If we are asked for "all" courses, find the ones # be an error. If we are asked for "all" courses, find the ones
...@@ -221,6 +262,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -221,6 +262,7 @@ class XMLModuleStore(ModuleStoreBase):
if course_descriptor is not None: if course_descriptor is not None:
self.courses[course_dir] = course_descriptor self.courses[course_dir] = course_descriptor
self._location_errors[course_descriptor.location] = errorlog self._location_errors[course_descriptor.location] = errorlog
self.parent_tracker.make_known(course_descriptor.location)
else: else:
# Didn't load course. Instead, save the errors elsewhere. # Didn't load course. Instead, save the errors elsewhere.
self.errored_courses[course_dir] = errorlog self.errored_courses[course_dir] = errorlog
...@@ -339,7 +381,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -339,7 +381,7 @@ class XMLModuleStore(ModuleStoreBase):
course_id = CourseDescriptor.make_id(org, course, url_name) 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)) course_descriptor = system.process_xml(etree.tostring(course_data))
...@@ -450,3 +492,19 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -450,3 +492,19 @@ class XMLModuleStore(ModuleStoreBase):
metadata: A nested dictionary of module metadata metadata: A nested dictionary of module metadata
""" """
raise NotImplementedError("XMLModuleStores are read-only") 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 ...@@ -6,7 +6,7 @@ from .exceptions import DuplicateItemError
log = logging.getLogger(__name__) 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'): default_class='xmodule.raw_module.RawDescriptor'):
""" """
Import the specified xml data_dir into the "store" modulestore, 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, ...@@ -19,7 +19,6 @@ def import_from_xml(store, data_dir, course_dirs=None, eager=True,
module_store = XMLModuleStore( module_store = XMLModuleStore(
data_dir, data_dir,
default_class=default_class, default_class=default_class,
eager=eager,
course_dirs=course_dirs course_dirs=course_dirs
) )
for course_id in module_store.modules.keys(): for course_id in module_store.modules.keys():
......
...@@ -12,9 +12,17 @@ def stringify_children(node): ...@@ -12,9 +12,17 @@ def stringify_children(node):
fixed from fixed from
http://stackoverflow.com/questions/4624062/get-all-text-inside-a-tag-in-lxml http://stackoverflow.com/questions/4624062/get-all-text-inside-a-tag-in-lxml
''' '''
parts = ([node.text] + # Useful things to know:
list(chain(*([etree.tostring(c), c.tail]
for c in node.getchildren()) # 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 # filter removes possible Nones in texts and tails
return ''.join(filter(None, parts)) return ''.join(filter(None, parts))
...@@ -49,7 +49,7 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -49,7 +49,7 @@ class RoundTripTestCase(unittest.TestCase):
copytree(data_dir / course_dir, root_dir / course_dir) copytree(data_dir / course_dir, root_dir / course_dir)
print "Starting import" 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() courses = initial_import.get_courses()
self.assertEquals(len(courses), 1) self.assertEquals(len(courses), 1)
...@@ -66,7 +66,7 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -66,7 +66,7 @@ class RoundTripTestCase(unittest.TestCase):
course_xml.write(xml) course_xml.write(xml)
print "Starting second import" 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() courses2 = second_import.get_courses()
self.assertEquals(len(courses2), 1) self.assertEquals(len(courses2), 1)
......
...@@ -193,7 +193,7 @@ class ImportTestCase(unittest.TestCase): ...@@ -193,7 +193,7 @@ class ImportTestCase(unittest.TestCase):
"""Make sure that metadata is inherited properly""" """Make sure that metadata is inherited properly"""
print "Starting import" 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() courses = initial_import.get_courses()
self.assertEquals(len(courses), 1) self.assertEquals(len(courses), 1)
...@@ -216,7 +216,7 @@ class ImportTestCase(unittest.TestCase): ...@@ -216,7 +216,7 @@ class ImportTestCase(unittest.TestCase):
def get_course(name): def get_course(name):
print "Importing {0}".format(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() courses = modulestore.get_courses()
self.assertEquals(len(courses), 1) self.assertEquals(len(courses), 1)
return courses[0] return courses[0]
...@@ -245,7 +245,7 @@ class ImportTestCase(unittest.TestCase): ...@@ -245,7 +245,7 @@ class ImportTestCase(unittest.TestCase):
happen--locations should uniquely name definitions. But in happen--locations should uniquely name definitions. But in
our imperfect XML world, it can (and likely will) happen.""" 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" toy_id = "edX/toy/2012_Fall"
two_toy_id = "edX/toy/TT_2012_Fall" two_toy_id = "edX/toy/TT_2012_Fall"
...@@ -261,7 +261,7 @@ class ImportTestCase(unittest.TestCase): ...@@ -261,7 +261,7 @@ class ImportTestCase(unittest.TestCase):
"""Ensure that colons in url_names convert to file paths properly""" """Ensure that colons in url_names convert to file paths properly"""
print "Starting import" print "Starting import"
modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy']) modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy'])
courses = modulestore.get_courses() courses = modulestore.get_courses()
self.assertEquals(len(courses), 1) 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 lxml import etree
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
...@@ -8,3 +8,32 @@ def test_stringify(): ...@@ -8,3 +8,32 @@ def test_stringify():
xml = etree.fromstring(html) xml = etree.fromstring(html)
out = stringify_children(xml) out = stringify_children(xml)
assert_equals(out, text) 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): ...@@ -544,7 +544,13 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
# Put import here to avoid circular import errors # Put import here to avoid circular import errors
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
msg = "Error loading from xml." 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) system.error_tracker(msg)
err_msg = msg + "\n" + exc_info_to_str(sys.exc_info()) err_msg = msg + "\n" + exc_info_to_str(sys.exc_info())
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course, descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course,
......
<sequential> <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"/> <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"> <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> <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): ...@@ -20,8 +20,8 @@ def index(request):
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
if settings.MITX_FEATURES.get('AUTH_USE_MIT_CERTIFICATES'): if settings.MITX_FEATURES.get('AUTH_USE_MIT_CERTIFICATES'):
from external_auth.views import edXauth_ssl_login from external_auth.views import ssl_login
return edXauth_ssl_login(request) return ssl_login(request)
university = branding.get_university(request.META.get('HTTP_HOST')) university = branding.get_university(request.META.get('HTTP_HOST'))
if university is None: if university is None:
......
...@@ -321,7 +321,7 @@ def _has_staff_access_to_location(user, location): ...@@ -321,7 +321,7 @@ def _has_staff_access_to_location(user, location):
return True return True
# If not global staff, is the user in the Auth group for this class? # 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) staff_group = _course_staff_group_name(location)
if staff_group in user_groups: if staff_group in user_groups:
debug("Allow: user in group %s", staff_group) debug("Allow: user in group %s", staff_group)
......
...@@ -57,7 +57,6 @@ def import_with_checks(course_dir, verbose=True): ...@@ -57,7 +57,6 @@ def import_with_checks(course_dir, verbose=True):
# module. # module.
modulestore = XMLModuleStore(data_dir, modulestore = XMLModuleStore(data_dir,
default_class=None, default_class=None,
eager=True,
course_dirs=course_dirs) course_dirs=course_dirs)
def str_of_err(tpl): def str_of_err(tpl):
......
...@@ -23,7 +23,6 @@ def import_course(course_dir, verbose=True): ...@@ -23,7 +23,6 @@ def import_course(course_dir, verbose=True):
# module. # module.
modulestore = XMLModuleStore(data_dir, modulestore = XMLModuleStore(data_dir,
default_class=None, default_class=None,
eager=True,
course_dirs=course_dirs) course_dirs=course_dirs)
def str_of_err(tpl): def str_of_err(tpl):
......
...@@ -195,7 +195,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi ...@@ -195,7 +195,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
descriptor.category, descriptor.category,
shared_state_key) shared_state_key)
instance_state = instance_module.state if instance_module is not None else None 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 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 ...@@ -254,7 +253,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
# by the replace_static_urls code below # by the replace_static_urls code below
replace_urls=replace_urls, replace_urls=replace_urls,
node_path=settings.NODE_PATH, 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 # pass position specified in URL to module through ModuleSystem
system.set('position', position) system.set('position', position)
......
...@@ -68,7 +68,6 @@ def xml_store_config(data_dir): ...@@ -68,7 +68,6 @@ def xml_store_config(data_dir):
'OPTIONS': { 'OPTIONS': {
'data_dir': data_dir, 'data_dir': data_dir,
'default_class': 'xmodule.hidden_module.HiddenDescriptor', 'default_class': 'xmodule.hidden_module.HiddenDescriptor',
'eager': True,
} }
} }
} }
...@@ -204,7 +203,8 @@ class PageLoader(ActivateLoginTestCase): ...@@ -204,7 +203,8 @@ class PageLoader(ActivateLoginTestCase):
self.assertEqual(len(courses), 1) self.assertEqual(len(courses), 1)
course = courses[0] course = courses[0]
self.enroll(course) self.enroll(course)
course_id = course.id
n = 0 n = 0
num_bad = 0 num_bad = 0
all_ok = True all_ok = True
...@@ -214,7 +214,8 @@ class PageLoader(ActivateLoginTestCase): ...@@ -214,7 +214,8 @@ class PageLoader(ActivateLoginTestCase):
print "Checking ", descriptor.location.url() print "Checking ", descriptor.location.url()
#print descriptor.__class__, descriptor.location #print descriptor.__class__, descriptor.location
resp = self.client.get(reverse('jump_to', 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) msg = str(resp.status_code)
if resp.status_code != 200: if resp.status_code != 200:
......
...@@ -5,8 +5,6 @@ import itertools ...@@ -5,8 +5,6 @@ import itertools
from functools import partial from functools import partial
from functools import partial
from django.conf import settings from django.conf import settings
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -152,7 +150,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -152,7 +150,7 @@ def index(request, course_id, chapter=None, section=None,
course_id, request.user, section_descriptor) course_id, request.user, section_descriptor)
module = get_module(request.user, request, module = get_module(request.user, request,
section_descriptor.location, section_descriptor.location,
student_module_cache, course_id) student_module_cache, course_id, position)
if module is None: if module is None:
# User is probably being clever and trying to access something # User is probably being clever and trying to access something
# they don't have access to. # they don't have access to.
...@@ -196,7 +194,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -196,7 +194,7 @@ def index(request, course_id, chapter=None, section=None,
@ensure_csrf_cookie @ensure_csrf_cookie
def jump_to(request, location): def jump_to(request, course_id, location):
''' '''
Show the page that contains a specific location. Show the page that contains a specific location.
...@@ -213,15 +211,18 @@ def jump_to(request, location): ...@@ -213,15 +211,18 @@ def jump_to(request, location):
# Complain if there's not data for this location # Complain if there's not data for this location
try: 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: except ItemNotFoundError:
raise Http404("No data at this location: {0}".format(location)) raise Http404("No data at this location: {0}".format(location))
except NoPathToItem: except NoPathToItem:
raise Http404("This location is not in any class: {0}".format(location)) raise Http404("This location is not in any class: {0}".format(location))
# Rely on index to do all error handling and access control. # 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 @ensure_csrf_cookie
def course_info(request, course_id): def course_info(request, course_id):
""" """
...@@ -328,6 +329,10 @@ def progress(request, course_id, student_id=None): ...@@ -328,6 +329,10 @@ def progress(request, course_id, student_id=None):
# NOTE: To make sure impersonation by instructor works, use # NOTE: To make sure impersonation by instructor works, use
# student instead of request.user in the rest of the function. # 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( student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course_id, student, course) course_id, student, course)
course_module = get_module(student, request, course.location, course_module = get_module(student, request, course.location,
......
...@@ -149,8 +149,8 @@ class HtmlResponse(HttpResponse): ...@@ -149,8 +149,8 @@ class HtmlResponse(HttpResponse):
def __init__(self, html=''): def __init__(self, html=''):
super(HtmlResponse, self).__init__(html, content_type='text/plain') super(HtmlResponse, self).__init__(html, content_type='text/plain')
class ViewNameMiddleware(object): class ViewNameMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs): def process_view(self, request, view_func, view_args, view_kwargs):
request.view_name = view_func.__name__ request.view_name = view_func.__name__
class QueryCountDebugMiddleware(object): class QueryCountDebugMiddleware(object):
......
...@@ -77,7 +77,7 @@ MITX_FEATURES = { ...@@ -77,7 +77,7 @@ MITX_FEATURES = {
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False, 'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False, 'AUTH_USE_OPENID': False,
'AUTH_USE_MIT_CERTIFICATES' : False, 'AUTH_USE_MIT_CERTIFICATES' : False,
'AUTH_USE_OPENID_PROVIDER': False,
} }
# Used for A/B testing # Used for A/B testing
...@@ -120,6 +120,10 @@ node_paths = [COMMON_ROOT / "static/js/vendor", ...@@ -120,6 +120,10 @@ node_paths = [COMMON_ROOT / "static/js/vendor",
] ]
NODE_PATH = ':'.join(node_paths) NODE_PATH = ':'.join(node_paths)
############################ OpenID Provider ##################################
OPENID_PROVIDER_TRUSTED_ROOTS = ['cs50.net', '*.cs50.net']
################################## MITXWEB ##################################### ################################## MITXWEB #####################################
# This is where we stick our compiled template files. Most of the app uses Mako # This is where we stick our compiled template files. Most of the app uses Mako
# templates # templates
...@@ -219,7 +223,6 @@ MODULESTORE = { ...@@ -219,7 +223,6 @@ MODULESTORE = {
'OPTIONS': { 'OPTIONS': {
'data_dir': DATA_DIR, 'data_dir': DATA_DIR,
'default_class': 'xmodule.hidden_module.HiddenDescriptor', 'default_class': 'xmodule.hidden_module.HiddenDescriptor',
'eager': True,
} }
} }
} }
......
...@@ -105,6 +105,7 @@ LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1'] ...@@ -105,6 +105,7 @@ LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1']
################################ OpenID Auth ################################# ################################ OpenID Auth #################################
MITX_FEATURES['AUTH_USE_OPENID'] = True MITX_FEATURES['AUTH_USE_OPENID'] = True
MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
MITX_FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True MITX_FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True
INSTALLED_APPS += ('external_auth',) INSTALLED_APPS += ('external_auth',)
...@@ -115,6 +116,8 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True ...@@ -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_SSO_SERVER_URL = 'https://www.google.com/accounts/o8/id' # TODO: accept more endpoints
OPENID_USE_AS_ADMIN_LOGIN = False OPENID_USE_AS_ADMIN_LOGIN = False
OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
################################ MIT Certificates SSL Auth ################################# ################################ MIT Certificates SSL Auth #################################
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
......
...@@ -123,6 +123,11 @@ CACHES = { ...@@ -123,6 +123,11 @@ CACHES = {
# Dummy secret key for dev # Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
################################## OPENID ######################################
MITX_FEATURES['AUTH_USE_OPENID'] = True
MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
############################ FILE UPLOADS (ASKBOT) ############################# ############################ FILE UPLOADS (ASKBOT) #############################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = TEST_ROOT / "uploads" MEDIA_ROOT = TEST_ROOT / "uploads"
......
...@@ -115,6 +115,30 @@ ...@@ -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 { span.register {
background: lighten($blue, 20%); background: lighten($blue, 20%);
border: 1px solid $blue; border: 1px solid $blue;
...@@ -125,7 +149,10 @@ ...@@ -125,7 +149,10 @@
padding: 10px 0px 8px; padding: 10px 0px 8px;
text-transform: uppercase; text-transform: uppercase;
text-align: center; 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 @@ ...@@ -261,7 +261,7 @@
padding: 12px 0px; padding: 12px 0px;
width: 100%; width: 100%;
a.university { .university {
background: rgba(255,255,255, 1); background: rgba(255,255,255, 1);
border: 1px solid rgb(180,180,180); border: 1px solid rgb(180,180,180);
@include border-radius(3px); @include border-radius(3px);
...@@ -269,17 +269,14 @@ ...@@ -269,17 +269,14 @@
color: $lighter-base-font-color; color: $lighter-base-font-color;
display: block; display: block;
font-style: italic; font-style: italic;
font-family: $sans-serif;
font-size: 16px;
font-weight: 800; font-weight: 800;
@include inline-block; @include inline-block;
margin-right: 10px; margin-right: 10px;
margin-bottom: 0;
padding: 5px 10px; padding: 5px 10px;
float: left; float: left;
&:hover {
color: $blue;
text-decoration: none;
}
} }
h3 { h3 {
...@@ -306,8 +303,12 @@ ...@@ -306,8 +303,12 @@
background: $yellow; background: $yellow;
border: 1px solid rgb(200,200,200); border: 1px solid rgb(200,200,200);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); @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; padding: 5px;
width: flex-grid(8);
float: left;
@include box-sizing(border-box);
p { p {
color: $lighter-base-font-color; color: $lighter-base-font-color;
...@@ -317,93 +318,46 @@ ...@@ -317,93 +318,46 @@
} }
} }
.meta { .enter-course {
@include clearfix; @include button(shiny, $blue);
margin-top: 22px; @include box-sizing(border-box);
position: relative; @include border-radius(3px);
@include transition(opacity, 0.15s, linear); display: block;
width: 100%; float: left;
font: normal 1rem/1.6rem $sans-serif;
letter-spacing: 1px;
.course-work-icon { padding: 6px 0px;
@include background-image(url('../images/portal-icons/pencil-icon.png')); text-transform: uppercase;
background-size: cover; text-align: center;
float: left; margin-top: 16px;
height: 22px; width: flex-grid(4);
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%;
}
}
}
} }
} }
&:hover { > a:hover {
.cover { .cover {
.shade { .shade {
background: rgba(255,255,255, 0.1); background: rgba(255,255,255, 0.1);
@include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%, @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 { .arrow {
opacity: 1; 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 @@ ...@@ -420,5 +374,6 @@
color: #333; color: #333;
} }
} }
} }
} }
...@@ -72,30 +72,24 @@ ...@@ -72,30 +72,24 @@
else: else:
course_target = reverse('about_course', args=[course.id]) 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> <a href="${course_target}">
<div class="arrow">&#10095;</div> <section class="cover" style="background-image: url('${course_image_url(course)}')">
</a> <div class="shade"></div>
<section class="info"> <div class="arrow">&#10095;</div>
<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>
</section> </section>
<section class="meta">
<div class="course-work-icon"></div> <section class="info">
<div class="progress"> <hgroup>
<div class="meter"> <h2 class="university">${get_course_about_section(course, 'university')}</h2>
<div class="meter-fill"></div> <h3>${course.number} ${course.title}</h3>
</div> </hgroup>
</div> <section class="course-status">
<div class="complete"> <p>Class Starts - <span>${course.start_date_text}</span></p>
##<p><span class="completeness">60%</span> complete</p> </section>
</div> <p class="enter-course">View Courseware</p>
</section> </section>
</section> </a>
</article> </article>
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">Unregister</a> <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 @@ ...@@ -74,7 +74,7 @@
%if show_link: %if show_link:
<a href="${course_target}"> <a href="${course_target}">
%endif %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: %if show_link:
</a> </a>
%endif %endif
......
...@@ -32,3 +32,27 @@ ...@@ -32,3 +32,27 @@
</section> </section>
</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: ...@@ -98,8 +98,9 @@ if settings.COURSEWARE_ENABLED:
urlpatterns += ( urlpatterns += (
# Hook django-masquerade, allowing staff to view site as other users # Hook django-masquerade, allowing staff to view site as other users
url(r'^masquerade/', include('masquerade.urls')), 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>[^/]*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.modx_dispatch', 'courseware.module_render.modx_dispatch',
name='modx_dispatch'), name='modx_dispatch'),
...@@ -142,6 +143,8 @@ if settings.COURSEWARE_ENABLED: ...@@ -142,6 +143,8 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.index', name="courseware_chapter"), 'courseware.views.index', name="courseware_chapter"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$',
'courseware.views.index', name="courseware_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$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/progress$',
'courseware.views.progress', name="progress"), 'courseware.views.progress', name="progress"),
# Takes optional student_id for instructor use--shows profile as that student sees it. # Takes optional student_id for instructor use--shows profile as that student sees it.
...@@ -164,7 +167,7 @@ if settings.COURSEWARE_ENABLED: ...@@ -164,7 +167,7 @@ if settings.COURSEWARE_ENABLED:
if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'): if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
urlpatterns += ( urlpatterns += (
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/news$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/news$',
'courseware.views.news', name="news"), 'courseware.views.news', name="news"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/',
include('django_comment_client.urls')) include('django_comment_client.urls'))
...@@ -215,9 +218,17 @@ if settings.DEBUG: ...@@ -215,9 +218,17 @@ if settings.DEBUG:
if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
urlpatterns += ( urlpatterns += (
url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'), 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'), 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'): if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
urlpatterns += ( 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