Commit 48cc87e9 by Matt Drayer Committed by Xavier Antoviaque

Added hint/feedback feature to multiple choice response

parent f96e73b1
......@@ -344,12 +344,15 @@ class LoncapaResponse(object):
hintmode = hintgroup.get('mode', 'always')
for hintpart in hintgroup.findall('hintpart'):
if hintpart.get('on') in hints_to_show:
if hintpart.find('text') is not None:
hint_text = hintpart.find('text').text
elif hintpart.text:
hint_text = hintpart.text
# make the hint appear after the last answer box in this
# response
aid = self.answer_ids[-1]
new_cmap.set_hint_and_mode(aid, hint_text, hintmode)
log.debug('after hint: new_cmap = %s', new_cmap)
@abc.abstractmethod
def get_score(self, student_answers):
......@@ -718,6 +721,7 @@ class MultipleChoiceResponse(LoncapaResponse):
# TODO: handle direction and randomize
tags = ['multiplechoiceresponse']
hint_tag = 'choicehint'
max_inputfields = 1
allowed_inputfields = ['choicegroup']
correct_choices = None
......@@ -727,6 +731,10 @@ class MultipleChoiceResponse(LoncapaResponse):
# attributes
self.mc_setup_response()
# These two fields are used for hint matching
self.regexp = True
self.case_insensitive = True
# define correct choices (after calling secondary setup)
xml = self.xml
cxml = xml.xpath('//*[@id=$id]//choice', id=xml.get('id'))
......@@ -771,6 +779,24 @@ class MultipleChoiceResponse(LoncapaResponse):
def get_answers(self):
return {self.answer_id: self.correct_choices}
def check_string(self, expected, given):
flags = re.IGNORECASE if self.case_insensitive else 0
regexp = re.compile('^' + '|'.join(expected) + '$', flags=flags | re.UNICODE)
result = re.search(regexp, given)
return bool(result)
def check_hint_condition(self, hxml_set, student_answers):
# stolen from StringResponse.check_hint_condition
given = student_answers[self.answer_id].strip()
hints_to_show = []
for hxml in hxml_set:
name = hxml.get('name')
hinted_answer = contextualize_text(hxml.get('answer'), self.context).strip()
if self.check_string([hinted_answer], given):
hints_to_show.append(name)
return hints_to_show
@registry.register
class TrueFalseResponse(MultipleChoiceResponse):
......@@ -1050,7 +1076,6 @@ class StringResponse(LoncapaResponse):
]
def setup_response(self):
self.backward = '_or_' in self.xml.get('answer').lower()
self.regexp = False
self.case_insensitive = False
......@@ -1147,7 +1172,6 @@ class CustomResponse(LoncapaResponse):
Custom response. The python code to be run should be in <answer>...</answer>
or in a <script>...</script>
"""
tags = ['customresponse']
allowed_inputfields = ['textline', 'textbox', 'crystallography',
......
<form class="choicegroup capa_inputtype" id="inputtype_${id}">
<style>
#fieldset_container {
margin: 0 auto;
overflow: hidden;
position: relative;
width: 100%;
}
#fieldset {
float: left;
margin: 0 auto;
width: 50%;
}
#fieldset_message {
background: #99CCFF;
color: #FFFFFF;
font-family: arial;
float: right;
height: 100%;
margin: 0 auto;
padding: 10px;
position: absolute;
right: 0;
width: 50%;
}
#fieldset_message_title {
color: #FFFFFF;
font-family: arial;
font-size: 18pt;
}
</style>
<div class="indicator_container">
% if input_type == 'checkbox' or not value:
% if status == 'unsubmitted' or show_correctness == 'never':
......@@ -12,9 +46,9 @@
% endif
% endif
</div>
<div id="fieldset_container">
<div id="fieldset">
<fieldset>
% for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}"
## If the student has selected this choice...
......@@ -59,6 +93,19 @@
% endfor
<span id="answer_${id}"></span>
</fieldset>
</div>
## Message/hint display block -- empty/invisible unless msg is populated
<div
% if (not msg is UNDEFINED) and (len(msg) > 0):
id="fieldset_message"
% endif
>
% if (not msg is UNDEFINED) and (len(msg) > 0):
<span id="fieldset_message_title">Feedback</span>
${msg}
% endif
</div>
</div>
% if show_correctness == "never" and (value or status not in ['unsubmitted']):
<div class="capa_alert">${submitted_message}</div>
......
......@@ -58,6 +58,7 @@ class ResponseXMLFactory(object):
script = kwargs.get('script', None)
num_responses = kwargs.get('num_responses', 1)
num_inputs = kwargs.get('num_inputs', 1)
hints = kwargs.get('hints', None)
# The root is <problem>
root = etree.Element("problem")
......@@ -83,6 +84,12 @@ class ResponseXMLFactory(object):
if not (None == input_element):
response_element.append(input_element)
# Add hintgroup, if specified
if hints is not None and hasattr(self, 'create_hintgroup_element'):
hintgroup_element = self.create_hintgroup_element(**kwargs)
if hintgroup_element is not None:
response_element.append(hintgroup_element)
# The problem has an explanation of the solution
if explanation_text:
explanation = etree.SubElement(root, "solution")
......@@ -161,6 +168,28 @@ class ResponseXMLFactory(object):
return group_element
@staticmethod
def hintgroup_input_xml(**kwargs):
""" Create a <hintgroup> XML element"""
# Gather the troops
choice_names = kwargs.get("choice_names")
hints = kwargs.get("hints")
# Build the <hintgroup> child tree
group_element = etree.Element("hintgroup")
for (choice_name, hint) in zip(choice_names, hints):
choicehint_element = etree.SubElement(group_element, "choicehint")
choicehint_answer = "choice_" + choice_name
choicehint_element.set("answer", choicehint_answer)
choicehint_name = choice_name + "_hint"
choicehint_element.set("name", choicehint_name)
hintpart_element = etree.SubElement(group_element, "hintpart")
hintpart_element.set("on", choicehint_name)
hintpart_element.text = hint
return group_element
class NumericalResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <numericalresponse> XML trees """
......@@ -618,7 +647,15 @@ class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
def create_input_element(self, **kwargs):
""" Create the <choicegroup> element"""
kwargs['choice_type'] = 'multiple'
return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
choice_group = ResponseXMLFactory.choicegroup_input_xml(**kwargs)
return choice_group
def create_hintgroup_element(self, **kwargs):
""" Create the <hintgroup> element"""
hintgroup_element = ResponseXMLFactory.hintgroup_input_xml(**kwargs)
return hintgroup_element
class TrueFalseResponseXMLFactory(ResponseXMLFactory):
......
......@@ -255,6 +255,34 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
xpath = "//div[@class='indicator_container']/span"
self.assert_no_xpath(xml, xpath, self.context)
def test_option_marked_incorrect_with_feedback(self):
"""
Test conditions under which a particular option
(not the entire problem) is marked incorrect, with feedback.
"""
conditions = [
{'input_type': 'radio', 'value': '2'},
{'input_type': 'radio', 'value': ['2']}]
self.context['status'] = 'incorrect'
self.context['msg'] = "This is the feedback"
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
# Should include a choicegroup_incorrect class
xpath = "//label[@class='choicegroup_incorrect']"
self.assert_has_xpath(xml, xpath, self.context)
# Should include a fieldset_message type
xpath = "//div[@id='fieldset_message']"
self.assert_has_xpath(xml, xpath, self.context)
# Should include a fieldset_message_title type
xpath = "//span[@id='fieldset_message_title']"
self.assert_has_xpath(xml, xpath, self.context)
def test_never_show_correctness(self):
"""
Test conditions under which we tell the template to
......
......@@ -93,6 +93,14 @@ class MultiChoiceResponseTest(ResponseTest):
self.assert_grade(problem, 'choice_foil_2', 'correct')
self.assert_grade(problem, 'choice_foil_3', 'incorrect')
def test_named_multiple_choice_grade_with_hint(self):
problem = self.build_problem(choices=[False],
choice_names=["foil_1"],
hints=["h1"])
# Ensure that we get the expected hint
self.assert_grade(problem, 'choice_foil_1', 'incorrect', 'h1')
class TrueFalseResponseTest(ResponseTest):
from capa.tests.response_xml_factory import TrueFalseResponseXMLFactory
......
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