Commit 6ff762b6 by muhammad-ammar

allow html inside label and descriptions

TNL-5557
parent 2b80c619
...@@ -31,6 +31,8 @@ import capa.responsetypes as responsetypes ...@@ -31,6 +31,8 @@ import capa.responsetypes as responsetypes
from capa.util import contextualize_text, convert_files_to_filenames from capa.util import contextualize_text, convert_files_to_filenames
import capa.xqueue_interface as xqueue_interface import capa.xqueue_interface as xqueue_interface
from capa.safe_exec import safe_exec from capa.safe_exec import safe_exec
from openedx.core.djangolib.markup import HTML
from xmodule.stringify import stringify_children
# extra things displayed after "show answers" is pressed # extra things displayed after "show answers" is pressed
...@@ -926,7 +928,7 @@ class LoncapaProblem(object): ...@@ -926,7 +928,7 @@ class LoncapaProblem(object):
group_label_tag.tag = 'p' group_label_tag.tag = 'p'
group_label_tag.set('id', responsetype_id) group_label_tag.set('id', responsetype_id)
group_label_tag.set('class', 'multi-inputs-group-label') group_label_tag.set('class', 'multi-inputs-group-label')
group_label_tag_text = group_label_tag.text group_label_tag_text = stringify_children(group_label_tag)
for inputfield in inputfields: for inputfield in inputfields:
problem_data[inputfield.get('id')] = { problem_data[inputfield.get('id')] = {
...@@ -938,7 +940,7 @@ class LoncapaProblem(object): ...@@ -938,7 +940,7 @@ class LoncapaProblem(object):
# Extract label value from <label> tag or label attribute from inside the responsetype # Extract label value from <label> tag or label attribute from inside the responsetype
responsetype_label_tag = response.find('label') responsetype_label_tag = response.find('label')
if responsetype_label_tag is not None: if responsetype_label_tag is not None:
label = responsetype_label_tag.text label = stringify_children(responsetype_label_tag)
# store <label> tag containing question text to delete # store <label> tag containing question text to delete
# it later otherwise question will be rendered twice # it later otherwise question will be rendered twice
element_to_be_deleted = responsetype_label_tag element_to_be_deleted = responsetype_label_tag
...@@ -950,21 +952,15 @@ class LoncapaProblem(object): ...@@ -950,21 +952,15 @@ class LoncapaProblem(object):
p_tag = response.xpath('preceding-sibling::*[1][self::p]') p_tag = response.xpath('preceding-sibling::*[1][self::p]')
if p_tag and p_tag[0].text == inputfields[0].attrib['label']: if p_tag and p_tag[0].text == inputfields[0].attrib['label']:
label = p_tag[0].text label = stringify_children(p_tag[0])
p_tag_children = list(p_tag[0])
if len(p_tag_children) == 0:
element_to_be_deleted = p_tag[0] element_to_be_deleted = p_tag[0]
else: else:
# Delete the text from the p-tag, but leave the children.
p_tag[0].text = ''
else:
# In this case the problems don't have tag or label attribute inside the responsetype # In this case the problems don't have tag or label attribute inside the responsetype
# so we will get the first preceding label tag w.r.t to this responsetype. # so we will get the first preceding label tag w.r.t to this responsetype.
# This will take care of those multi-question problems that are not using --- in their markdown. # This will take care of those multi-question problems that are not using --- in their markdown.
label_tag = response.xpath('preceding-sibling::*[1][self::label]') label_tag = response.xpath('preceding-sibling::*[1][self::label]')
if label_tag: if label_tag:
label = label_tag[0].text label = stringify_children(label_tag[0])
element_to_be_deleted = label_tag[0] element_to_be_deleted = label_tag[0]
# delete label or p element only if inputtype is fully accessible # delete label or p element only if inputtype is fully accessible
...@@ -978,11 +974,11 @@ class LoncapaProblem(object): ...@@ -978,11 +974,11 @@ class LoncapaProblem(object):
for description in description_tags: for description in description_tags:
descriptions[ descriptions[
"description_%s_%i" % (responsetype_id, description_id) "description_%s_%i" % (responsetype_id, description_id)
] = description.text ] = HTML(stringify_children(description))
response.remove(description) response.remove(description)
description_id += 1 description_id += 1
problem_data[inputfields[0].get('id')] = { problem_data[inputfields[0].get('id')] = {
'label': label.strip() if label else '', 'label': HTML(label.strip()) if label else '',
'descriptions': descriptions 'descriptions': descriptions
} }
...@@ -322,14 +322,14 @@ class InputTypeBase(object): ...@@ -322,14 +322,14 @@ class InputTypeBase(object):
'msg': self.msg, 'msg': self.msg,
'response_data': self.response_data, 'response_data': self.response_data,
'STATIC_URL': self.capa_system.STATIC_URL, 'STATIC_URL': self.capa_system.STATIC_URL,
'describedby': '', 'describedby_html': '',
} }
# Don't add aria-describedby attribute if there are no descriptions # Don't add aria-describedby attribute if there are no descriptions
if self.response_data.get('descriptions'): if self.response_data.get('descriptions'):
description_ids = ' '.join(self.response_data.get('descriptions').keys()) description_ids = ' '.join(self.response_data.get('descriptions').keys())
context.update( context.update(
{'describedby': 'aria-describedby="{}"'.format(description_ids)} {'describedby_html': 'aria-describedby="{}"'.format(description_ids)}
) )
context.update( context.update(
......
<%! from capa.util import remove_markup %>
<div id="chemicalequationinput_${id}" class="chemicalequationinput"> <div id="chemicalequationinput_${id}" class="chemicalequationinput">
<div class="script_placeholder" data-src="${previewer}"/> <div class="script_placeholder" data-src="${previewer}"/>
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}" id="status_${id}">
<input type="text" name="input_${id}" id="input_${id}" aria-label="${response_data['label']}" aria-describedby="answer_${id}" data-input-id="${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-label="${remove_markup(response_data['label'])}" aria-describedby="answer_${id}" data-input-id="${id}" value="${value|h}"
% if size: % if size:
size="${size}" size="${size}"
% endif % endif
......
<%page expression_filter="h"/>
<%! from openedx.core.djangolib.markup import HTML %> <%! from openedx.core.djangolib.markup import HTML %>
<% <%
def is_radio_input(choice_id): def is_radio_input(choice_id):
...@@ -6,7 +7,7 @@ ...@@ -6,7 +7,7 @@
)) ))
%> %>
<form class="choicegroup capa_inputtype" id="inputtype_${id}"> <form class="choicegroup capa_inputtype" id="inputtype_${id}">
<fieldset ${describedby}> <fieldset ${HTML(describedby_html)}>
% if response_data['label']: % if response_data['label']:
<legend id="${id}-legend" class="response-fieldset-legend field-group-hd">${response_data['label']}</legend> <legend id="${id}-legend" class="response-fieldset-legend field-group-hd">${response_data['label']}</legend>
% endif % endif
...@@ -36,7 +37,7 @@ ...@@ -36,7 +37,7 @@
% endif % endif
% endif % endif
class="${label_class}" class="${label_class}"
${describedby} ${HTML(describedby_html)}
> >
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" class="field-input input-${input_type}" value="${choice_id}" <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" class="field-input input-${input_type}" value="${choice_id}"
## If the student selected this choice... ## If the student selected this choice...
...@@ -45,7 +46,7 @@ ...@@ -45,7 +46,7 @@
% elif input_type != 'radio' and choice_id in value: % elif input_type != 'radio' and choice_id in value:
checked="true" checked="true"
% endif % endif
/> ${choice_label} /> ${HTML(choice_label)}
% if is_radio_input(choice_id): % if is_radio_input(choice_id):
% if status in ('correct', 'partially-correct', 'incorrect') and not show_correctness == 'never': % if status in ('correct', 'partially-correct', 'incorrect') and not show_correctness == 'never':
......
<%! from capa.util import remove_markup %>
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<% element_checked = False %> <% element_checked = False %>
% for choice_id, _ in choices: % for choice_id, _ in choices:
...@@ -10,7 +11,7 @@ ...@@ -10,7 +11,7 @@
<form class="choicetextgroup capa_inputtype" id="inputtype_${id}"> <form class="choicetextgroup capa_inputtype" id="inputtype_${id}">
<div class="script_placeholder" data-src="${STATIC_URL}js/capa/choicetextinput.js"/> <div class="script_placeholder" data-src="${STATIC_URL}js/capa/choicetextinput.js"/>
<fieldset aria-label="${response_data['label']}"> <fieldset aria-label="${remove_markup(response_data['label'])}">
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<% choice_id = choice_id %> <% choice_id = choice_id %>
<section id="forinput${choice_id}" <section id="forinput${choice_id}"
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
% endfor % endfor
<input type="text" name="input_${id}" id="input_${id}" <input type="text" name="input_${id}" id="input_${id}"
data-input-id="${id}" value="${value}" data-input-id="${id}" value="${value}"
${describedby | n, decode.utf8} ${HTML(describedby_html)}
% if size: % if size:
size="${size}" size="${size}"
% endif % endif
......
<%page expression_filter="h"/>
<%! from openedx.core.djangolib.markup import HTML %> <%! from openedx.core.djangolib.markup import HTML %>
<% doinline = "inline" if inline else "" %> <% doinline = "inline" if inline else "" %>
...@@ -10,7 +11,7 @@ ...@@ -10,7 +11,7 @@
<p class="question-description" id="${description_id}">${description_text}</p> <p class="question-description" id="${description_id}">${description_text}</p>
% endfor % endfor
<select name="input_${id}" id="input_${id}" ${describedby}> <select name="input_${id}" id="input_${id}" ${HTML(describedby_html)}>
<option value="option_${id}_dummy_default">${default_option_text}</option> <option value="option_${id}_dummy_default">${default_option_text}</option>
% for option_id, option_description in options: % for option_id, option_description in options:
<option value="${option_id}" <option value="${option_id}"
......
<%! from capa.util import remove_markup %>
<div> <div>
<div class="script_placeholder" data-src="${setup_script}"/> <div class="script_placeholder" data-src="${setup_script}"/>
<input type="hidden" <input type="hidden"
...@@ -8,7 +9,7 @@ ...@@ -8,7 +9,7 @@
analyses="${analyses}" analyses="${analyses}"
name="input_${id}" name="input_${id}"
id="input_${id}" id="input_${id}"
aria-label="${response_data['label']}" aria-label="${remove_markup(response_data['label'])}"
aria-describedby="answer_${id}" aria-describedby="answer_${id}"
value="${value|h}" value="${value|h}"
initial_value="${initial_value|h}" initial_value="${initial_value|h}"
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
% for description_id, description_text in response_data['descriptions'].items(): % for description_id, description_text in response_data['descriptions'].items():
<p class="question-description" id="${description_id}">${description_text}</p> <p class="question-description" id="${description_id}">${description_text}</p>
% endfor % endfor
<input type="text" name="input_${id}" id="input_${id}" ${describedby | n, decode.utf8} value="${value}" <input type="text" name="input_${id}" id="input_${id}" ${HTML(describedby_html)} value="${value}"
% if do_math: % if do_math:
class="math" class="math"
% endif % endif
......
...@@ -13,7 +13,12 @@ from capa.tests.helpers import new_loncapa_problem ...@@ -13,7 +13,12 @@ from capa.tests.helpers import new_loncapa_problem
class CAPAProblemTest(unittest.TestCase): class CAPAProblemTest(unittest.TestCase):
""" CAPA problem related tests""" """ CAPA problem related tests"""
def test_label_and_description_inside_responsetype(self): @ddt.unpack
@ddt.data(
{'question': 'Select the correct synonym of paranoid?'},
{'question': 'Select the correct <em>synonym</em> of <strong>paranoid</strong>?'},
)
def test_label_and_description_inside_responsetype(self, question):
""" """
Verify that Verify that
* label is extracted * label is extracted
...@@ -25,7 +30,7 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -25,7 +30,7 @@ class CAPAProblemTest(unittest.TestCase):
xml = """ xml = """
<problem> <problem>
<choiceresponse> <choiceresponse>
<label>Select the correct synonym of paranoid?</label> <label>{question}</label>
<description>Only the paranoid survive.</description> <description>Only the paranoid survive.</description>
<checkboxgroup> <checkboxgroup>
<choice correct="true">over-suspicious</choice> <choice correct="true">over-suspicious</choice>
...@@ -33,25 +38,35 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -33,25 +38,35 @@ class CAPAProblemTest(unittest.TestCase):
</checkboxgroup> </checkboxgroup>
</choiceresponse> </choiceresponse>
</problem> </problem>
""" """.format(question=question)
problem = new_loncapa_problem(xml) problem = new_loncapa_problem(xml)
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
{ {
'1_2_1': '1_2_1':
{ {
'label': 'Select the correct synonym of paranoid?', 'label': question,
'descriptions': {'description_1_1_1': 'Only the paranoid survive.'} 'descriptions': {'description_1_1_1': 'Only the paranoid survive.'}
} }
} }
) )
self.assertEqual(len(problem.tree.xpath('//label')), 0) self.assertEqual(len(problem.tree.xpath('//label')), 0)
def test_legacy_problem(self): @ddt.unpack
@ddt.data(
{
'question': 'Once we become predictable, we become ______?',
'label_attr': 'Once we become predictable, we become ______?'
},
{
'question': 'Once we become predictable, we become ______?<img src="img/src"/>',
'label_attr': 'Once we become predictable, we become ______?'
},
)
def test_legacy_problem(self, question, label_attr):
""" """
Verify that legacy problem is handled correctly. Verify that legacy problem is handled correctly.
""" """
question = "Once we become predictable, we become ______?"
xml = """ xml = """
<problem> <problem>
<p>Be sure to check your spelling.</p> <p>Be sure to check your spelling.</p>
...@@ -60,7 +75,7 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -60,7 +75,7 @@ class CAPAProblemTest(unittest.TestCase):
<textline label="{}" size="40"/> <textline label="{}" size="40"/>
</stringresponse> </stringresponse>
</problem> </problem>
""".format(question, question) """.format(question, label_attr)
problem = new_loncapa_problem(xml) problem = new_loncapa_problem(xml)
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
...@@ -77,7 +92,18 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -77,7 +92,18 @@ class CAPAProblemTest(unittest.TestCase):
0 0
) )
def test_neither_label_tag_nor_attribute(self): @ddt.unpack
@ddt.data(
{
'question1': 'People who say they have nothing to ____ almost always do?',
'question2': 'Select the correct synonym of paranoid?'
},
{
'question1': '<b>People</b> who say they have <mark>nothing</mark> to ____ almost always do?',
'question2': 'Select the <sup>correct</sup> synonym of <mark>paranoid</mark>?'
},
)
def test_neither_label_tag_nor_attribute(self, question1, question2):
""" """
Verify that label is extracted correctly. Verify that label is extracted correctly.
...@@ -86,8 +112,6 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -86,8 +112,6 @@ class CAPAProblemTest(unittest.TestCase):
tag and label attribute inside responsetype. But we have a label tag tag and label attribute inside responsetype. But we have a label tag
before the responsetype. before the responsetype.
""" """
question1 = 'People who say they have nothing to ____ almost always do?'
question2 = 'Select the correct synonym of paranoid?'
xml = """ xml = """
<problem> <problem>
<p>Be sure to check your spelling.</p> <p>Be sure to check your spelling.</p>
...@@ -131,17 +155,19 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -131,17 +155,19 @@ class CAPAProblemTest(unittest.TestCase):
""" """
Verify that multiple descriptions are handled correctly. Verify that multiple descriptions are handled correctly.
""" """
desc1 = "The problem with trying to be the <em>bad guy</em>, there's always someone <strong>worse</strong>."
desc2 = "Anyone who looks the world as if it was a game of chess deserves to lose."
xml = """ xml = """
<problem> <problem>
<p>Be sure to check your spelling.</p> <p>Be sure to check your spelling.</p>
<stringresponse answer="War" type="ci"> <stringresponse answer="War" type="ci">
<label>___ requires sacrifices.</label> <label>___ requires sacrifices.</label>
<description>The problem with trying to be the bad guy, there's always someone worse.</description> <description>{}</description>
<description>Anyone who looks the world as if it was a game of chess deserves to lose.</description> <description>{}</description>
<textline size="40"/> <textline size="40"/>
</stringresponse> </stringresponse>
</problem> </problem>
""" """.format(desc1, desc2)
problem = new_loncapa_problem(xml) problem = new_loncapa_problem(xml)
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
...@@ -150,8 +176,8 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -150,8 +176,8 @@ class CAPAProblemTest(unittest.TestCase):
{ {
'label': '___ requires sacrifices.', 'label': '___ requires sacrifices.',
'descriptions': { 'descriptions': {
'description_1_1_1': "The problem with trying to be the bad guy, there's always someone worse.", 'description_1_1_1': desc1,
'description_1_1_2': "Anyone who looks the world as if it was a game of chess deserves to lose." 'description_1_1_2': desc2
} }
} }
} }
...@@ -298,11 +324,15 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -298,11 +324,15 @@ class CAPAProblemTest(unittest.TestCase):
1 1
) )
def test_multiple_inputtypes(self): @ddt.unpack
@ddt.data(
{'group_label': 'Choose the correct color'},
{'group_label': 'Choose the <b>correct</b> <mark>color</mark>'},
)
def test_multiple_inputtypes(self, group_label):
""" """
Verify that group label and labels for individual inputtypes are extracted correctly. Verify that group label and labels for individual inputtypes are extracted correctly.
""" """
group_label = 'Choose the correct color'
input1_label = 'What color is the sky?' input1_label = 'What color is the sky?'
input2_label = 'What color are pine needles?' input2_label = 'What color are pine needles?'
xml = """ xml = """
...@@ -424,39 +454,6 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -424,39 +454,6 @@ class CAPAProblemTest(unittest.TestCase):
self.assert_question_tag(question1, question2, tag='label', label_attr=False) self.assert_question_tag(question1, question2, tag='label', label_attr=False)
self.assert_question_tag(question1, question2, tag='p', label_attr=True) self.assert_question_tag(question1, question2, tag='p', label_attr=True)
def test_question_tag_child_left(self):
"""
If the "old" question tag has children, don't delete the children when
transforming to the new label tag.
"""
xml = """
<problem>
<p>Question<img src='img/src'/></p>
<choiceresponse>
<checkboxgroup label="Question">
<choice correct="true">choice1</choice>
<choice correct="false">choice2</choice>
</checkboxgroup>
</choiceresponse>
</problem>
"""
problem = new_loncapa_problem(xml)
self.assertEqual(
problem.problem_data,
{
'1_2_1':
{
'label': "Question",
'descriptions': {}
}
}
)
# img tag is still present within the paragraph, but p text has been deleted
self.assertEqual(len(problem.tree.xpath('//p')), 1)
self.assertEqual(problem.tree.xpath('//p')[0].text, '')
self.assertEqual(len(problem.tree.xpath('//p/img')), 1)
@ddt.ddt @ddt.ddt
class CAPAMultiInputProblemTest(unittest.TestCase): class CAPAMultiInputProblemTest(unittest.TestCase):
......
...@@ -186,7 +186,7 @@ class CapaHtmlRenderTest(unittest.TestCase): ...@@ -186,7 +186,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
'trailing_text': '', 'trailing_text': '',
'size': None, 'size': None,
'response_data': {'label': '', 'descriptions': {}}, 'response_data': {'label': '', 'descriptions': {}},
'describedby': '' 'describedby_html': ''
} }
expected_solution_context = {'id': '1_solution_1'} expected_solution_context = {'id': '1_solution_1'}
......
...@@ -11,6 +11,7 @@ from lxml import etree ...@@ -11,6 +11,7 @@ from lxml import etree
from mako.template import Template as MakoTemplate from mako.template import Template as MakoTemplate
from mako import exceptions from mako import exceptions
from capa.inputtypes import Status from capa.inputtypes import Status
from xmodule.stringify import stringify_children
class TemplateError(Exception): class TemplateError(Exception):
...@@ -31,7 +32,12 @@ class TemplateTestCase(unittest.TestCase): ...@@ -31,7 +32,12 @@ class TemplateTestCase(unittest.TestCase):
# for example: choicegroup.html # for example: choicegroup.html
TEMPLATE_NAME = None TEMPLATE_NAME = None
DESCRIBEDBY = 'aria-describedby="desc-1 desc-2"' DESCRIBEDBY = 'aria-describedby="desc-1 desc-2"'
DESCRIPTIONS = OrderedDict([('desc-1', 'description text 1'), ('desc-2', 'description text 2')]) DESCRIPTIONS = OrderedDict(
[
('desc-1', 'description text 1'),
('desc-2', '<em>description</em> <mark>text</mark> 2')
]
)
DESCRIPTION_IDS = ' '.join(DESCRIPTIONS.keys()) DESCRIPTION_IDS = ' '.join(DESCRIPTIONS.keys())
RESPONSE_DATA = { RESPONSE_DATA = {
'label': 'question text 101', 'label': 'question text 101',
...@@ -48,7 +54,7 @@ class TemplateTestCase(unittest.TestCase): ...@@ -48,7 +54,7 @@ class TemplateTestCase(unittest.TestCase):
'templates', 'templates',
self.TEMPLATE_NAME) self.TEMPLATE_NAME)
with open(self.template_path) as f: with open(self.template_path) as f:
self.template = MakoTemplate(f.read()) self.template = MakoTemplate(f.read(), default_filters=['decode.utf8'])
self.context = {} self.context = {}
...@@ -118,25 +124,22 @@ class TemplateTestCase(unittest.TestCase): ...@@ -118,25 +124,22 @@ class TemplateTestCase(unittest.TestCase):
self.assertGreater(len(element_list), 0, "Could not find element at '%s'" % str(xpath)) self.assertGreater(len(element_list), 0, "Could not find element at '%s'" % str(xpath))
if exact: if exact:
self.assertEqual(text, element_list[0].text) self.assertEqual(text, element_list[0].text.strip())
else: else:
self.assertIn(text, element_list[0].text) self.assertIn(text, element_list[0].text.strip())
def assert_description(self, describedby_xpaths, descriptions=True): def assert_description(self, describedby_xpaths):
""" """
Verify that descriptions information is correct. Verify that descriptions information is correct.
Arguments: Arguments:
describedby_xpaths (list): list of xpaths to check aria-describedby attribute describedby_xpaths (list): list of xpaths to check aria-describedby attribute
descriptions (bool): tells whether we need to check description <p> tags
""" """
xml = self.render_to_xml(self.context) xml = self.render_to_xml(self.context)
# TODO! This check should be removed once description <p> tags are added into all templates.
if descriptions:
# Verify that each description <p> tag has correct id, text and order # Verify that each description <p> tag has correct id, text and order
descriptions = OrderedDict( descriptions = OrderedDict(
(tag.get('id'), tag.text) for tag in xml.xpath('//p[@class="question-description"]') (tag.get('id'), stringify_children(tag)) for tag in xml.xpath('//p[@class="question-description"]')
) )
self.assertEqual(self.DESCRIPTIONS, descriptions) self.assertEqual(self.DESCRIPTIONS, descriptions)
...@@ -157,7 +160,7 @@ class TemplateTestCase(unittest.TestCase): ...@@ -157,7 +160,7 @@ class TemplateTestCase(unittest.TestCase):
Arguments: Arguments:
describedby_xpaths (list): list of xpaths to check aria-describedby attribute describedby_xpaths (list): list of xpaths to check aria-describedby attribute
""" """
self.context['describedby'] = '' self.context['describedby_html'] = ''
xml = self.render_to_xml(self.context) xml = self.render_to_xml(self.context)
# for each xpath verify that description_ids are set correctly # for each xpath verify that description_ids are set correctly
...@@ -199,6 +202,44 @@ class TemplateTestCase(unittest.TestCase): ...@@ -199,6 +202,44 @@ class TemplateTestCase(unittest.TestCase):
self.context['status'].display_tooltip self.context['status'].display_tooltip
) )
def assert_label(self, xpath=None, aria_label=False):
"""
Verify label is rendered correctly.
Arguments:
xpath (str): xpath expression for label element
aria_label (bool): check aria-label attribute value
"""
labels = [
{
'actual': "You see, but you do not observe. The distinction is clear.",
'expected': "You see, but you do not observe. The distinction is clear.",
},
{
'actual': "I choose to have <mark>faith</mark> because without that, I have <em>nothing</em>.",
'expected': "I choose to have faith because without that, I have nothing.",
}
]
response_data = {
'response_data': {
'descriptions': {},
'label': ''
}
}
self.context.update(response_data)
for label in labels:
self.context['response_data']['label'] = label['actual']
xml = self.render_to_xml(self.context)
if aria_label:
self.assert_has_xpath(xml, "//*[@aria-label='%s']" % label['expected'], self.context)
else:
element_list = xml.xpath(xpath)
self.assertEqual(len(element_list), 1)
self.assertEqual(stringify_children(element_list[0]), label['actual'])
class ChoiceGroupTemplateTest(TemplateTestCase): class ChoiceGroupTemplateTest(TemplateTestCase):
""" """
...@@ -218,7 +259,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -218,7 +259,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
'name_array_suffix': '1', 'name_array_suffix': '1',
'value': '3', 'value': '3',
'response_data': self.RESPONSE_DATA, 'response_data': self.RESPONSE_DATA,
'describedby': self.DESCRIBEDBY, 'describedby_html': self.DESCRIBEDBY,
} }
def test_problem_marked_correct(self): def test_problem_marked_correct(self):
...@@ -429,9 +470,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -429,9 +470,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
self.assert_no_xpath(xml, "//div[@class='capa_alert']", self.context) self.assert_no_xpath(xml, "//div[@class='capa_alert']", self.context)
def test_label(self): def test_label(self):
xml = self.render_to_xml(self.context) """
xpath = "//legend" Verify label element value rendering.
self.assert_has_text(xml, xpath, self.context['response_data']['label']) """
self.assert_label(xpath="//legend")
def test_description(self): def test_description(self):
""" """
...@@ -464,7 +506,7 @@ class TextlineTemplateTest(TemplateTestCase): ...@@ -464,7 +506,7 @@ class TextlineTemplateTest(TemplateTestCase):
'preprocessor': None, 'preprocessor': None,
'trailing_text': None, 'trailing_text': None,
'response_data': self.RESPONSE_DATA, 'response_data': self.RESPONSE_DATA,
'describedby': self.DESCRIBEDBY, 'describedby_html': self.DESCRIBEDBY,
} }
def test_section_class(self): def test_section_class(self):
...@@ -487,8 +529,10 @@ class TextlineTemplateTest(TemplateTestCase): ...@@ -487,8 +529,10 @@ class TextlineTemplateTest(TemplateTestCase):
self.assert_status(status_div=True) self.assert_status(status_div=True)
def test_label(self): def test_label(self):
xml = self.render_to_xml(self.context) """
self.assert_has_xpath(xml, "//label[@class='problem-group-label']", self.RESPONSE_DATA['label']) Verify label element value rendering.
"""
self.assert_label(xpath="//label[@class='problem-group-label']")
def test_hidden(self): def test_hidden(self):
self.context['hidden'] = True self.context['hidden'] = True
...@@ -588,7 +632,7 @@ class FormulaEquationInputTemplateTest(TemplateTestCase): ...@@ -588,7 +632,7 @@ class FormulaEquationInputTemplateTest(TemplateTestCase):
'reported_status': 'REPORTED_STATUS', 'reported_status': 'REPORTED_STATUS',
'trailing_text': None, 'trailing_text': None,
'response_data': self.RESPONSE_DATA, 'response_data': self.RESPONSE_DATA,
'describedby': self.DESCRIBEDBY, 'describedby_html': self.DESCRIBEDBY,
} }
def test_no_size(self): def test_no_size(self):
...@@ -615,6 +659,12 @@ class FormulaEquationInputTemplateTest(TemplateTestCase): ...@@ -615,6 +659,12 @@ class FormulaEquationInputTemplateTest(TemplateTestCase):
""" """
self.assert_status(status_div=True) self.assert_status(status_div=True)
def test_label(self):
"""
Verify label element value rendering.
"""
self.assert_label(xpath="//label[@class='problem-group-label']")
class AnnotationInputTemplateTest(TemplateTestCase): class AnnotationInputTemplateTest(TemplateTestCase):
""" """
...@@ -802,13 +852,13 @@ class OptionInputTemplateTest(TemplateTestCase): ...@@ -802,13 +852,13 @@ class OptionInputTemplateTest(TemplateTestCase):
'value': 0, 'value': 0,
'default_option_text': 'Select an option', 'default_option_text': 'Select an option',
'response_data': self.RESPONSE_DATA, 'response_data': self.RESPONSE_DATA,
'describedby': self.DESCRIBEDBY, 'describedby_html': self.DESCRIBEDBY,
} }
def test_select_options(self): def test_select_options(self):
# Create options 0-4, and select option 2 # Create options 0-4, and select option 2
self.context['options'] = [(id_num, '<b>Option {0}</b>'.format(id_num)) self.context['options'] = [(id_num, 'Option {0}'.format(id_num))
for id_num in range(5)] for id_num in range(5)]
self.context['value'] = 2 self.context['value'] = 2
...@@ -818,15 +868,12 @@ class OptionInputTemplateTest(TemplateTestCase): ...@@ -818,15 +868,12 @@ class OptionInputTemplateTest(TemplateTestCase):
xpath = "//option[@value='option_2_dummy_default']" xpath = "//option[@value='option_2_dummy_default']"
self.assert_has_xpath(xml, xpath, self.context) self.assert_has_xpath(xml, xpath, self.context)
# Should have each of the options, with the correct description
# The description HTML should NOT be escaped
# (that's why we descend into the <b> tag)
for id_num in range(5): for id_num in range(5):
xpath = "//option[@value='{0}']/b".format(id_num) xpath = "//option[@value='{0}']".format(id_num)
self.assert_has_text(xml, xpath, 'Option {0}'.format(id_num)) self.assert_has_text(xml, xpath, 'Option {0}'.format(id_num))
# Should have the correct option selected # Should have the correct option selected
xpath = "//option[@selected='true']/b" xpath = "//option[@selected='true']"
self.assert_has_text(xml, xpath, 'Option 2') self.assert_has_text(xml, xpath, 'Option 2')
def test_status(self): def test_status(self):
...@@ -836,9 +883,10 @@ class OptionInputTemplateTest(TemplateTestCase): ...@@ -836,9 +883,10 @@ class OptionInputTemplateTest(TemplateTestCase):
self.assert_status(status_class=True) self.assert_status(status_class=True)
def test_label(self): def test_label(self):
xml = self.render_to_xml(self.context) """
xpath = "//label[@class='problem-group-label']" Verify label element value rendering.
self.assert_has_xpath(xml, xpath, self.RESPONSE_DATA['label']) """
self.assert_label(xpath="//label[@class='problem-group-label']")
def test_description(self): def test_description(self):
""" """
...@@ -1077,7 +1125,59 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase): ...@@ -1077,7 +1125,59 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase):
xpath = "//div[@class='indicator-container']/span" xpath = "//div[@class='indicator-container']/span"
self.assert_no_xpath(xml, xpath, self.context) self.assert_no_xpath(xml, xpath, self.context)
def test_label(self): def test_aria_label(self):
xml = self.render_to_xml(self.context) """
xpath = "//fieldset[@aria-label='%s']" % self.context['response_data']['label'] Verify aria-label attribute rendering.
self.assert_has_xpath(xml, xpath, self.context) """
self.assert_label(aria_label=True)
class ChemicalEquationTemplateTest(TemplateTestCase):
"""Test mako template for `<chemicalequationinput>` input"""
TEMPLATE_NAME = 'chemicalequationinput.html'
def setUp(self):
super(ChemicalEquationTemplateTest, self).setUp()
self.context = {
'id': '1',
'status': Status('correct'),
'previewer': 'dummy.js',
'value': '101',
}
def test_aria_label(self):
"""
Verify aria-label attribute rendering.
"""
self.assert_label(aria_label=True)
class SchematicInputTemplateTest(TemplateTestCase):
"""Test mako template for `<schematic>` input"""
TEMPLATE_NAME = 'schematicinput.html'
def setUp(self):
super(SchematicInputTemplateTest, self).setUp()
self.context = {
'id': '1',
'status': Status('correct'),
'previewer': 'dummy.js',
'value': '101',
'STATIC_URL': '/dummy-static/',
'msg': '',
'initial_value': 'two large batteries',
'width': '100',
'height': '100',
'parts': 'resistors, capacitors, and flowers',
'setup_script': '/dummy-static/js/capa/schematicinput.js',
'analyses': 'fast, slow, and pink',
'submit_analyses': 'maybe',
}
def test_aria_label(self):
"""
Verify aria-label attribute rendering.
"""
self.assert_label(aria_label=True)
...@@ -78,7 +78,7 @@ class OptionInputTest(unittest.TestCase): ...@@ -78,7 +78,7 @@ class OptionInputTest(unittest.TestCase):
'id': 'sky_input', 'id': 'sky_input',
'default_option_text': 'Select an option', 'default_option_text': 'Select an option',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -147,7 +147,7 @@ class ChoiceGroupTest(unittest.TestCase): ...@@ -147,7 +147,7 @@ class ChoiceGroupTest(unittest.TestCase):
'submitted_message': 'Answer received.', 'submitted_message': 'Answer received.',
'name_array_suffix': expected_suffix, # what is this for?? 'name_array_suffix': expected_suffix, # what is this for??
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -201,7 +201,7 @@ class JavascriptInputTest(unittest.TestCase): ...@@ -201,7 +201,7 @@ class JavascriptInputTest(unittest.TestCase):
'display_class': display_class, 'display_class': display_class,
'problem_state': problem_state, 'problem_state': problem_state,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -239,7 +239,7 @@ class TextLineTest(unittest.TestCase): ...@@ -239,7 +239,7 @@ class TextLineTest(unittest.TestCase):
'trailing_text': '', 'trailing_text': '',
'preprocessor': None, 'preprocessor': None,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -278,7 +278,7 @@ class TextLineTest(unittest.TestCase): ...@@ -278,7 +278,7 @@ class TextLineTest(unittest.TestCase):
'script_src': script, 'script_src': script,
}, },
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -323,7 +323,7 @@ class TextLineTest(unittest.TestCase): ...@@ -323,7 +323,7 @@ class TextLineTest(unittest.TestCase):
'trailing_text': expected_text, 'trailing_text': expected_text,
'preprocessor': None, 'preprocessor': None,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -366,7 +366,7 @@ class FileSubmissionTest(unittest.TestCase): ...@@ -366,7 +366,7 @@ class FileSubmissionTest(unittest.TestCase):
'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]', 'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
'required_files': '["cookies.py"]', 'required_files': '["cookies.py"]',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -422,7 +422,7 @@ class CodeInputTest(unittest.TestCase): ...@@ -422,7 +422,7 @@ class CodeInputTest(unittest.TestCase):
'tabsize': int(tabsize), 'tabsize': int(tabsize),
'queue_len': '3', 'queue_len': '3',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -484,7 +484,7 @@ class MatlabTest(unittest.TestCase): ...@@ -484,7 +484,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '3', 'queue_len': '3',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js', 'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': {}, 'response_data': {},
'describedby': '' 'describedby_html': ''
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -519,7 +519,7 @@ class MatlabTest(unittest.TestCase): ...@@ -519,7 +519,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '3', 'queue_len': '3',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js', 'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -553,7 +553,7 @@ class MatlabTest(unittest.TestCase): ...@@ -553,7 +553,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '0', 'queue_len': '0',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js', 'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -587,7 +587,7 @@ class MatlabTest(unittest.TestCase): ...@@ -587,7 +587,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '1', 'queue_len': '1',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js', 'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -705,13 +705,14 @@ class MatlabTest(unittest.TestCase): ...@@ -705,13 +705,14 @@ class MatlabTest(unittest.TestCase):
textwrap.dedent(""" textwrap.dedent("""
<div>{\'status\': Status(\'queued\'), \'button_enabled\': True, <div>{\'status\': Status(\'queued\'), \'button_enabled\': True,
\'rows\': \'10\', \'queue_len\': \'3\', \'mode\': \'\', \'rows\': \'10\', \'queue_len\': \'3\', \'mode\': \'\',
\'tabsize\': 4, \'cols\': \'80\', \'STATIC_URL\': \'/dummy-static/\', \'tabsize\': 4, \'cols\': \'80\',
\'describedby\': \'\', \'queue_msg\': \'\', \'STATIC_URL\': \'/dummy-static/\', \'linenumbers\': \'true\', \'queue_msg\': \'\',
\'value\': \'print "good evening"\', \'value\': \'print "good evening"\',
\'msg\': u\'Submitted. As soon as a response is returned, \'msg\': u\'Submitted. As soon as a response is returned,
this message will be replaced by that feedback.\', this message will be replaced by that feedback.\',
\'matlab_editor_js\': \'/dummy-static/js/vendor/CodeMirror/octave.js\', \'matlab_editor_js\': \'/dummy-static/js/vendor/CodeMirror/octave.js\',
\'hidden\': \'\', \'linenumbers\': \'true\', \'id\': \'prob_1_2\', \'response_data\': {}}</div> \'hidden\': \'\', \'id\': \'prob_1_2\', \'describedby_html\': \'\',
\'response_data\': {}}</div>
""").replace('\n', ' ').strip() """).replace('\n', ' ').strip()
) )
...@@ -818,7 +819,7 @@ class MatlabTest(unittest.TestCase): ...@@ -818,7 +819,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '3', 'queue_len': '3',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js', 'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': {}, 'response_data': {},
'describedby': '' 'describedby_html': ''
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -929,7 +930,7 @@ class SchematicTest(unittest.TestCase): ...@@ -929,7 +930,7 @@ class SchematicTest(unittest.TestCase):
'analyses': analyses, 'analyses': analyses,
'submit_analyses': submit_analyses, 'submit_analyses': submit_analyses,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -975,7 +976,7 @@ class ImageInputTest(unittest.TestCase): ...@@ -975,7 +976,7 @@ class ImageInputTest(unittest.TestCase):
'gy': egy, 'gy': egy,
'msg': '', 'msg': '',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -1031,7 +1032,7 @@ class CrystallographyTest(unittest.TestCase): ...@@ -1031,7 +1032,7 @@ class CrystallographyTest(unittest.TestCase):
'width': width, 'width': width,
'height': height, 'height': height,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -1079,7 +1080,7 @@ class VseprTest(unittest.TestCase): ...@@ -1079,7 +1080,7 @@ class VseprTest(unittest.TestCase):
'molecules': molecules, 'molecules': molecules,
'geometries': geometries, 'geometries': geometries,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -1115,7 +1116,7 @@ class ChemicalEquationTest(unittest.TestCase): ...@@ -1115,7 +1116,7 @@ class ChemicalEquationTest(unittest.TestCase):
'size': self.size, 'size': self.size,
'previewer': '/dummy-static/js/capa/chemical_equation_preview.js', 'previewer': '/dummy-static/js/capa/chemical_equation_preview.js',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -1210,7 +1211,7 @@ class FormulaEquationTest(unittest.TestCase): ...@@ -1210,7 +1211,7 @@ class FormulaEquationTest(unittest.TestCase):
'inline': False, 'inline': False,
'trailing_text': '', 'trailing_text': '',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -1256,7 +1257,7 @@ class FormulaEquationTest(unittest.TestCase): ...@@ -1256,7 +1257,7 @@ class FormulaEquationTest(unittest.TestCase):
'inline': False, 'inline': False,
'trailing_text': expected_text, 'trailing_text': expected_text,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -1388,7 +1389,7 @@ class DragAndDropTest(unittest.TestCase): ...@@ -1388,7 +1389,7 @@ class DragAndDropTest(unittest.TestCase):
'msg': '', 'msg': '',
'drag_and_drop_json': json.dumps(user_input), 'drag_and_drop_json': json.dumps(user_input),
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
# as we are dumping 'draggables' dicts while dumping user_input, string # as we are dumping 'draggables' dicts while dumping user_input, string
...@@ -1457,7 +1458,7 @@ class AnnotationInputTest(unittest.TestCase): ...@@ -1457,7 +1458,7 @@ class AnnotationInputTest(unittest.TestCase):
'debug': False, 'debug': False,
'return_to_annotation': True, 'return_to_annotation': True,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
self.maxDiff = None self.maxDiff = None
...@@ -1522,7 +1523,7 @@ class TestChoiceText(unittest.TestCase): ...@@ -1522,7 +1523,7 @@ class TestChoiceText(unittest.TestCase):
'show_correctness': 'always', 'show_correctness': 'always',
'submitted_message': 'Answer received.', 'submitted_message': 'Answer received.',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY 'describedby_html': DESCRIBEDBY
} }
expected.update(state) expected.update(state)
the_input = lookup_tag(tag)(test_capa_system(), element, state) the_input = lookup_tag(tag)(test_capa_system(), element, state)
......
...@@ -5,7 +5,7 @@ import unittest ...@@ -5,7 +5,7 @@ import unittest
from lxml import etree from lxml import etree
from capa.tests.helpers import test_capa_system from capa.tests.helpers import test_capa_system
from capa.util import compare_with_tolerance, sanitize_html, get_inner_html_from_xpath from capa.util import compare_with_tolerance, sanitize_html, get_inner_html_from_xpath, remove_markup
class UtilTest(unittest.TestCase): class UtilTest(unittest.TestCase):
...@@ -126,3 +126,12 @@ class UtilTest(unittest.TestCase): ...@@ -126,3 +126,12 @@ class UtilTest(unittest.TestCase):
""" """
xpath_node = etree.XML('<hint style="smtng">aa<a href="#">bb</a>cc</hint>') xpath_node = etree.XML('<hint style="smtng">aa<a href="#">bb</a>cc</hint>')
self.assertEqual(get_inner_html_from_xpath(xpath_node), 'aa<a href="#">bb</a>cc') self.assertEqual(get_inner_html_from_xpath(xpath_node), 'aa<a href="#">bb</a>cc')
def test_remove_markup(self):
"""
Test for markup removal with bleach.
"""
self.assertEqual(
remove_markup("The <mark>Truth</mark> is <em>Out There</em> & you need to <strong>find</strong> it"),
"The Truth is Out There &amp; you need to find it"
)
...@@ -8,6 +8,7 @@ from calc import evaluator ...@@ -8,6 +8,7 @@ from calc import evaluator
from cmath import isinf, isnan from cmath import isinf, isnan
import re import re
from lxml import etree from lxml import etree
from openedx.core.djangolib.markup import HTML
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# #
# Utility functions used in CAPA responsetypes # Utility functions used in CAPA responsetypes
...@@ -195,3 +196,15 @@ def get_inner_html_from_xpath(xpath_node): ...@@ -195,3 +196,15 @@ def get_inner_html_from_xpath(xpath_node):
# strips outer tag from html string # strips outer tag from html string
inner_html = re.sub('(?ms)<%s[^>]*>(.*)</%s>' % (xpath_node.tag, xpath_node.tag), '\\1', html) inner_html = re.sub('(?ms)<%s[^>]*>(.*)</%s>' % (xpath_node.tag, xpath_node.tag), '\\1', html)
return inner_html.strip() return inner_html.strip()
def remove_markup(html):
"""
Return html with markup stripped and text HTML-escaped.
>>> bleach.clean("<b>Rock & Roll</b>", tags=[], strip=True)
u'Rock &amp; Roll'
>>> bleach.clean("<b>Rock &amp; Roll</b>", tags=[], strip=True)
u'Rock &amp; Roll'
"""
return HTML(bleach.clean(html, tags=[], strip=True))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment