Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions xblocks_contrib/problem/capa/capa_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,19 @@ def _preprocess_problem(self, tree, minimal_init): # private

return problem_data

def response_a11y_data( # pylint: disable=too-many-locals,too-many-branches
@staticmethod
def _preceding_prompt_ids(response, responsetype_id):
"""Assign ids to contiguous preceding ``<p>`` siblings and return them in document order."""
ids = []
sibling = response.getprevious()
while sibling is not None and isinstance(sibling.tag, str) and sibling.tag.lower() == "p":
pid = sibling.get("id") or f"prompt_{responsetype_id}_{len(ids) + 1}"
sibling.set("id", pid)
ids.append(pid)
sibling = sibling.getprevious()
return list(reversed(ids))

def response_a11y_data( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
self, response, inputfields, responsetype_id, problem_data
):
"""
Expand Down Expand Up @@ -1193,12 +1205,21 @@ def response_a11y_data( # pylint: disable=too-many-locals,too-many-branches
if group_description_ids:
response.set("multiinput-group_description_ids", " ".join(group_description_ids))

preceding_prompt_ids = (
self._preceding_prompt_ids(response, responsetype_id)
if inputfields[0].tag in ACCESSIBLE_CAPA_INPUT_TYPES
else []
)

for inputfield in inputfields:
problem_data[inputfield.get("id")] = {
entry = {
"group_label": group_label_tag_text,
"label": HTML(inputfield.attrib.get("label", "")),
"descriptions": {},
}
if preceding_prompt_ids:
entry["additional_describedby_ids"] = list(preceding_prompt_ids)
problem_data[inputfield.get("id")] = entry
else:
# Extract label value from <label> tag or label attribute from inside the responsetype
responsetype_label_tag = response.find("label")
Expand Down Expand Up @@ -1239,7 +1260,12 @@ def response_a11y_data( # pylint: disable=too-many-locals,too-many-branches
response.remove(description)
description_id += 1

problem_data[inputfields[0].get("id")] = {
entry = {
"label": HTML(label.strip()) if label else "",
"descriptions": descriptions,
}
if inputfields[0].tag in ACCESSIBLE_CAPA_INPUT_TYPES:
preceding_prompt_ids = self._preceding_prompt_ids(response, responsetype_id)
if preceding_prompt_ids:
entry["additional_describedby_ids"] = preceding_prompt_ids
problem_data[inputfields[0].get("id")] = entry
1 change: 1 addition & 0 deletions xblocks_contrib/problem/capa/inputtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ def _get_render_context(self):
status_id = "status_" + self.input_id
descriptions.append(status_id)
descriptions.extend(list(self.response_data.get("descriptions", {}).keys()))
descriptions.extend(self.response_data.get("additional_describedby_ids", []))
description_ids = " ".join(descriptions)
context.update({"describedby_html": HTML('aria-describedby="{}"').format(description_ids)})

Expand Down
1 change: 1 addition & 0 deletions xblocks_contrib/problem/capa/templates/choicegroup.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<div class="field">
<input type="{{ input_type }}" name="input_{{ id }}{{ name_array_suffix }}" id="input_{{ id }}_{{ choice_id }}"
class="field-input input-{{ input_type }}{% if input_type == 'radio' and choice_id == value or input_type != 'radio' and choice_id in value %} submitted{% endif %}" value="{{ choice_id }}"
{{ describedby_html }}
{# If the student selected this choice... #}
{% if input_type == 'radio' and choice_id == value or input_type != 'radio' and choice_id in value %}
checked="true"
Expand Down
59 changes: 56 additions & 3 deletions xblocks_contrib/problem/capa/tests/test_capa_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ def test_legacy_problem(self, question, label_attr):
</problem>
"""
problem = new_loncapa_problem(xml)
assert problem.problem_data == {"1_2_1": {"label": question, "descriptions": {}}}
assert problem.problem_data == {
"1_2_1": {
"label": question,
"descriptions": {},
"additional_describedby_ids": ["prompt_1_1_1"],
}
}
assert len(problem.tree.xpath(f"//*[normalize-space(text())='{question}']")) == 0

@ddt.unpack
Expand Down Expand Up @@ -121,7 +127,11 @@ def test_neither_label_tag_nor_attribute(self, question1, question2):
"""
problem = new_loncapa_problem(xml)
assert problem.problem_data == {
"1_2_1": {"label": question1, "descriptions": {}},
"1_2_1": {
"label": question1,
"descriptions": {},
"additional_describedby_ids": ["prompt_1_1_1"],
},
"1_3_1": {"label": question2, "descriptions": {}},
}
for question in (question1, question2):
Expand Down Expand Up @@ -149,6 +159,7 @@ def test_multiple_descriptions(self):
"1_2_1": {
"label": "___ requires sacrifices.",
"descriptions": {"description_1_1_1": desc1, "description_1_1_2": desc2},
"additional_describedby_ids": ["prompt_1_1_1"],
}
}

Expand Down Expand Up @@ -273,7 +284,13 @@ def test_question_title_not_removed_got_children(self):
</problem>
"""
problem = new_loncapa_problem(xml)
assert problem.problem_data == {"1_2_1": {"label": "", "descriptions": {}}}
assert problem.problem_data == {
"1_2_1": {
"label": "",
"descriptions": {},
"additional_describedby_ids": ["prompt_1_1_3", "prompt_1_1_2", "prompt_1_1_1"],
}
}
assert len(problem.tree.xpath("//p/img")) == 1

@ddt.unpack
Expand Down Expand Up @@ -303,6 +320,42 @@ def test_multiple_inputtypes(self, group_label):
"1_2_2": {"group_label": group_label, "label": input2_label, "descriptions": {}},
}

def test_multiple_inputtypes_with_preceding_prompts(self):
"""
Verify that preceding <p> prompts are associated with each inputfield
of a multi-input responsetype via additional_describedby_ids.
"""
group_label = "Choose the correct color"
input1_label = "What color is the sky?"
input2_label = "What color are pine needles?"
xml = f"""
<problem>
<p>Instructions: pick a color from the dropdown.</p>
<p>Consider the context of each item.</p>
<optionresponse>
<label>{group_label}</label>
<optioninput options="('yellow','blue','green')" correct="blue" label="{input1_label}"/>
<optioninput options="('orange','blue','green')" correct="green" label="{input2_label}"/>
</optionresponse>
</problem>
"""
problem = new_loncapa_problem(xml)
expected_prompt_ids = ["prompt_1_1_2", "prompt_1_1_1"]
assert problem.problem_data == {
"1_2_1": {
"group_label": group_label,
"label": input1_label,
"descriptions": {},
"additional_describedby_ids": expected_prompt_ids,
},
"1_2_2": {
"group_label": group_label,
"label": input2_label,
"descriptions": {},
"additional_describedby_ids": expected_prompt_ids,
},
}

def test_single_inputtypes(self):
"""
Verify that HTML is correctly rendered when there is single inputtype.
Expand Down