Merge branch 'feature/alex/poll-merged' of github.com:MITx/mitx into feature/alex/poll-merged

This commit is contained in:
Vasyl Nakvasiuk
2013-03-06 19:52:28 +02:00
67 changed files with 1653 additions and 335 deletions

View File

@@ -512,7 +512,9 @@ class LoncapaProblem(object):
# let each Response render itself
if problemtree in self.responders:
return self.responders[problemtree].render_html(self._extract_html)
overall_msg = self.correct_map.get_overall_message()
return self.responders[problemtree].render_html(self._extract_html,
response_msg=overall_msg)
# let each custom renderer render itself:
if problemtree.tag in customrender.registry.registered_tags():

View File

@@ -27,6 +27,7 @@ class CorrectMap(object):
self.cmap = dict()
self.items = self.cmap.items
self.keys = self.cmap.keys
self.overall_message = ""
self.set(*args, **kwargs)
def __getitem__(self, *args, **kwargs):
@@ -104,16 +105,21 @@ class CorrectMap(object):
return self.is_queued(answer_id) and self.cmap[answer_id]['queuestate']['key'] == test_key
def get_queuetime_str(self, answer_id):
return self.cmap[answer_id]['queuestate']['time']
if self.cmap[answer_id]['queuestate']:
return self.cmap[answer_id]['queuestate']['time']
else:
return None
def get_npoints(self, answer_id):
npoints = self.get_property(answer_id, 'npoints')
if npoints is not None:
return npoints
elif self.is_correct(answer_id):
return 1
# if not correct and no points have been assigned, return 0
return 0
""" Return the number of points for an answer:
If the answer is correct, return the assigned
number of points (default: 1 point)
Otherwise, return 0 points """
if self.is_correct(answer_id):
npoints = self.get_property(answer_id, 'npoints')
return npoints if npoints is not None else 1
else:
return 0
def set_property(self, answer_id, property, value):
if answer_id in self.cmap:
@@ -153,3 +159,15 @@ class CorrectMap(object):
if not isinstance(other_cmap, CorrectMap):
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
self.cmap.update(other_cmap.get_dict())
self.set_overall_message(other_cmap.get_overall_message())
def set_overall_message(self, message_str):
""" Set a message that applies to the question as a whole,
rather than to individual inputs. """
self.overall_message = str(message_str) if message_str else ""
def get_overall_message(self):
""" Retrieve a message that applies to the question as a whole.
If no message is available, returns the empty string """
return self.overall_message

View File

