workflows: add support for form details odt section in documents (#36627)
This commit is contained in:
parent
0441839f27
commit
c5a0811131
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
||||
|
|
|
@ -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 <> 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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue