diff --git a/cms/djangoapps/contentstore/tests/test_crud.py b/cms/djangoapps/contentstore/tests/test_crud.py index f7f73c9a26..4153646f97 100644 --- a/cms/djangoapps/contentstore/tests/test_crud.py +++ b/cms/djangoapps/contentstore/tests/test_crud.py @@ -35,8 +35,8 @@ class TemplateTests(ModuleStoreTestCase): self.assertIsNotNone(dropdown) self.assertIn('markdown', dropdown['metadata']) self.assertIn('data', dropdown) - self.assertRegexpMatches(dropdown['metadata']['markdown'], r'^Dropdown.*') - self.assertRegexpMatches(dropdown['data'], r'\s*

Dropdown.*') + self.assertRegexpMatches(dropdown['metadata']['markdown'], r'.*dropdown problems.*') + self.assertRegexpMatches(dropdown['data'], r'\s*\s*

.*dropdown problems.*') def test_get_some_templates(self): self.assertEqual(len(SequenceDescriptor.templates()), 0) diff --git a/cms/static/cms/js/require-config.js b/cms/static/cms/js/require-config.js index 45aadf6498..426fae9f59 100644 --- a/cms/static/cms/js/require-config.js +++ b/cms/static/cms/js/require-config.js @@ -28,6 +28,7 @@ 'mustache': 'js/vendor/mustache', 'codemirror': 'js/vendor/codemirror-compressed', 'codemirror/stex': 'js/vendor/CodeMirror/stex', + 'pretty-print': 'js/lib/pretty-print', 'jquery': 'common/js/vendor/jquery', 'jquery-migrate': 'common/js/vendor/jquery-migrate', 'jquery.ui': 'js/vendor/jquery-ui.min', diff --git a/common/djangoapps/pipeline_js/templates/xmodule.js b/common/djangoapps/pipeline_js/templates/xmodule.js index fb0c22d2c1..1b257bbd97 100644 --- a/common/djangoapps/pipeline_js/templates/xmodule.js +++ b/common/djangoapps/pipeline_js/templates/xmodule.js @@ -6,9 +6,8 @@ ## and attach them to the global context manually. define(["jquery", "underscore", "codemirror", "tinymce", "jquery.tinymce", "jquery.qtip", "jquery.scrollTo", "jquery.flot", - "jquery.cookie", - "utility"], - function($, _, CodeMirror, tinymce) { + "jquery.cookie", "pretty-print", "utility"], + function($, _, CodeMirror) { window.$ = $; window._ = _; require(['mathjax']); diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index b8af102243..ae967438d7 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -13,6 +13,7 @@ Main module which shows problems (of "capa" type). This is used by capa_module. """ +from collections import OrderedDict from copy import deepcopy from datetime import datetime import logging @@ -35,6 +36,16 @@ from capa.safe_exec import safe_exec # extra things displayed after "show answers" is pressed solution_tags = ['solution'] +# fully accessible capa input types +ACCESSIBLE_CAPA_INPUT_TYPES = [ + 'checkboxgroup', + 'radiogroup', + 'choicegroup', + 'optioninput', + 'textline', + 'formulaequationinput', +] + # these get captured as student responses response_properties = ["codeparam", "responseparam", "answer", "openendedparam"] @@ -176,7 +187,7 @@ class LoncapaProblem(object): # transformations. This also creates the dict (self.responders) of Response # instances for each question in the problem. The dict has keys = xml subtree of # Response, values = Response instance - self._preprocess_problem(self.tree) + self.problem_data = self._preprocess_problem(self.tree) if not self.student_answers: # True when student_answers is an empty dict self.set_initial_display() @@ -752,7 +763,9 @@ class LoncapaProblem(object): if problemtree.tag in inputtypes.registry.registered_tags(): # If this is an inputtype subtree, let it render itself. - status = "unsubmitted" + response_data = self.problem_data[problemid] + + status = 'unsubmitted' msg = '' hint = '' hintmode = None @@ -766,7 +779,7 @@ class LoncapaProblem(object): hintmode = self.correct_map.get_hintmode(pid) answervariable = self.correct_map.get_property(pid, 'answervariable') - value = "" + value = '' if self.student_answers and problemid in self.student_answers: value = self.student_answers[problemid] @@ -780,6 +793,7 @@ class LoncapaProblem(object): 'id': input_id, 'input_state': self.input_state[input_id], 'answervariable': answervariable, + 'response_data': response_data, 'feedback': { 'message': msg, 'hint': hint, @@ -836,27 +850,30 @@ class LoncapaProblem(object): Obtain all responder answers and save as self.responder_answers dict (key = response) """ response_id = 1 + problem_data = {} self.responders = {} for response in tree.xpath('//' + "|//".join(responsetypes.registry.registered_tags())): - response_id_str = self.problem_id + "_" + str(response_id) + responsetype_id = self.problem_id + "_" + str(response_id) # create and save ID for this response - response.set('id', response_id_str) + response.set('id', responsetype_id) response_id += 1 answer_id = 1 input_tags = inputtypes.registry.registered_tags() inputfields = tree.xpath( - "|".join(['//' + response.tag + '[@id=$id]//' + x for x in input_tags + solution_tags]), - id=response_id_str + "|".join(['//' + response.tag + '[@id=$id]//' + x for x in input_tags]), + id=responsetype_id ) - # assign one answer_id for each input type or solution type + # assign one answer_id for each input type for entry in inputfields: entry.attrib['response_id'] = str(response_id) entry.attrib['answer_id'] = str(answer_id) entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id) answer_id = answer_id + 1 + self.response_a11y_data(response, inputfields, responsetype_id, problem_data) + # instantiate capa Response responsetype_cls = responsetypes.registry.get_class_for_tag(response.tag) responder = responsetype_cls(response, inputfields, self.context, self.capa_system, self.capa_module) @@ -881,3 +898,75 @@ class LoncapaProblem(object): for solution in tree.findall('.//solution'): solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id) solution_id += 1 + + return problem_data + + def response_a11y_data(self, response, inputfields, responsetype_id, problem_data): + """ + Construct data to be used for a11y. + + Arguments: + response (object): xml response object + inputfields (list): list of inputfields in a responsetype + responsetype_id (str): responsetype id + problem_data (dict): dict to be filled with response data + """ + # if there are no inputtypes then don't do anything + if not inputfields: + return + + element_to_be_deleted = None + label = '' + + if len(inputfields) > 1: + response.set('multiple_inputtypes', 'true') + group_label_tag = response.find('label') + group_label_tag_text = '' + if group_label_tag is not None: + group_label_tag.tag = 'p' + group_label_tag.set('id', responsetype_id) + group_label_tag.set('class', 'multi-inputs-group-label') + group_label_tag_text = group_label_tag.text + + for inputfield in inputfields: + problem_data[inputfield.get('id')] = { + 'group_label': group_label_tag_text, + 'label': inputfield.attrib.get('label', ''), + 'descriptions': {} + } + else: + # Extract label value from

to wrap all inputtypes + content = etree.SubElement(tree, 'div') + content.set('class', 'multi-inputs-group') + content.set('role', 'group') + content.set('aria-labelledby', self.xml.get('id')) + else: + content = tree # problem author can make this span display:inline if self.xml.get('inline', ''): @@ -261,12 +280,12 @@ class LoncapaResponse(object): # call provided procedure to do the rendering item_xhtml = renderer(item) if item_xhtml is not None: - tree.append(item_xhtml) + content.append(item_xhtml) tree.tail = self.xml.tail # Add a
for the message at the end of the response if response_msg: - tree.append(self._render_response_msg_html(response_msg)) + content.append(self._render_response_msg_html(response_msg)) return tree diff --git a/common/lib/capa/capa/templates/annotationinput.html b/common/lib/capa/capa/templates/annotationinput.html index 1aa304458c..3e20c975a3 100644 --- a/common/lib/capa/capa/templates/annotationinput.html +++ b/common/lib/capa/capa/templates/annotationinput.html @@ -1,3 +1,4 @@ +<%! from openedx.core.djangolib.markup import HTML %>
@@ -59,6 +60,5 @@ % if msg: -${msg|n} +${HTML(msg)} % endif - diff --git a/common/lib/capa/capa/templates/chemicalequationinput.html b/common/lib/capa/capa/templates/chemicalequationinput.html index 6a6c727bde..944560a55f 100644 --- a/common/lib/capa/capa/templates/chemicalequationinput.html +++ b/common/lib/capa/capa/templates/chemicalequationinput.html @@ -3,7 +3,7 @@
- +<% + def is_radio_input(choice_id): + return input_type == 'radio' and ((isinstance(value, basestring) and (choice_id == value)) or ( + not isinstance(value, basestring) and choice_id in value + )) +%>
-
- % for choice_id, choice_description in choices: -
% if input_type == 'checkbox' or not value: - - - %for choice_id, choice_description in choices: - % if choice_id in value: - ${choice_description}, - %endif - %endfor - - - ${status.display_tooltip} - + + ${status.display_tooltip} % endif
@@ -60,6 +68,6 @@
${submitted_message}
%endif % if msg: - ${msg|n} + ${HTML(msg)} % endif
diff --git a/common/lib/capa/capa/templates/choicetext.html b/common/lib/capa/capa/templates/choicetext.html index f07fa22665..ca2d3ff5a9 100644 --- a/common/lib/capa/capa/templates/choicetext.html +++ b/common/lib/capa/capa/templates/choicetext.html @@ -1,18 +1,18 @@ <%! from django.utils.translation import ugettext as _ %> <% element_checked = False %> % for choice_id, _ in choices: - <%choice_id = choice_id %> + <% choice_id = choice_id %> %if choice_id in value: <% element_checked = True %> %endif -%endfor +% endfor
- -
+ +
% for choice_id, choice_description in choices: - <%choice_id= choice_id %> + <% choice_id = choice_id %>
- +
% if input_type == 'checkbox' or not element_checked: diff --git a/common/lib/capa/capa/templates/drag_and_drop_input.html b/common/lib/capa/capa/templates/drag_and_drop_input.html index d9bb8da90f..57803bf909 100644 --- a/common/lib/capa/capa/templates/drag_and_drop_input.html +++ b/common/lib/capa/capa/templates/drag_and_drop_input.html @@ -1,3 +1,4 @@ +<%! from openedx.core.djangolib.markup import HTML %>
@@ -23,7 +24,7 @@

% if msg: - ${msg|n} + ${HTML(msg)} % endif % if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: diff --git a/common/lib/capa/capa/templates/filesubmission.html b/common/lib/capa/capa/templates/filesubmission.html index 17209b8564..7b3c41dcaf 100644 --- a/common/lib/capa/capa/templates/filesubmission.html +++ b/common/lib/capa/capa/templates/filesubmission.html @@ -1,3 +1,4 @@ +<%! from openedx.core.djangolib.markup import HTML %>
@@ -7,7 +8,7 @@ % endif

${status}

- +
-
${msg|n}
+
${HTML(msg)}
diff --git a/common/lib/capa/capa/templates/formulaequationinput.html b/common/lib/capa/capa/templates/formulaequationinput.html index 1e7d8c0e9f..124e6633ab 100644 --- a/common/lib/capa/capa/templates/formulaequationinput.html +++ b/common/lib/capa/capa/templates/formulaequationinput.html @@ -1,35 +1,38 @@ <%page expression_filter="h"/> <%! from openedx.core.djangolib.markup import HTML %> <% doinline = 'style="display:inline-block;vertical-align:top"' if inline else "" %> -
-
- - ${trailing_text} +
+
+ % if response_data['label']: + + % endif + % for description_id, description_text in response_data['descriptions'].items(): +

${description_text}

+ % endfor + + ${trailing_text} - - - ${status.display_name} + + ${status.display_tooltip} - -

+

-
- \(\) - Loading -
-
+
+ \(\) + Loading +
+
-
+
% if msg: ${HTML(msg)} % endif -
+
diff --git a/common/lib/capa/capa/templates/optioninput.html b/common/lib/capa/capa/templates/optioninput.html index 21a716e57e..58962eb997 100644 --- a/common/lib/capa/capa/templates/optioninput.html +++ b/common/lib/capa/capa/templates/optioninput.html @@ -1,11 +1,20 @@ +<%! from openedx.core.djangolib.markup import HTML %> <% doinline = "inline" if inline else "" %> - + % for option_id, option_description in options: @@ -13,15 +22,12 @@
- - ${value|h} - ${status.display_tooltip} + + ${status.display_tooltip}

% if msg: - ${msg|n} + ${HTML(msg)} % endif - diff --git a/common/lib/capa/capa/templates/schematicinput.html b/common/lib/capa/capa/templates/schematicinput.html index d5e1027f40..3fc6c6ffe2 100644 --- a/common/lib/capa/capa/templates/schematicinput.html +++ b/common/lib/capa/capa/templates/schematicinput.html @@ -8,7 +8,7 @@ analyses="${analyses}" name="input_${id}" id="input_${id}" - aria-label="${label}" + aria-label="${response_data['label']}" aria-describedby="answer_${id}" value="${value|h}" initial_value="${initial_value|h}" diff --git a/common/lib/capa/capa/templates/solutionspan.html b/common/lib/capa/capa/templates/solutionspan.html index 4e85d3aaf4..70a900c8f5 100644 --- a/common/lib/capa/capa/templates/solutionspan.html +++ b/common/lib/capa/capa/templates/solutionspan.html @@ -1,3 +1,3 @@ -
+
-
+
diff --git a/common/lib/capa/capa/templates/textline.html b/common/lib/capa/capa/templates/textline.html index 8181dcc48a..111396a1bd 100644 --- a/common/lib/capa/capa/templates/textline.html +++ b/common/lib/capa/capa/templates/textline.html @@ -2,62 +2,57 @@ <%! from openedx.core.djangolib.markup import HTML %> <% doinline = "inline" if inline else "" %> -
- - % if preprocessor is not None: +
+% if preprocessor is not None:
- % endif - - % if status in ('unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'): -
- % endif - % if hidden: -
- % endif - - - ${trailing_text} - - - - %if value: - ${value} - % else: - ${label} - %endif - - - ${status.display_name} - - - -

- - % if do_math: -
`{::}`
- - - % endif - -% if status in ('unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'): -
% endif - % if msg: - ${HTML(msg)} - % endif +% if status in ('unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'): +
+% endif + +% if hidden: +
+% endif + +% if response_data['label']: + +% endif + +% for description_id, description_text in response_data['descriptions'].items(): +

${description_text}

+% endfor + +${trailing_text} + + + ${status.display_tooltip} + + +

+ +% if do_math: +
`{::}`
+ +% endif + +% if status in ('unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'): +
+% endif + +% if msg: + ${HTML(msg)} +% endif
diff --git a/common/lib/capa/capa/templates/vsepr_input.html b/common/lib/capa/capa/templates/vsepr_input.html index 49812e3fb2..c3af6222ae 100644 --- a/common/lib/capa/capa/templates/vsepr_input.html +++ b/common/lib/capa/capa/templates/vsepr_input.html @@ -1,3 +1,4 @@ +<%! from openedx.core.djangolib.markup import HTML %>
@@ -26,7 +27,7 @@

% if msg: - ${msg|n} + ${HTML(msg)} % endif % if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py index 658833b614..7bd12bb65f 100644 --- a/common/lib/capa/capa/tests/__init__.py +++ b/common/lib/capa/capa/tests/__init__.py @@ -1,6 +1,7 @@ """Tools for helping with testing capa.""" import gettext +from path import path # pylint: disable=no-name-in-module import os import os.path @@ -9,12 +10,29 @@ import fs.osfs from capa.capa_problem import LoncapaProblem, LoncapaSystem from capa.inputtypes import Status from mock import Mock, MagicMock +from mako.lookup import TemplateLookup import xml.sax.saxutils as saxutils TEST_DIR = os.path.dirname(os.path.realpath(__file__)) +def get_template(template_name): + """ + Return template for a capa inputtype. + """ + return TemplateLookup( + directories=[path(__file__).dirname().dirname() / 'templates'] + ).get_template(template_name) + + +def capa_render_template(template, context): + """ + Render template for a capa inputtype. + """ + return get_template(template).render_unicode(**context) + + def tst_render_template(template, context): """ A test version of render to template. Renders to the repr of the context, completely ignoring @@ -30,7 +48,7 @@ xqueue_interface = MagicMock() xqueue_interface.send_to_queue.return_value = (0, 'Success!') -def test_capa_system(): +def test_capa_system(render_template=None): """ Construct a mock LoncapaSystem instance. @@ -46,7 +64,7 @@ def test_capa_system(): filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")), i18n=gettext.NullTranslations(), node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), - render_template=tst_render_template, + render_template=render_template or tst_render_template, seed=0, STATIC_URL='/dummy-static/', STATUS_CLASS=Status, @@ -66,9 +84,10 @@ def mock_capa_module(): return capa_module -def new_loncapa_problem(xml, capa_system=None, seed=723): +def new_loncapa_problem(xml, capa_system=None, seed=723, use_capa_render_template=False): """Construct a `LoncapaProblem` suitable for unit tests.""" - return LoncapaProblem(xml, id='1', seed=seed, capa_system=capa_system or test_capa_system(), + render_template = capa_render_template if use_capa_render_template else None + return LoncapaProblem(xml, id='1', seed=seed, capa_system=capa_system or test_capa_system(render_template), capa_module=mock_capa_module()) diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py index 20081b9536..b01dbf4fc8 100644 --- a/common/lib/capa/capa/tests/response_xml_factory.py +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -267,6 +267,9 @@ class CustomResponseXMLFactory(ResponseXMLFactory): *answer_attr*: The "answer" attribute on the tag itself (treated as an alias to "expect", though "expect" takes priority if both are given) + + *group_label*: Text to represent group of inputs when there are + multiple inputs. """ # Retrieve **kwargs @@ -276,6 +279,7 @@ class CustomResponseXMLFactory(ResponseXMLFactory): answer = kwargs.get('answer', None) options = kwargs.get('options', None) cfn_extra_args = kwargs.get('cfn_extra_args', None) + group_label = kwargs.get('group_label', None) # Create the response element response_element = etree.Element("customresponse") @@ -293,6 +297,10 @@ class CustomResponseXMLFactory(ResponseXMLFactory): answer_element = etree.SubElement(response_element, "answer") answer_element.text = str(answer) + if group_label: + group_label_element = etree.SubElement(response_element, "label") + group_label_element.text = group_label + if options: response_element.set('options', str(options)) diff --git a/common/lib/capa/capa/tests/test_capa_problem.py b/common/lib/capa/capa/tests/test_capa_problem.py new file mode 100644 index 0000000000..352c00f302 --- /dev/null +++ b/common/lib/capa/capa/tests/test_capa_problem.py @@ -0,0 +1,446 @@ +""" +Test capa problem. +""" +import ddt +import textwrap +from lxml import etree +import unittest + +from . import new_loncapa_problem + + +class CAPAProblemTest(unittest.TestCase): + """ CAPA problem related tests""" + + def test_label_and_description_inside_responsetype(self): + """ + Verify that + * label is extracted + *