Projet

Général

Profil

0004-workflows-add-support-for-form-details-odt-section-i.patch

Frédéric Péters, 03 mars 2020 14:23

Télécharger (33,7 ko)

Voir les différences:

Subject: [PATCH 4/4] workflows: add support for form details odt section in
 documents (#36627)

 tests/template-form-details-no-styles.odt | Bin 0 -> 8001 bytes
 tests/template-form-details.odt           | Bin 0 -> 9503 bytes
 tests/test_form_pages.py                  |   2 +
 tests/test_workflows.py                   |  75 +++++++++++++++-
 wcs/fields.py                             |  24 ++---
 wcs/wf/export_to_model.py                 | 104 +++++++++++++++++++++-
 6 files changed, 183 insertions(+), 22 deletions(-)
 create mode 100644 tests/template-form-details-no-styles.odt
 create mode 100644 tests/template-form-details.odt
tests/test_form_pages.py
61 61
    z2 = zipfile.ZipFile(stream2)
62 62
    assert set(z1.namelist()) == set(z2.namelist())
63 63
    for name in z1.namelist():
64
        if name == 'styles.xml':
65
            continue
64 66
        t1, t2 = z1.read(name), z2.read(name)
65 67
        assert t1 == t2, 'file "%s" differs' % name
66 68

  
tests/test_workflows.py
10 10
import mock
11 11

  
12 12
from django.utils import six
13
from django.utils.encoding import force_bytes
13
from django.utils.encoding import force_bytes, force_text
14 14
from django.utils.six import BytesIO, StringIO
15 15
from django.utils.six.moves.urllib import parse as urlparse
16 16

  
......
23 23
from wcs.formdef import FormDef
24 24
from wcs import sessions
25 25
from wcs.fields import (StringField, DateField, MapField, FileField, ItemField,
26
        ItemsField, CommentField)
26
        ItemsField, CommentField, EmailField, PageField, TitleField,
27
        SubtitleField, TextField, BoolField, TableField)
27 28
from wcs.formdata import Evolution
28 29
from wcs.logged_errors import LoggedError
29 30
from wcs.roles import Role
......
3130 3131
    assert b'>A &lt;&gt; name<' in new_content
3131 3132

  
3132 3133

  
3134
@pytest.mark.parametrize('filename', ['template-form-details.odt', 'template-form-details-no-styles.odt'])
3135
def test_export_to_model_form_details_section(pub, filename):
3136
    FormDef.wipe()
3137
    formdef = FormDef()
3138
    formdef.name = 'foo-export-details'
3139
    formdef.fields = [
3140
        PageField(id='1', label='Page 1', type='page'),
3141
        TitleField(id='2', label='Title', type='title'),
3142
        SubtitleField(id='3', label='Subtitle', type='subtitle'),
3143
        StringField(id='4', label='String', type='string', varname='string'),
3144
        EmailField(id='5', label='Email', type='email'),
3145
        TextField(id='6', label='Text', type='text'),
3146
        BoolField(id='8', label='Bool', type='bool'),
3147
        FileField(id='9', label='File', type='file'),
3148
        DateField(id='10', label='Date', type='date'),
3149
        ItemField(id='11', label='Item', type='item', items=['foo', 'bar']),
3150
        ItemsField(id='11', label='Items', type='items', items=['foo', 'bar']),
3151
        TableField(id='12', label='Table', type='table', columns=['a', 'b'], rows=['c', 'd'])
3152
    ]
3153
    formdef.store()
3154
    formdef.data_class().wipe()
3155
    upload = PicklableUpload('test.jpeg', 'image/jpeg')