@@ -174,13 +174,14 @@ class LoncapaResponse(object):
'''
return sum(self.maxpoints.values())
def render_html(self, renderer):
def render_html(self, renderer, response_msg=''):
'''
Return XHTML Element tree representation of this Response.
Arguments:
- renderer : procedure which produces HTML given an ElementTree
- response_msg: a message displayed at the end of the Response
'''
# render ourself as a <span> + our content
tree = etree.Element('span')
@@ -195,6 +196,11 @@ class LoncapaResponse(object):
if item_xhtml is not None:
tree.append(item_xhtml)
tree.tail = self.xml.tail
# Add a <div> for the message at the end of the response
if response_msg:
tree.append(self._render_response_msg_html(response_msg))
return tree
def evaluate_answers(self, student_answers, old_cmap):
@@ -319,6 +325,29 @@ class LoncapaResponse(object):
def __unicode__(self):
return u'LoncapaProblem Response %s' % self.xml.tag
def _render_response_msg_html(self, response_msg):
""" Render a <div> for a message that applies to the entire response.
*response_msg* is a string, which may contain XHTML markup
Returns an etree element representing the response message <div> """
# First try wrapping the text in a <div> and parsing
# it as an XHTML tree
try:
response_msg_div = etree.XML('<div>%s</div>' % str(response_msg))
# If we can't do that, create the <div> and set the message
# as the text of the <div>
except:
response_msg_div = etree.Element('div')
response_msg_div.text = str(response_msg)
# Set the css class of the message <div>
response_msg_div.set("class", "response_message")
return response_msg_div
#-----------------------------------------------------------------------------
@@ -965,6 +994,7 @@ def sympy_check2():
# not expecting 'unknown's
correct = ['unknown'] * len(idset)
messages = [''] * len(idset)
overall_message = ""
# put these in the context of the check function evaluator
# note that this doesn't help the "cfn" version - only the exec version
@@ -996,6 +1026,10 @@ def sympy_check2():
# the list of messages to be filled in by the check function
'messages': messages,
# a message that applies to the entire response
# instead of a particular input
'overall_message': overall_message,
# any options to be passed to the cfn
'options': self.xml.get('options'),
'testdat': 'hello world',
@@ -1010,6 +1044,7 @@ def sympy_check2():
exec self.code in self.context['global_context'], self.context
correct = self.context['correct']
messages = self.context['messages']
overall_message = self.context['overall_message']
except Exception as err:
print "oops in customresponse (code) error %s" % err
print "context = ", self.context
@@ -1044,34 +1079,100 @@ def sympy_check2():
log.error(traceback.format_exc())
raise Exception("oops in customresponse (cfn) error %s" % err)
log.debug("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
if type(ret) == dict:
correct = ['correct'] * len(idset) if ret['ok'] else ['incorrect'] * len(idset)
msg = ret['msg']
if 1:
# try to clean up message html
msg = '<html>' + msg + '</html>'
msg = msg.replace('&#60;', '&lt;')
#msg = msg.replace('&lt;','<')
msg = etree.tostring(fromstring_bs(msg, convertEntities=None),
pretty_print=True)
#msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
msg = msg.replace('&#13;', '')
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
msg = re.sub('(?ms)<html>(.*)</html>', '\\1', msg)
# One kind of dictionary the check function can return has the
# form {'ok': BOOLEAN, 'msg': STRING}
# If there are multiple inputs, they all get marked
# to the same correct/incorrect value
if 'ok' in ret:
correct = ['correct'] * len(idset) if ret['ok'] else ['incorrect'] * len(idset)
msg = ret.get('msg', None)
msg = self.clean_message_html(msg)
messages[0] = msg
# If there is only one input, apply the message to that input
# Otherwise, apply the message to the whole problem
if len(idset) > 1:
overall_message = msg
else:
messages[0] = msg
# Another kind of dictionary the check function can return has
# the form:
# {'overall_message': STRING,
# 'input_list': [{ 'ok': BOOLEAN, 'msg': STRING }, ...] }
#
# This allows the function to return an 'overall message'
# that applies to the entire problem, as well as correct/incorrect
# status and messages for individual inputs
elif 'input_list' in ret:
overall_message = ret.get('overall_message', '')
input_list = ret['input_list']
correct = []
messages = []
for input_dict in input_list:
correct.append('correct' if input_dict['ok'] else 'incorrect')
msg = self.clean_message_html(input_dict['msg']) if 'msg' in input_dict else None
messages.append(msg)
# Otherwise, we do not recognize the dictionary
# Raise an exception
else:
log.error(traceback.format_exc())
raise Exception("CustomResponse: check function returned an invalid dict")
# The check function can return a boolean value,
# indicating whether all inputs should be marked
# correct or incorrect
else:
correct = ['correct'] * len(idset) if ret else ['incorrect'] * len(idset)
# build map giving "correct"ness of the answer(s)
correct_map = CorrectMap()
overall_message = self.clean_message_html(overall_message)
correct_map.set_overall_message(overall_message)
for k in range(len(idset)):
npoints = self.maxpoints[idset[k]] if correct[k] == 'correct' else 0
correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints)
return correct_map
def clean_message_html(self, msg):
# If *msg* is an empty string, then the code below
# will return "</html>". To avoid this, we first check
# that *msg* is a non-empty string.
if msg:
# When we parse *msg* using etree, there needs to be a root
# element, so we wrap the *msg* text in <html> tags
msg = '<html>' + msg + '</html>'
# Replace < characters
msg = msg.replace('&#60;', '&lt;')
# Use etree to prettify the HTML
msg = etree.tostring(fromstring_bs(msg, convertEntities=None),
pretty_print=True)
msg = msg.replace('&#13;', '')
# Remove the <html> tags we introduced earlier, so we're
# left with just the prettified message markup
msg = re.sub('(?ms)<html>(.*)</html>', '\\1', msg)
# Strip leading and trailing whitespace
return msg.strip()
# If we start with an empty string, then return an empty string
else:
return ""
def get_answers(self):
'''
Give correct answer expected for this response.

View File

