Commit 6ff762b6 by muhammad-ammar

allow html inside label and descriptions

TNL-5557
parent 2b80c619
......@@ -31,6 +31,8 @@ import capa.responsetypes as responsetypes
from capa.util import contextualize_text, convert_files_to_filenames
import capa.xqueue_interface as xqueue_interface
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
......@@ -926,7 +928,7 @@ class LoncapaProblem(object):
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
group_label_tag_text = stringify_children(group_label_tag)
for inputfield in inputfields:
problem_data[inputfield.get('id')] = {
......@@ -938,7 +940,7 @@ class LoncapaProblem(object):
# Extract label value from <label> tag or label attribute from inside the responsetype
responsetype_label_tag = response.find('label')
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
# it later otherwise question will be rendered twice
element_to_be_deleted = responsetype_label_tag
......@@ -950,21 +952,15 @@ class LoncapaProblem(object):
p_tag = response.xpath('preceding-sibling::*[1][self::p]')
if p_tag and p_tag[0].text == inputfields[0].attrib['label']:
label = p_tag[0].text
p_tag_children = list(p_tag[0])
if len(p_tag_children) == 0:
element_to_be_deleted = p_tag[0]
else:
# Delete the text from the p-tag, but leave the children.
p_tag[0].text = ''
label = stringify_children(p_tag[0])
element_to_be_deleted = p_tag[0]
else:
# 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.
# 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]')
if label_tag:
label = label_tag[0].text
label = stringify_children(label_tag[0])
element_to_be_deleted = label_tag[0]
# delete label or p element only if inputtype is fully accessible
......@@ -978,11 +974,11 @@ class LoncapaProblem(object):
for description in description_tags:
descriptions[
"description_%s_%i" % (responsetype_id, description_id)
] = description.text
] = HTML(stringify_children(description))
response.remove(description)
description_id += 1
problem_data[inputfields[0].get('id')] = {
'label': label.strip() if label else '',
'label': HTML(label.strip()) if label else '',
'descriptions': descriptions
}
......@@ -322,14 +322,14 @@ class InputTypeBase(object):
'msg': self.msg,
'response_data': self.response_data,
'STATIC_URL': self.capa_system.STATIC_URL,
'describedby': '',
'describedby_html': '',
}
# Don't add aria-describedby attribute if there are no descriptions
if self.response_data.get('descriptions'):
description_ids = ' '.join(self.response_data.get('descriptions').keys())
context.update(
{'describedby': 'aria-describedby="{}"'.format(description_ids)}
{'describedby_html': 'aria-describedby="{}"'.format(description_ids)}
)
context.update(
......
<%! from capa.util import remove_markup %>
<div id="chemicalequationinput_${id}" class="chemicalequationinput">
<div class="script_placeholder" data-src="${previewer}"/>
<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:
size="${size}"
% endif
......
<%page expression_filter="h"/>
<%! from openedx.core.djangolib.markup import HTML %>
<%
def is_radio_input(choice_id):
......@@ -6,7 +7,7 @@
))
%>
<form class="choicegroup capa_inputtype" id="inputtype_${id}">
<fieldset ${describedby}>
<fieldset ${HTML(describedby_html)}>
% if response_data['label']:
<legend id="${id}-legend" class="response-fieldset-legend field-group-hd">${response_data['label']}</legend>
% endif
......@@ -36,7 +37,7 @@
% endif
% endif
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}"
## If the student selected this choice...
......@@ -45,7 +46,7 @@
% elif input_type != 'radio' and choice_id in value:
checked="true"
% endif
/> ${choice_label}
/> ${HTML(choice_label)}
% if is_radio_input(choice_id):
% 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 _ %>
<% element_checked = False %>
% for choice_id, _ in choices:
......@@ -10,7 +11,7 @@
<form class="choicetextgroup capa_inputtype" id="inputtype_${id}">
<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:
<% choice_id = choice_id %>
<section id="forinput${choice_id}"
......
......@@ -11,7 +11,7 @@
% endfor
<input type="text" name="input_${id}" id="input_${id}"
data-input-id="${id}" value="${value}"
${describedby | n, decode.utf8}
${HTML(describedby_html)}
% if size:
size="${size}"
% endif
......
<%page expression_filter="h"/>
<%! from openedx.core.djangolib.markup import HTML %>
<% doinline = "inline" if inline else "" %>
......@@ -10,7 +11,7 @@
<p class="question-description" id="${description_id}">${description_text}</p>
% 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>
% for option_id, option_description in options:
<option value="${option_id}"
......
<%! from capa.util import remove_markup %>
<div>
<div class="script_placeholder" data-src="${setup_script}"/>
<input type="hidden"
......@@ -8,7 +9,7 @@
analyses="${analyses}"
name="input_${id}"
id="input_${id}"
aria-label="${response_data['label']}"
aria-label="${remove_markup(response_data['label'])}"
aria-describedby="answer_${id}"
value="${value|h}"
initial_value="${initial_value|h}"
......
......@@ -23,7 +23,7 @@
% for description_id, description_text in response_data['descriptions'].items():
<p class="question-description" id="${description_id}">${description_text}</p>
% 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:
class="math"
% endif
......
......@@ -13,7 +13,12 @@ from capa.tests.helpers import new_loncapa_problem
class CAPAProblemTest(unittest.TestCase):
""" 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
* label is extracted
......@@ -25,7 +30,7 @@ class CAPAProblemTest(unittest.TestCase):
xml = """
<problem>
<choiceresponse>
<label>Select the correct synonym of paranoid?</label>
<label>{question}</label>
<description>Only the paranoid survive.</description>
<checkboxgroup>
<choice correct="true">over-suspicious</choice>
......@@ -33,25 +38,35 @@ class CAPAProblemTest(unittest.TestCase):
</checkboxgroup>
</choiceresponse>
</problem>
"""
""".format(question=question)
problem = new_loncapa_problem(xml)
self.assertEqual(
problem.problem_data,
{
'1_2_1':
{
'label': 'Select the correct synonym of paranoid?',
'label': question,
'descriptions': {'description_1_1_1': 'Only the paranoid survive.'}
}
}
)
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.
"""
question = "Once we become predictable, we become ______?"
xml = """
<problem>
<p>Be sure to check your spelling.</p>
......@@ -60,7 +75,7 @@ class CAPAProblemTest(unittest.TestCase):
<textline label="{}" size="40"/>
</stringresponse>
</problem>
""".format(question, question)
""".format(question, label_attr)
problem = new_loncapa_problem(xml)
self.assertEqual(
problem.problem_data,
......@@ -77,7 +92,18 @@ class CAPAProblemTest(unittest.TestCase):
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.
......@@ -86,8 +112,6 @@ class CAPAProblemTest(unittest.TestCase):
tag and label attribute inside responsetype. But we have a label tag
before the responsetype.
"""
question1 = 'People who say they have nothing to ____ almost always do?'
question2 = 'Select the correct synonym of paranoid?'
xml = """
<problem>
<p>Be sure to check your spelling.</p>
......@@ -131,17 +155,19 @@ class CAPAProblemTest(unittest.TestCase):
"""
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 = """
<problem>
<p>Be sure to check your spelling.</p>
<stringresponse answer="War" type="ci">
<label>___ requires sacrifices.</label>
<description>The problem with trying to be the bad guy, there's always someone worse.</description>
<description>Anyone who looks the world as if it was a game of chess deserves to lose.</description>
<description>{}</description>
<description>{}</description>
<textline size="40"/>
</stringresponse>
</problem>
"""
""".format(desc1, desc2)
problem = new_loncapa_problem(xml)
self.assertEqual(
problem.problem_data,
......@@ -150,8 +176,8 @@ class CAPAProblemTest(unittest.TestCase):
{
'label': '___ requires sacrifices.',
'descriptions': {
'description_1_1_1': "The problem with trying to be the bad guy, there's always someone worse.",
'description_1_1_2': "Anyone who looks the world as if it was a game of chess deserves to lose."
'description_1_1_1': desc1,
'description_1_1_2': desc2
}
}
}
......@@ -298,11 +324,15 @@ class CAPAProblemTest(unittest.TestCase):
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.
"""
group_label = 'Choose the correct color'
input1_label = 'What color is the sky?'
input2_label = 'What color are pine needles?'
xml = """
......@@ -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='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
class CAPAMultiInputProblemTest(unittest.TestCase):
......
......@@ -186,7 +186,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
'trailing_text': '',
'size': None,
'response_data': {'label': '', 'descriptions': {}},
'describedby': ''
'describedby_html': ''
}
expected_solution_context = {'id': '1_solution_1'}
......
......@@ -78,7 +78,7 @@ class OptionInputTest(unittest.TestCase):
'id': 'sky_input',
'default_option_text': 'Select an option',
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -147,7 +147,7 @@ class ChoiceGroupTest(unittest.TestCase):
'submitted_message': 'Answer received.',
'name_array_suffix': expected_suffix, # what is this for??
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -201,7 +201,7 @@ class JavascriptInputTest(unittest.TestCase):
'display_class': display_class,
'problem_state': problem_state,
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -239,7 +239,7 @@ class TextLineTest(unittest.TestCase):
'trailing_text': '',
'preprocessor': None,
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -278,7 +278,7 @@ class TextLineTest(unittest.TestCase):
'script_src': script,
},
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -323,7 +323,7 @@ class TextLineTest(unittest.TestCase):
'trailing_text': expected_text,
'preprocessor': None,
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -366,7 +366,7 @@ class FileSubmissionTest(unittest.TestCase):
'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
'required_files': '["cookies.py"]',
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -422,7 +422,7 @@ class CodeInputTest(unittest.TestCase):
'tabsize': int(tabsize),
'queue_len': '3',
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -484,7 +484,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '3',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': {},
'describedby': ''
'describedby_html': ''
}
self.assertEqual(context, expected)
......@@ -519,7 +519,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '3',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -553,7 +553,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '0',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -587,7 +587,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '1',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -705,13 +705,14 @@ class MatlabTest(unittest.TestCase):
textwrap.dedent("""
<div>{\'status\': Status(\'queued\'), \'button_enabled\': True,
\'rows\': \'10\', \'queue_len\': \'3\', \'mode\': \'\',
\'tabsize\': 4, \'cols\': \'80\', \'STATIC_URL\': \'/dummy-static/\',
\'describedby\': \'\', \'queue_msg\': \'\',
\'tabsize\': 4, \'cols\': \'80\',
\'STATIC_URL\': \'/dummy-static/\', \'linenumbers\': \'true\', \'queue_msg\': \'\',
\'value\': \'print "good evening"\',
\'msg\': u\'Submitted. As soon as a response is returned,
this message will be replaced by that feedback.\',
\'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()
)
......@@ -818,7 +819,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '3',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': {},
'describedby': ''
'describedby_html': ''
}
self.assertEqual(context, expected)
......@@ -929,7 +930,7 @@ class SchematicTest(unittest.TestCase):
'analyses': analyses,
'submit_analyses': submit_analyses,
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -975,7 +976,7 @@ class ImageInputTest(unittest.TestCase):
'gy': egy,
'msg': '',
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -1031,7 +1032,7 @@ class CrystallographyTest(unittest.TestCase):
'width': width,
'height': height,
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -1079,7 +1080,7 @@ class VseprTest(unittest.TestCase):
'molecules': molecules,
'geometries': geometries,
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -1115,7 +1116,7 @@ class ChemicalEquationTest(unittest.TestCase):
'size': self.size,
'previewer': '/dummy-static/js/capa/chemical_equation_preview.js',
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -1210,7 +1211,7 @@ class FormulaEquationTest(unittest.TestCase):
'inline': False,
'trailing_text': '',
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -1256,7 +1257,7 @@ class FormulaEquationTest(unittest.TestCase):
'inline': False,
'trailing_text': expected_text,
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.assertEqual(context, expected)
......@@ -1388,7 +1389,7 @@ class DragAndDropTest(unittest.TestCase):
'msg': '',
'drag_and_drop_json': json.dumps(user_input),
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
# as we are dumping 'draggables' dicts while dumping user_input, string
......@@ -1457,7 +1458,7 @@ class AnnotationInputTest(unittest.TestCase):
'debug': False,
'return_to_annotation': True,
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
self.maxDiff = None
......@@ -1522,7 +1523,7 @@ class TestChoiceText(unittest.TestCase):
'show_correctness': 'always',
'submitted_message': 'Answer received.',
'response_data': RESPONSE_DATA,
'describedby': DESCRIBEDBY
'describedby_html': DESCRIBEDBY
}
expected.update(state)
the_input = lookup_tag(tag)(test_capa_system(), element, state)
......
......@@ -5,7 +5,7 @@ import unittest
from lxml import etree
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):
......@@ -126,3 +126,12 @@ class UtilTest(unittest.TestCase):
"""
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')
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
from cmath import isinf, isnan
import re
from lxml import etree
from openedx.core.djangolib.markup import HTML
#-----------------------------------------------------------------------------
#
# Utility functions used in CAPA responsetypes
......@@ -195,3 +196,15 @@ def get_inner_html_from_xpath(xpath_node):
# strips outer tag from html string
inner_html = re.sub('(?ms)<%s[^>]*>(.*)</%s>' % (xpath_node.tag, xpath_node.tag), '\\1', html)
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