0001-fields-new-field-RichTextField-36498.patch
tests/admin_pages/test_form.py | ||
---|---|---|
3274 | 3274 |
assert resp.form['data_source$type'].options == [('None', True, 'None'), ('carddef:baz', False, 'Baz')] |
3275 | 3275 | |
3276 | 3276 | |
3277 |
def test_form_new_richtext_field(pub): |
|
3278 |
create_superuser(pub) |
|
3279 |
create_role(pub) |
|
3280 | ||
3281 |
pub.site_options.set('options', 'enable-richtext-field', 'true') |
|
3282 |
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd: |
|
3283 |
pub.site_options.write(fd) |
|
3284 | ||
3285 |
FormDef.wipe() |
|
3286 |
formdef = FormDef() |
|
3287 |
formdef.name = 'form title' |
|
3288 |
formdef.fields = [] |
|
3289 |
formdef.store() |
|
3290 | ||
3291 |
app = login(get_app(pub)) |
|
3292 |
resp = app.get('/backoffice/forms/1/') |
|
3293 |
resp = resp.click(href='fields/') |
|
3294 | ||
3295 |
resp.forms[0]['label'] = 'foobar' |
|
3296 |
resp.forms[0]['type'] = 'richtext' |
|
3297 |
resp = resp.forms[0].submit().follow() |
|
3298 | ||
3299 |
assert len(FormDef.get(1).fields) == 1 |
|
3300 |
field = FormDef.get(1).fields[0] |
|
3301 |
assert field.key == 'richtext' |
|
3302 |
assert field.label == 'foobar' |
|
3303 | ||
3304 | ||
3277 | 3305 |
def test_form_category_management_roles(pub, backoffice_user, backoffice_role): |
3278 | 3306 |
app = login(get_app(pub), username='backoffice-user', password='backoffice-user') |
3279 | 3307 |
app.get('/backoffice/forms/', status=403) |
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.RichTextField(id='6', label='rich text', varname='richtext', type='richtext'), |
|
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 | ||
---|---|---|
120 | 120 |
assert 'rows="12"' in str(form.render()) |
121 | 121 | |
122 | 122 | |
123 |
def test_richtext(pub): |
|
124 |
assert fields.RichTextField().get_view_short_value('<p>foo</p>' * 15) == ('foo' * 10)[:27] + '(…)' |
|
125 |
assert fields.RichTextField().get_view_value('<script></script><p>foo</p>') == '<p>foo</p>' |
|
126 | ||
127 | ||
123 | 128 |
def test_email(): |
124 | 129 |
assert ( |
125 | 130 |
fields.EmailField().get_view_value('foo@localhost') |
... | ... | |
787 | 792 |
('computed', 'Computed Data', 'computed'), |
788 | 793 |
] |
789 | 794 | |
795 |
pub.site_options.set('options', 'enable-richtext-field', 'true') |
|
796 |
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd: |
|
797 |
pub.site_options.write(fd) |
|
798 | ||
799 |
assert fields.get_field_options(blacklisted_types=[]) == [ |
|
800 |
('string', 'Text (line)', 'string'), |
|
801 |
('text', 'Long Text', 'text'), |
|
802 |
('richtext', 'Long Text (Rich)', 'richtext'), |
|
803 |
('email', 'Email', 'email'), |
|
804 |
('bool', 'Check Box (single choice)', 'bool'), |
|
805 |
('file', 'File Upload', 'file'), |
|
806 |
('date', 'Date', 'date'), |
|
807 |
('item', 'List', 'item'), |
|
808 |
('items', 'Multiple choice list', 'items'), |
|
809 |
('table-select', 'Table of Lists', 'table-select'), |
|
810 |
('tablerows', 'Table with rows', 'tablerows'), |
|
811 |
('map', 'Map', 'map'), |
|
812 |
('ranked-items', 'Ranked Items', 'ranked-items'), |
|
813 |
('', '—', ''), |
|
814 |
('title', 'Title', 'title'), |
|
815 |
('subtitle', 'Subtitle', 'subtitle'), |
|
816 |
('comment', 'Comment', 'comment'), |
|
817 |
('page', 'Page', 'page'), |
|
818 |
('', '—', ''), |
|
819 |
('computed', 'Computed Data', 'computed'), |
|
820 |
] |
|
821 | ||
790 | 822 | |
791 | 823 |
def test_block_do_not_pickle_cache(pub): |
792 | 824 |
FormDef.wipe() |
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 |
... | ... | |
1380 | 1382 |
register_field_class(TextField) |
1381 | 1383 | |
1382 | 1384 | |
1385 |
class RichTextField(TextField): |
|
1386 |
key = 'richtext' |
|
1387 |
description = _('Long Text (Rich)') |
|
1388 | ||
1389 |
widget_class = RichTextWidget |
|
1390 |
get_opendocument_node_value = None |
|
1391 | ||
1392 |
def get_view_value(self, value, **kwargs): |
|
1393 |
return htmltext(strip_some_tags(value, self.widget_class.ALL_TAGS)) |
|
1394 | ||
1395 |
def get_view_short_value(self, value, max_len=30): |
|
1396 |
return ellipsize(str(strip_tags(value)), max_len) |
|
1397 | ||
1398 |
def get_json_value(self, value, **kwargs): |
|
1399 |
return str(self.get_view_value(value)) |
|
1400 | ||
1401 | ||
1402 |
register_field_class(RichTextField) |
|
1403 | ||
1404 | ||
1383 | 1405 |
class EmailField(WidgetField): |
1384 | 1406 |
key = 'email' |
1385 | 1407 |
description = _('Email') |
... | ... | |
3992 | 4014 |
widgets, non_widgets = [], [] |
3993 | 4015 |
disabled_fields = (get_publisher().get_site_option('disabled-fields') or '').split(',') |
3994 | 4016 |
disabled_fields = [f.strip() for f in disabled_fields if f.strip()] |
4017 |
if not get_publisher().get_site_option('enable-richtext-field'): |
|
4018 |
disabled_fields += ['richtext'] |
|
3995 | 4019 |
for klass in field_classes: |
3996 | 4020 |
if klass is ComputedField: |
3997 | 4021 |
continue |
wcs/formdef.py | ||
---|---|---|
1663 | 1663 |
fields.CommentField, |
1664 | 1664 |
fields.PageField, |
1665 | 1665 |
fields.ComputedField, |
1666 |
fields.RichTextField, |
|
1666 | 1667 |
), |
1667 | 1668 |
): |
1668 | 1669 |
continue |
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/sql.py | ||
---|---|---|
78 | 78 |
'comment': None, |
79 | 79 |
'page': None, |
80 | 80 |
'text': 'text', |
81 |
'richtext': 'text', |
|
81 | 82 |
'bool': 'boolean', |
82 | 83 |
'file': 'bytea', |
83 | 84 |
'date': 'date', |
84 |
- |