@@ -0,0 +1,152 @@
import unittest
from capa.correctmap import CorrectMap
import datetime
class CorrectMapTest(unittest.TestCase):
def setUp(self):
self.cmap = CorrectMap()
def test_set_input_properties(self):
# Set the correctmap properties for two inputs
self.cmap.set(answer_id='1_2_1',
correctness='correct',
npoints=5,
msg='Test message',
hint='Test hint',
hintmode='always',
queuestate={'key':'secretstring',
'time':'20130228100026'})
self.cmap.set(answer_id='2_2_1',
correctness='incorrect',
npoints=None,
msg=None,
hint=None,
hintmode=None,
queuestate=None)
# Assert that each input has the expected properties
self.assertTrue(self.cmap.is_correct('1_2_1'))
self.assertFalse(self.cmap.is_correct('2_2_1'))
self.assertEqual(self.cmap.get_correctness('1_2_1'), 'correct')
self.assertEqual(self.cmap.get_correctness('2_2_1'), 'incorrect')
self.assertEqual(self.cmap.get_npoints('1_2_1'), 5)
self.assertEqual(self.cmap.get_npoints('2_2_1'), 0)
self.assertEqual(self.cmap.get_msg('1_2_1'), 'Test message')
self.assertEqual(self.cmap.get_msg('2_2_1'), None)
self.assertEqual(self.cmap.get_hint('1_2_1'), 'Test hint')
self.assertEqual(self.cmap.get_hint('2_2_1'), None)
self.assertEqual(self.cmap.get_hintmode('1_2_1'), 'always')
self.assertEqual(self.cmap.get_hintmode('2_2_1'), None)
self.assertTrue(self.cmap.is_queued('1_2_1'))
self.assertFalse(self.cmap.is_queued('2_2_1'))
self.assertEqual(self.cmap.get_queuetime_str('1_2_1'), '20130228100026')
self.assertEqual(self.cmap.get_queuetime_str('2_2_1'), None)
self.assertTrue(self.cmap.is_right_queuekey('1_2_1', 'secretstring'))
self.assertFalse(self.cmap.is_right_queuekey('1_2_1', 'invalidstr'))
self.assertFalse(self.cmap.is_right_queuekey('1_2_1', ''))
self.assertFalse(self.cmap.is_right_queuekey('1_2_1', None))
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', 'secretstring'))
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', 'invalidstr'))
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', ''))
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', None))
def test_get_npoints(self):
# Set the correctmap properties for 4 inputs
# 1) correct, 5 points
# 2) correct, None points
# 3) incorrect, 5 points
# 4) incorrect, None points
# 5) correct, 0 points
self.cmap.set(answer_id='1_2_1',
correctness='correct',
npoints=5)
self.cmap.set(answer_id='2_2_1',
correctness='correct',
npoints=None)
self.cmap.set(answer_id='3_2_1',
correctness='incorrect',
npoints=5)
self.cmap.set(answer_id='4_2_1',
correctness='incorrect',
npoints=None)
self.cmap.set(answer_id='5_2_1',
correctness='correct',
npoints=0)
# Assert that we get the expected points
# If points assigned and correct --> npoints
# If no points assigned and correct --> 1 point
# Otherwise --> 0 points
self.assertEqual(self.cmap.get_npoints('1_2_1'), 5)
self.assertEqual(self.cmap.get_npoints('2_2_1'), 1)
self.assertEqual(self.cmap.get_npoints('3_2_1'), 0)
self.assertEqual(self.cmap.get_npoints('4_2_1'), 0)
self.assertEqual(self.cmap.get_npoints('5_2_1'), 0)
def test_set_overall_message(self):
# Default is an empty string string
self.assertEqual(self.cmap.get_overall_message(), "")
# Set a message that applies to the whole question
self.cmap.set_overall_message("Test message")
# Retrieve the message
self.assertEqual(self.cmap.get_overall_message(), "Test message")
# Setting the message to None --> empty string
self.cmap.set_overall_message(None)
self.assertEqual(self.cmap.get_overall_message(), "")
def test_update_from_correctmap(self):
# Initialize a CorrectMap with some properties
self.cmap.set(answer_id='1_2_1',
correctness='correct',
npoints=5,
msg='Test message',
hint='Test hint',
hintmode='always',
queuestate={'key':'secretstring',
'time':'20130228100026'})
self.cmap.set_overall_message("Test message")
# Create a second cmap, then update it to have the same properties
# as the first cmap
other_cmap = CorrectMap()
other_cmap.update(self.cmap)
# Assert that it has all the same properties
self.assertEqual(other_cmap.get_overall_message(),
self.cmap.get_overall_message())
self.assertEqual(other_cmap.get_dict(),
self.cmap.get_dict())
def test_update_from_invalid(self):
# Should get an exception if we try to update() a CorrectMap
# with a non-CorrectMap value
invalid_list = [None, "string", 5, datetime.datetime.today()]
for invalid in invalid_list:
with self.assertRaises(Exception):
self.cmap.update(invalid)

View File