3156
    upload.receive([open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb').read()])
3157
    formdata = formdef.data_class()()
3158
    formdata.data = {
3159
        '4': 'string',
3160
        '5': 'foo@localhost',
3161
        '6': 'para1\npara2',
3162
        '8': False,
3163
        '9': upload,
3164
        '10': time.strptime('2015-05-12', '%Y-%m-%d'),
3165
        '11': 'foo',
3166
        '12': [['1', '2'], ['3', '4']],
3167
    }
3168
    formdata.just_created()
3169
    formdata.store()
3170
    pub.substitutions.feed(formdata)
3171

  
3172
    item = ExportToModel()
3173
    item.method = 'non-interactive'
3174
    item.attach_to_history = True
3175
    template_filename = os.path.join(os.path.dirname(__file__), filename)
3176
    template = open(template_filename, 'rb').read()
3177
    upload = QuixoteUpload(filename, content_type='application/octet-stream')
3178
    upload.fp = BytesIO()
3179
    upload.fp.write(template)
3180
    upload.fp.seek(0)
3181
    item.model_file = UploadedFile(pub.app_dir, None, upload)
3182
    item.convert_to_pdf = False
3183
    item.perform(formdata)
3184

  
3185
    new_content = force_text(zipfile.ZipFile(open(formdata.evolution[0].parts[0].filename, 'rb')).read('content.xml'))
3186
    assert 'Titre de page' not in new_content  # section contents has been replaced
3187
    assert '>Page 1<' in new_content
3188
    assert '>Title<' in new_content
3189
    assert '>Subtitle<' in new_content
3190
    assert '<text:span>string</text:span>' in new_content
3191
    assert '>para1<' in new_content
3192
    assert '>para2<' in new_content
3193
    assert '<text:span>No</text:span>' in new_content
3194
    assert 'xlink:href="http://example.net/foo-export-details/1/download?f=9"' in new_content
3195
    assert '>test.jpeg</text:a' in new_content
3196
    assert '>2015-05-12<' in new_content
3197
    assert new_content.count('/table:table-cell') == 8
3198

  
3199
    if filename == 'template-form-details-no-styles.odt':
3200
        new_styles = force_text(zipfile.ZipFile(open(formdata.evolution[0].parts[0].filename, 'rb')).read('styles.xml'))
3201
        assert 'Field_20_Label' in new_styles
3202

  
3203

  
3133 3204
def test_global_timeouts(two_pubs):
3134 3205
    pub = two_pubs
3135 3206
    FormDef.wipe()
wcs/fields.py
875 875
                return ''
876 876

  
877 877
    def get_opendocument_node_value(self, value, formdata=None, **kwargs):
878
        if self.pre:
879
            p = ET.Element('{%s}p' % OD_NS['text'])
880
            line_break = '<nsa:line-break xmlns:nsa="%(ns)s"/>' % {'ns': OD_NS['text']}
881
            as_node = ET.fromstring(str(htmlescape(value)).replace('\n', line_break))
882
            p.text = as_node.text
883
            p.tail = as_node.tail
884
            for child in as_node.getchildren():
885
                p.append(child)
886
            return p
887
        else:
888
            paragraphs = []
889
            for paragraph in value.splitlines():
890
                if paragraph.strip():
891
                    p = ET.Element('{%s}p' % OD_NS['text'])
892
                    p.text = paragraph
893
                    paragraphs.append(p)
894
            return paragraphs
878
        paragraphs = []
879
        for paragraph in value.splitlines():
880
            if paragraph.strip():
881
                p = ET.Element('{%s}p' % OD_NS['text'])
882
                p.text = paragraph
883
                paragraphs.append(p)
884
        return paragraphs
895 885

  
896 886
    def get_view_short_value(self, value, max_len = 30):
897 887
        return ellipsize(str(value), max_len)
wcs/wf/export_to_model.py
48 48

  
49 49
OO_TEXT_NS = 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'
50 50
OO_OFFICE_NS = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'
51
OO_STYLE_NS = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'
51 52
OO_DRAW_NS = 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'
53
OO_FO_NS = 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'
52 54
XLINK_NS = 'http://www.w3.org/1999/xlink'
53 55
USER_FIELD_DECL = '{%s}user-field-decl' % OO_TEXT_NS
54 56
USER_FIELD_GET = '{%s}user-field-get' % OO_TEXT_NS
57
SECTION_NODE = '{%s}section' % OO_TEXT_NS
58
SECTION_NAME = '{%s}name' % OO_TEXT_NS
55 59
STRING_VALUE = '{%s}string-value' % OO_OFFICE_NS
56 60
DRAW_FRAME = '{%s}frame' % OO_DRAW_NS
57 61
DRAW_NAME = '{%s}name' % OO_DRAW_NS
......
94 98
    new_images = {}
95 99
    assert 'content.xml' in zin.namelist()
96 100
    for filename in zin.namelist():
97
        # first pass to process meta.xml and content.xml
98
        if filename not in ('meta.xml', 'content.xml'):
101
        # first pass to process meta.xml, content.xml and styles.xml
102
        if filename not in ('meta.xml', 'content.xml', 'styles.xml'):
99 103
            continue
100 104
        content = zin.read(filename)
101 105
        root = ET.fromstring(content)
......
105 109

  
106 110
    for filename in zin.namelist():
107 111
        # second pass to copy/replace other files
108
        if filename in ('meta.xml', 'content.xml'):
112
        if filename in ('meta.xml', 'content.xml', 'styles.xml'):
109 113
            continue
110 114
        if filename in new_images:
111 115
            content = new_images[filename].get_content()
......
418 422
    def apply_od_template_to_formdata(self, formdata):
419 423
        context = get_formdata_template_context(formdata)
420 424

  
425
        def process_styles(root):
426
            styles_node = root.find('{%s}styles' % OO_OFFICE_NS)
427
            if styles_node is None:
428
                return
429
            style_names = set([x.attrib.get('{%s}name' % OO_STYLE_NS) for x in styles_node.getchildren()])
430
            for style_name in ['Page_20_Title', 'Form_20_Title', 'Form_20_Subtitle',
431
                               'Field_20_Label', 'Field_20_Value']:
432
                # if any style name is defined, don't alter styles
433
                if style_name in style_names:
434
                    return
435
            for i, style_name in enumerate(['Field_20_Label', 'Field_20_Value',
436
                                            'Form_20_Subtitle', 'Form_20_Title', 'Page_20_Title']):
437
                style_node = ET.SubElement(styles_node, '{%s}style' % OO_STYLE_NS)
438
                style_node.attrib['{%s}name' % OO_STYLE_NS] = style_name
439
                style_node.attrib['{%s}display-name' % OO_STYLE_NS] = style_name.replace('_20_', ' ')
440
                style_node.attrib['{%s}family' % OO_STYLE_NS] = 'paragraph'
441
                para_props = ET.SubElement(style_node, '{%s}paragraph-properties' % OO_STYLE_NS)
442
                if 'Value' not in style_name:
443
                    para_props.attrib['{%s}margin-top' % OO_FO_NS] = '0.5cm'
444
                else:
445
                    para_props.attrib['{%s}margin-left' % OO_FO_NS] = '0.25cm'
446
                if 'Title' in style_name:
447
                    text_props = ET.SubElement(style_node, '{%s}text-properties' % OO_STYLE_NS)
448
                    text_props.attrib['{%s}font-size' % OO_FO_NS] = '%s%%' % (90 + i * 10)
449
                    text_props.attrib['{%s}font-weight' % OO_FO_NS] = 'bold'
450

  
421 451
        def process_root(root, new_images):
452
            if root.tag == '{%s}document-styles' % OO_OFFICE_NS:
453
                return process_styles(root)
454

  
422 455
            # cache for keeping computed user-field-decl value around
423 456
            user_field_values = {}
424 457

  
425 458
            def process_text(t):
426 459
                t = template_on_context(context, force_str(t), autoescape=False)
427 460
                return force_text(t, get_publisher().site_charset)
461
            nodes = []
428 462
            for node in root.iter():
463
                nodes.append(node)
464
            for node in nodes:
429 465
                got_blank_lines = False
466
                if node.tag == SECTION_NODE and node.attrib.get(SECTION_NAME) == 'form_details':
467
                    # custom behaviour for {{form_details}}, create real odt
468
                    # markup.
469
                    for child in node.getchildren():
470
                        node.remove(child)
471
                    self.insert_form_details(node, formdata)
472

  
430 473
                # apply template to user-field-decl and update user-field-get
431 474
                if node.tag == USER_FIELD_DECL and STRING_VALUE in node.attrib:
432 475
                    node.attrib[STRING_VALUE] = process_text(node.attrib[STRING_VALUE])
......
481 524
        outstream.seek(0)
482 525
        return outstream
483 526

  
527
    def insert_form_details(self, node, formdata):
528
        field_details = formdata.get_summary_field_details()
529
        section_node = node
530
        for field_value_info in field_details:
531
            f = field_value_info['field']
532

  
533
            if f.type == 'page':
534
                page_title = ET.SubElement(section_node, '{%s}h' % OO_TEXT_NS)
535
                page_title.attrib['{%s}outline-level' % OO_TEXT_NS] = '1'
536
                page_title.attrib['{%s}style-name' % OO_TEXT_NS] = 'Page_20_Title'
537
                page_title.text = f.label
538
                continue
539

  
540
            if f.type in ('title', 'subtitle'):
541
                label = template_on_formdata(None, f.label, autoescape=False)
542
                title = ET.SubElement(section_node, '{%s}h' % OO_TEXT_NS)
543
                title.attrib['{%s}outline-level' % OO_TEXT_NS] = '2'
544
                title.attrib['{%s}style-name' % OO_TEXT_NS] = 'Form_20_Title'
545
                if f.type == 'subtitle':
546
                    title.attrib['{%s}outline-level' % OO_TEXT_NS] = '3'
547
                    title.attrib['{%s}style-name' % OO_TEXT_NS] = 'Form_20_Subtitle'
548
                title.text = label
549
                continue
550

  
551
            if f.type == 'comment':
552
                # comment can be free form HTML, ignore them.
553
                continue
554

  
555
            if not f.get_opendocument_node_value:
556
                # unsupported field type
557
                continue
558

  
559
            label_p = ET.SubElement(section_node, '{%s}p' % OO_TEXT_NS)
560
            label_p.attrib['{%s}style-name' % OO_TEXT_NS] = 'Field_20_Label'
561
            label_p.text = f.label
562
            value = field_value_info['value']
563
            if value is None:
564
                unset_value_p = ET.SubElement(section_node, '{%s}p' % OO_TEXT_NS)
565
                unset_value_p.attrib['{%s}style-name' % OO_TEXT_NS] = 'Field_20_Value'
566
                unset_value_i = ET.SubElement(unset_value_p, '{%s}span' % OO_TEXT_NS)
567
                unset_value_i.text = _('Not set')
568
            else:
569
                node_value = f.get_opendocument_node_value(value, formdata)
570
                if isinstance(node_value, list):
571
                    for node in node_value:
572
                        section_node.append(node)
573
                        node.attrib['{%s}style-name' % OO_TEXT_NS] = 'Field_20_Value'
574
                elif node_value.tag in ('{%s}span' % OO_TEXT_NS, '{%s}a' % OO_TEXT_NS):
575
                    value_p = ET.SubElement(section_node, '{%s}p' % OO_TEXT_NS)
576
                    value_p.attrib['{%s}style-name' % OO_TEXT_NS] = 'Field_20_Value'
577
                    value_p.append(node_value)
578
                else:
579
                    node_value.attrib['{%s}style-name' % OO_TEXT_NS] = 'Field_20_Value'
580
                    section_node.append(node_value)
581

  
484 582
    def model_file_export_to_xml(self, xml_item, charset, include_id=False):
485 583
        if not self.model_file:
486 584
            return
487
-