Files
edx-platform/xmodule/capa/tests/response_xml_factory.py
2026-01-07 16:39:11 +05:00

941 lines
35 KiB
Python

"""Factories to build CAPA response XML."""
from abc import ABCMeta, abstractmethod
import six
from lxml import etree
from six.moves import range, zip
class ResponseXMLFactory(six.with_metaclass(ABCMeta, object)):
"""Abstract base class for capa response XML factories.
Subclasses override create_response_element and
create_input_element to produce XML of particular response types"""
@abstractmethod
def create_response_element(self, **kwargs):
"""Subclasses override to return an etree element
representing the capa response XML
(e.g. <numericalresponse>).
The tree should NOT contain any input elements
(such as <textline />) as these will be added later."""
return None
@abstractmethod
def create_input_element(self, **kwargs):
"""Subclasses override this to return an etree element
representing the capa input XML (such as <textline />)"""
return None
def build_xml(self, **kwargs): # pylint: disable=too-many-locals
"""Construct an XML string for a capa response
based on **kwargs.
**kwargs is a dictionary that will be passed
to create_response_element() and create_input_element().
See the subclasses below for other keyword arguments
you can specify.
For all response types, **kwargs can contain:
*question_text*: The text of the question to display,
wrapped in <label> tags.
*explanation_text*: The detailed explanation that will
be shown if the user answers incorrectly.
*script*: The embedded Python script (a string)
*num_responses*: The number of responses to create [DEFAULT: 1]
*num_inputs*: The number of input elements
to create [DEFAULT: 1]
*credit_type*: String of comma-separated words specifying the
partial credit grading scheme.
Returns a string representation of the XML tree.
"""
# Retrieve keyward arguments
question_text = kwargs.get("question_text", "")
explanation_text = kwargs.get("explanation_text", "")
script = kwargs.get("script", None)
num_responses = kwargs.get("num_responses", 1)
num_inputs = kwargs.get("num_inputs", 1)
credit_type = kwargs.get("credit_type", None)
# The root is <problem>
root = etree.Element("problem")
# Add a script if there is one
if script:
script_element = etree.SubElement(root, "script")
script_element.set("type", "loncapa/python")
script_element.text = str(script)
# Add the response(s)
for __ in range(int(num_responses)):
response_element = self.create_response_element(**kwargs)
# Set partial credit
if credit_type is not None:
response_element.set("partial_credit", str(credit_type))
root.append(response_element)
# Add the question label
question = etree.SubElement(response_element, "label")
question.text = question_text
# Add input elements
for __ in range(int(num_inputs)):
input_element = self.create_input_element(**kwargs)
if input_element is not None:
response_element.append(input_element)
# The problem has an explanation of the solution
if explanation_text:
explanation = etree.SubElement(root, "solution")
explanation_div = etree.SubElement(explanation, "div")
explanation_div.set("class", "detailed-solution")
explanation_div.text = explanation_text
return etree.tostring(root).decode("utf-8")
@staticmethod
def textline_input_xml(**kwargs):
"""Create a <textline/> XML element
Uses **kwargs:
*math_display*: If True, then includes a MathJax display of user input
*size*: An integer representing the width of the text line
"""
math_display = kwargs.get("math_display", False)
size = kwargs.get("size", None)
input_element_label = kwargs.get("input_element_label", "")
input_element = etree.Element("textline")
if input_element_label:
input_element.set("label", input_element_label)
if math_display:
input_element.set("math", "1")
if size:
input_element.set("size", str(size))
return input_element
@staticmethod
def choicegroup_input_xml(**kwargs):
"""Create a <choicegroup> XML element
Uses **kwargs:
*choice_type*: Can be "checkbox", "radio", or "multiple"
*choices*: List of True/False values indicating whether
a particular choice is correct or not.
Users must choose *all* correct options in order
to be marked correct.
DEFAULT: [True]
*choice_names": List of strings identifying the choices.
If specified, you must ensure that
len(choice_names) == len(choices)
*points*: List of strings giving partial credit values (0-1)
for each choice. Interpreted as floats in problem.
If specified, ensure len(points) == len(choices)
"""
# Names of group elements
group_element_names = {"checkbox": "checkboxgroup", "radio": "radiogroup", "multiple": "choicegroup"}
# Retrieve **kwargs
choices = kwargs.get("choices", [True])
choice_type = kwargs.get("choice_type", "multiple")
choice_names = kwargs.get("choice_names", [None] * len(choices))
points = kwargs.get("points", [None] * len(choices))
# Create the <choicegroup>, <checkboxgroup>, or <radiogroup> element
assert choice_type in group_element_names
group_element = etree.Element(group_element_names[choice_type])
# Create the <choice> elements
for correct_val, name, pointval in zip(choices, choice_names, points):
choice_element = etree.SubElement(group_element, "choice")
if correct_val is True:
correctness = "true"
elif correct_val is False:
correctness = "false"
elif "partial" in correct_val:
correctness = "partial"
else:
correctness = correct_val
choice_element.set("correct", correctness)
# Add a name identifying the choice, if one exists
# For simplicity, we use the same string as both the
# name attribute and the text of the element
if name:
choice_element.text = str(name)
choice_element.set("name", str(name))
# Add point values for partially-correct choices.
if pointval:
choice_element.set("point_value", str(pointval))
return group_element
class NumericalResponseXMLFactory(ResponseXMLFactory):
"""Factory for producing <numericalresponse> XML trees"""
def create_response_element(self, **kwargs):
"""Create a <numericalresponse> XML element.
Uses **kwarg keys:
*answer*: The correct answer (e.g. "5")
*correcthint*: The feedback describing correct answer.
*additional_answers*: A dict of additional answers along with their correcthint.
*tolerance*: The tolerance within which a response
is considered correct. Can be a decimal (e.g. "0.01")
or percentage (e.g. "2%")
*credit_type*: String of comma-separated words specifying the
partial credit grading scheme.
*partial_range*: The multiplier for the tolerance that will
still provide partial credit in the "close" grading style
*partial_answers*: A string of comma-separated alternate
answers that will receive partial credit in the "list" style
"""
answer = kwargs.get("answer", None)
correcthint = kwargs.get("correcthint", "")
additional_answers = kwargs.get("additional_answers", {})
tolerance = kwargs.get("tolerance", None)
credit_type = kwargs.get("credit_type", None)
partial_range = kwargs.get("partial_range", None)
partial_answers = kwargs.get("partial_answers", None)
response_element = etree.Element("numericalresponse")
if answer:
if isinstance(answer, float):
response_element.set("answer", repr(answer))
else:
response_element.set("answer", str(answer))
for additional_answer, additional_correcthint in additional_answers.items():
additional_element = etree.SubElement(response_element, "additional_answer")
additional_element.set("answer", str(additional_answer))
if additional_correcthint:
correcthint_element = etree.SubElement(additional_element, "correcthint")
correcthint_element.text = str(additional_correcthint)
if tolerance:
responseparam_element = etree.SubElement(response_element, "responseparam")
responseparam_element.set("type", "tolerance")
responseparam_element.set("default", str(tolerance))
if partial_range is not None and "close" in credit_type:
responseparam_element.set("partial_range", str(partial_range))
if partial_answers is not None and "list" in credit_type:
# The line below throws a false positive pylint violation, so it's excepted.
responseparam_element = etree.SubElement(response_element, "responseparam")
responseparam_element.set("partial_answers", partial_answers)
if correcthint:
correcthint_element = etree.SubElement(response_element, "correcthint")
correcthint_element.text = str(correcthint)
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
class CustomResponseXMLFactory(ResponseXMLFactory):
"""Factory for producing <customresponse> XML trees"""
def create_response_element(self, **kwargs):
"""Create a <customresponse> XML element.
Uses **kwargs:
*cfn*: the Python code to run. Can be inline code,
or the name of a function defined in earlier <script> tags.
Should have the form: cfn(expect, answer_given, student_answers)
where expect is a value (see below),
answer_given is a single value (for 1 input)
or a list of values (for multiple inputs),
and student_answers is a dict of answers by input ID.
*expect*: The value passed to the function cfn
*answer*: Inline script that calculates the answer
*answer_attr*: The "answer" attribute on the tag itself (treated as an
alias to "expect", though "expect" takes priority if both are given)
"""
# Retrieve **kwargs
cfn = kwargs.get("cfn", None)
expect = kwargs.get("expect", None)
answer_attr = kwargs.get("answer_attr", None)
answer = kwargs.get("answer", None)
options = kwargs.get("options", None)
cfn_extra_args = kwargs.get("cfn_extra_args", None)
# Create the response element
response_element = etree.Element("customresponse")
if cfn:
response_element.set("cfn", str(cfn))
if expect:
response_element.set("expect", str(expect))
if answer_attr:
response_element.set("answer", str(answer_attr))
if answer:
answer_element = etree.SubElement(response_element, "answer")
answer_element.text = str(answer)
if options:
response_element.set("options", str(options))
if cfn_extra_args:
response_element.set("cfn_extra_args", str(cfn_extra_args))
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
class SchematicResponseXMLFactory(ResponseXMLFactory):
"""Factory for creating <schematicresponse> XML trees"""
def create_response_element(self, **kwargs):
"""Create the <schematicresponse> XML element.
Uses *kwargs*:
*answer*: The Python script used to evaluate the answer.
"""
answer_script = kwargs.get("answer", None)
# Create the <schematicresponse> element
response_element = etree.Element("schematicresponse")
# Insert the <answer> script if one is provided
if answer_script:
answer_element = etree.SubElement(response_element, "answer")
answer_element.set("type", "loncapa/python")
answer_element.text = str(answer_script)
return response_element
def create_input_element(self, **kwargs):
"""Create the <schematic> XML element.
Although <schematic> can have several attributes,
(*height*, *width*, *parts*, *analyses*, *submit_analysis*, and *initial_value*),
none of them are used in the capa block.
For testing, we create a bare-bones version of <schematic>."""
return etree.Element("schematic")
class CodeResponseXMLFactory(ResponseXMLFactory):
"""Factory for creating <coderesponse> XML trees"""
def build_xml(self, **kwargs):
"""Build a <coderesponse> XML tree."""
# Since we are providing an <answer> tag,
# we should override the default behavior
# of including a <solution> tag as well
kwargs["explanation_text"] = None
return super().build_xml(**kwargs)
def create_response_element(self, **kwargs):
"""
Create a <coderesponse> XML element.
Uses **kwargs:
*initial_display*: The code that initially appears in the textbox
[DEFAULT: "Enter code here"]
*answer_display*: The answer to display to the student
[DEFAULT: "This is the correct answer!"]
*grader_payload*: A JSON-encoded string sent to the grader
[DEFAULT: empty dict string]
*allowed_files*: A space-separated string of file names.
[DEFAULT: None]
*required_files*: A space-separated string of file names.
[DEFAULT: None]
"""
# Get **kwargs
initial_display = kwargs.get("initial_display", "Enter code here")
answer_display = kwargs.get("answer_display", "This is the correct answer!")
grader_payload = kwargs.get("grader_payload", "{}")
allowed_files = kwargs.get("allowed_files", None)
required_files = kwargs.get("required_files", None)
# Create the <coderesponse> element
response_element = etree.Element("coderesponse")
# If files are involved, create the <filesubmission> element.
has_files = allowed_files or required_files
if has_files:
filesubmission_element = etree.SubElement(response_element, "filesubmission")
if allowed_files:
filesubmission_element.set("allowed_files", allowed_files)
if required_files:
filesubmission_element.set("required_files", required_files)
# Create the <codeparam> element.
codeparam_element = etree.SubElement(response_element, "codeparam")
# Set the initial display text
initial_element = etree.SubElement(codeparam_element, "initial_display")
initial_element.text = str(initial_display)
# Set the answer display text
answer_element = etree.SubElement(codeparam_element, "answer_display")
answer_element.text = str(answer_display)
# Set the grader payload string
grader_element = etree.SubElement(codeparam_element, "grader_payload")
grader_element.text = str(grader_payload)
# Create the input within the response
if not has_files:
input_element = etree.SubElement(response_element, "textbox")
input_element.set("mode", "python")
return response_element
def create_input_element(self, **kwargs):
# Since we create this in create_response_element(),
# return None here
return None
class ChoiceResponseXMLFactory(ResponseXMLFactory):
"""Factory for creating <choiceresponse> XML trees"""
def create_response_element(self, **kwargs):
"""Create a <choiceresponse> element"""
return etree.Element("choiceresponse")
def create_input_element(self, **kwargs):
"""Create a <checkboxgroup> element."""
return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
class FormulaResponseXMLFactory(ResponseXMLFactory):
"""Factory for creating <formularesponse> XML trees"""
def create_response_element(self, **kwargs): # pylint: disable=too-many-locals
"""Create a <formularesponse> element.
*sample_dict*: A dictionary of the form:
{ VARIABLE_NAME: (MIN, MAX), ....}
This specifies the range within which
to numerically sample each variable to check
student answers.
[REQUIRED]
*num_samples*: The number of times to sample the student's answer
to numerically compare it to the correct answer.
*tolerance*: The tolerance within which answers will be accepted
[DEFAULT: 0.01]
*answer*: The answer to the problem. Can be a formula string
or a Python variable defined in a script
(e.g. "$calculated_answer" for a Python variable
called calculated_answer)
[REQUIRED]
*hints*: List of (hint_prompt, hint_name, hint_text) tuples
Where *hint_prompt* is the formula for which we show the hint,
*hint_name* is an internal identifier for the hint,
and *hint_text* is the text we show for the hint.
"""
# Retrieve kwargs
sample_dict = kwargs.get("sample_dict", None)
num_samples = kwargs.get("num_samples", None)
tolerance = kwargs.get("tolerance", 0.01)
answer = kwargs.get("answer", None)
hint_list = kwargs.get("hints", None)
assert answer
assert sample_dict and num_samples
# Create the <formularesponse> element
response_element = etree.Element("formularesponse")
# Set the sample information
sample_str = self._sample_str(sample_dict, num_samples, tolerance)
response_element.set("samples", sample_str)
# Set the tolerance
responseparam_element = etree.SubElement(response_element, "responseparam")
responseparam_element.set("type", "tolerance")
responseparam_element.set("default", str(tolerance))
# Set the answer
response_element.set("answer", str(answer))
# Include hints, if specified
if hint_list:
hintgroup_element = etree.SubElement(response_element, "hintgroup")
for hint_prompt, hint_name, hint_text in hint_list:
# For each hint, create a <formulahint> element
formulahint_element = etree.SubElement(hintgroup_element, "formulahint")
# We could sample a different range, but for simplicity,
# we use the same sample string for the hints
# that we used previously.
formulahint_element.set("samples", sample_str)
formulahint_element.set("answer", str(hint_prompt))
formulahint_element.set("name", str(hint_name))
# For each hint, create a <hintpart> element
# corresponding to the <formulahint>
hintpart_element = etree.SubElement(hintgroup_element, "hintpart")
hintpart_element.set("on", str(hint_name))
text_element = etree.SubElement(hintpart_element, "text")
text_element.text = str(hint_text)
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
def _sample_str(self, sample_dict, num_samples, tolerance): # pylint: disable=unused-argument
"""Generate a sample string for Loncapa using variable ranges and repetition count."""
# Loncapa uses a special format for sample strings:
# "x,y,z@4,5,3:10,12,8#4" means plug in values for (x,y,z)
# from within the box defined by points (4,5,3) and (10,12,8)
# The "#4" means to repeat 4 times.
low_range_vals = [str(f[0]) for f in sample_dict.values()]
high_range_vals = [str(f[1]) for f in sample_dict.values()]
sample_str = (
",".join(list(sample_dict.keys()))
+ "@"
+ ",".join(low_range_vals)
+ ":"
+ ",".join(high_range_vals)
+ "#"
+ str(num_samples)
)
return sample_str
class ImageResponseXMLFactory(ResponseXMLFactory):
"""Factory for producing <imageresponse> XML"""
def create_response_element(self, **kwargs):
"""Create the <imageresponse> element."""
return etree.Element("imageresponse")
def create_input_element(self, **kwargs):
"""Create the <imageinput> element.
Uses **kwargs:
*src*: URL for the image file [DEFAULT: "/static/image.jpg"]
*width*: Width of the image [DEFAULT: 100]
*height*: Height of the image [DEFAULT: 100]
*rectangle*: String representing the rectangles the user should select.
Take the form "(x1,y1)-(x2,y2)", where the two (x,y)
tuples define the corners of the rectangle.
Can include multiple rectangles separated by a semicolon, e.g.
"(490,11)-(556,98);(242,202)-(296,276)"
*regions*: String representing the regions a user can select
Take the form "[ [[x1,y1], [x2,y2], [x3,y3]],
[[x1,y1], [x2,y2], [x3,y3]] ]"
(Defines two regions, each with 3 points)
REQUIRED: Either *rectangle* or *region* (or both)
"""
# Get the **kwargs
src = kwargs.get("src", "/static/image.jpg")
width = kwargs.get("width", 100)
height = kwargs.get("height", 100)
rectangle = kwargs.get("rectangle", None)
regions = kwargs.get("regions", None)
assert rectangle or regions
# Create the <imageinput> element
input_element = etree.Element("imageinput")
input_element.set("src", str(src))
input_element.set("width", str(width))
input_element.set("height", str(height))
if rectangle:
input_element.set("rectangle", rectangle)
if regions:
input_element.set("regions", regions)
return input_element
class JSInputXMLFactory(CustomResponseXMLFactory):
"""
Factory for producing <jsinput> XML.
Note that this factory currently does not create a functioning problem.
It will only create an empty iframe.
"""
def create_input_element(self, **kwargs):
"""Create the <jsinput> element"""
return etree.Element("jsinput")
class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
"""Factory for producing <multiplechoiceresponse> XML"""
def create_response_element(self, **kwargs):
"""Create the <multiplechoiceresponse> element"""
return etree.Element("multiplechoiceresponse")
def create_input_element(self, **kwargs):
"""Create the <choicegroup> element"""
kwargs["choice_type"] = "multiple"
return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
class TrueFalseResponseXMLFactory(ResponseXMLFactory):
"""Factory for producing <truefalseresponse> XML"""
def create_response_element(self, **kwargs):
"""Create the <truefalseresponse> element"""
return etree.Element("truefalseresponse")
def create_input_element(self, **kwargs):
"""Create the <choicegroup> element"""
kwargs["choice_type"] = "multiple"
return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
class OptionResponseXMLFactory(ResponseXMLFactory):
"""Factory for producing <optionresponse> XML"""
def create_response_element(self, **kwargs):
"""Create the <optionresponse> element"""
return etree.Element("optionresponse")
def create_input_element(self, **kwargs):
"""Create the <optioninput> element.
Uses **kwargs:
*options*: a list of possible options the user can choose from [REQUIRED]
You must specify at least 2 options.
*correct_option*: the correct choice from the list of options [REQUIRED]
"""
options_list = kwargs.get("options", None)
correct_option = kwargs.get("correct_option", None)
assert options_list and correct_option
assert len(options_list) > 1
assert correct_option in options_list
# Create the <optioninput> element
optioninput_element = etree.Element("optioninput")
# Set the "options" attribute
# Format: "('first', 'second', 'third')"
options_attr_string = ",".join([f"'{o}'" for o in options_list])
options_attr_string = f"({options_attr_string})"
optioninput_element.set("options", options_attr_string)
# Set the "correct" attribute
optioninput_element.set("correct", str(correct_option))
return optioninput_element
class StringResponseXMLFactory(ResponseXMLFactory):
"""Factory for producing <stringresponse> XML"""
def create_response_element(self, **kwargs): # pylint: disable=too-many-locals
"""Create a <stringresponse> XML element.
Uses **kwargs:
*answer*: The correct answer (a string) [REQUIRED]
*case_sensitive*: Whether the response is case-sensitive (True/False)
[DEFAULT: True]
*hints*: List of (hint_prompt, hint_name, hint_text) tuples
Where *hint_prompt* is the string for which we show the hint,
*hint_name* is an internal identifier for the hint,
and *hint_text* is the text we show for the hint.
*hintfn*: The name of a function in the script to use for hints.
*regexp*: Whether the response is regexp
*additional_answers*: list of additional answers.
*non_attribute_answers*: list of additional answers to be coded in the
non-attribute format
"""
# Retrieve the **kwargs
answer = kwargs.get("answer", None)
case_sensitive = kwargs.get("case_sensitive", None)
hint_list = kwargs.get("hints", None)
hint_fn = kwargs.get("hintfn", None)
regexp = kwargs.get("regexp", None)
additional_answers = kwargs.get("additional_answers", [])
non_attribute_answers = kwargs.get("non_attribute_answers", [])
assert answer
# Create the <stringresponse> element
response_element = etree.Element("stringresponse")
# Set the answer attribute
response_element.set("answer", str(answer))
# Set the case sensitivity and regexp:
type_value = ""
if case_sensitive is not None:
type_value += "cs" if case_sensitive else "ci"
type_value += " regexp" if regexp else ""
if type_value:
response_element.set("type", type_value.strip())
# Add the hints if specified
if hint_list or hint_fn:
hintgroup_element = etree.SubElement(response_element, "hintgroup")
if hint_list:
assert not hint_fn
for hint_prompt, hint_name, hint_text in hint_list:
stringhint_element = etree.SubElement(hintgroup_element, "stringhint")
stringhint_element.set("answer", str(hint_prompt))
stringhint_element.set("name", str(hint_name))
hintpart_element = etree.SubElement(hintgroup_element, "hintpart")
hintpart_element.set("on", str(hint_name))
hint_text_element = etree.SubElement(hintpart_element, "text")
hint_text_element.text = str(hint_text)
if hint_fn:
assert not hint_list
hintgroup_element.set("hintfn", hint_fn)
for additional_answer in additional_answers:
additional_node = etree.SubElement(response_element, "additional_answer")
additional_node.set("answer", additional_answer)
for answer in non_attribute_answers:
additional_node = etree.SubElement(response_element, "additional_answer")
additional_node.text = answer
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
class AnnotationResponseXMLFactory(ResponseXMLFactory):
"""Factory for creating <annotationresponse> XML trees"""
def create_response_element(self, **kwargs):
"""Create a <annotationresponse> element"""
return etree.Element("annotationresponse")
def create_input_element(self, **kwargs):
"""Create a <annotationinput> element."""
input_element = etree.Element("annotationinput")
text_children = [
{"tag": "title", "text": kwargs.get("title", "super cool annotation")},
{"tag": "text", "text": kwargs.get("text", "texty text")},
{"tag": "comment", "text": kwargs.get("comment", "blah blah erudite comment blah blah")},
{"tag": "comment_prompt", "text": kwargs.get("comment_prompt", "type a commentary below")},
{"tag": "tag_prompt", "text": kwargs.get("tag_prompt", "select one tag")},
]
for child in text_children:
etree.SubElement(input_element, child["tag"]).text = child["text"]
default_options = [("green", "correct"), ("eggs", "incorrect"), ("ham", "partially-correct")]
options = kwargs.get("options", default_options)
options_element = etree.SubElement(input_element, "options")
for description, correctness in options:
option_element = etree.SubElement(options_element, "option", {"choice": correctness})
option_element.text = description
return input_element
class SymbolicResponseXMLFactory(ResponseXMLFactory):
"""Factory for producing <symbolicresponse> xml"""
def create_response_element(self, **kwargs):
"""Build the <symbolicresponse> XML element.
Uses **kwargs:
*expect*: The correct answer (a sympy string)
*options*: list of option strings to pass to symmath_check
(e.g. 'matrix', 'qbit', 'imaginary', 'numerical')"""
# Retrieve **kwargs
expect = kwargs.get("expect", "")
options = kwargs.get("options", [])
# Symmath check expects a string of options
options_str = ",".join(options)
# Construct the <symbolicresponse> element
response_element = etree.Element("symbolicresponse")
if expect:
response_element.set("expect", str(expect))
if options_str:
response_element.set("options", str(options_str))
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
class ChoiceTextResponseXMLFactory(ResponseXMLFactory):
"""Factory for producing <choicetextresponse> xml"""
def create_response_element(self, **kwargs):
"""Create a <choicetextresponse> element"""
return etree.Element("choicetextresponse")
def create_input_element(self, **kwargs):
"""Create a <checkboxgroup> element.
choices can be specified in the following format:
[("true", [{"answer": "5", "tolerance": 0}]),
("false", [{"answer": "5", "tolerance": 0}])
]
This indicates that the first checkbox/radio is correct and it
contains a numtolerance_input with an answer of 5 and a tolerance of 0
It also indicates that the second has a second incorrect radiobutton
or checkbox with a numtolerance_input.
"""
choices = kwargs.get("choices", [("true", {})])
choice_inputs = []
# Ensure that the first element of choices is an ordered
# collection. It will start as a list, a tuple, or not a Container.
if not isinstance(choices[0], (list, tuple)):
choices = [choices]
for choice in choices:
correctness, answers = choice
numtolerance_inputs = []
# If the current `choice` contains any("answer": number)
# elements, turn those into numtolerance_inputs
if answers:
# `answers` will be a list or tuple of answers or a single
# answer, representing the answers for numtolerance_inputs
# inside of this specific choice.
# Make sure that `answers` is an ordered collection for
# convenience.
if not isinstance(answers, (list, tuple)):
answers = [answers]
numtolerance_inputs = [self._create_numtolerance_input_element(answer) for answer in answers]
choice_inputs.append(self._create_choice_element(correctness=correctness, inputs=numtolerance_inputs))
# Default type is 'radiotextgroup'
input_type = kwargs.get("type", "radiotextgroup")
input_element = etree.Element(input_type)
for ind, choice in enumerate(choice_inputs):
# Give each choice text equal to it's position(0,1,2...)
choice.text = f"choice_{ind}"
input_element.append(choice)
return input_element
def _create_choice_element(self, **kwargs):
"""
Creates a choice element for a choictextproblem.
Defaults to a correct choice with no numtolerance_input
"""
text = kwargs.get("text", "")
correct = kwargs.get("correctness", "true")
inputs = kwargs.get("inputs", [])
choice_element = etree.Element("choice")
choice_element.set("correct", correct)
choice_element.text = text
for inp in inputs:
# Add all of the inputs as children of this choice
choice_element.append(inp)
return choice_element
def _create_numtolerance_input_element(self, params):
"""
Creates a <numtolerance_input/> or <decoy_input/> element with
optionally specified tolerance and answer.
"""
answer = params["answer"] if "answer" in params else None
# If there is not an answer specified, Then create a <decoy_input/>
# otherwise create a <numtolerance_input/> and set its tolerance
# and answer attributes.
if answer:
text_input = etree.Element("numtolerance_input")
text_input.set("answer", answer)
# If tolerance was specified, was specified use it, otherwise
# Set the tolerance to "0"
text_input.set("tolerance", params["tolerance"] if "tolerance" in params else "0")
else:
text_input = etree.Element("decoy_input")
return text_input