@@ -0,0 +1,195 @@
import unittest
from lxml import etree
import os
import textwrap
import json
import mock
from capa.capa_problem import LoncapaProblem
from response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory
from . import test_system
class CapaHtmlRenderTest(unittest.TestCase):
def test_include_html(self):
# Create a test file to include
self._create_test_file('test_include.xml',
'<test>Test include</test>')
# Generate some XML with an <include>
xml_str = textwrap.dedent("""
<problem>
<include file="test_include.xml"/>
</problem>
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
# Expect that the include file was embedded in the problem
test_element = rendered_html.find("test")
self.assertEqual(test_element.tag, "test")
self.assertEqual(test_element.text, "Test include")
def test_process_outtext(self):
# Generate some XML with <startouttext /> and <endouttext />
xml_str = textwrap.dedent("""
<problem>
<startouttext/>Test text<endouttext/>
</problem>
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
# Expect that the <startouttext /> and <endouttext />
# were converted to <span></span> tags
span_element = rendered_html.find('span')
self.assertEqual(span_element.text, 'Test text')
def test_render_script(self):
# Generate some XML with a <script> tag
xml_str = textwrap.dedent("""
<problem>
<script>test=True</script>
</problem>
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
# Expect that the script element has been removed from the rendered HTML
script_element = rendered_html.find('script')
self.assertEqual(None, script_element)
def test_render_response_xml(self):
# Generate some XML for a string response
kwargs = {'question_text': "Test question",
'explanation_text': "Test explanation",
'answer': 'Test answer',
'hints': [('test prompt', 'test_hint', 'test hint text')]}
xml_str = StringResponseXMLFactory().build_xml(**kwargs)
# Mock out the template renderer
test_system.render_template = mock.Mock()
test_system.render_template.return_value = "<div>Input Template Render</div>"
# Create the problem and render the HTML
problem = LoncapaProblem(xml_str, '1', system=test_system)
rendered_html = etree.XML(problem.get_html())
# Expect problem has been turned into a <div>
self.assertEqual(rendered_html.tag, "div")
# Expect question text is in a <p> child
question_element = rendered_html.find("p")
self.assertEqual(question_element.text, "Test question")
# Expect that the response has been turned into a <span>
response_element = rendered_html.find("span")
self.assertEqual(response_element.tag, "span")
# Expect that the response <span>
# that contains a <div> for the textline
textline_element = response_element.find("div")
self.assertEqual(textline_element.text, 'Input Template Render')
# Expect a child <div> for the solution
# with the rendered template
solution_element = rendered_html.find("div")
self.assertEqual(solution_element.text, 'Input Template Render')
# Expect that the template renderer was called with the correct
# arguments, once for the textline input and once for
# the solution
expected_textline_context = {'status': 'unsubmitted',
'value': '',
'preprocessor': None,
'msg': '',
'inline': False,
'hidden': False,
'do_math': False,
'id': '1_2_1',
'size': None}
expected_solution_context = {'id': '1_solution_1'}
expected_calls = [mock.call('textline.html', expected_textline_context),
mock.call('solutionspan.html', expected_solution_context)]
self.assertEqual(test_system.render_template.call_args_list,
expected_calls)
def test_render_response_with_overall_msg(self):
# CustomResponse script that sets an overall_message
script=textwrap.dedent("""
def check_func(*args):
msg = '<p>Test message 1<br /></p><p>Test message 2</p>'
return {'overall_message': msg,
'input_list': [ {'ok': True, 'msg': '' } ] }
""")
# Generate some XML for a CustomResponse
kwargs = {'script':script, 'cfn': 'check_func'}
xml_str = CustomResponseXMLFactory().build_xml(**kwargs)
# Create the problem and render the html
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Grade the problem
correctmap = problem.grade_answers({'1_2_1': 'test'})
# Render the html
rendered_html = etree.XML(problem.get_html())
# Expect that there is a <div> within the response <div>
# with css class response_message
msg_div_element = rendered_html.find(".//div[@class='response_message']")
self.assertEqual(msg_div_element.tag, "div")
self.assertEqual(msg_div_element.get('class'), "response_message")
# Expect that the <div> contains our message (as part of the XML tree)
msg_p_elements = msg_div_element.findall('p')
self.assertEqual(msg_p_elements[0].tag, "p")
self.assertEqual(msg_p_elements[0].text, "Test message 1")
self.assertEqual(msg_p_elements[1].tag, "p")
self.assertEqual(msg_p_elements[1].text, "Test message 2")
def test_substitute_python_vars(self):
# Generate some XML with Python variables defined in a script
# and used later as attributes
xml_str = textwrap.dedent("""
<problem>
<script>test="TEST"</script>
<span attr="$test"></span>
</problem>
""")
# Create the problem and render the HTML
problem = LoncapaProblem(xml_str, '1', system=test_system)
rendered_html = etree.XML(problem.get_html())
# Expect that the variable $test has been replaced with its value
span_element = rendered_html.find('span')
self.assertEqual(span_element.get('attr'), "TEST")
def _create_test_file(self, path, content_str):
test_fp = test_system.filestore.open(path, "w")
test_fp.write(content_str)
test_fp.close()
self.addCleanup(lambda: os.remove(test_fp.name))

View File

@@ -8,6 +8,7 @@ import json
from nose.plugins.skip import SkipTest
import os
import unittest
import textwrap
from . import test_system
@@ -663,30 +664,43 @@ class CustomResponseTest(ResponseTest):
# Inline code can update the global messages list
# to pass messages to the CorrectMap for a particular input
inline_script = """messages[0] = "Test Message" """
# The code can also set the global overall_message (str)
# to pass a message that applies to the whole response
inline_script = textwrap.dedent("""
messages[0] = "Test Message"
overall_message = "Overall message"
""")
problem = self.build_problem(answer=inline_script)
input_dict = {'1_2_1': '0'}
msg = problem.grade_answers(input_dict).get_msg('1_2_1')
self.assertEqual(msg, "Test Message")
correctmap = problem.grade_answers(input_dict)
def test_function_code(self):
# Check that the message for the particular input was received
input_msg = correctmap.get_msg('1_2_1')
self.assertEqual(input_msg, "Test Message")
# For function code, we pass in three arguments:
# Check that the overall message (for the whole response) was received
overall_msg = correctmap.get_overall_message()
self.assertEqual(overall_msg, "Overall message")
def test_function_code_single_input(self):
# For function code, we pass in these arguments:
#
# 'expect' is the expect attribute of the <customresponse>
#
# 'answer_given' is the answer the student gave (if there is just one input)
# or an ordered list of answers (if there are multiple inputs)
#
# 'student_answers' is a dictionary of answers by input ID
#
#
# The function should return a dict of the form
# { 'ok': BOOL, 'msg': STRING }
#
script = """def check_func(expect, answer_given, student_answers):
return {'ok': answer_given == expect, 'msg': 'Message text'}"""
script = textwrap.dedent("""
def check_func(expect, answer_given):
return {'ok': answer_given == expect, 'msg': 'Message text'}
""")
problem = self.build_problem(script=script, cfn="check_func", expect="42")
@@ -698,7 +712,7 @@ class CustomResponseTest(ResponseTest):
msg = correct_map.get_msg('1_2_1')
self.assertEqual(correctness, 'correct')
self.assertEqual(msg, "Message text\n")
self.assertEqual(msg, "Message text")
# Incorrect answer
input_dict = {'1_2_1': '0'}
@@ -708,19 +722,108 @@ class CustomResponseTest(ResponseTest):
msg = correct_map.get_msg('1_2_1')
self.assertEqual(correctness, 'incorrect')
self.assertEqual(msg, "Message text\n")
self.assertEqual(msg, "Message text")
def test_multiple_inputs(self):
def test_function_code_multiple_input_no_msg(self):
# Check functions also have the option of returning
# a single boolean value
# If true, mark all the inputs correct
# If false, mark all the inputs incorrect
script = textwrap.dedent("""
def check_func(expect, answer_given):
return (answer_given[0] == expect and
answer_given[1] == expect)
""")
problem = self.build_problem(script=script, cfn="check_func",
expect="42", num_inputs=2)
# Correct answer -- expect both inputs marked correct
input_dict = {'1_2_1': '42', '1_2_2': '42'}
correct_map = problem.grade_answers(input_dict)
correctness = correct_map.get_correctness('1_2_1')
self.assertEqual(correctness, 'correct')
correctness = correct_map.get_correctness('1_2_2')
self.assertEqual(correctness, 'correct')
# One answer incorrect -- expect both inputs marked incorrect
input_dict = {'1_2_1': '0', '1_2_2': '42'}
correct_map = problem.grade_answers(input_dict)
correctness = correct_map.get_correctness('1_2_1')
self.assertEqual(correctness, 'incorrect')
correctness = correct_map.get_correctness('1_2_2')
self.assertEqual(correctness, 'incorrect')
def test_function_code_multiple_inputs(self):
# If the <customresponse> has multiple inputs associated with it,
# the check function can return a dict of the form:
#
# {'overall_message': STRING,
# 'input_list': [{'ok': BOOL, 'msg': STRING}, ...] }
#
# 'overall_message' is displayed at the end of the response
#
# 'input_list' contains dictionaries representing the correctness
# and message for each input.
script = textwrap.dedent("""
def check_func(expect, answer_given):
check1 = (int(answer_given[0]) == 1)
check2 = (int(answer_given[1]) == 2)
check3 = (int(answer_given[2]) == 3)
return {'overall_message': 'Overall message',
'input_list': [
{'ok': check1, 'msg': 'Feedback 1'},
{'ok': check2, 'msg': 'Feedback 2'},
{'ok': check3, 'msg': 'Feedback 3'} ] }
""")
problem = self.build_problem(script=script,
cfn="check_func", num_inputs=3)
# Grade the inputs (one input incorrect)
input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3' }
correct_map = problem.grade_answers(input_dict)
# Expect that we receive the overall message (for the whole response)
self.assertEqual(correct_map.get_overall_message(), "Overall message")
# Expect that the inputs were graded individually
self.assertEqual(correct_map.get_correctness('1_2_1'), 'incorrect')
self.assertEqual(correct_map.get_correctness('1_2_2'), 'correct')
self.assertEqual(correct_map.get_correctness('1_2_3'), 'correct')
# Expect that we received messages for each individual input
self.assertEqual(correct_map.get_msg('1_2_1'), 'Feedback 1')
self.assertEqual(correct_map.get_msg('1_2_2'), 'Feedback 2')
self.assertEqual(correct_map.get_msg('1_2_3'), 'Feedback 3')
def test_multiple_inputs_return_one_status(self):
# When given multiple inputs, the 'answer_given' argument
# to the check_func() is a list of inputs
#
# The sample script below marks the problem as correct
# if and only if it receives answer_given=[1,2,3]
# (or string values ['1','2','3'])
script = """def check_func(expect, answer_given, student_answers):
check1 = (int(answer_given[0]) == 1)
check2 = (int(answer_given[1]) == 2)
check3 = (int(answer_given[2]) == 3)
return {'ok': (check1 and check2 and check3), 'msg': 'Message text'}"""
#
# Since we return a dict describing the status of one input,
# we expect that the same 'ok' value is applied to each
# of the inputs.
script = textwrap.dedent("""
def check_func(expect, answer_given):
check1 = (int(answer_given[0]) == 1)
check2 = (int(answer_given[1]) == 2)
check3 = (int(answer_given[2]) == 3)
return {'ok': (check1 and check2 and check3),
'msg': 'Message text'}
""")
problem = self.build_problem(script=script,
cfn="check_func", num_inputs=3)
@@ -743,6 +846,37 @@ class CustomResponseTest(ResponseTest):
self.assertEqual(correct_map.get_correctness('1_2_2'), 'correct')
self.assertEqual(correct_map.get_correctness('1_2_3'), 'correct')
# Message is interpreted as an "overall message"
self.assertEqual(correct_map.get_overall_message(), 'Message text')
def test_script_exception(self):
# Construct a script that will raise an exception
script = textwrap.dedent("""
def check_func(expect, answer_given):
raise Exception("Test")
""")
problem = self.build_problem(script=script, cfn="check_func")
# Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception):
problem.grade_answers({'1_2_1': '42'})
def test_invalid_dict_exception(self):
# Construct a script that passes back an invalid dict format
script = textwrap.dedent("""
def check_func(expect, answer_given):
return {'invalid': 'test'}
""")
problem = self.build_problem(script=script, cfn="check_func")
# Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception):
problem.grade_answers({'1_2_1': '42'})
class SchematicResponseTest(ResponseTest):
from response_xml_factory import SchematicResponseXMLFactory

View File

@@ -35,7 +35,8 @@ class StaticContent(object):
@staticmethod
def compute_location(org, course, name, revision=None, is_thumbnail=False):
name = name.replace('/', '_')
return Location([XASSET_LOCATION_TAG, org, course, 'asset' if not is_thumbnail else 'thumbnail', Location.clean(name), revision])
return Location([XASSET_LOCATION_TAG, org, course, 'asset' if not is_thumbnail else 'thumbnail',
Location.clean_keeping_underscores(name), revision])
def get_id(self):
return StaticContent.get_id_from_location(self.location)

View File

@@ -225,7 +225,7 @@ class CourseDescriptor(SequenceDescriptor):
# NOTE (THK): This is a last-minute addition for Fall 2012 launch to dynamically
# disable the syllabus content for courses that do not provide a syllabus
self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
self._grading_policy = {}
self.set_grading_policy(self.grading_policy)
self.test_center_exams = []
@@ -295,9 +295,9 @@ class CourseDescriptor(SequenceDescriptor):
grading_policy.update(course_policy)
# Here is where we should parse any configurations, so that we can fail early
grading_policy['RAW_GRADER'] = grading_policy['GRADER'] # used for cms access
grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER'])
self._grading_policy = grading_policy
# Use setters so that side effecting to .definitions works
self.raw_grader = grading_policy['GRADER'] # used for cms access
self.grade_cutoffs = grading_policy['GRADE_CUTOFFS']
@classmethod
def read_grading_policy(cls, paths, system):
@@ -390,7 +390,7 @@ class CourseDescriptor(SequenceDescriptor):
@property
def grader(self):
return self._grading_policy['GRADER']
return grader_from_conf(self.raw_grader)
@property
def raw_grader(self):

View File

@@ -71,6 +71,17 @@ class Location(_LocationBase):
"""
return Location._clean(value, INVALID_CHARS)
@staticmethod
def clean_keeping_underscores(value):
"""
Return value, replacing INVALID_CHARS, but not collapsing multiple '_' chars.
This for cleaning asset names, as the YouTube ID's may have underscores in them, and we need the
transcript asset name to match. In the future we may want to change the behavior of _clean.
"""
return INVALID_CHARS.sub('_', value)
@staticmethod
def clean_for_url_name(value):
"""

View File

@@ -139,7 +139,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
location = Location(location)
json_data = self.module_data.get(location)
if json_data is None:
return self.modulestore.get_item(location)
module = self.modulestore.get_item(location)
if module is not None:
# update our own cache after going to the DB to get cache miss
self.module_data.update(module.system.module_data)
return module
else:
# load the module and apply the inherited metadata
try:

View File

@@ -77,6 +77,9 @@ class CombinedOpenEndedV1Module():
INTERMEDIATE_DONE = 'intermediate_done'
DONE = 'done'
#Where the templates live for this problem
TEMPLATE_DIR = "combinedopenended"
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, metadata = None, static_data = None, **kwargs):
@@ -334,7 +337,7 @@ class CombinedOpenEndedV1Module():
Output: rendered html
"""
context = self.get_context()
html = self.system.render_template('combined_open_ended.html', context)
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
return html
def get_html_nonsystem(self):
@@ -345,7 +348,7 @@ class CombinedOpenEndedV1Module():
Output: HTML rendered directly via Mako
"""
context = self.get_context()
html = self.system.render_template('combined_open_ended.html', context)
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
return html
def get_html_base(self):
@@ -522,7 +525,7 @@ class CombinedOpenEndedV1Module():
'task_name' : 'Scored Rubric',
'class_name' : 'combined-rubric-container'
}
html = self.system.render_template('combined_open_ended_results.html', context)
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
def get_legend(self, get):
@@ -534,7 +537,7 @@ class CombinedOpenEndedV1Module():
context = {
'legend_list' : LEGEND_LIST,
}
html = self.system.render_template('combined_open_ended_legend.html', context)
html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
def get_results(self, get):
@@ -565,7 +568,7 @@ class CombinedOpenEndedV1Module():
'submission_id' : ri['submission_ids'][i],
}
context_list.append(context)
feedback_table = self.system.render_template('open_ended_result_table.html', {
feedback_table = self.system.render_template('{0}/open_ended_result_table.html'.format(self.TEMPLATE_DIR), {
'context_list' : context_list,
'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT,
'human_grader_types' : HUMAN_GRADER_TYPE,
@@ -577,7 +580,7 @@ class CombinedOpenEndedV1Module():
'task_name' : "Feedback",
'class_name' : "result-container",
}
html = self.system.render_template('combined_open_ended_results.html', context)
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
def get_status_ajax(self, get):
@@ -691,7 +694,7 @@ class CombinedOpenEndedV1Module():
'legend_list' : LEGEND_LIST,
'render_via_ajax' : render_via_ajax,
}
status_html = self.system.render_template("combined_open_ended_status.html", context)
status_html = self.system.render_template("{0}/combined_open_ended_status.html".format(self.TEMPLATE_DIR), context)
return status_html

View File

@@ -30,6 +30,8 @@ class RubricParsingError(Exception):
class CombinedOpenEndedRubric(object):
TEMPLATE_DIR = "combinedopenended/openended"
def __init__ (self, system, view_only = False):
self.has_score = False
self.view_only = view_only
@@ -57,9 +59,9 @@ class CombinedOpenEndedRubric(object):
rubric_scores = [cat['score'] for cat in rubric_categories]
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
max_score = max(max_scores)
rubric_template = 'open_ended_rubric.html'
rubric_template = '{0}/open_ended_rubric.html'.format(self.TEMPLATE_DIR)
if self.view_only:
rubric_template = 'open_ended_view_only_rubric.html'
rubric_template = '{0}/open_ended_view_only_rubric.html'.format(self.TEMPLATE_DIR)
html = self.system.render_template(rubric_template,
{'categories': rubric_categories,
'has_score': self.has_score,
@@ -207,7 +209,7 @@ class CombinedOpenEndedRubric(object):
for grader_type in tuple[3]:
rubric_categories[i]['options'][j]['grader_types'].append(grader_type)
html = self.system.render_template('open_ended_combined_rubric.html',
html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
{'categories': rubric_categories,
'has_score': True,
'view_only': True,

View File

@@ -40,6 +40,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
</openended>
"""
TEMPLATE_DIR = "combinedopenended/openended"
def setup_response(self, system, location, definition, descriptor):
"""
Sets up the response type.
@@ -397,10 +399,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_scores = rubric_dict['rubric_scores']
if not response_items['success']:
return system.render_template("open_ended_error.html",
return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
{'errors': feedback})
feedback_template = system.render_template("open_ended_feedback.html", {
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
'grader_type': response_items['grader_type'],
'score': "{0} / {1}".format(response_items['score'], self.max_score()),
'feedback': feedback,
@@ -558,7 +560,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
@return: Rendered html
"""
context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50}
html = system.render_template('open_ended_evaluation.html', context)
html = system.render_template('{0}/open_ended_evaluation.html'.format(self.TEMPLATE_DIR), context)
return html
def handle_ajax(self, dispatch, get, system):
@@ -692,7 +694,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'accept_file_upload': self.accept_file_upload,
'eta_message' : eta_string,
}
html = system.render_template('open_ended.html', context)
html = system.render_template('{0}/open_ended.html'.format(self.TEMPLATE_DIR), context)
return html

