diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 4a719a5711..15dd3f538a 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -176,7 +176,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 +752,10 @@ class LoncapaProblem(object): if problemtree.tag in inputtypes.registry.registered_tags(): # If this is an inputtype subtree, let it render itself. - status = "unsubmitted" + response_id = self.problem_id + '_' + problemtree.get('response_id') + response_data = self.problem_data[response_id] + + status = 'unsubmitted' msg = '' hint = '' hintmode = None @@ -766,7 +769,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 +783,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,6 +840,7 @@ 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) @@ -857,6 +862,12 @@ class LoncapaProblem(object): entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id) answer_id = answer_id + 1 + # Find the label and save it for html transformation step + responsetype_label = response.find('label') + problem_data[self.problem_id + '_' + str(response_id)] = { + 'label': responsetype_label.text if responsetype_label is not None else '' + } + # 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 +892,5 @@ 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 diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index f5d079ac64..53ff76c4d5 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -224,7 +224,8 @@ class InputTypeBase(object): self.hint = feedback.get('hint', '') self.hintmode = feedback.get('hintmode', None) self.input_state = state.get('input_state', {}) - self.answervariable = state.get("answervariable", None) + self.answervariable = state.get('answervariable', None) + self.response_data = state.get('response_data', None) # put hint above msg if it should be displayed if self.hintmode == 'always': @@ -316,8 +317,10 @@ class InputTypeBase(object): 'value': self.value, 'status': Status(self.status, self.capa_system.i18n.ugettext), 'msg': self.msg, + 'response_data': self.response_data, 'STATIC_URL': self.capa_system.STATIC_URL, } + context.update( (a, v) for (a, v) in self.loaded_attributes.iteritems() if a in self.to_render ) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 921148b1f7..15112853cb 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -250,8 +250,18 @@ class LoncapaResponse(object): - renderer : procedure which produces HTML given an ElementTree - response_msg: a message displayed at the end of the Response """ - # render ourself as a + our content - tree = etree.Element('span') + _ = self.capa_system.i18n.ugettext + + # get responsetype index to make responsetype label + response_index = self.xml.attrib['id'].split('_')[-1] + # Translators: index here could be 1,2,3 and so on + response_label = _(u'Question {index}').format(index=response_index) + + # wrap the content inside a section + tree = etree.Element('section') + tree.set('class', 'wrapper-problem-response') + tree.set('tabindex', '-1') + tree.set('aria-label', response_label) # problem author can make this span display:inline if self.xml.get('inline', ''): diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html index 55314db731..f3ce336b54 100644 --- a/common/lib/capa/capa/templates/choicegroup.html +++ b/common/lib/capa/capa/templates/choicegroup.html @@ -1,43 +1,56 @@ +<% + 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: -
@@ -45,9 +58,9 @@ % if input_type == 'checkbox' or not value: - %for choice_id, choice_description in choices: + %for choice_id, choice_label in choices: % if choice_id in value: - ${choice_description}, + ${choice_label}, %endif %endfor - diff --git a/common/lib/capa/capa/tests/test_html_render.py b/common/lib/capa/capa/tests/test_html_render.py index a83cd55cdb..d87343ba8a 100644 --- a/common/lib/capa/capa/tests/test_html_render.py +++ b/common/lib/capa/capa/tests/test_html_render.py @@ -155,11 +155,12 @@ class CapaHtmlRenderTest(unittest.TestCase): question_element = rendered_html.find("p") self.assertEqual(question_element.text, "Test question") - # Expect that the response has been turned into a - response_element = rendered_html.find("span") - self.assertEqual(response_element.tag, "span") + # Expect that the response has been turned into a
with correct attributes + response_element = rendered_html.find("section") + self.assertEqual(response_element.tag, "section") + self.assertEqual(response_element.attrib["aria-label"], "Question 1") - # Expect that the response + # Expect that the response
# that contains a
for the textline textline_element = response_element.find("div") self.assertEqual(textline_element.text, 'Input Template Render') @@ -201,6 +202,29 @@ class CapaHtmlRenderTest(unittest.TestCase): expected_calls ) + def test_correct_aria_label(self): + xml = """ + + + + over-suspicious + funny + + + + + Urdu + Finnish + + + + """ + problem = new_loncapa_problem(xml) + rendered_html = etree.XML(problem.get_html()) + sections = rendered_html.findall('section') + self.assertEqual(sections[0].attrib['aria-label'], 'Question 1') + self.assertEqual(sections[1].attrib['aria-label'], 'Question 2') + def test_render_response_with_overall_msg(self): # CustomResponse script that sets an overall_message script = textwrap.dedent(""" diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py index c20dc28d9c..66296d6251 100644 --- a/common/lib/capa/capa/tests/test_input_templates.py +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -122,13 +122,16 @@ class ChoiceGroupTemplateTest(TemplateTestCase): def setUp(self): choices = [('1', 'choice 1'), ('2', 'choice 2'), ('3', 'choice 3')] - self.context = {'id': '1', - 'choices': choices, - 'status': Status('correct'), - 'label': 'test', - 'input_type': 'checkbox', - 'name_array_suffix': '1', - 'value': '3'} + self.context = { + 'id': '1', + 'choices': choices, + 'status': Status('correct'), + 'label': 'test', + 'input_type': 'checkbox', + 'name_array_suffix': '1', + 'value': '3', + 'response_data': {'label': 'test'} + } super(ChoiceGroupTemplateTest, self).setUp() def test_problem_marked_correct(self): @@ -229,7 +232,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): for test_conditions in conditions: self.context.update(test_conditions) xml = self.render_to_xml(self.context) - xpath = "//label[@class='choicegroup_correct']" + xpath = "//label[contains(@class, 'choicegroup_correct')]" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark the whole problem @@ -250,7 +253,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): for test_conditions in conditions: self.context.update(test_conditions) xml = self.render_to_xml(self.context) - xpath = "//label[@class='choicegroup_incorrect']" + xpath = "//label[contains(@class, 'choicegroup_incorrect')]" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark the whole problem @@ -340,8 +343,8 @@ class ChoiceGroupTemplateTest(TemplateTestCase): def test_label(self): xml = self.render_to_xml(self.context) - xpath = "//fieldset[@aria-label='%s']" % self.context['label'] - self.assert_has_xpath(xml, xpath, self.context) + xpath = "//legend" + self.assert_has_text(xml, xpath, self.context['label']) class TextlineTemplateTest(TemplateTestCase): diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 8c37b7cc7d..f88c9e6def 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -66,7 +66,7 @@ class @Problem detail = @el.data('progress_detail') status = @el.data('progress_status') - # Render 'x/y point(s)' if student has attempted question + # Render 'x/y point(s)' if student has attempted question if status != 'none' and detail? and (jQuery.type(detail) == "string") and detail.indexOf('/') > 0 a = detail.split('/') earned = parseFloat(a[0]) @@ -628,10 +628,10 @@ class @Problem choicegroup: (element, display, answers) => element = $(element) - input_id = element.attr('id').replace(/inputtype_/,'') + input_id = element.attr('id').replace(/inputtype_/, '') answer = answers[input_id] for choice in answer - element.find("label[for='input_#{input_id}_#{choice}']").addClass 'choicegroup_correct' + element.find("#input_#{input_id}_#{choice}").parent("label").addClass 'choicegroup_correct' javascriptinput: (element, display, answers) => answer_id = $(element).attr('id').split("_")[1...].join("_") @@ -641,7 +641,7 @@ class @Problem choicetextgroup: (element, display, answers) => element = $(element) - input_id = element.attr('id').replace(/inputtype_/,'') + input_id = element.attr('id').replace(/inputtype_/, '') answer = answers[input_id] for choice in answer element.find("section#forinput#{choice}").addClass 'choicetextgroup_show_correct' @@ -821,4 +821,3 @@ class @Problem ] hint_container.attr('hint_index', response.hint_index) @$('.hint-button').focus() # a11y focus on click, like the Check button - diff --git a/common/test/acceptance/pages/lms/problem.py b/common/test/acceptance/pages/lms/problem.py index 0429637dc5..3af153fa4b 100644 --- a/common/test/acceptance/pages/lms/problem.py +++ b/common/test/acceptance/pages/lms/problem.py @@ -129,6 +129,11 @@ class ProblemPage(PageObject): self.q(css='div.problem button.reset').click() self.wait_for_ajax() + def click_show_hide_button(self): + """ Click the Show/Hide button. """ + self.q(css='div.problem div.action .show').click() + self.wait_for_ajax() + def wait_for_status_icon(self): """ wait for status icon @@ -199,3 +204,20 @@ class ProblemPage(PageObject): """ self.wait_for_element_visibility('body > .tooltip', 'A tooltip is visible.') return self.q(css='body > .tooltip').text[0] + + def is_solution_tag_present(self): + """ + Check if solution/explanation is shown. + """ + solution_selector = '.solution-span div.detailed-solution' + return self.q(css=solution_selector).is_present() + + def is_correct_choice_highlighted(self, correct_choices): + """ + Check if correct answer/choice highlighted for choice group. + """ + xpath = '//fieldset/div[contains(@class, "field")][{0}]/label[contains(@class, "choicegroup_correct")]' + for choice in correct_choices: + if not self.q(xpath=xpath.format(choice)).is_present(): + return False + return True diff --git a/common/test/acceptance/tests/lms/test_certificate_web_view.py b/common/test/acceptance/tests/lms/test_certificate_web_view.py index 1b928dd47e..7ea52cf30e 100644 --- a/common/test/acceptance/tests/lms/test_certificate_web_view.py +++ b/common/test/acceptance/tests/lms/test_certificate_web_view.py @@ -218,11 +218,11 @@ class CertificateProgressPageTest(UniqueCourseTest): self.course_nav.q(css='select option[value="{}"]'.format('blue')).first.click() # Select correct radio button for the answer - self.course_nav.q(css='fieldset label:nth-child(3) input').nth(0).click() + self.course_nav.q(css='fieldset div.field:nth-child(4) input').nth(0).click() # Select correct radio buttons for the answer - self.course_nav.q(css='fieldset label:nth-child(1) input').nth(1).click() - self.course_nav.q(css='fieldset label:nth-child(3) input').nth(1).click() + self.course_nav.q(css='fieldset div.field:nth-child(2) input').nth(1).click() + self.course_nav.q(css='fieldset div.field:nth-child(4) input').nth(1).click() # Submit the answer self.course_nav.q(css='button.check.Check').click() diff --git a/common/test/acceptance/tests/lms/test_lms_problems.py b/common/test/acceptance/tests/lms/test_lms_problems.py index daa0fcb64b..209151b559 100644 --- a/common/test/acceptance/tests/lms/test_lms_problems.py +++ b/common/test/acceptance/tests/lms/test_lms_problems.py @@ -51,7 +51,7 @@ class ProblemsTest(UniqueCourseTest): email=self.email, password=self.password, course_id=self.course_id, - staff=False + staff=True ).visit() def get_problem(self): diff --git a/common/test/acceptance/tests/lms/test_problem_types.py b/common/test/acceptance/tests/lms/test_problem_types.py index 6370fd48f8..5e36b67b15 100644 --- a/common/test/acceptance/tests/lms/test_problem_types.py +++ b/common/test/acceptance/tests/lms/test_problem_types.py @@ -324,7 +324,8 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): 'question_text': 'The correct answer is Choice 0 and Choice 2', 'choice_type': 'checkbox', 'choices': [True, False, True, False], - 'choice_names': ['Choice 0', 'Choice 1', 'Choice 2', 'Choice 3'] + 'choice_names': ['Choice 0', 'Choice 1', 'Choice 2', 'Choice 3'], + 'explanation_text': 'This is explanation text' } def setUp(self, *args, **kwargs): @@ -332,15 +333,6 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): Additional setup for CheckboxProblemTypeTest """ super(CheckboxProblemTypeTest, self).setUp(*args, **kwargs) - self.problem_page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section', # TODO: AC-491 - 'aria-allowed-attr', # TODO: AC-251 - 'aria-valid-attr', # TODO: AC-251 - 'aria-roles', # TODO: AC-251 - 'checkboxgroup', # TODO: AC-251 - ] - }) def answer_problem(self, correct): """ @@ -352,6 +344,30 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): else: self.problem_page.click_choice("choice_1") + @attr('shard_7') + def test_can_show_hide_answer(self): + """ + Scenario: Verifies that show/hide answer button is working as expected. + + Given that I am on courseware page + And I can see a CAPA problem with show answer button + When I click "Show Answer" button + Then I should see "Hide Answer" text on button + And I should see question's solution + And I should see correct choices highlighted + When I click "Hide Answer" button + Then I should see "Show Answer" text on button + And I should not see question's solution + And I should not see correct choices highlighted + """ + self.problem_page.click_show_hide_button() + self.assertTrue(self.problem_page.is_solution_tag_present()) + self.assertTrue(self.problem_page.is_correct_choice_highlighted(correct_choices=[1, 3])) + + self.problem_page.click_show_hide_button() + self.assertFalse(self.problem_page.is_solution_tag_present()) + self.assertFalse(self.problem_page.is_correct_choice_highlighted(correct_choices=[1, 3])) + class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): """ @@ -378,13 +394,6 @@ class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): Additional setup for MultipleChoiceProblemTypeTest """ super(MultipleChoiceProblemTypeTest, self).setUp(*args, **kwargs) - self.problem_page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section', # TODO: AC-491 - 'aria-valid-attr', # TODO: AC-251 - 'radiogroup', # TODO: AC-251 - ] - }) def answer_problem(self, correct): """ @@ -422,13 +431,6 @@ class RadioProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): Additional setup for RadioProblemTypeTest """ super(RadioProblemTypeTest, self).setUp(*args, **kwargs) - self.problem_page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section', # TODO: AC-491 - 'aria-valid-attr', # TODO: AC-292 - 'radiogroup', # TODO: AC-292 - ] - }) def answer_problem(self, correct): """ @@ -798,13 +800,6 @@ class RadioTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTestMix Additional setup for RadioTextProblemTypeTest """ super(RadioTextProblemTypeTest, self).setUp(*args, **kwargs) - self.problem_page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section', # TODO: AC-491 - 'label', # TODO: AC-285 - 'radiogroup', # TODO: AC-285 - ] - }) class CheckboxTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTestMixin): @@ -831,13 +826,6 @@ class CheckboxTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTest Additional setup for CheckboxTextProblemTypeTest """ super(CheckboxTextProblemTypeTest, self).setUp(*args, **kwargs) - self.problem_page.a11y_audit.config.set_rules({ - 'ignore': [ - 'section', # TODO: AC-491 - 'label', # TODO: AC-284 - 'checkboxgroup', # TODO: AC-284 - ] - }) class ImageProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): @@ -885,9 +873,9 @@ class SymbolicProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): } status_indicators = { - 'correct': ['span div.correct'], - 'incorrect': ['span div.incorrect'], - 'unanswered': ['span div.unanswered'], + 'correct': ['div.capa_inputtype div.correct'], + 'incorrect': ['div.capa_inputtype div.incorrect'], + 'unanswered': ['div.capa_inputtype div.unanswered'], } def setUp(self, *args, **kwargs):