workflows: add support for form details odt section in documents (#36627)

This commit is contained in:
Frédéric Péters 2020-02-08 11:01:17 +01:00
parent 0441839f27
commit c5a0811131
6 changed files with 188 additions and 22 deletions

Binary file not shown.

Binary file not shown.

View File

@ -61,6 +61,8 @@ def assert_equal_zip(stream1, stream2):
z2 = zipfile.ZipFile(stream2)
assert set(z1.namelist()) == set(z2.namelist())
for name in z1.namelist():
if name == 'styles.xml':
continue
t1, t2 = z1.read(name), z2.read(name)
assert t1 == t2, 'file "%s" differs' % name

View File

@ -10,7 +10,7 @@ import zipfile
import mock
from django.utils import six
from django.utils.encoding import force_bytes
from django.utils.encoding import force_bytes, force_text
from django.utils.six import BytesIO, StringIO
from django.utils.six.moves.urllib import parse as urlparse
@ -24,7 +24,8 @@ from wcs.formdef import FormDef
from wcs.carddef import CardDef
from wcs import sessions
from wcs.fields import (StringField, DateField, MapField, FileField, ItemField,
ItemsField, CommentField, BoolField)
ItemsField, CommentField, EmailField, PageField, TitleField,
SubtitleField, TextField, BoolField, TableField)
from wcs.formdata import Evolution
from wcs.logged_errors import LoggedError
from wcs.roles import Role
@ -3143,6 +3144,80 @@ def test_export_to_model_django_template(pub):
assert b'>A &lt;&gt; name<' in new_content
@pytest.mark.parametrize('filename', ['template-form-details.odt', 'template-form-details-no-styles.odt'])
def test_export_to_model_form_details_section(pub, filename):
FormDef.wipe()
formdef = FormDef()
formdef.name = 'foo-export-details'
formdef.fields = [
PageField(id='1', label='Page 1', type='page'),
TitleField(id='2', label='Title', type='title'),
SubtitleField(id='3', label='Subtitle', type='subtitle'),
StringField(id='4', label='String', type='string', varname='string'),
EmailField(id='5', label='Email', type='email'),
TextField(id='6', label='Text', type='text'),
BoolField(id='8', label='Bool', type='bool'),
FileField(id='9', label='File', type='file'),
DateField(id='10', label='Date', type='date'),
ItemField(id='11', label='Item', type='item', items=['foo', 'bar']),
ItemsField(id='11', label='Items', type='items', items=['foo', 'bar']),
TableField(id='12', label='Table', type='table', columns=['a', 'b'], rows=['c', 'd']),
PageField(id='13', label='Empty Page', type='page'),
TitleField(id='14', label='Empty Title', type='title'),
StringField(id='15', label='Empty String', type='string', varname='invisiblestr'),
]
formdef.store()
formdef.data_class().wipe()
upload = PicklableUpload('test.jpeg', 'image/jpeg')
upload.receive([open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb').read()])
formdata = formdef.data_class()()
formdata.data = {
'4': 'string',
'5': 'foo@localhost',
'6': 'para1\npara2',
'8': False,
'9': upload,
'10': time.strptime('2015-05-12', '%Y-%m-%d'),
'11': 'foo',
'12': [['1', '2'], ['3', '4']],
}
formdata.just_created()
formdata.store()
pub.substitutions.feed(formdata)
item = ExportToModel()
item.method = 'non-interactive'
item.attach_to_history = True
template_filename = os.path.join(os.path.dirname(__file__), filename)
template = open(template_filename, 'rb').read()
upload = QuixoteUpload(filename, content_type='application/octet-stream')
upload.fp = BytesIO()
upload.fp.write(template)
upload.fp.seek(0)
item.model_file = UploadedFile(pub.app_dir, None, upload)
item.convert_to_pdf = False
item.perform(formdata)
new_content = force_text(zipfile.ZipFile(open(formdata.evolution[0].parts[0].filename, 'rb')).read('content.xml'))
assert 'Titre de page' not in new_content # section contents has been replaced
assert '>Page 1<' in new_content
assert '>Title<' in new_content
assert '>Subtitle<' in new_content
assert '<text:span>string</text:span>' in new_content
assert '>para1<' in new_content
assert '>para2<' in new_content
assert '<text:span>No</text:span>' in new_content
assert 'xlink:href="http://example.net/foo-export-details/1/download?f=9"' in new_content
assert '>test.jpeg</text:a' in new_content
assert '>2015-05-12<' in new_content
assert 'Invisible' not in new_content
assert new_content.count('/table:table-cell') == 8
if filename == 'template-form-details-no-styles.odt':
new_styles = force_text(zipfile.ZipFile(open(formdata.evolution[0].parts[0].filename, 'rb')).read('styles.xml'))
assert 'Field_20_Label' in new_styles
def test_global_timeouts(two_pubs):
pub = two_pubs
FormDef.wipe()

View File

@ -875,23 +875,13 @@ class TextField(WidgetField):
return ''
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
if self.pre:
p = ET.Element('{%s}p' % OD_NS['text'])
line_break = '<nsa:line-break xmlns:nsa="%(ns)s"/>' % {'ns': OD_NS['text']}
as_node = ET.fromstring(str(htmlescape(value)).replace('\n', line_break))
p.text = as_node.text
p.tail = as_node.tail
for child in as_node.getchildren():
p.append(child)
return p
else:
paragraphs = []
for paragraph in value.splitlines():
if paragraph.strip():
p = ET.Element('{%s}p' % OD_NS['text'])
p.text = paragraph
paragraphs.append(p)
return paragraphs
paragraphs = []
for paragraph in value.splitlines():
if paragraph.strip():
p = ET.Element('{%s}p' % OD_NS['text'])
p.text = paragraph
paragraphs.append(p)
return paragraphs
def get_view_short_value(self, value, max_len = 30):
return ellipsize(str(value), max_len)

View File

@ -48,10 +48,14 @@ from wcs.portfolio import has_portfolio, push_document
OO_TEXT_NS = 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'
OO_OFFICE_NS = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'
OO_STYLE_NS = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'
OO_DRAW_NS = 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'
OO_FO_NS = 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'
XLINK_NS = 'http://www.w3.org/1999/xlink'
USER_FIELD_DECL = '{%s}user-field-decl' % OO_TEXT_NS
USER_FIELD_GET = '{%s}user-field-get' % OO_TEXT_NS
SECTION_NODE = '{%s}section' % OO_TEXT_NS
SECTION_NAME = '{%s}name' % OO_TEXT_NS
STRING_VALUE = '{%s}string-value' % OO_OFFICE_NS
DRAW_FRAME = '{%s}frame' % OO_DRAW_NS
DRAW_NAME = '{%s}name' % OO_DRAW_NS
@ -94,8 +98,8 @@ def transform_opendocument(instream, outstream, process):
new_images = {}
assert 'content.xml' in zin.namelist()
for filename in zin.namelist():
# first pass to process meta.xml and content.xml
if filename not in ('meta.xml', 'content.xml'):
# first pass to process meta.xml, content.xml and styles.xml
if filename not in ('meta.xml', 'content.xml', 'styles.xml'):
continue
content = zin.read(filename)
root = ET.fromstring(content)
@ -105,7 +109,7 @@ def transform_opendocument(instream, outstream, process):
for filename in zin.namelist():
# second pass to copy/replace other files
if filename in ('meta.xml', 'content.xml'):
if filename in ('meta.xml', 'content.xml', 'styles.xml'):
continue
if filename in new_images:
content = new_images[filename].get_content()
@ -418,15 +422,55 @@ class ExportToModel(WorkflowStatusItem):
def apply_od_template_to_formdata(self, formdata):
context = get_formdata_template_context(formdata)
def process_styles(root):
styles_node = root.find('{%s}styles' % OO_OFFICE_NS)
if styles_node is None:
return
style_names = set([x.attrib.get('{%s}name' % OO_STYLE_NS) for x in styles_node.getchildren()])
for style_name in ['Page_20_Title', 'Form_20_Title', 'Form_20_Subtitle',
'Field_20_Label', 'Field_20_Value']:
# if any style name is defined, don't alter styles
if style_name in style_names:
return
for i, style_name in enumerate(['Field_20_Label', 'Field_20_Value',
'Form_20_Subtitle', 'Form_20_Title', 'Page_20_Title']):
style_node = ET.SubElement(styles_node, '{%s}style' % OO_STYLE_NS)
style_node.attrib['{%s}name' % OO_STYLE_NS] = style_name
style_node.attrib['{%s}display-name' % OO_STYLE_NS] = style_name.replace('_20_', ' ')
style_node.attrib['{%s}family' % OO_STYLE_NS] = 'paragraph'
para_props = ET.SubElement(style_node, '{%s}paragraph-properties' % OO_STYLE_NS)
if 'Value' not in style_name:
para_props.attrib['{%s}margin-top' % OO_FO_NS] = '0.5cm'
else:
para_props.attrib['{%s}margin-left' % OO_FO_NS] = '0.25cm'
if 'Title' in style_name:
text_props = ET.SubElement(style_node, '{%s}text-properties' % OO_STYLE_NS)
text_props.attrib['{%s}font-size' % OO_FO_NS] = '%s%%' % (90 + i * 10)
text_props.attrib['{%s}font-weight' % OO_FO_NS] = 'bold'
def process_root(root, new_images):
if root.tag == '{%s}document-styles' % OO_OFFICE_NS:
return process_styles(root)
# cache for keeping computed user-field-decl value around
user_field_values = {}
def process_text(t):
t = template_on_context(context, force_str(t), autoescape=False)
return force_text(t, get_publisher().site_charset)
nodes = []
for node in root.iter():
nodes.append(node)
for node in nodes:
got_blank_lines = False
if node.tag == SECTION_NODE and 'form_details' in node.attrib.get(SECTION_NAME, ''):
# custom behaviour for a section named form_details
# (actually any name containing form_details), create
# real odt markup.
for child in node.getchildren():
node.remove(child)
self.insert_form_details(node, formdata)
# apply template to user-field-decl and update user-field-get
if node.tag == USER_FIELD_DECL and STRING_VALUE in node.attrib:
node.attrib[STRING_VALUE] = process_text(node.attrib[STRING_VALUE])
@ -481,6 +525,61 @@ class ExportToModel(WorkflowStatusItem):
outstream.seek(0)
return outstream
def insert_form_details(self, node, formdata):
field_details = formdata.get_summary_field_details()
section_node = node
for field_value_info in field_details:
f = field_value_info['field']
if f.type == 'page':
page_title = ET.SubElement(section_node, '{%s}h' % OO_TEXT_NS)
page_title.attrib['{%s}outline-level' % OO_TEXT_NS] = '1'
page_title.attrib['{%s}style-name' % OO_TEXT_NS] = 'Page_20_Title'
page_title.text = f.label
continue
if f.type in ('title', 'subtitle'):
label = template_on_formdata(None, f.label, autoescape=False)
title = ET.SubElement(section_node, '{%s}h' % OO_TEXT_NS)
title.attrib['{%s}outline-level' % OO_TEXT_NS] = '2'
title.attrib['{%s}style-name' % OO_TEXT_NS] = 'Form_20_Title'
if f.type == 'subtitle':
title.attrib['{%s}outline-level' % OO_TEXT_NS] = '3'
title.attrib['{%s}style-name' % OO_TEXT_NS] = 'Form_20_Subtitle'
title.text = label
continue
if f.type == 'comment':
# comment can be free form HTML, ignore them.
continue
if not f.get_opendocument_node_value:
# unsupported field type
continue
label_p = ET.SubElement(section_node, '{%s}p' % OO_TEXT_NS)
label_p.attrib['{%s}style-name' % OO_TEXT_NS] = 'Field_20_Label'
label_p.text = f.label
value = field_value_info['value']
if value is None:
unset_value_p = ET.SubElement(section_node, '{%s}p' % OO_TEXT_NS)
unset_value_p.attrib['{%s}style-name' % OO_TEXT_NS] = 'Field_20_Value'
unset_value_i = ET.SubElement(unset_value_p, '{%s}span' % OO_TEXT_NS)
unset_value_i.text = _('Not set')
else:
node_value = f.get_opendocument_node_value(value, formdata)
if isinstance(node_value, list):
for node in node_value:
section_node.append(node)
node.attrib['{%s}style-name' % OO_TEXT_NS] = 'Field_20_Value'
elif node_value.tag in ('{%s}span' % OO_TEXT_NS, '{%s}a' % OO_TEXT_NS):
value_p = ET.SubElement(section_node, '{%s}p' % OO_TEXT_NS)
value_p.attrib['{%s}style-name' % OO_TEXT_NS] = 'Field_20_Value'
value_p.append(node_value)
else:
node_value.attrib['{%s}style-name' % OO_TEXT_NS] = 'Field_20_Value'
section_node.append(node_value)
def model_file_export_to_xml(self, xml_item, charset, include_id=False):
if not self.model_file:
return