View File

@@ -3,11 +3,8 @@ import logging
from lxml import etree
from xmodule.capa_module import ComplexEncoder
from xmodule.editing_module import EditingDescriptor
from xmodule.progress import Progress
from xmodule.stringify import stringify_children
from xmodule.xml_module import XmlDescriptor
from xblock.core import List, Integer, String, Scope
import openendedchild
from combined_open_ended_rubric import CombinedOpenEndedRubric
@@ -33,27 +30,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
</selfassessment>
"""
# states
INITIAL = 'initial'
ASSESSING = 'assessing'
REQUEST_HINT = 'request_hint'
DONE = 'done'
student_answers = List(scope=Scope.student_state, default=[])
scores = List(scope=Scope.student_state, default=[])
hints = List(scope=Scope.student_state, default=[])
state = String(scope=Scope.student_state, default=INITIAL)
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
max_score = Integer(scope=Scope.settings, default=openendedchild.MAX_SCORE)
max_attempts = Integer(scope=Scope.settings, default=openendedchild.MAX_ATTEMPTS)
attempts = Integer(scope=Scope.student_state, default=0)
rubric = String(scope=Scope.content)
prompt = String(scope=Scope.content)
submitmessage = String(scope=Scope.content)
hintprompt = String(scope=Scope.content)
TEMPLATE_DIR = "combinedopenended/selfassessment"
def setup_response(self, system, location, definition, descriptor):
"""
@@ -91,7 +68,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
'accept_file_upload': self.accept_file_upload,
}
html = system.render_template('self_assessment_prompt.html', context)
html = system.render_template('{0}/self_assessment_prompt.html'.format(self.TEMPLATE_DIR), context)
return html
@@ -152,7 +129,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
#This is a dev_facing_error
raise ValueError("Self assessment module is in an illegal state '{0}'".format(self.state))
return system.render_template('self_assessment_rubric.html', context)
return system.render_template('{0}/self_assessment_rubric.html'.format(self.TEMPLATE_DIR), context)
def get_hint_html(self, system):
"""
@@ -178,7 +155,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
#This is a dev_facing_error
raise ValueError("Self Assessment module is in an illegal state '{0}'".format(self.state))
return system.render_template('self_assessment_hint.html', context)
return system.render_template('{0}/self_assessment_hint.html'.format(self.TEMPLATE_DIR), context)
def save_answer(self, get, system):

View File

@@ -460,6 +460,9 @@ class PeerGradingModule(XModule):
#This is a student_facing_error
error_text = "Could not get list of problems to peer grade. Please notify course staff."
success = False
except:
log.exception("Could not contact peer grading service.")
success = False
def _find_corresponding_module_for_location(location):
@@ -562,3 +565,40 @@ class PeerGradingDescriptor(RawDescriptor):
stores_state = True
has_score = True
template_dir_name = "peer_grading"
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
Pull out the individual tasks, the rubric, and the prompt, and parse
Returns:
{
'rubric': 'some-html',
'prompt': 'some-html',
'task_xml': dictionary of xml strings,
}
"""
expected_children = []
for child in expected_children:
if len(xml_object.xpath(child)) == 0:
#This is a staff_facing_error
raise ValueError("Peer grading definition must include at least one '{0}' tag. Contact the learning sciences group for assistance.".format(child))
def parse_task(k):
"""Assumes that xml_object has child k"""
return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0, len(xml_object.xpath(k)))]
def parse(k):
"""Assumes that xml_object has child k"""
return xml_object.xpath(k)[0]
return {}, []
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('peergrading')
return elt

