misc: allow "rich" text in comments (#27994)

This commit is contained in:
Frédéric Péters 2021-12-14 10:43:44 +01:00
parent f0c3e9306f
commit 7338e136e5
11 changed files with 129 additions and 13 deletions

1
debian/control vendored
View File

@ -31,6 +31,7 @@ Depends: graphviz,
python3-unidecode,
python3-uwsgidecorators,
python3-vobject,
python3-xstatic-godo,
python3-xstatic-leaflet,
python3-xstatic-leaflet-gesturehandling,
uwsgi,

View File

@ -1753,7 +1753,7 @@ def test_form_tracking_code(pub, nocache):
assert 'form_comment' in resp.text # makes sure user is treated as submitter
resp.forms[0]['comment'] = 'hello world'
resp = resp.forms[0].submit()
assert formdef.data_class().get(formdata_id).evolution[-1].comment == 'hello world'
assert formdef.data_class().get(formdata_id).evolution[-1].get_plain_text_comment() == 'hello world'
# check we can also use it with lowercase letters.
app = get_app(pub)
@ -1847,7 +1847,7 @@ def test_form_tracking_code_as_user(pub, nocache):
assert 'form_comment' in resp.text # makes sure user is treated as submitter
resp.forms[0]['comment'] = 'hello world'
resp = resp.forms[0].submit()
assert formdef.data_class().get(formdata_id).evolution[-1].comment == 'hello world'
assert formdef.data_class().get(formdata_id).evolution[-1].get_plain_text_comment() == 'hello world'
# and check we can also get back to it as anonymous
app = get_app(pub)
@ -7954,7 +7954,7 @@ def test_choice_button_ignore_form_errors(pub):
resp.form['comment'] = 'plop'
resp = resp.form.submit('button_x1').follow()
assert '<p>plop</p>' in resp.text
assert resp.pyquery('.comment').text() == 'plop'
assert '<span class="status">Status2' in resp.text
# no comment but no check
@ -8837,3 +8837,54 @@ def test_workflow_form_structured_data(pub):
formdata.refresh_from_storage()
assert not formdata.workflow_data
def test_rich_commentable_action(pub):
create_user(pub)
formdef = create_formdef()
formdef.data_class().wipe()
formdef.roles = [logged_users_role().id]
formdef.store()
wf = Workflow(name='status')
st1 = wf.add_status('Status1', 'st1')
commentable = CommentableWorkflowStatusItem()
commentable.id = '_commentable'
commentable.by = [logged_users_role().id]
commentable.required = True
st1.items.append(commentable)
commentable.parent = st1
choice = ChoiceWorkflowStatusItem()
choice.label = 'Submit'
choice.by = [logged_users_role().id]
choice.id = '_x1'
choice.status = st1.id
st1.items.append(choice)
choice.parent = st1
wf.store()
formdef.workflow = wf
formdef.store()
# comment
resp = login(get_app(pub), username='foo', password='foo').get('/test/')
resp = resp.form.submit('submit') # -> validation page
resp = resp.form.submit('submit') # -> submission
resp = resp.follow()
resp.form['comment'] = '<p>hello <i>world</i></p>'
resp = resp.form.submit('button_x1').follow()
assert resp.pyquery('div.comment').text() == 'hello world'
assert '<p>hello <i>world</i></p>' in resp.text
formdata = formdef.data_class().select()[0]
assert formdata.evolution[-1].parts[-1].comment == '<p>hello <i>world</i></p>'
resp.form['comment'] = '<p>hello <script>evil</script></p>'
resp = resp.form.submit('button_x1').follow()
assert '<p>hello evil</p>' in resp.text
formdata = formdef.data_class().select()[0]
assert formdata.evolution[-1].parts[-1].comment == '<p>hello evil</p>'

View File

@ -36,6 +36,7 @@ deps =
docutils
langdetect
git+https://git.entrouvert.org/debian/django-ckeditor.git
git+https://git.entrouvert.org/godo.js.git
django22: django>=2.2,<2.3
django-ratelimit<3
pyproj

View File

@ -186,6 +186,14 @@ class Evolution:
self._display_parts = l
return self._display_parts
def get_plain_text_comment(self):
from .workflows import WorkflowCommentPart
for part in reversed(self.parts or []):
if isinstance(part, WorkflowCommentPart):
return part.get_as_plain_text()
return self.comment
def get_json_export_dict(self, user, anonymise=False, include_files=True):
data = {
'time': datetime.datetime(*self.time[:6]) if self.time else None,

View File

@ -1661,8 +1661,9 @@ class FormDef(StorableObject):
if evo.status:
details.append(_('Status'))
details.append(' %s' % formdata.get_status_label())
if evo.comment:
details.append('\n%s\n' % evo.comment)
comment = evo.get_plain_text_comment()
if comment:
details.append('\n%s\n' % comment)
return '\n\n----\n\n' + '\n'.join([str(x) for x in details])
def is_of_concern_for_role_id(self, role_id):

View File

@ -2269,6 +2269,7 @@ class WysiwygTextWidget(TextWidget):
tags=self.ALL_TAGS,
attributes=self.ALL_ATTRS,
styles=self.ALL_STYLES,
strip=True,
strip_comments=False,
)
if self.value.startswith('<br />'):
@ -2305,6 +2306,17 @@ class WysiwygTextWidget(TextWidget):
)
class MiniRichTextWidget(WysiwygTextWidget):
template_name = 'qommon/forms/widgets/mini-rich-text.html'
ALL_TAGS = ['p', 'b', 'strong', 'i', 'em']
ALL_ATTRS = []
ALL_STYLES = []
def add_media(self):
get_response().add_css_include('../xstatic/css/godo.css')
class TableWidget(CompositeWidget):
readonly = False

