ChoiceGroup refactor
- Make it into a class. - Combine ChoiceGroup, RadioGroup, CheckboxGroup implementation. (All three tags still work--this just unifies the code) - add tests
This commit is contained in:
@@ -97,8 +97,8 @@ class InputTypeBase(object):
|
||||
have a render_template function.
|
||||
- xml : Element tree of this Input element
|
||||
- state : a dictionary with optional keys:
|
||||
* 'value'
|
||||
* 'id'
|
||||
* 'value' -- the current value of this input (what the student entered last time)
|
||||
* 'id' -- the id of this input, typically "{problem-location}_{response-num}_{input-num}"
|
||||
* 'status' (answered, unanswered, unsubmitted)
|
||||
* 'feedback' (dictionary containing keys for hints, errors, or other
|
||||
feedback from previous attempt. Specifically 'message', 'hint', 'hintmode'. If 'hintmode'
|
||||
@@ -234,49 +234,70 @@ register_input_class(OptionInput)
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
def choicegroup(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Radio button inputs: multiple choice or true/false
|
||||
|
||||
class ChoiceGroup(InputTypeBase):
|
||||
"""
|
||||
Radio button or checkbox inputs: multiple choice or true/false
|
||||
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use
|
||||
"location" attribute, ie random, top, bottom.
|
||||
'''
|
||||
eid = element.get('id')
|
||||
if element.get('type') == "MultipleChoice":
|
||||
element_type = "radio"
|
||||
elif element.get('type') == "TrueFalse":
|
||||
element_type = "checkbox"
|
||||
else:
|
||||
element_type = "radio"
|
||||
choices = []
|
||||
for choice in element:
|
||||
if not choice.tag == 'choice':
|
||||
raise Exception("[courseware.capa.inputtypes.choicegroup] "
|
||||
"Error: only <choice> tags should be immediate children "
|
||||
"of a <choicegroup>, found %s instead" % choice.tag)
|
||||
ctext = ""
|
||||
# TODO: what if choice[0] has math tags in it?
|
||||
ctext += ''.join([etree.tostring(x) for x in choice])
|
||||
if choice.text is not None:
|
||||
# TODO: fix order?
|
||||
ctext += choice.text
|
||||
choices.append((choice.get("name"), ctext))
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'input_type': element_type,
|
||||
'choices': choices,
|
||||
'name_array_suffix': ''}
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
_reg(choicegroup)
|
||||
Example:
|
||||
|
||||
<choicegroup>
|
||||
<choice correct="false" name="foil1">
|
||||
<text>This is foil One.</text>
|
||||
</choice>
|
||||
<choice correct="false" name="foil2">
|
||||
<text>This is foil Two.</text>
|
||||
</choice>
|
||||
<choice correct="true" name="foil3">
|
||||
<text>This is foil Three.</text>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
"""
|
||||
template = "choicegroup.html"
|
||||
tags = ['choicegroup', 'radiogroup', 'checkboxgroup']
|
||||
|
||||
def __init__(self, system, xml, state):
|
||||
super(ChoiceGroup, self).__init__(system, xml, state)
|
||||
|
||||
if self.tag == 'choicegroup':
|
||||
self.suffix = ''
|
||||
if self.xml.get('type') == "MultipleChoice":
|
||||
self.element_type = "radio"
|
||||
elif self.xml.get('type') == "TrueFalse":
|
||||
# Huh? Why TrueFalse->checkbox? Each input can be true / false separately?
|
||||
self.element_type = "checkbox"
|
||||
else:
|
||||
self.element_type = "radio"
|
||||
|
||||
elif self.tag == 'radiogroup':
|
||||
self.element_type = "radio"
|
||||
self.suffix = '[]'
|
||||
elif self.tag == 'checkboxgroup':
|
||||
self.element_type = "checkbox"
|
||||
self.suffix = '[]'
|
||||
else:
|
||||
raise Exception("ChoiceGroup: unexpected tag {0}".format(self.tag))
|
||||
|
||||
self.choices = extract_choices(self.xml)
|
||||
|
||||
def _get_render_context(self):
|
||||
context = {'id': self.id,
|
||||
'value': self.value,
|
||||
'state': self.status,
|
||||
'input_type': self.element_type,
|
||||
'choices': self.choices,
|
||||
'name_array_suffix': self.suffix}
|
||||
return context
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
def extract_choices(element):
|
||||
'''
|
||||
Extracts choices for a few input types, such as radiogroup and
|
||||
checkboxgroup.
|
||||
Extracts choices for a few input types, such as ChoiceGroup, RadioGroup and
|
||||
CheckboxGroup.
|
||||
|
||||
returns list of (choice_name, choice_text) tuples
|
||||
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use
|
||||
"location" attribute, ie random, top, bottom.
|
||||
@@ -285,63 +306,25 @@ def extract_choices(element):
|
||||
choices = []
|
||||
|
||||
for choice in element:
|
||||
if not choice.tag == 'choice':
|
||||
if choice.tag != 'choice':
|
||||
raise Exception("[courseware.capa.inputtypes.extract_choices] \
|
||||
Expected a <choice> tag; got %s instead"
|
||||
% choice.tag)
|
||||
choice_text = ''.join([etree.tostring(x) for x in choice])
|
||||
if choice.text is not None:
|
||||
# TODO: fix order?
|
||||
choice_text += choice.text
|
||||
|
||||
choices.append((choice.get("name"), choice_text))
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
register_input_class(ChoiceGroup)
|
||||
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
def radiogroup(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Radio button inputs: (multiple choice)
|
||||
'''
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
eid = element.get('id')
|
||||
|
||||
choices = extract_choices(element)
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'input_type': 'radio',
|
||||
'choices': choices,
|
||||
'name_array_suffix': '[]'}
|
||||
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
_reg(radiogroup)
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
def checkboxgroup(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Checkbox inputs: (select one or more choices)
|
||||
'''
|
||||
|
||||
eid = element.get('id')
|
||||
|
||||
choices = extract_choices(element)
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'input_type': 'checkbox',
|
||||
'choices': choices,
|
||||
'name_array_suffix': '[]'}
|
||||
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
_reg(checkboxgroup)
|
||||
|
||||
def javascriptinput(element, value, status, render_template, msg='null'):
|
||||
'''
|
||||
|
||||
@@ -35,7 +35,7 @@ class OptionInputTest(unittest.TestCase):
|
||||
state = {'value': 'Down',
|
||||
'id': 'sky_input',
|
||||
'status': 'answered'}
|
||||
option_input = inputtypes.OptionInput(system, element, state)
|
||||
option_input = inputtypes.get_class_for_tag('optioninput')(system, element, state)
|
||||
|
||||
context = option_input._get_render_context()
|
||||
|
||||
@@ -53,40 +53,81 @@ class ChoiceGroupTest(unittest.TestCase):
|
||||
Test choice groups.
|
||||
'''
|
||||
def test_mult_choice(self):
|
||||
xml_str = """
|
||||
<choicegroup>
|
||||
<choice correct="false" name="foil1">
|
||||
<startouttext />This is foil One.<endouttext />
|
||||
</choice>
|
||||
<choice correct="false" name="foil2">
|
||||
<startouttext />This is foil Two.<endouttext />
|
||||
</choice>
|
||||
<choice correct="true" name="foil3">
|
||||
<startouttext />This is foil Three.<endouttext />
|
||||
</choice>
|
||||
<choice correct="false" name="foil4">
|
||||
<startouttext />This is foil Four.<endouttext />
|
||||
</choice>
|
||||
<choice correct="false" name="foil5">
|
||||
<startouttext />This is foil Five.<endouttext />
|
||||
</choice>
|
||||
xml_template = """
|
||||
<choicegroup {0}>
|
||||
<choice correct="false" name="foil1"><text>This is foil One.</text></choice>
|
||||
<choice correct="false" name="foil2"><text>This is foil Two.</text></choice>
|
||||
<choice correct="true" name="foil3">This is foil Three.</choice>
|
||||
</choicegroup>
|
||||
"""
|
||||
|
||||
def check_type(type_str, expected_input_type):
|
||||
print "checking for type_str='{0}'".format(type_str)
|
||||
xml_str = xml_template.format(type_str)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'foil3',
|
||||
'id': 'sky_input',
|
||||
'status': 'answered'}
|
||||
|
||||
option_input = inputtypes.get_class_for_tag('choicegroup')(system, element, state)
|
||||
|
||||
context = option_input._get_render_context()
|
||||
|
||||
expected = {'id': 'sky_input',
|
||||
'value': 'foil3',
|
||||
'state': 'answered',
|
||||
'input_type': expected_input_type,
|
||||
'choices': [('foil1', '<text>This is foil One.</text>'),
|
||||
('foil2', '<text>This is foil Two.</text>'),
|
||||
('foil3', 'This is foil Three.'),],
|
||||
'name_array_suffix': '', # what is this for??
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
check_type('', 'radio')
|
||||
check_type('type=""', 'radio')
|
||||
check_type('type="MultipleChoice"', 'radio')
|
||||
check_type('type="TrueFalse"', 'checkbox')
|
||||
# fallback.
|
||||
check_type('type="StrangeUnknown"', 'radio')
|
||||
|
||||
|
||||
def check_group(self, tag, expected_input_type, expected_suffix):
|
||||
xml_str = """
|
||||
<{tag}>
|
||||
<choice correct="false" name="foil1"><text>This is foil One.</text></choice>
|
||||
<choice correct="false" name="foil2"><text>This is foil Two.</text></choice>
|
||||
<choice correct="true" name="foil3">This is foil Three.</choice>
|
||||
</{tag}>
|
||||
""".format(tag=tag)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'Down',
|
||||
state = {'value': 'foil3',
|
||||
'id': 'sky_input',
|
||||
'status': 'answered'}
|
||||
option_input = inputtypes.OptionInput(system, element, state)
|
||||
|
||||
context = option_input._get_render_context()
|
||||
the_input = inputtypes.get_class_for_tag(tag)(system, element, state)
|
||||
|
||||
expected = {'value': 'Down',
|
||||
'options': [('Up', 'Up'), ('Down', 'Down')],
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'sky_input',
|
||||
'value': 'foil3',
|
||||
'state': 'answered',
|
||||
'msg': '',
|
||||
'inline': '',
|
||||
'id': 'sky_input'}
|
||||
'input_type': expected_input_type,
|
||||
'choices': [('foil1', '<text>This is foil One.</text>'),
|
||||
('foil2', '<text>This is foil Two.</text>'),
|
||||
('foil3', 'This is foil Three.'),],
|
||||
'name_array_suffix': expected_suffix, # what is this for??
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_radiogroup(self):
|
||||
self.check_group('radiogroup', 'radio', '[]')
|
||||
|
||||
def test_checkboxgroup(self):
|
||||
self.check_group('checkboxgroup', 'checkbox', '[]')
|
||||
|
||||
Reference in New Issue
Block a user