941 lines
35 KiB
Python
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
|