View File

@ -675,8 +675,13 @@ div.form-preview > br {
clear: both; /* stop grid */
}
div.godo {
overflow: visible;
}
div.TextWidget textarea:focus,
div.widget input[type="text"]:focus {
div.widget input[type="text"]:focus,
div.godo--editor.ProseMirror-focused {
box-shadow: 0 0 0px 1px #1999cd;
}

View File

@ -0,0 +1,10 @@
{% extends "qommon/forms/widget.html" %}
{% block widget-control %}
<textarea style="display: none" id="form_{{widget.name}}" name="{{widget.name}}"
{% for attr in widget.attrs.items %}{{attr.0}}="{{attr.1}}" {% endfor %}
class="mini-rich-text">{{widget.value|default:""}}</textarea>
<script type="module">
import Godo from "/static/xstatic/js/godo.js";
new Godo(document.getElementById('form_{{widget.name}}'), {schema: 'basic'});
</script>
{% endblock %}

View File

@ -195,6 +195,7 @@ class StaticsDirectory(Directory):
'xstatic:jquery',
'xstatic:jquery_ui',
'xstatic:font_awesome',
'xstatic:godo',
'xstatic:opensans',
'xstatic:leaflet',
'xstatic:leaflet_gesturehandling',

View File

@ -552,8 +552,9 @@ class LazyFormData(LazyFormDef):
@property
def comment(self):
if self._formdata.evolution and self._formdata.evolution[-1].comment:
return self._formdata.evolution[-1].comment
if self._formdata.evolution:
latest_evolution = self._formdata.evolution[-1]
return latest_evolution.get_plain_text_comment() or latest_evolution.comment
return ''
@property

View File

@ -18,6 +18,7 @@ import base64
import collections
import copy
import datetime
import html
import itertools
import os
import random
@ -26,6 +27,7 @@ import uuid
import xml.etree.ElementTree as ET
from django.utils.encoding import force_text
from django.utils.html import strip_tags
from quixote import get_publisher, get_request, get_response
from quixote.html import TemplateIO, htmltext
@ -47,6 +49,7 @@ from .qommon.form import (
ComputedExpressionWidget,
ConditionWidget,
Form,
MiniRichTextWidget,
SingleSelectWidget,
SingleSelectWidgetWithOther,
StringWidget,
@ -2717,6 +2720,18 @@ def register_item_class(klass):
klass.init()
class WorkflowCommentPart(EvolutionPart):
def __init__(self, comment, varname=None):
self.comment = comment
self.varname = varname
def get_as_plain_text(self):
return html.unescape(strip_tags(self.comment))
def view(self):
return htmltext('<div class="comment">%s</div>' % self.comment)
class CommentableWorkflowStatusItem(WorkflowStatusItem):
description = _('Comment')
key = 'commentable'
@ -2746,7 +2761,13 @@ class CommentableWorkflowStatusItem(WorkflowStatusItem):
else:
title = self.label
form.add(
TextWidget, 'comment', title=title, required=self.required, cols=40, rows=10, hint=self.hint
MiniRichTextWidget,
'comment',
title=title,
required=self.required,
cols=40,
rows=10,
hint=self.hint,
)
form.get_widget('comment').attrs['class'] = 'comment'
if self.button_label == 0:
@ -2758,10 +2779,14 @@ class CommentableWorkflowStatusItem(WorkflowStatusItem):
def submit_form(self, form, formdata, user, evo):
if form.get_widget('comment'):
evo.comment = form.get_widget('comment').parse()
if self.varname:
workflow_data = {'comment_%s' % self.varname: evo.comment}
formdata.update_workflow_data(workflow_data)
comment = form.get_widget('comment').parse()
if comment:
comment_part = WorkflowCommentPart(comment, varname=self.varname)
evo.add_part(comment_part)
if self.varname:
formdata.update_workflow_data(
{'comment_%s' % self.varname: comment_part.get_as_plain_text()}
)
def submit_admin_form(self, form):
for f in self.get_parameters():