View File

@@ -0,0 +1,44 @@
---
metadata:
display_name: Open Ended Response
max_attempts: 1
max_score: 1
is_graded: False
version: 1
display_name: Open Ended Response
skip_spelling_checks: False
accept_file_upload: False
data: |
<combinedopenended>
<rubric>
<rubric>
<category>
<description>Category 1</description>
<option>
The response does not incorporate what is needed for a one response.
</option>
<option>
The response is correct for category 1.
</option>
</category>
</rubric>
</rubric>
<prompt>
<p>Why is the sky blue?</p>
</prompt>
<task>
<selfassessment/>
</task>
<task>
<openended min_score_to_attempt="1" max_score_to_attempt="2">
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "peer_grading.conf", "problem_id" : "700x/Demo"}</grader_payload>
</openendedparam>
</openended>
</task>
</combinedopenended>
children: []

View File

@@ -0,0 +1,13 @@
---
metadata:
display_name: Peer Grading Interface
attempts: 1
use_for_single_location: False
link_to_location: None
is_graded: False
max_grade: 1
data: |
<peergrading>
</peergrading>
children: []

View File

@@ -19,9 +19,14 @@ class ContentTest(unittest.TestCase):
content = StaticContent('loc', 'name', 'content_type', 'data')
self.assertIsNone(content.thumbnail_location)
def test_generate_thumbnail_nonimage(self):
def test_generate_thumbnail_image(self):
contentStore = ContentStore()
content = Content(Location(u'c4x', u'mitX', u'800', u'asset', u'monsters.jpg'), None)
content = Content(Location(u'c4x', u'mitX', u'800', u'asset', u'monsters__.jpg'), None)
(thumbnail_content, thumbnail_file_location) = contentStore.generate_thumbnail(content)
self.assertIsNone(thumbnail_content)
self.assertEqual(Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg'), thumbnail_file_location)
self.assertEqual(Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters__.jpg'), thumbnail_file_location)
def test_compute_location(self):
# We had a bug that __ got converted into a single _. Make sure that substitution of INVALID_CHARS (like space)
# still happen.
asset_location = StaticContent.compute_location('mitX', '400', 'subs__1eo_jXvZnE .srt.sjson')
self.assertEqual(Location(u'c4x', u'mitX', u'400', u'asset', u'subs__1eo_jXvZnE_.srt.sjson', None), asset_location)