+% endif
+
diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py
index c72d2a1538..b06975f6ce 100644
--- a/common/lib/capa/capa/tests/__init__.py
+++ b/common/lib/capa/capa/tests/__init__.py
@@ -4,13 +4,23 @@ import os
from mock import Mock
+import xml.sax.saxutils as saxutils
+
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
+def tst_render_template(template, context):
+ """
+ A test version of render to template. Renders to the repr of the context, completely ignoring
+ the template name. To make the output valid xml, quotes the content, and wraps it in a
+ """
+ return '
{0}
'.format(saxutils.escape(repr(context)))
+
+
test_system = Mock(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
- render_template=Mock(),
+ render_template=tst_render_template,
replace_urls=Mock(),
user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
diff --git a/common/lib/capa/capa/tests/test_customrender.py b/common/lib/capa/capa/tests/test_customrender.py
new file mode 100644
index 0000000000..7208ab2941
--- /dev/null
+++ b/common/lib/capa/capa/tests/test_customrender.py
@@ -0,0 +1,76 @@
+from lxml import etree
+import unittest
+import xml.sax.saxutils as saxutils
+
+from . import test_system
+from capa import customrender
+
+# just a handy shortcut
+lookup_tag = customrender.registry.get_class_for_tag
+
+def extract_context(xml):
+ """
+ Given an xml element corresponding to the output of test_system.render_template, get back the
+ original context
+ """
+ return eval(xml.text)
+
+def quote_attr(s):
+ return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
+
+class HelperTest(unittest.TestCase):
+ '''
+ Make sure that our helper function works!
+ '''
+ def check(self, d):
+ xml = etree.XML(test_system.render_template('blah', d))
+ self.assertEqual(d, extract_context(xml))
+
+ def test_extract_context(self):
+ self.check({})
+ self.check({1, 2})
+ self.check({'id', 'an id'})
+ self.check({'with"quote', 'also"quote'})
+
+
+class SolutionRenderTest(unittest.TestCase):
+ '''
+ Make sure solutions render properly.
+ '''
+
+ def test_rendering(self):
+ solution = 'To compute unicorns, count them.'
+ xml_str = """
{s}""".format(s=solution)
+ element = etree.fromstring(xml_str)
+
+ renderer = lookup_tag('solution')(test_system, element)
+
+ self.assertEqual(renderer.id, 'solution_12')
+
+ # our test_system "renders" templates to a div with the repr of the context
+ xml = renderer.get_html()
+ context = extract_context(xml)
+ self.assertEqual(context, {'id' : 'solution_12'})
+
+
+class MathRenderTest(unittest.TestCase):
+ '''
+ Make sure math renders properly.
+ '''
+
+ def check_parse(self, latex_in, mathjax_out):
+ xml_str = """
""".format(tex=latex_in)
+ element = etree.fromstring(xml_str)
+
+ renderer = lookup_tag('math')(test_system, element)
+
+ self.assertEqual(renderer.mathstr, mathjax_out)
+
+ def test_parsing(self):
+ self.check_parse('$abc$', '[mathjaxinline]abc[/mathjaxinline]')
+ self.check_parse('$abc', '$abc')
+ self.check_parse(r'$\displaystyle 2+2$', '[mathjax] 2+2[/mathjax]')
+
+
+ # NOTE: not testing get_html yet because I don't understand why it's doing what it's doing.
+
diff --git a/common/lib/capa/capa/tests/test_files/imageresponse.xml b/common/lib/capa/capa/tests/test_files/imageresponse.xml
index 72bf06401a..34dba37e3b 100644
--- a/common/lib/capa/capa/tests/test_files/imageresponse.xml
+++ b/common/lib/capa/capa/tests/test_files/imageresponse.xml
@@ -8,8 +8,14 @@ Hello
Click on the image where the top skier will stop momentarily if the top skier starts from rest.
Click on the image where the lower skier will stop momentarily if the lower skier starts from rest.
+
+
Click on either of the two positions as discussed previously.
+
+
Click on either of the two positions as discussed previously.
+
+
Click on either of the two positions as discussed previously.
Use conservation of energy.
-
\ No newline at end of file
+
diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py
index 9ef642d468..826d304717 100644
--- a/common/lib/capa/capa/tests/test_inputtypes.py
+++ b/common/lib/capa/capa/tests/test_inputtypes.py
@@ -1,50 +1,39 @@
"""
-Tests of input types (and actually responsetypes too)
+Tests of input types.
+
+TODO:
+- refactor: so much repetive code (have factory methods that build xml elements directly, etc)
+
+- test error cases
+
+- check rendering -- e.g. msg should appear in the rendered output. If possible, test that
+ templates are escaping things properly.
+
+
+- test unicode in values, parameters, etc.
+- test various html escapes
+- test funny xml chars -- should never get xml parse error if things are escaped properly.
+
"""
-from datetime import datetime
-import json
-from mock import Mock
-from nose.plugins.skip import SkipTest
-import os
+from lxml import etree
import unittest
+import xml.sax.saxutils as saxutils
from . import test_system
from capa import inputtypes
-from lxml import etree
-
-def tst_render_template(template, context):
- """
- A test version of render to template. Renders to the repr of the context, completely ignoring the template name.
- """
- return repr(context)
+# just a handy shortcut
+lookup_tag = inputtypes.registry.get_class_for_tag
-system = Mock(render_template=tst_render_template)
+def quote_attr(s):
+ return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
class OptionInputTest(unittest.TestCase):
'''
Make sure option inputs work
'''
- def test_rendering_new(self):
- xml = """
"""
- element = etree.fromstring(xml)
-
- value = 'Down'
- status = 'answered'
- context = inputtypes._optioninput(element, value, status, test_system.render_template)
- print 'context: ', context
-
- expected = {'value': 'Down',
- 'options': [('Up', 'Up'), ('Down', 'Down')],
- 'state': 'answered',
- 'msg': '',
- 'inline': '',
- 'id': 'sky_input'}
-
- self.assertEqual(context, expected)
-
def test_rendering(self):
xml_str = """
"""
@@ -53,16 +42,466 @@ class OptionInputTest(unittest.TestCase):
state = {'value': 'Down',
'id': 'sky_input',
'status': 'answered'}
- option_input = inputtypes.OptionInput(system, element, state)
+ option_input = lookup_tag('optioninput')(test_system, element, state)
context = option_input._get_render_context()
expected = {'value': 'Down',
'options': [('Up', 'Up'), ('Down', 'Down')],
- 'state': 'answered',
+ 'status': 'answered',
'msg': '',
'inline': '',
'id': 'sky_input'}
self.assertEqual(context, expected)
+ def test_option_parsing(self):
+ f = inputtypes.OptionInput.parse_options
+ def check(input, options):
+ """Take list of options, confirm that output is in the silly doubled format"""
+ expected = [(o, o) for o in options]
+ self.assertEqual(f(input), expected)
+
+ check("('a','b')", ['a', 'b'])
+ check("('a', 'b')", ['a', 'b'])
+ check("('a b','b')", ['a b', 'b'])
+ check("('My \"quoted\"place','b')", ['My \"quoted\"place', 'b'])
+
+
+class ChoiceGroupTest(unittest.TestCase):
+ '''
+ Test choice groups, radio groups, and checkbox groups
+ '''
+
+ def check_group(self, tag, expected_input_type, expected_suffix):
+ xml_str = """
+ <{tag}>
+
This is foil One.
+
This is foil Two.
+
This is foil Three.
+ {tag}>
+ """.format(tag=tag)
+
+ element = etree.fromstring(xml_str)
+
+ state = {'value': 'foil3',
+ 'id': 'sky_input',
+ 'status': 'answered'}
+
+ the_input = lookup_tag(tag)(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'sky_input',
+ 'value': 'foil3',
+ 'status': 'answered',
+ 'msg': '',
+ 'input_type': expected_input_type,
+ 'choices': [('foil1', '
This is foil One.'),
+ ('foil2', '
This is foil Two.'),
+ ('foil3', 'This is foil Three.'),],
+ 'name_array_suffix': expected_suffix, # what is this for??
+ }
+
+ self.assertEqual(context, expected)
+
+ def test_choicegroup(self):
+ self.check_group('choicegroup', 'radio', '')
+
+ def test_radiogroup(self):
+ self.check_group('radiogroup', 'radio', '[]')
+
+ def test_checkboxgroup(self):
+ self.check_group('checkboxgroup', 'checkbox', '[]')
+
+
+
+class JavascriptInputTest(unittest.TestCase):
+ '''
+ The javascript input is a pretty straightforward pass-thru, but test it anyway
+ '''
+
+ def test_rendering(self):
+ params = "(1,2,3)"
+
+ problem_state = "abc12',12&hi
"
+ display_class = "a_class"
+ display_file = "my_files/hi.js"
+
+ xml_str = """""".format(
+ params=params,
+ ps=quote_attr(problem_state),
+ dc=display_class, df=display_file)
+
+ element = etree.fromstring(xml_str)
+
+ state = {'value': '3',}
+ the_input = lookup_tag('javascriptinput')(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'status': 'unanswered',
+ 'msg': '',
+ 'value': '3',
+ 'params': params,
+ 'display_file': display_file,
+ 'display_class': display_class,
+ 'problem_state': problem_state,}
+
+ self.assertEqual(context, expected)
+
+
+class TextLineTest(unittest.TestCase):
+ '''
+ Check that textline inputs work, with and without math.
+ '''
+
+ def test_rendering(self):
+ size = "42"
+ xml_str = """""".format(size=size)
+
+ element = etree.fromstring(xml_str)
+
+ state = {'value': 'BumbleBee',}
+ the_input = lookup_tag('textline')(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'value': 'BumbleBee',
+ 'status': 'unanswered',
+ 'size': size,
+ 'msg': '',
+ 'hidden': False,
+ 'inline': False,
+ 'do_math': False,
+ 'preprocessor': None}
+ self.assertEqual(context, expected)
+
+
+ def test_math_rendering(self):
+ size = "42"
+ preprocessorClass = "preParty"
+ script = "foo/party.js"
+
+ xml_str = """""".format(size=size, pp=preprocessorClass, sc=script)
+
+ element = etree.fromstring(xml_str)
+
+ state = {'value': 'BumbleBee',}
+ the_input = lookup_tag('textline')(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'value': 'BumbleBee',
+ 'status': 'unanswered',
+ 'size': size,
+ 'msg': '',
+ 'hidden': False,
+ 'inline': False,
+ 'do_math': True,
+ 'preprocessor': {'class_name': preprocessorClass,
+ 'script_src': script}}
+ self.assertEqual(context, expected)
+
+
+class FileSubmissionTest(unittest.TestCase):
+ '''
+ Check that file submission inputs work
+ '''
+
+ def test_rendering(self):
+ allowed_files = "runme.py nooooo.rb ohai.java"
+ required_files = "cookies.py"
+
+ xml_str = """""".format(af=allowed_files,
+ rf=required_files,)
+
+
+ element = etree.fromstring(xml_str)
+
+ state = {'value': 'BumbleBee.py',
+ 'status': 'incomplete',
+ 'feedback' : {'message': '3'}, }
+ input_class = lookup_tag('filesubmission')
+ the_input = input_class(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'status': 'queued',
+ 'msg': input_class.submitted_msg,
+ 'value': 'BumbleBee.py',
+ 'queue_len': '3',
+ 'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
+ 'required_files': '["cookies.py"]'}
+
+ self.assertEqual(context, expected)
+
+
+class CodeInputTest(unittest.TestCase):
+ '''
+ Check that codeinput inputs work
+ '''
+
+ def test_rendering(self):
+ mode = "parrot"
+ linenumbers = 'false'
+ rows = '37'
+ cols = '11'
+ tabsize = '7'
+
+ xml_str = """""".format(m=mode, c=cols, r=rows, ln=linenumbers, ts=tabsize)
+
+ element = etree.fromstring(xml_str)
+
+ escapedict = {'"': '"'}
+ esc = lambda s: saxutils.escape(s, escapedict)
+
+ state = {'value': 'print "good evening"',
+ 'status': 'incomplete',
+ 'feedback' : {'message': '3'}, }
+
+ input_class = lookup_tag('codeinput')
+ the_input = input_class(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'value': 'print "good evening"',
+ 'status': 'queued',
+ 'msg': input_class.submitted_msg,
+ 'mode': mode,
+ 'linenumbers': linenumbers,
+ 'rows': rows,
+ 'cols': cols,
+ 'hidden': '',
+ 'tabsize': int(tabsize),
+ 'queue_len': '3',
+ }
+
+ self.assertEqual(context, expected)
+
+
+class SchematicTest(unittest.TestCase):
+ '''
+ Check that schematic inputs work
+ '''
+
+ def test_rendering(self):
+ height = '12'
+ width = '33'
+ parts = 'resistors, capacitors, and flowers'
+ analyses = 'fast, slow, and pink'
+ initial_value = 'two large batteries'
+ submit_analyses = 'maybe'
+
+
+ xml_str = """""".format(h=height, w=width, p=parts, a=analyses,
+ iv=initial_value, sa=submit_analyses)
+
+ element = etree.fromstring(xml_str)
+
+ value = 'three resistors and an oscilating pendulum'
+ state = {'value': value,
+ 'status': 'unsubmitted'}
+
+ the_input = lookup_tag('schematic')(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'value': value,
+ 'status': 'unsubmitted',
+ 'msg': '',
+ 'initial_value': initial_value,
+ 'width': width,
+ 'height': height,
+ 'parts': parts,
+ 'analyses': analyses,
+ 'submit_analyses': submit_analyses,
+ }
+
+ self.assertEqual(context, expected)
+
+
+class ImageInputTest(unittest.TestCase):
+ '''
+ Check that image inputs work
+ '''
+
+ def check(self, value, egx, egy):
+ height = '78'
+ width = '427'
+ src = 'http://www.edx.org/cowclicker.jpg'
+
+ xml_str = """""".format(s=src, h=height, w=width)
+
+ element = etree.fromstring(xml_str)
+
+ state = {'value': value,
+ 'status': 'unsubmitted'}
+
+ the_input = lookup_tag('imageinput')(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'value': value,
+ 'status': 'unsubmitted',
+ 'width': width,
+ 'height': height,
+ 'src': src,
+ 'gx': egx,
+ 'gy': egy,
+ 'msg': ''}
+
+ self.assertEqual(context, expected)
+
+ def test_with_value(self):
+ # Check that compensating for the dot size works properly.
+ self.check('[50,40]', 35, 25)
+
+ def test_without_value(self):
+ self.check('', 0, 0)
+
+ def test_corrupt_values(self):
+ self.check('[12', 0, 0)
+ self.check('[12, a]', 0, 0)
+ self.check('[12 10]', 0, 0)
+ self.check('[12]', 0, 0)
+ self.check('[12 13 14]', 0, 0)
+
+
+
+class CrystallographyTest(unittest.TestCase):
+ '''
+ Check that crystallography inputs work
+ '''
+
+ def test_rendering(self):
+ height = '12'
+ width = '33'
+ size = '10'
+
+ xml_str = """""".format(h=height, w=width, s=size)
+
+ element = etree.fromstring(xml_str)
+
+ value = 'abc'
+ state = {'value': value,
+ 'status': 'unsubmitted'}
+
+ the_input = lookup_tag('crystallography')(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'value': value,
+ 'status': 'unsubmitted',
+ 'size': size,
+ 'msg': '',
+ 'hidden': '',
+ 'width': width,
+ 'height': height,
+ }
+
+ self.assertEqual(context, expected)
+
+
+class VseprTest(unittest.TestCase):
+ '''
+ Check that vsepr inputs work
+ '''
+
+ def test_rendering(self):
+ height = '12'
+ width = '33'
+ molecules = "H2O, C2O"
+ geometries = "AX12,TK421"
+
+ xml_str = """""".format(h=height, w=width, m=molecules, g=geometries)
+
+ element = etree.fromstring(xml_str)
+
+ value = 'abc'
+ state = {'value': value,
+ 'status': 'unsubmitted'}
+
+ the_input = lookup_tag('vsepr_input')(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'value': value,
+ 'status': 'unsubmitted',
+ 'msg': '',
+ 'width': width,
+ 'height': height,
+ 'molecules': molecules,
+ 'geometries': geometries,
+ }
+
+ self.assertEqual(context, expected)
+
+
+
+class ChemicalEquationTest(unittest.TestCase):
+ '''
+ Check that chemical equation inputs work.
+ '''
+
+ def test_rendering(self):
+ size = "42"
+ xml_str = """""".format(size=size)
+
+ element = etree.fromstring(xml_str)
+
+ state = {'value': 'H2OYeah',}
+ the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
+
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'value': 'H2OYeah',
+ 'status': 'unanswered',
+ 'msg': '',
+ 'size': size,
+ 'previewer': '/static/js/capa/chemical_equation_preview.js',
+ }
+ self.assertEqual(context, expected)
+
diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py
index f2fa873080..bcac555b5e 100644
--- a/common/lib/capa/capa/tests/test_responsetypes.py
+++ b/common/lib/capa/capa/tests/test_responsetypes.py
@@ -53,12 +53,22 @@ class ImageResponseTest(unittest.TestCase):
imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml"
test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system)
correct_answers = {'1_2_1': '(490,11)-(556,98)',
- '1_2_2': '(242,202)-(296,276)'}
+ '1_2_2': '(242,202)-(296,276)',
+ '1_2_3': '(490,11)-(556,98);(242,202)-(296,276)',
+ '1_2_4': '(490,11)-(556,98);(242,202)-(296,276)',
+ '1_2_5': '(490,11)-(556,98);(242,202)-(296,276)',
+ }
test_answers = {'1_2_1': '[500,20]',
'1_2_2': '[250,300]',
+ '1_2_3': '[500,20]',
+ '1_2_4': '[250,250]',
+ '1_2_5': '[10,10]',
}
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
+ self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_3'), 'correct')
+ self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_4'), 'correct')
+ self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_5'), 'incorrect')
class SymbolicResponseTest(unittest.TestCase):
diff --git a/common/lib/xmodule/.coveragerc b/common/lib/xmodule/.coveragerc
new file mode 100644
index 0000000000..310c8e778b
--- /dev/null
+++ b/common/lib/xmodule/.coveragerc
@@ -0,0 +1,13 @@
+# .coveragerc for common/lib/xmodule
+[run]
+data_file = reports/common/lib/xmodule/.coverage
+source = common/lib/xmodule
+
+[report]
+ignore_errors = True
+
+[html]
+directory = reports/common/lib/xmodule/cover
+
+[xml]
+output = reports/common/lib/xmodule/coverage.xml
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index c123756655..24cddd2047 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -26,8 +26,9 @@ setup(
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"error = xmodule.error_module:ErrorDescriptor",
"problem = xmodule.capa_module:CapaDescriptor",
- "problemset = xmodule.vertical_module:VerticalDescriptor",
+ "problemset = xmodule.seq_module:SequenceDescriptor",
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
+ "selfassessment = xmodule.self_assessment_module:SelfAssessmentDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor",
@@ -35,6 +36,10 @@ setup(
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor",
"discussion = xmodule.discussion_module:DiscussionDescriptor",
+ "course_info = xmodule.html_module:CourseInfoDescriptor",
+ "static_tab = xmodule.html_module:StaticTabDescriptor",
+ "custom_tag_template = xmodule.raw_module:RawDescriptor",
+ "about = xmodule.html_module:AboutDescriptor"
]
}
)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index d75e0ff860..ea17c23bb4 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -10,7 +10,6 @@ import sys
from datetime import timedelta
from lxml import etree
-from lxml.html import rewrite_links
from pkg_resources import resource_string
from capa.capa_problem import LoncapaProblem
@@ -30,15 +29,17 @@ TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?)
def only_one(lst, default="", process=lambda x: x):
"""
If lst is empty, returns default
- If lst has a single element, applies process to that element and returns it
- Otherwise, raises an exeception
+
+ If lst has a single element, applies process to that element and returns it.
+
+ Otherwise, raises an exception.
"""
if len(lst) == 0:
return default
elif len(lst) == 1:
return process(lst[0])
else:
- raise Exception('Malformed XML')
+ raise Exception('Malformed XML: expected at most one element in list.')
def parse_timedelta(time_str):
@@ -120,6 +121,8 @@ class CapaModule(XModule):
self.show_answer = self.metadata.get('showanswer', 'closed')
+ self.force_save_button = self.metadata.get('force_save_button', 'false')
+
if self.show_answer == "":
self.show_answer = "closed"
@@ -290,11 +293,11 @@ class CapaModule(XModule):
# check button is context-specific.
# Put a "Check" button if unlimited attempts or still some left
- if self.max_attempts is None or self.attempts < self.max_attempts-1:
+ if self.max_attempts is None or self.attempts < self.max_attempts-1:
check_button = "Check"
else:
# Will be final check so let user know that
- check_button = "Final Check"
+ check_button = "Final Check"
reset_button = True
save_button = True
@@ -320,9 +323,10 @@ class CapaModule(XModule):
if not self.lcp.done:
reset_button = False
- # We don't need a "save" button if infinite number of attempts and
- # non-randomized
- if self.max_attempts is None and self.rerandomize != "always":
+ # We may not need a "save" button if infinite number of attempts and
+ # non-randomized. The problem author can force it. It's a bit weird for
+ # randomization to control this; should perhaps be cleaned up.
+ if (self.force_save_button == "false") and (self.max_attempts is None and self.rerandomize != "always"):
save_button = False
context = {'problem': content,
@@ -342,17 +346,6 @@ class CapaModule(XModule):
html = ''.format(
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "
"
- # cdodge: OK, we have to do two rounds of url reference subsitutions
- # one which uses the 'asset library' that is served by the contentstore and the
- # more global /static/ filesystem based static content.
- # NOTE: rewrite_content_links is defined in XModule
- # This is a bit unfortunate and I'm sure we'll try to considate this into
- # a one step process.
- try:
- html = rewrite_links(html, self.rewrite_content_links)
- except:
- logging.error('error rewriting links in {0}'.format(html))
-
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
return self.system.replace_urls(html, self.metadata['data_dir'])
@@ -527,26 +520,20 @@ class CapaModule(XModule):
# Problem queued. Students must wait a specified waittime before they are allowed to submit
if self.lcp.is_queued():
current_time = datetime.datetime.now()
- prev_submit_time = self.lcp.get_recentmost_queuetime()
+ prev_submit_time = self.lcp.get_recentmost_queuetime()
waittime_between_requests = self.system.xqueue['waittime']
if (current_time-prev_submit_time).total_seconds() < waittime_between_requests:
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
- return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
+ return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
try:
old_state = self.lcp.get_state()
lcp_id = self.lcp.problem_id
correct_map = self.lcp.grade_answers(answers)
except StudentInputError as inst:
- # TODO (vshnayder): why is this line here?
- #self.lcp = LoncapaProblem(self.definition['data'],
- # id=lcp_id, state=old_state, system=self.system)
log.exception("StudentInputError in capa_module:problem_check")
return {'success': inst.message}
except Exception, err:
- # TODO: why is this line here?
- #self.lcp = LoncapaProblem(self.definition['data'],
- # id=lcp_id, state=old_state, system=self.system)
if self.system.DEBUG:
msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc()
@@ -678,10 +665,10 @@ class CapaDescriptor(RawDescriptor):
'problems/' + path[8:],
path[8:],
]
-
+
def __init__(self, *args, **kwargs):
super(CapaDescriptor, self).__init__(*args, **kwargs)
-
+
weight_string = self.metadata.get('weight', None)
if weight_string:
self.weight = float(weight_string)
diff --git a/common/lib/xmodule/xmodule/contentstore/content.py b/common/lib/xmodule/xmodule/contentstore/content.py
index 6dcf70fbe1..a7a76fa242 100644
--- a/common/lib/xmodule/xmodule/contentstore/content.py
+++ b/common/lib/xmodule/xmodule/contentstore/content.py
@@ -62,6 +62,13 @@ class StaticContent(object):
@staticmethod
def get_id_from_path(path):
return get_id_from_location(get_location_from_path(path))
+
+ @staticmethod
+ def convert_legacy_static_url(path, course_namespace):
+ loc = StaticContent.compute_location(course_namespace.org, course_namespace.course, path)
+ return StaticContent.get_url_path_from_location(loc)
+
+
class ContentStore(object):
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index a1549185b1..e4d2961723 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -30,13 +30,13 @@ class CourseDescriptor(SequenceDescriptor):
self.book_url = book_url
self.table_of_contents = self._get_toc_from_s3()
self.start_page = int(self.table_of_contents[0].attrib['page'])
-
+
# The last page should be the last element in the table of contents,
# but it may be nested. So recurse all the way down the last element
last_el = self.table_of_contents[-1]
while last_el.getchildren():
last_el = last_el[-1]
-
+
self.end_page = int(last_el.attrib['page'])
@property
@@ -94,6 +94,7 @@ class CourseDescriptor(SequenceDescriptor):
self.enrollment_start = self._try_parse_time("enrollment_start")
self.enrollment_end = self._try_parse_time("enrollment_end")
+ self.end = self._try_parse_time("end")
# NOTE: relies on the modulestore to call set_grading_policy() right after
# init. (Modulestore is in charge of figuring out where to load the policy from)
@@ -237,6 +238,16 @@ class CourseDescriptor(SequenceDescriptor):
return definition
+ def has_ended(self):
+ """
+ Returns True if the current time is after the specified course end date.
+ Returns False if there is no end date specified.
+ """
+ if self.end is None:
+ return False
+
+ return time.gmtime() > self.end
+
def has_started(self):
return time.gmtime() > self.start
@@ -255,6 +266,10 @@ class CourseDescriptor(SequenceDescriptor):
"""
return self.metadata.get('tabs')
+ @tabs.setter
+ def tabs(self, value):
+ self.metadata['tabs'] = value
+
@property
def show_calculator(self):
return self.metadata.get("show_calculator", None) == "Yes"
@@ -346,7 +361,12 @@ class CourseDescriptor(SequenceDescriptor):
@property
def start_date_text(self):
- return time.strftime("%b %d, %Y", self.start)
+ displayed_start = self._try_parse_time('advertised_start') or self.start
+ return time.strftime("%b %d, %Y", displayed_start)
+
+ @property
+ def end_date_text(self):
+ return time.strftime("%b %d, %Y", self.end)
# An extra property is used rather than the wiki_slug/number because
# there are courses that change the number for different runs. This allows
@@ -374,6 +394,21 @@ class CourseDescriptor(SequenceDescriptor):
more sensible framework later."""
return self.metadata.get('discussion_link', None)
+ @property
+ def forum_posts_allowed(self):
+ try:
+ blackout_periods = [(parse_time(start), parse_time(end))
+ for start, end
+ in self.metadata.get('discussion_blackouts', [])]
+ now = time.gmtime()
+ for start, end in blackout_periods:
+ if start <= now <= end:
+ return False
+ except:
+ log.exception("Error parsing discussion_blackouts for course {0}".format(self.id))
+
+ return True
+
@property
def hide_progress_tab(self):
"""TODO: same as above, intended to let internal CS50 hide the progress tab
@@ -381,6 +416,16 @@ class CourseDescriptor(SequenceDescriptor):
# Explicit comparison to True because we always want to return a bool.
return self.metadata.get('hide_progress_tab') == True
+ @property
+ def end_of_course_survey_url(self):
+ """
+ Pull from policy. Once we have our own survey module set up, can change this to point to an automatically
+ created survey for each class.
+
+ Returns None if no url specified.
+ """
+ return self.metadata.get('end_of_course_survey_url')
+
@property
def title(self):
return self.display_name
@@ -394,3 +439,4 @@ class CourseDescriptor(SequenceDescriptor):
return self.location.org
+
diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss
index af204b2cc1..bf575e74a3 100644
--- a/common/lib/xmodule/xmodule/css/video/display.scss
+++ b/common/lib/xmodule/xmodule/css/video/display.scss
@@ -359,6 +359,34 @@ div.video {
}
}
+ a.quality_control {
+ background: url(../images/hd.png) center no-repeat;
+ border-right: 1px solid #000;
+ @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
+ color: #797979;
+ display: block;
+ float: left;
+ line-height: 46px; //height of play pause buttons
+ margin-left: 0;
+ padding: 0 lh(.5);
+ text-indent: -9999px;
+ @include transition();
+ width: 30px;
+
+ &:hover {
+ background-color: #444;
+ color: #fff;
+ text-decoration: none;
+ }
+
+ &.active {
+ background-color: #F44;
+ color: #0ff;
+ text-decoration: none;
+ }
+ }
+
+
a.hide-subtitles {
background: url('../images/cc.png') center no-repeat;
color: #797979;
diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py
index 00577912c8..cae099845a 100644
--- a/common/lib/xmodule/xmodule/html_module.py
+++ b/common/lib/xmodule/xmodule/html_module.py
@@ -4,7 +4,6 @@ import logging
import os
import sys
from lxml import etree
-from lxml.html import rewrite_links
from path import path
from .x_module import XModule
@@ -29,14 +28,7 @@ class HtmlModule(XModule):
js_module_name = "HTMLModule"
def get_html(self):
- # cdodge: perform link substitutions for any references to course static content (e.g. images)
- _html = self.html
- try:
- _html = rewrite_links(_html, self.rewrite_content_links)
- except:
- logging.error('error rewriting links on the following HTML content: {0}'.format(_html))
-
- return _html
+ return self.html
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
@@ -178,3 +170,25 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
elt = etree.Element('html')
elt.set("filename", relname)
return elt
+
+
+class AboutDescriptor(HtmlDescriptor):
+ """
+ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
+ in order to be able to create new ones
+ """
+ template_dir_name = "about"
+
+class StaticTabDescriptor(HtmlDescriptor):
+ """
+ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
+ in order to be able to create new ones
+ """
+ template_dir_name = "statictab"
+
+class CourseInfoDescriptor(HtmlDescriptor):
+ """
+ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
+ in order to be able to create new ones
+ """
+ template_dir_name = "courseinfo"
diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
index cc389c3fc9..1c0ace9e59 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -216,7 +216,9 @@ class @Problem
for choice in value
@$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true'
else
- @$("#answer_#{key}, #solution_#{key}").html(value)
+ answer = @$("#answer_#{key}, #solution_#{key}")
+ answer.html(value)
+ Collapsible.setCollapsibles(answer)
# TODO remove the above once everything is extracted into its own
# inputtype functions.
diff --git a/common/lib/xmodule/xmodule/js/src/capa/imageinput.js b/common/lib/xmodule/xmodule/js/src/capa/imageinput.js
index 5b4978ee11..173cb50f54 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/imageinput.js
+++ b/common/lib/xmodule/xmodule/js/src/capa/imageinput.js
@@ -11,8 +11,8 @@
function image_input_click(id,event){
iidiv = document.getElementById("imageinput_"+id);
- pos_x = event.offsetX?(event.offsetX):event.pageX-document.iidiv.offsetLeft;
- pos_y = event.offsetY?(event.offsetY):event.pageY-document.iidiv.offsetTop;
+ pos_x = event.offsetX?(event.offsetX):event.pageX-iidiv.offsetLeft;
+ pos_y = event.offsetY?(event.offsetY):event.pageY-iidiv.offsetTop;
result = "[" + pos_x + "," + pos_y + "]";
cx = (pos_x-15) +"px";
cy = (pos_y-15) +"px" ;
diff --git a/common/lib/xmodule/xmodule/js/src/capa/schematic.js b/common/lib/xmodule/xmodule/js/src/capa/schematic.js
index b01f6e12e8..b033dbaf46 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/schematic.js
+++ b/common/lib/xmodule/xmodule/js/src/capa/schematic.js
@@ -1995,7 +1995,7 @@ cktsim = (function() {
// set up each schematic entry widget
function update_schematics() {
// set up each schematic on the page
- var schematics = document.getElementsByClassName('schematic');
+ var schematics = $('.schematic');
for (var i = 0; i < schematics.length; ++i)
if (schematics[i].getAttribute("loaded") != "true") {
try {
@@ -2036,7 +2036,7 @@ function add_schematic_handler(other_onload) {
// ask each schematic input widget to update its value field for submission
function prepare_schematics() {
- var schematics = document.getElementsByClassName('schematic');
+ var schematics = $('.schematic');
for (var i = schematics.length - 1; i >= 0; i--)
schematics[i].schematic.update_value();
}
@@ -3339,23 +3339,28 @@ schematic = (function() {
}
// add method to canvas to compute relative coords for event
- HTMLCanvasElement.prototype.relMouseCoords = function(event){
- // run up the DOM tree to figure out coords for top,left of canvas
- var totalOffsetX = 0;
- var totalOffsetY = 0;
- var currentElement = this;
- do {
- totalOffsetX += currentElement.offsetLeft;
- totalOffsetY += currentElement.offsetTop;
- }
- while (currentElement = currentElement.offsetParent);
-
- // now compute relative position of click within the canvas
- this.mouse_x = event.pageX - totalOffsetX;
- this.mouse_y = event.pageY - totalOffsetY;
-
- this.page_x = event.pageX;
- this.page_y = event.pageY;
+ try {
+ if (HTMLCanvasElement)
+ HTMLCanvasElement.prototype.relMouseCoords = function(event){
+ // run up the DOM tree to figure out coords for top,left of canvas
+ var totalOffsetX = 0;
+ var totalOffsetY = 0;
+ var currentElement = this;
+ do {
+ totalOffsetX += currentElement.offsetLeft;
+ totalOffsetY += currentElement.offsetTop;
+ }
+ while (currentElement = currentElement.offsetParent);
+
+ // now compute relative position of click within the canvas
+ this.mouse_x = event.pageX - totalOffsetX;
+ this.mouse_y = event.pageY - totalOffsetY;
+
+ this.page_x = event.pageX;
+ this.page_y = event.pageY;
+ }
+ }
+ catch (err) { // ignore
}
///////////////////////////////////////////////////////////////////////////////
@@ -4091,48 +4096,52 @@ schematic = (function() {
// add dashed lines!
// from http://davidowens.wordpress.com/2010/09/07/html-5-canvas-and-dashed-lines/
- CanvasRenderingContext2D.prototype.dashedLineTo = function(fromX, fromY, toX, toY, pattern) {
- // Our growth rate for our line can be one of the following:
- // (+,+), (+,-), (-,+), (-,-)
- // Because of this, our algorithm needs to understand if the x-coord and
- // y-coord should be getting smaller or larger and properly cap the values
- // based on (x,y).
- var lt = function (a, b) { return a <= b; };
- var gt = function (a, b) { return a >= b; };
- var capmin = function (a, b) { return Math.min(a, b); };
- var capmax = function (a, b) { return Math.max(a, b); };
-
- var checkX = { thereYet: gt, cap: capmin };
- var checkY = { thereYet: gt, cap: capmin };
-
- if (fromY - toY > 0) {
- checkY.thereYet = lt;
- checkY.cap = capmax;
- }
- if (fromX - toX > 0) {
- checkX.thereYet = lt;
- checkX.cap = capmax;
- }
-
- this.moveTo(fromX, fromY);
- var offsetX = fromX;
- var offsetY = fromY;
- var idx = 0, dash = true;
- while (!(checkX.thereYet(offsetX, toX) && checkY.thereYet(offsetY, toY))) {
- var ang = Math.atan2(toY - fromY, toX - fromX);
- var len = pattern[idx];
-
- offsetX = checkX.cap(toX, offsetX + (Math.cos(ang) * len));
- offsetY = checkY.cap(toY, offsetY + (Math.sin(ang) * len));
-
- if (dash) this.lineTo(offsetX, offsetY);
- else this.moveTo(offsetX, offsetY);
-
- idx = (idx + 1) % pattern.length;
- dash = !dash;
- }
- };
-
+ try {
+ if (CanvasRenderingContext2D)
+ CanvasRenderingContext2D.prototype.dashedLineTo = function(fromX, fromY, toX, toY, pattern) {
+ // Our growth rate for our line can be one of the following:
+ // (+,+), (+,-), (-,+), (-,-)
+ // Because of this, our algorithm needs to understand if the x-coord and
+ // y-coord should be getting smaller or larger and properly cap the values
+ // based on (x,y).
+ var lt = function (a, b) { return a <= b; };
+ var gt = function (a, b) { return a >= b; };
+ var capmin = function (a, b) { return Math.min(a, b); };
+ var capmax = function (a, b) { return Math.max(a, b); };
+
+ var checkX = { thereYet: gt, cap: capmin };
+ var checkY = { thereYet: gt, cap: capmin };
+
+ if (fromY - toY > 0) {
+ checkY.thereYet = lt;
+ checkY.cap = capmax;
+ }
+ if (fromX - toX > 0) {
+ checkX.thereYet = lt;
+ checkX.cap = capmax;
+ }
+
+ this.moveTo(fromX, fromY);
+ var offsetX = fromX;
+ var offsetY = fromY;
+ var idx = 0, dash = true;
+ while (!(checkX.thereYet(offsetX, toX) && checkY.thereYet(offsetY, toY))) {
+ var ang = Math.atan2(toY - fromY, toX - fromX);
+ var len = pattern[idx];
+
+ offsetX = checkX.cap(toX, offsetX + (Math.cos(ang) * len));
+ offsetY = checkY.cap(toY, offsetY + (Math.sin(ang) * len));
+
+ if (dash) this.lineTo(offsetX, offsetY);
+ else this.moveTo(offsetX, offsetY);
+
+ idx = (idx + 1) % pattern.length;
+ dash = !dash;
+ }
+ };
+ }
+ catch (err) { //noop
+ }
// given a range of values, return a new range [vmin',vmax'] where the limits
// have been chosen "nicely". Taken from matplotlib.ticker.LinearLocator
function view_limits(vmin,vmax) {
diff --git a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee
new file mode 100644
index 0000000000..951eb42fce
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee
@@ -0,0 +1,133 @@
+class @SelfAssessment
+ constructor: (element) ->
+ @el = $(element).find('section.self-assessment')
+ @id = @el.data('id')
+ @ajax_url = @el.data('ajax-url')
+ @state = @el.data('state')
+ @allow_reset = @el.data('allow_reset')
+ # valid states: 'initial', 'assessing', 'request_hint', 'done'
+
+ # Where to put the rubric once we load it
+ @errors_area = @$('.error')
+ @answer_area = @$('textarea.answer')
+
+ @rubric_wrapper = @$('.rubric-wrapper')
+ @hint_wrapper = @$('.hint-wrapper')
+ @message_wrapper = @$('.message-wrapper')
+ @submit_button = @$('.submit-button')
+ @reset_button = @$('.reset-button')
+ @reset_button.click @reset
+
+ @find_assessment_elements()
+ @find_hint_elements()
+
+ @rebind()
+
+ # locally scoped jquery.
+ $: (selector) ->
+ $(selector, @el)
+
+ rebind: () =>
+ # rebind to the appropriate function for the current state
+ @submit_button.unbind('click')
+ @submit_button.show()
+ @reset_button.hide()
+ @hint_area.attr('disabled', false)
+ if @state == 'initial'
+ @answer_area.attr("disabled", false)
+ @submit_button.prop('value', 'Submit')
+ @submit_button.click @save_answer
+ else if @state == 'assessing'
+ @answer_area.attr("disabled", true)
+ @submit_button.prop('value', 'Submit assessment')
+ @submit_button.click @save_assessment
+ else if @state == 'request_hint'
+ @answer_area.attr("disabled", true)
+ @submit_button.prop('value', 'Submit hint')
+ @submit_button.click @save_hint
+ else if @state == 'done'
+ @answer_area.attr("disabled", true)
+ @hint_area.attr('disabled', true)
+ @submit_button.hide()
+ if @allow_reset
+ @reset_button.show()
+ else
+ @reset_button.hide()
+
+
+ find_assessment_elements: ->
+ @assessment = @$('select.assessment')
+
+ find_hint_elements: ->
+ @hint_area = @$('textarea.hint')
+
+ save_answer: (event) =>
+ event.preventDefault()
+ if @state == 'initial'
+ data = {'student_answer' : @answer_area.val()}
+ $.postWithPrefix "#{@ajax_url}/save_answer", data, (response) =>
+ if response.success
+ @rubric_wrapper.html(response.rubric_html)
+ @state = 'assessing'
+ @find_assessment_elements()
+ @rebind()
+ else
+ @errors_area.html(response.error)
+ else
+ @errors_area.html('Problem state got out of sync. Try reloading the page.')
+
+ save_assessment: (event) =>
+ event.preventDefault()
+ if @state == 'assessing'
+ data = {'assessment' : @assessment.find(':selected').text()}
+ $.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) =>
+ if response.success
+ @state = response.state
+
+ if @state == 'request_hint'
+ @hint_wrapper.html(response.hint_html)
+ @find_hint_elements()
+ else if @state == 'done'
+ @message_wrapper.html(response.message_html)
+ @allow_reset = response.allow_reset
+
+ @rebind()
+ else
+ @errors_area.html(response.error)
+ else
+ @errors_area.html('Problem state got out of sync. Try reloading the page.')
+
+
+ save_hint: (event) =>
+ event.preventDefault()
+ if @state == 'request_hint'
+ data = {'hint' : @hint_area.val()}
+
+ $.postWithPrefix "#{@ajax_url}/save_hint", data, (response) =>
+ if response.success
+ @message_wrapper.html(response.message_html)
+ @state = 'done'
+ @allow_reset = response.allow_reset
+ @rebind()
+ else
+ @errors_area.html(response.error)
+ else
+ @errors_area.html('Problem state got out of sync. Try reloading the page.')
+
+
+ reset: (event) =>
+ event.preventDefault()
+ if @state == 'done'
+ $.postWithPrefix "#{@ajax_url}/reset", {}, (response) =>
+ if response.success
+ @answer_area.html('')
+ @rubric_wrapper.html('')
+ @hint_wrapper.html('')
+ @message_wrapper.html('')
+ @state = 'initial'
+ @rebind()
+ @reset_button.hide()
+ else
+ @errors_area.html(response.error)
+ else
+ @errors_area.html('Problem state got out of sync. Try reloading the page.')
diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee
index b1e41afc3c..cdd74c5d07 100644
--- a/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee
+++ b/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee
@@ -22,7 +22,7 @@ class @VideoCaption extends Subview
"""
@$('.video-controls .secondary-controls').append """
Captions
- """
+ """#"
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
@fetchCaption()
@@ -144,7 +144,7 @@ class @VideoCaption extends Subview
@el.removeClass('closed')
@scrollCaption()
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
-
+
captionHeight: ->
if @el.hasClass('fullscreen')
$(window).height() - @$('.video-controls').height()
diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_control.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_control.coffee
index 5053f1dcb1..856549c3e2 100644
--- a/common/lib/xmodule/xmodule/js/src/video/display/video_control.coffee
+++ b/common/lib/xmodule/xmodule/js/src/video/display/video_control.coffee
@@ -16,7 +16,7 @@ class @VideoControl extends Subview
Fill Browser