Commit 2532d0df by Matt Drayer Committed by Xavier Antoviaque

capa-mrq: Added hinting/feedback mechanism for multiple choice problem type

- Added hinting/feedback mechanism for multiple choice problem type
- fix styles to make feedback act as an overlay as opposed to a side-by-side block
parent e21d750d
......@@ -347,7 +347,10 @@ class LoncapaResponse(object):
hintmode = hintgroup.get('mode', 'always')
for hintpart in hintgroup.findall('hintpart'):
if hintpart.get('on') in hints_to_show:
hint_text = hintpart.find('text').text
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]
......@@ -718,9 +721,14 @@ class ChoiceResponse(LoncapaResponse):
@registry.register
class MultipleChoiceResponse(LoncapaResponse):
"""
MultipleChoiceResponse is a specialized ChoiceResponse type responsible
for handling all aspects of MCQ behavior, including hinting
"""
# TODO: handle direction and randomize
tags = ['multiplechoiceresponse']
hint_tag = 'choicehint'
max_inputfields = 1
allowed_inputfields = ['choicegroup']
correct_choices = None
......@@ -774,6 +782,29 @@ class MultipleChoiceResponse(LoncapaResponse):
def get_answers(self):
return {self.answer_id: self.correct_choices}
def check_string(self, expected, given):
"""
Attempt to match the given value with what the caller expects
For now we ignore the case -- at some point we might add a switch
We're also a bit lenient in terms of matching at the present time
"""
flags = re.IGNORECASE
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):
......
<%
fieldset_width = 775
fieldset_message_width = 0
if (not msg is UNDEFINED) and (len(msg) > 0):
fieldset_width = 480
fieldset_message_width = 295
%>
<form class="choicegroup capa_inputtype" id="inputtype_${id}">
<style>
#fieldset_container_${id} {
margin: 0 auto;
overflow: hidden;
position: relative;
height: 100;
width: 100%;
}
#fieldset_${id} {
float: left;
margin-right: 20px;
word-wrap: normal;
height: 100%;
}
#fieldset_message_${id} {
background: #5c9dd5;
opacity: 0.9;
color: #FFFFFF;
font-size: 14px;
float: right;
height: 100%;
margin: 0 auto;
padding: 10px;
position: absolute;
right: 10px;
width: 300px;
}
#fieldset_message_title_${id} {
color: #FFFFFF;
font-family: arial;
font-size: 18pt;
}
.fieldset_message_close {
color: #FFFFFF;
float: right;
width: 10px;
height: 10px;
margin-right: -6px;
margin-top: -10px;
}
</style>
<script type="text/javascript">
$( function() {
$('.fieldset_message_close').click(function() {
$(this).parent().hide();
});
});
</script>
<div class="indicator_container">
% if input_type == 'checkbox' or not value:
<span class="status ${status_class if show_correctness != 'never' else 'unanswered'}"
......@@ -16,6 +76,8 @@
</span>
% endif
</div>
<div id="fieldset_container_${id}">
<div id="fieldset_${id}">
<fieldset role="${input_type}group" aria-label="${label}">
......@@ -43,22 +105,39 @@
% elif input_type != 'radio' and choice_id in value:
checked="true"
% endif
% if input_type != 'radio':
aria-multiselectable="true"
% endif
/> ${choice_description}
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
% if status in ('correct', 'incorrect') and not show_correctness=='never':
<span class="sr status">${choice_description|h} - ${status_display}</span>
<%
if status == 'correct':
correctness = 'correct'
elif status == 'incorrect':
correctness = 'incorrect'
else:
correctness = None
%>
% if correctness and not show_correctness=='never':
<span class="sr" aria-describedby="input_${id}_${choice_id}">Status: ${correctness}</span>
% endif
% endif
</label>
% 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_${id}"
% endif
>
<div class="fieldset_message_close">X</div>
% if (not msg is UNDEFINED) and (len(msg) > 0):
${msg}
% endif
</div>
</div>
% if show_correctness == "never" and (value or status not in ['unsubmitted']):
<div class="capa_alert">${submitted_message}</div>
%endif
......
......@@ -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,14 @@ 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):
......
......@@ -258,6 +258,36 @@ 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.context['id'] + "']"
self.assert_has_xpath(xml, xpath, self.context)
# Should include a fieldset_message_title type
xpath = "//span[@id='fieldset_message_title_" + self.context['id'] + "']"
self.assert_has_xpath(xml, xpath, self.context)
def test_never_show_correctness(self):
"""
Test conditions under which we tell the template to
......
......@@ -94,6 +94,15 @@ class MultiChoiceResponseTest(ResponseTest):
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
xml_factory_class = 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