0001-fields-display_mode-rich-for-TextField-36498.patch
tests/admin_pages/test_form.py | ||
---|---|---|
2022 | 2022 |
assert 'syntax error' in resp.text |
2023 | 2023 | |
2024 | 2024 | |
2025 |
def test_form_edit_text_field(pub): |
|
2026 |
create_superuser(pub) |
|
2027 |
create_role(pub) |
|
2028 | ||
2029 |
FormDef.wipe() |
|
2030 |
formdef = FormDef() |
|
2031 |
formdef.name = 'form title' |
|
2032 |
formdef.fields = [fields.TextField(id='1', label='1st field', type='text')] |
|
2033 |
formdef.store() |
|
2034 | ||
2035 |
app = login(get_app(pub)) |
|
2036 |
resp = app.get('/backoffice/forms/1/fields/1/') |
|
2037 |
assert resp.form['display_mode'].options == [ |
|
2038 |
('Plain Text (paragraphs)', True, None), |
|
2039 |
('Preformatted Text', False, None), |
|
2040 |
] |
|
2041 | ||
2042 |
pub.site_options.set('options', 'enable-richtext-field', 'true') |
|
2043 |
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd: |
|
2044 |
pub.site_options.write(fd) |
|
2045 |
resp = app.get('/backoffice/forms/1/fields/1/') |
|
2046 |
assert resp.form['display_mode'].options == [ |
|
2047 |
('Plain Text (paragraphs)', True, None), |
|
2048 |
('Preformatted Text', False, None), |
|
2049 |
('Rich Text', False, None), |
|
2050 |
] |
|
2051 | ||
2052 | ||
2025 | 2053 |
def test_form_edit_item_field(pub): |
2026 | 2054 |
create_superuser(pub) |
2027 | 2055 |
create_role(pub) |
tests/api/test_formdata.py | ||
---|---|---|
139 | 139 |
fields.FileField(id='3', label='foobar4', varname='file'), |
140 | 140 |
fields.ItemField(id='4', label='foobar5', varname='item', data_source={'type': 'foobar'}), |
141 | 141 |
fields.BlockField(id='5', label='test', varname='blockdata', type='block:foobar', max_items=3), |
142 |
fields.TextField(id='6', label='rich text', varname='richtext', type='text', display_mode='rich'), |
|
142 | 143 |
] |
143 | 144 |
Workflow.wipe() |
144 | 145 |
workflow = Workflow(name='foo') |
... | ... | |
168 | 169 |
'schema': {}, # not important here |
169 | 170 |
}, |
170 | 171 |
'5_display': 'hello', |
172 |
'6': '<script></script><p>foo</p>', |
|
171 | 173 |
} |
172 | 174 |
formdata.data['4_display'] = item_field.store_display_value(formdata.data, item_field.id) |
173 | 175 |
formdata.data['4_structured'] = item_field.store_structured_value(formdata.data, item_field.id) |
... | ... | |
186 | 188 | |
187 | 189 |
assert datetime.datetime.strptime(resp.json['last_update_time'], '%Y-%m-%dT%H:%M:%S') |
188 | 190 |
assert datetime.datetime.strptime(resp.json['receipt_time'], '%Y-%m-%dT%H:%M:%S') |
189 |
assert len(resp.json['fields']) == 8
|
|
191 |
assert len(resp.json['fields']) == 9
|
|
190 | 192 |
assert 'foobar' in resp.json['fields'] |
191 | 193 |
assert 'foobar2' not in resp.json['fields'] # foobar2 has no varname, not in json |
192 | 194 |
assert resp.json['user']['name'] == local_user.name |
... | ... | |
204 | 206 |
assert resp.json['fields']['blockdata_raw'] == [ |
205 | 207 |
{'foo': 'plop', 'bar': 'foo', 'bar_raw': '1', 'bar_structured': 'XXX'} |
206 | 208 |
] |
209 |
assert resp.json['fields']['richtext'] == '<p>foo</p>' # only allowed tags |
|
207 | 210 |
assert resp.json['workflow']['status']['name'] == 'New' |
208 | 211 |
assert resp.json['workflow']['real_status']['name'] == 'New' |
209 | 212 |
assert resp.json['submission'] == {'backoffice': False, 'channel': 'web'} |
tests/test_fields.py | ||
---|---|---|
105 | 105 |
assert fields.TextField().get_view_short_value('foo' * 15) == ('foo' * 10)[:27] + '(…)' |
106 | 106 |
assert fields.TextField().get_view_value('foo') == '<p>foo</p>' |
107 | 107 |
assert fields.TextField().get_view_value('foo\n\nfoo') == '<p>foo\n</p><p>\nfoo</p>' |
108 |
assert fields.TextField(pre=True).get_view_value('foo') == '<pre>foo</pre>' |
|
108 |
assert fields.TextField(display_mode='pre').get_view_value('foo') == '<pre>foo</pre>' |
|
109 |
assert ( |
|
110 |
fields.TextField(display_mode='rich').get_view_short_value('<p>foo</p>' * 15) |
|
111 |
== ('foo' * 10)[:27] + '(…)' |
|
112 |
) |
|
113 |
assert fields.TextField(display_mode='rich').get_view_value('<script></script><p>foo</p>') == '<p>foo</p>' |
|
109 | 114 | |
110 | 115 |
form = Form(use_tokens=False) |
111 | 116 |
fields.TextField().add_to_form(form) |
tests/test_formdef_import.py | ||
---|---|---|
148 | 148 |
formdef = FormDef() |
149 | 149 |
formdef.name = 'Blah' |
150 | 150 |
formdef.fields = [ |
151 |
fields.TextField(type='text', label='Bar', pre=True),
|
|
151 |
fields.TextField(type='text', label='Bar', display_mode='pre'),
|
|
152 | 152 |
fields.EmailField(type='email', label='Bar'), |
153 | 153 |
fields.BoolField(type='bool', label='Bar'), |
154 | 154 |
fields.DateField(type='date', label='Bar', minimum_date='2014-01-01'), |
... | ... | |
204 | 204 |
formdef = FormDef() |
205 | 205 |
formdef.name = 'Blah' |
206 | 206 |
formdef.fields = [ |
207 |
fields.TextField(type='text', label='Bar', pre=True),
|
|
207 |
fields.TextField(type='text', label='Bar', display_mode='pre'),
|
|
208 | 208 |
fields.EmailField(type='email', label='Bar'), |
209 | 209 |
fields.BoolField(type='bool', label='Bar'), |
210 | 210 |
fields.DateField(type='date', label='Bar', minimum_date='2014-01-01'), |
tests/test_templates.py | ||
---|---|---|
1477 | 1477 |
context = {'template': 'Hello {{ form_var_foo }}'} |
1478 | 1478 |
assert Template('{{ template }}').render(context) == 'Hello {{ form_var_foo }}' |
1479 | 1479 |
assert Template('{{ template|as_template }}').render(context) == 'Hello Foo Bar' |
1480 | ||
1481 | ||
1482 |
def test_stripsometags(pub): |
|
1483 |
context = { |
|
1484 |
'value': ( |
|
1485 |
'<h1>title 1</h1>' |
|
1486 |
'<script>my-script</script><p><em>foo</em></p><a href="link" other-attr="foobar">link</a><br />' |
|
1487 |
'<strong>strong</strong>' |
|
1488 |
'<ul><li>li 1</li><li>li 2</li></ul>' |
|
1489 |
) |
|
1490 |
} |
|
1491 | ||
1492 |
assert Template('{{ value|stripsometags }}').render(context) == 'title 1my-scriptfoolinkstrongli 1li 2' |
|
1493 |
assert ( |
|
1494 |
Template('{{ value|stripsometags:"p,br" }}').render(context) |
|
1495 |
== 'title 1my-script<p>foo</p>link<br />strongli 1li 2' |
|
1496 |
) |
|
1497 |
assert ( |
|
1498 |
Template('{{ value|stripsometags:"strong,em" }}').render(context) |
|
1499 |
== 'title 1my-script<em>foo</em>link<strong>strong</strong>li 1li 2' |
|
1500 |
) |
|
1501 |
assert Template('{{ value|stripsometags:"p,br,h1,ul,li" }}').render(context) == ( |
|
1502 |
'<h1>title 1</h1>my-script<p>foo</p>link<br />strong<ul><li>li 1</li><li>li 2</li></ul>' |
|
1503 |
) |
wcs/fields.py | ||
---|---|---|
28 | 28 | |
29 | 29 |
from django.utils.encoding import force_bytes, force_text, smart_text |
30 | 30 |
from django.utils.formats import date_format as django_date_format |
31 |
from django.utils.html import urlize |
|
31 |
from django.utils.html import strip_tags, urlize
|
|
32 | 32 |
from quixote import get_publisher, get_request, get_session |
33 | 33 |
from quixote.html import TemplateIO, htmlescape, htmltag, htmltext |
34 | 34 | |
... | ... | |
59 | 59 |
PasswordEntryWidget, |
60 | 60 |
RadiobuttonsWidget, |
61 | 61 |
RankedItemsWidget, |
62 |
RichTextWidget, |
|
62 | 63 |
SingleSelectHintWidget, |
63 | 64 |
SingleSelectTableWidget, |
64 | 65 |
SingleSelectWidget, |
... | ... | |
80 | 81 |
get_document_type_value_options, |
81 | 82 |
get_document_types, |
82 | 83 |
strftime, |
84 |
strip_some_tags, |
|
83 | 85 |
xml_node_text, |
84 | 86 |
) |
85 | 87 |
from .qommon.ods import NS as OD_NS |
... | ... | |
1322 | 1324 |
widget_class = TextWidget |
1323 | 1325 |
cols = None |
1324 | 1326 |
rows = None |
1325 |
pre = False |
|
1327 |
pre = None |
|
1328 |
display_mode = 'plain' |
|
1326 | 1329 |
maxlength = None |
1327 | 1330 |
extra_attributes = ['cols', 'rows', 'maxlength'] |
1328 | 1331 | |
1332 |
def migrate(self): |
|
1333 |
changed = super().migrate() |
|
1334 |
if isinstance(getattr(self, 'pre', None), bool): |
|
1335 |
if self.pre: |
|
1336 |
self.display_mode = 'pre' |
|
1337 |
else: |
|
1338 |
self.display_mode = 'plain' |
|
1339 |
self.pre = None |
|
1340 |
changed = True |
|
1341 |
return changed |
|
1342 | ||
1343 |
def perform_more_widget_changes(self, *args, **kwargs): |
|
1344 |
if self.display_mode == 'rich': |
|
1345 |
self.widget_class = RichTextWidget |
|
1346 | ||
1329 | 1347 |
def fill_admin_form(self, form): |
1330 | 1348 |
WidgetField.fill_admin_form(self, form) |
1331 | 1349 |
if self.cols: |
... | ... | |
1343 | 1361 |
form.add(HiddenWidget, 'cols', value=None) |
1344 | 1362 |
form.add(StringWidget, 'rows', title=_('Number of rows'), value=self.rows) |
1345 | 1363 |
form.add(StringWidget, 'maxlength', title=_('Maximum number of characters'), value=self.maxlength) |
1346 |
form.add(CheckboxWidget, 'pre', title=_('Preformatted Text'), value=self.pre, advanced=True) |
|
1364 |
display_options = [ |
|
1365 |
('plain', _('Plain Text (paragraphs)')), |
|
1366 |
('pre', _('Preformatted Text')), |
|
1367 |
] |
|
1368 |
if get_publisher().get_site_option('enable-richtext-field'): |
|
1369 |
display_options += [ |
|
1370 |
('rich', _('Rich Text')), |
|
1371 |
] |
|
1372 |
form.add( |
|
1373 |
RadiobuttonsWidget, |
|
1374 |
'display_mode', |
|
1375 |
title=_('Text display'), |
|
1376 |
value=self.display_mode, |
|
1377 |
default_value='plain', |
|
1378 |
options=display_options, |
|
1379 |
extra_css_class='widget-inline-radio no-bottom-margin', |
|
1380 |
advanced=True, |
|
1381 |
) |
|
1347 | 1382 | |
1348 | 1383 |
def get_admin_attributes(self): |
1349 |
return WidgetField.get_admin_attributes(self) + ['cols', 'rows', 'pre', 'maxlength']
|
|
1384 |
return WidgetField.get_admin_attributes(self) + ['cols', 'rows', 'display_mode', 'maxlength']
|
|
1350 | 1385 | |
1351 | 1386 |
def convert_value_from_str(self, value): |
1352 | 1387 |
return value |
1353 | 1388 | |
1354 | 1389 |
def get_view_value(self, value, **kwargs): |
1355 |
if self.pre:
|
|
1390 |
if self.display_mode == 'pre':
|
|
1356 | 1391 |
return htmltext('<pre>') + value + htmltext('</pre>') |
1392 |
elif self.display_mode == 'rich': |
|
1393 |
return htmltext(strip_some_tags(value, RichTextWidget.ALL_TAGS)) |
|
1357 | 1394 |
else: |
1358 | 1395 |
try: |
1359 | 1396 |
return ( |
... | ... | |
1365 | 1402 |
return '' |
1366 | 1403 | |
1367 | 1404 |
def get_opendocument_node_value(self, value, formdata=None, **kwargs): |
1405 |
if self.display_mode == 'rich': |
|
1406 |
return |
|
1368 | 1407 |
paragraphs = [] |
1369 | 1408 |
for paragraph in value.splitlines(): |
1370 | 1409 |
if paragraph.strip(): |
... | ... | |
1374 | 1413 |
return paragraphs |
1375 | 1414 | |
1376 | 1415 |
def get_view_short_value(self, value, max_len=30): |
1416 |
if self.display_mode == 'rich': |
|
1417 |
return ellipsize(str(strip_tags(value)), max_len) |
|
1377 | 1418 |
return ellipsize(str(value), max_len) |
1378 | 1419 | |
1420 |
def get_json_value(self, value, **kwargs): |
|
1421 |
if self.display_mode == 'rich': |
|
1422 |
return str(self.get_view_value(value)) |
|
1423 |
return value |
|
1424 | ||
1379 | 1425 | |
1380 | 1426 |
register_field_class(TextField) |
1381 | 1427 |
wcs/formdef.py | ||
---|---|---|
1666 | 1666 |
), |
1667 | 1667 |
): |
1668 | 1668 |
continue |
1669 |
if isinstance(field, fields.TextField) and field.display_mode == 'rich': |
|
1670 |
continue |
|
1669 | 1671 |
if data is None: |
1670 | 1672 |
continue |
1671 | 1673 |
if data.get(field.id) is None: |
wcs/qommon/form.py | ||
---|---|---|
2440 | 2440 |
get_response().add_css_include('../xstatic/css/godo.css') |
2441 | 2441 | |
2442 | 2442 | |
2443 |
class RichTextWidget(MiniRichTextWidget): |
|
2444 |
ALL_TAGS = ['p', 'b', 'strong', 'i', 'em', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'li'] |
|
2445 | ||
2446 | ||
2443 | 2447 |
class TableWidget(CompositeWidget): |
2444 | 2448 |
readonly = False |
2445 | 2449 |
wcs/qommon/misc.py | ||
---|---|---|
52 | 52 |
from django.utils import datetime_safe |
53 | 53 |
from django.utils.encoding import force_bytes, force_text |
54 | 54 |
from django.utils.formats import localize |
55 |
from django.utils.functional import keep_lazy_text |
|
56 |
from django.utils.html import MLStripper as DjangoMLStripper |
|
55 | 57 |
from django.utils.html import strip_tags |
58 |
from django.utils.safestring import mark_safe |
|
56 | 59 |
from django.utils.text import Truncator |
57 | 60 |
from django.utils.timezone import is_aware, make_naive |
58 | 61 |
from quixote import get_publisher, get_request, get_response, redirect |
... | ... | |
1147 | 1150 |
raise UnauthorizedPythonUsage() |
1148 | 1151 |
# noqa pylint: disable=eval-used |
1149 | 1152 |
return eval(expression, *args, **kwargs) |
1153 | ||
1154 | ||
1155 |
class MLStripper(DjangoMLStripper): |
|
1156 |
def __init__(self, allowed_tags): |
|
1157 |
super().__init__() |
|
1158 |
self.reset() |
|
1159 |
self.fed = [] |
|
1160 |
self.allowed_tags = allowed_tags |
|
1161 | ||
1162 |
def handle_starttag(self, tag, attrs): |
|
1163 |
if tag not in self.allowed_tags: |
|
1164 |
return |
|
1165 | ||
1166 |
if tag == 'a': |
|
1167 |
for attr in attrs: |
|
1168 |
if attr[0] == 'href': |
|
1169 |
self.fed.append('<a href="%s">' % attr[1]) |
|
1170 |
return |
|
1171 |
if tag == 'br': |
|
1172 |
self.fed.append('<br />') |
|
1173 |
return |
|
1174 | ||
1175 |
self.fed.append('<%s>' % tag) |
|
1176 | ||
1177 |
def handle_endtag(self, tag): |
|
1178 |
if tag not in self.allowed_tags: |
|
1179 |
return |
|
1180 | ||
1181 |
if tag == 'br': |
|
1182 |
return |
|
1183 | ||
1184 |
self.fed.append('</%s>' % tag) |
|
1185 | ||
1186 | ||
1187 |
def _strip_once(value, allowed_tags): |
|
1188 |
""" |
|
1189 |
Internal tag stripping utility used by strip_some_tags. |
|
1190 |
""" |
|
1191 |
s = MLStripper(allowed_tags) |
|
1192 |
s.feed(value) |
|
1193 |
s.close() |
|
1194 |
return s.get_data() |
|
1195 | ||
1196 | ||
1197 |
@keep_lazy_text |
|
1198 |
def strip_some_tags(value, allowed_tags): |
|
1199 |
"""Return the given HTML with all tags stripped except allowed_tags.""" |
|
1200 |
# Note: in typical case this loop executes _strip_once once. Loop condition |
|
1201 |
# is redundant, but helps to reduce number of executions of _strip_once. |
|
1202 |
value = str(value) |
|
1203 |
while '<' in value and '>' in value: |
|
1204 |
new_value = _strip_once(value, allowed_tags) |
|
1205 |
if value.count('<') == new_value.count('<'): |
|
1206 |
# _strip_once wasn't able to detect more tags. |
|
1207 |
break |
|
1208 |
value = new_value |
|
1209 |
return mark_safe(value) |
wcs/qommon/templatetags/qommon.py | ||
---|---|---|
53 | 53 |
from wcs.qommon import calendar, evalutils, upload_storage |
54 | 54 |
from wcs.qommon.admin.texts import TextsDirectory |
55 | 55 |
from wcs.qommon.humantime import seconds2humanduration |
56 |
from wcs.qommon.misc import validate_phone_fr |
|
56 |
from wcs.qommon.misc import strip_some_tags, validate_phone_fr
|
|
57 | 57 | |
58 | 58 |
register = template.Library() |
59 | 59 | |
... | ... | |
1022 | 1022 |
from wcs.workflows import WorkflowStatusItem |
1023 | 1023 | |
1024 | 1024 |
return WorkflowStatusItem.compute(unlazy(value)) |
1025 | ||
1026 | ||
1027 |
@register.filter(is_safe=True) |
|
1028 |
def stripsometags(value, arg=None): |
|
1029 |
arg = arg or '' |
|
1030 |
allowed_tags = arg.split(',') |
|
1031 |
allowed_tags = [t.strip() for t in allowed_tags] |
|
1032 |
allowed_tags = [t for t in allowed_tags if t] |
|
1033 |
return strip_some_tags(unlazy(value), allowed_tags) |
wcs/wf/export_to_model.py | ||
---|---|---|
729 | 729 |
unset_value_i.text = _('Not set') |
730 | 730 |
else: |
731 | 731 |
node_value = f.get_opendocument_node_value(value, formdata) |
732 |
if node_value is None: |
|
733 |
continue |
|
732 | 734 |
if isinstance(node_value, list): |
733 | 735 |
for node in node_value: |
734 | 736 |
section_node.append(node) |
735 |
- |