Projet

Général

Profil

0001-workflow-added-attachments-for-RegisterComment.patch

version 2 - Michaël Bideau, 18 avril 2019 12:00

Télécharger (24,1 ko)

Voir les différences:

Subject: [PATCH]     workflow: added attachments for RegisterComment          
     (attachments methods added to class WorkflowStatusItem)

    misc: updated evalutils.attachment() to handle content already encoded to base64
          and support varname parameter
 tests/test_misc.py         |  65 +++++++++++++++
 tests/test_workflows.py    | 102 ++++++++++++++++++++++-
 wcs/qommon/evalutils.py    |  13 +--
 wcs/wf/register_comment.py |  12 ++-
 wcs/workflows.py           | 201 ++++++++++++++++++++++++++++++---------------
 5 files changed, 320 insertions(+), 73 deletions(-)
tests/test_misc.py
440 440
    assert not '<ul' in html
441 441
    assert 'arabic simple' in html
442 442
    assert 'M. Francis Kuntz' in html
443

  
444
def test_attachment():
445
    hello_word_b64 = base64.encodestring('hello world')
446

  
447
    d = evalutils.attachment(varname='var1')
448
    assert d == {'varname'     : 'var1'
449
                ,'filename'    : ''
450
                ,'content_type': 'application/octet-stream'
451
                ,'b64_content' : ''}
452

  
453
    d = evalutils.attachment(varname ='var1'
454
                            ,filename='hello.txt')
455
    assert d == {'varname'     : 'var1'
456
                ,'filename'    : 'hello.txt'
457
                ,'content_type': 'application/octet-stream'
458
                ,'b64_content' : ''}
459

  
460
    d = evalutils.attachment(varname     ='var1'
461
                            ,content_type='text/plain')
462
    assert d == {'varname'     : 'var1'
463
                ,'filename'    : ''
464
                ,'content_type': 'text/plain'
465
                ,'b64_content' : ''}
466

  
467
    d = evalutils.attachment(varname='var1'
468
                            ,content='hello world')
469
    assert d == {'varname'     : 'var1'
470
                ,'filename'    : ''
471
                ,'content_type': 'application/octet-stream'
472
                ,'b64_content' : hello_word_b64}
473

  
474
    d = evalutils.attachment(varname    ='var1'
475
                            ,b64_content=hello_word_b64)
476
    assert d == {'varname'     : 'var1'
477
                ,'filename'    : ''
478
                ,'content_type': 'application/octet-stream'
479
                ,'b64_content' : hello_word_b64}
480

  
481
    d = evalutils.attachment(varname     ='var1'
482
                            ,filename    ='hello.txt'
483
                            ,content_type='text/plain'
484
                            ,content     ='hello world')
485
    assert d == {'varname'     : 'var1'
486
                ,'filename'    : 'hello.txt'
487
                ,'content_type': 'text/plain'
488
                ,'b64_content' : hello_word_b64}
489

  
490
    d = evalutils.attachment(varname     ='var1'
491
                            ,filename    ='hello.txt'
492
                            ,content_type='text/plain'
493
                            ,b64_content =hello_word_b64)
494
    assert d == {'varname'     : 'var1'
495
                ,'filename'    : 'hello.txt'
496
                ,'content_type': 'text/plain'
497
                ,'b64_content' : hello_word_b64}
498

  
499
    d = evalutils.attachment(varname     ='var1'
500
                            ,filename    ='hello.txt'
501
                            ,content_type='text/plain'
502
                            ,content     ='hello world'
503
                            ,b64_content =hello_word_b64)
504
    assert d == {'varname'     : 'var1'
505
                ,'filename'    : 'hello.txt'
506
                ,'content_type': 'text/plain'
507
                ,'b64_content' : hello_word_b64}
tests/test_workflows.py
37 37
from wcs.wf.jump import JumpWorkflowStatusItem, _apply_timeouts
38 38
from wcs.wf.timeout_jump import TimeoutWorkflowStatusItem
39 39
from wcs.wf.profile import UpdateUserProfileStatusItem
40
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
40
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem, JournalEvolutionPart
41 41
from wcs.wf.remove import RemoveWorkflowStatusItem
42 42
from wcs.wf.roles import AddRoleWorkflowStatusItem, RemoveRoleWorkflowStatusItem
43 43
from wcs.wf.wscall import WebserviceCallStatusItem
......
851 851
    formdata.evolution[-1]._display_parts = None
852 852
    assert formdata.evolution[-1].display_parts()[-1] == '<div><p>hello</p></div>'
853 853

  
854
def test_register_comment_attachment(pub):
854
def test_register_comment_attachment_substitution(pub):
855 855
    pub.substitutions.feed(MockSubstitutionVariables())
856 856

  
857 857
    formdef = FormDef()
......
922 922
    assert url3 == url4
923 923

  
924 924

  
925
def test_register_comment_attachment(pub):
926
    wf = Workflow(name='comment with attachments')
927
    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
928
    wf.backoffice_fields_formdef.fields = [
929
        FileField(id='bo1', label='bo field 1', type='file', varname='backoffice_file1'),
930
    ]
931
    st1 = wf.add_status('Status1')
932
    wf.store()
933

  
934
    upload = PicklableUpload('test.jpeg', 'image/jpeg')
935
    jpg = open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg')).read()
936
    upload.receive([jpg])
937

  
938
    formdef = FormDef()
939
    formdef.name = 'baz'
940
    formdef.fields = [
941
        FileField(id='1', label='File', type='file', varname='frontoffice_file'),
942
    ]
943
    formdef.workflow_id = wf.id
944
    formdef.store()
945

  
946
    formdata = formdef.data_class()()
947
    formdata.data = {'1': upload}
948
    formdata.just_created()
949
    formdata.store()
950

  
951
    pub.substitutions.feed(formdata)
952

  
953
    setbo = SetBackofficeFieldsWorkflowStatusItem()
954
    setbo.parent = st1
955
    setbo.fields = [{'field_id': 'bo1', 'value': '=form_var_frontoffice_file_raw'}]
956
    setbo.perform(formdata)
957

  
958
    if os.path.exists(os.path.join(get_publisher().app_dir, 'attachments')):
959
        shutil.rmtree(os.path.join(get_publisher().app_dir, 'attachments'))
960

  
961
    item = RegisterCommenterWorkflowStatusItem()
962
    item.attachments = ['form_var_backoffice_file1_raw']
963
    item.comment = '<div>{{ attachments.%s.url }}</div>' % 'backoffice_file1'
964
    item.perform(formdata)
965

  
966
    assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments'))) == 1
967
    for subdir in os.listdir(os.path.join(get_publisher().app_dir, 'attachments')):
968
        assert len(subdir) == 4
969
        assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments', subdir))) == 1
970

  
971
    assert len(formdata.evolution[-1].parts) == 2
972
    assert isinstance(formdata.evolution[-1].parts[0], AttachmentEvolutionPart)
973
    assert formdata.evolution[-1].parts[0].varname == 'backoffice_file1'
974
    assert formdata.evolution[-1].parts[0].orig_filename == upload.orig_filename
975

  
976
    assert isinstance(formdata.evolution[-1].parts[1], JournalEvolutionPart)
977
    assert len(formdata.evolution[-1].parts[1].content) > 0
978
    comment_view = str(formdata.evolution[-1].parts[1].view())
979
    assert len(comment_view) > len('<div></div>')
980
    assert re.search('<div>[^<]+</div>', comment_view)
981

  
982
    if os.path.exists(os.path.join(get_publisher().app_dir, 'attachments')):
983
        shutil.rmtree(os.path.join(get_publisher().app_dir, 'attachments'))
984

  
985
    formdata.evolution[-1].parts = []
986
    formdata.store()
987

  
988
    ws_response_varname = 'ws_response_afile'
989
    wf_data = {'%s_filename'     % ws_response_varname : 'hello.txt'
990
              ,'%s_content_type' % ws_response_varname : 'text/plain'
991
              ,'%s_b64_content'  % ws_response_varname : base64.encodestring('hello world')}
992
    formdata.update_workflow_data(wf_data)
993
    formdata.store()
994
    assert hasattr(formdata, 'workflow_data')
995
    assert isinstance(formdata.workflow_data, dict)
996

  
997
    item = RegisterCommenterWorkflowStatusItem()
998
    item.attachments = ["utils.attachment(varname='{0}'"
999
                                        ",filename={1}_filename"
1000
                                        ",b64_content ={1}_b64_content"
1001
                                        ",content_type={1}_content_type)"
1002
                        "".format('afile', ws_response_varname)]
1003
    item.comment = '{{ attachments.%s.url }}' % 'afile'
1004
    item.perform(formdata)
1005

  
1006
    assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments'))) == 1
1007
    for subdir in os.listdir(os.path.join(get_publisher().app_dir, 'attachments')):
1008
        assert len(subdir) == 4
1009
        assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments', subdir))) == 1
1010

  
1011
    assert len(formdata.evolution[-1].parts) == 2
1012
    assert isinstance(formdata.evolution[-1].parts[0], AttachmentEvolutionPart)
1013
    assert formdata.evolution[-1].parts[0].varname == 'afile'
1014
    assert formdata.evolution[-1].parts[0].orig_filename == 'hello.txt'
1015

  
1016
    assert isinstance(formdata.evolution[-1].parts[1], JournalEvolutionPart)
1017
    assert len(formdata.evolution[-1].parts[1].content) > 0
1018
    comment_view = str(formdata.evolution[-1].parts[1].view())
1019
    assert len(comment_view) > len('<div></div>')
1020
    assert re.search('<div>[^<]+</div>', comment_view)
1021

  
1022

  
925 1023
def test_email(pub, emails):
926 1024
    pub.substitutions.feed(MockSubstitutionVariables())
927 1025

  
wcs/qommon/evalutils.py
127 127
    return make_date(date) + datetime.timedelta(days=count)
128 128

  
129 129

  
130
def attachment(content, filename='', content_type='application/octet-stream'):
130
def attachment(content='', filename='', content_type='application/octet-stream', b64_content='', varname=None):
131 131
    '''Serialize content as an attachment'''
132 132
    import base64
133 133

  
134
    return {
135
        'filename': filename,
136
        'content_type': content_type,
137
        'b64_content': base64.b64encode(content),
134
    d = {
135
        'filename'    : filename if filename else '',
136
        'content_type': content_type if content_type else 'application/octet-stream',
137
        'b64_content' : b64_content if b64_content else base64.encodestring(content)
138 138
    }
139
    if varname:
140
        d['varname'] = varname
141
    return d
wcs/wf/register_comment.py
23 23

  
24 24
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
25 25

  
26

  
27 26
class JournalEvolutionPart: #pylint: disable=C1001
28 27
    content = None
29 28

  
......
84 83
                value=self.comment, cols=80, rows=10)
85 84

  
86 85
    def get_parameters(self):
87
        return ('comment', 'condition')
86
        return ('comment', 'attachments', 'condition')
88 87

  
89 88
    def perform(self, formdata):
90 89
        if not formdata.evolution:
91 90
            return
91

  
92
        # process attachments first, they might be used in the comment
93
        # (with substitution vars)
94
        if self.attachments:
95
            uploads = self.convert_attachments_to_uploads()
96
            self.attach_uploads_to_formdata(formdata, uploads)
97
            formdata.store()  # store and invalidate cache, so references can be used in the comment message.
98

  
99
        # the comment can use attachments done above
92 100
        try:
93 101
            formdata.evolution[-1].add_part(JournalEvolutionPart(formdata, self.comment))
94 102
            formdata.store()
wcs/workflows.py
45 45
from wcs.formdef import FormDef
46 46
from wcs.formdata import Evolution
47 47

  
48
from wcs.qommon.form import PicklableUpload as PUpload
49
from collections import OrderedDict
50

  
48 51
if not __name__.startswith('wcs.') and not __name__ == "__main__":
49 52
    raise ImportError('Import of workflows module must be absolute (import wcs.workflows)')
50 53

  
......
1523 1526
    ok_in_global_action = True # means it can be used in a global action
1524 1527
    directory_name = None
1525 1528
    directory_class = None
1526
    support_substitution_variables = False
1529

  
1530
    support_substitution_variables = True
1531
    attachments = None
1532

  
1527 1533

  
1528 1534
    @classmethod
1529 1535
    def init(cls):
......
1603 1609
                     value=self.condition, size=40,
1604 1610
                     advanced=not(self.condition))
1605 1611

  
1612
        if 'attachments' in parameters:
1613
            attachments_options, attachments = self.get_attachments_options()
1614
            if len(attachments_options) > 1:
1615
                form.add(WidgetList, '%sattachments' % prefix, title=_('Attachments'),
1616
                         element_type=SingleSelectWidgetWithOther,
1617
                         value=attachments,
1618
                         add_element_label=_('Add attachment'),
1619
                         element_kwargs={'render_br': False, 'options': attachments_options})
1620
            else:
1621
                form.add(WidgetList, '%sattachments' % prefix,
1622
                         title=_('Attachments (Python expressions)'),
1623
                         element_type=StringWidget,
1624
                         value=attachments,
1625
                         add_element_label=_('Add attachment'),
1626
                         element_kwargs={'render_br': False, 'size': 50},
1627
                         advanced=not(bool(attachments)))
1628

  
1606 1629
    def get_parameters(self):
1607 1630
        return ('condition',)
1608 1631

  
......
1842 1865
            del odict['parent']
1843 1866
        return odict
1844 1867

  
1868
    def attachments_init_with_xml(self, elem, charset, include_id=False):
1869
        if elem is None:
1870
            self.attachments = None
1871
        else:
1872
            self.attachments = [item.text.encode(charset) for item in elem.findall('attachment')]
1873

  
1874
    def get_attachments_options(self):
1875
        attachments_options = [(None, '---', None)]
1876
        varnameless = []
1877
        for field in self.parent.parent.get_backoffice_fields():
1878
            if field.key != 'file':
1879
                continue
1880
            if field.varname:
1881
                codename = 'form_var_%s_raw' % field.varname
1882
            else:
1883
                codename = 'form_f%s' % field.id  # = form_fbo<n>
1884
                varnameless.append(codename)
1885
            attachments_options.append((codename, field.label, codename))
1886
        # filter: do not consider removed fields without varname
1887
        attachments = [attachment for attachment in self.attachments or []
1888
                       if ((not attachment.startswith('form_fbo')) or
1889
                           (attachment in varnameless))]
1890
        return attachments_options, attachments
1891

  
1892
    def get_attachment_varname(self, attachment, upload=None):
1893
        varname = str(attachment)
1894
        if isinstance(attachment, str) and re.match(r'form_var_', attachment):
1895
            varname = attachment
1896
            m = re.search(r'^form_var_(\w+)$', attachment)
1897
            if m:
1898
                varname = re.sub(r'_raw$', '', m.group(1)) if re.search(r'_raw$', m.group(1)) else m.group(1)
1899
        elif upload:
1900
            if isinstance(upload, PUpload):
1901
                varname = str(upload)
1902
            elif isinstance(upload, dict) and 'varname' in upload:
1903
                varname = upload['varname']
1904
            elif hasattr(upload, varname):
1905
                varname = upload.varname
1906
        return varname
1907

  
1908
    def convert_attachments_to_uploads(self):
1909
        # forget previous attachment converted to uploads
1910
        # (file with same content and same varname, will not be duplicated
1911
        #  because the path of the file is made of the hash of the content)
1912
        uploads = OrderedDict()
1913

  
1914
        if self.attachments:
1915
            global_eval_dict = get_publisher().get_global_eval_dict()
1916
            local_eval_dict = get_publisher().substitutions.get_context_variables()
1917
            for attachment in self.attachments:
1918
                try:
1919
                    # execute any Python expression
1920
                    # and magically convert string like 'form_var_*_raw' to a PicklableUpload
1921
                    picklableupload = eval(attachment, global_eval_dict, local_eval_dict)
1922
                except:
1923
                    get_publisher().notify_of_exception(sys.exc_info(),
1924
                                                        context='[workflow/attachments]')
1925
                    continue
1926

  
1927
                if not picklableupload:
1928
                    continue
1929

  
1930
                varname = self.get_attachment_varname(attachment, picklableupload)
1931

  
1932
                try:
1933
                    # magically convert any value to a PicklableUpload
1934
                    # usualy a dict like one provided by qommon/evalutils:attachment()
1935
                    picklableupload = FileField.convert_value_from_anything(picklableupload)
1936
                except ValueError:
1937
                    get_publisher().notify_of_exception(sys.exc_info(),
1938
                                                        context='[workflow/attachments]')
1939
                    continue
1940

  
1941
                # exsiting attachments upload will be replaced by the new one (last one wins)
1942
                uploads[varname] = picklableupload
1943

  
1944
        return uploads
1945

  
1946
    def formdata_has_attachment(self, formdata, varname, attachment):
1947
        if not formdata.evolution or not formdata.evolution[-1].parts:
1948
            return False
1949
        if not varname and hasattr(attachment, 'varname'):
1950
            varname = attachment.varname
1951
        for part in formdata.evolution[-1].parts:
1952
            if isinstance(part, AttachmentEvolutionPart):
1953
                if attachment:
1954
                    # not testing content here, only metadata
1955
                    if  part.orig_filename == attachment.orig_filename \
1956
                    and part.base_filename == attachment.base_filename \
1957
                    and  part.content_type == attachment.content_type  \
1958
                    and       part.charset == attachment.charset       \
1959
                    and (not varname or part.varname == varname):
1960
                        return True
1961
                elif varname and part.varname == varname:
1962
                    return True
1963
        return False
1964

  
1965
    def attach_uploads_to_formdata(self, formdata, uploads):
1966
        if not formdata.evolution[-1].parts:
1967
            formdata.evolution[-1].parts = []
1968
        for varname, upload in uploads.items():
1969
            # attach to form only if new (to prevent double attachments)
1970
            if not self.formdata_has_attachment(formdata, varname=varname, attachment=upload):
1971
                try:
1972
                    # useless but required to restore upload.fp from serialized state, needed by 'AttachmentEvolutionPart.from_upload()
1973
                    fp = upload.get_file_pointer()
1974
                    formdata.evolution[-1].add_part(AttachmentEvolutionPart.from_upload(upload, varname=varname))
1975
                except:
1976
                    get_publisher().notify_of_exception(sys.exc_info(),
1977
                                                        context='[workflow/attachments]')
1978
                continue
1979

  
1845 1980

  
1846 1981
class WorkflowStatusJumpItem(WorkflowStatusItem):
1847 1982
    status = None
......
2182 2317
    description = N_('Email')
2183 2318
    key = 'sendmail'
2184 2319
    category = 'interaction'
2185
    support_substitution_variables = True
2186 2320

  
2187 2321
    to = []
2188 2322
    subject = None
2189 2323
    body = None
2190 2324
    custom_from = None
2191
    attachments = None
2192 2325

  
2193 2326
    comment = None
2194 2327

  
......
2204 2337
        return super(SendmailWorkflowStatusItem, self)._get_role_id_from_xml(
2205 2338
                elem, charset, include_id=include_id)
2206 2339

  
2207
    def attachments_init_with_xml(self, elem, charset, include_id=False):
2208
        if elem is None:
2209
            self.attachments = None
2210
        else:
2211
            self.attachments = [item.text.encode(charset) for item in elem.findall('attachment')]
2212

  
2213 2340
    def render_list_of_roles_or_emails(self, roles):
2214 2341
        t = []
2215 2342
        for r in roles:
......
2239 2366
    def fill_admin_form(self, form):
2240 2367
        self.add_parameters_widgets(form, self.get_parameters())
2241 2368

  
2242
    def get_attachments_options(self):
2243
        attachments_options = [(None, '---', None)]
2244
        varnameless = []
2245
        for field in self.parent.parent.get_backoffice_fields():
2246
            if field.key != 'file':
2247
                continue
2248
            if field.varname:
2249
                codename = 'form_var_%s_raw' % field.varname
2250
            else:
2251
                codename = 'form_f%s' % field.id  # = form_fbo<n>
2252
                varnameless.append(codename)
2253
            attachments_options.append((codename, field.label, codename))
2254
        # filter: do not consider removed fields without varname
2255
        attachments = [attachment for attachment in self.attachments or []
2256
                       if ((not attachment.startswith('form_fbo')) or
2257
                           (attachment in varnameless))]
2258
        return attachments_options, attachments
2259

  
2260 2369
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
2261 2370
        super(SendmailWorkflowStatusItem, self).add_parameters_widgets(
2262 2371
                form, parameters, prefix=prefix, formdef=formdef)
......
2277 2386
                     value=self.body, cols=80, rows=10,
2278 2387
                     validation_function=ComputedExpressionWidget.validate_template)
2279 2388

  
2280
        if 'attachments' in parameters:
2281
            attachments_options, attachments = self.get_attachments_options()
2282
            if len(attachments_options) > 1:
2283
                form.add(WidgetList, '%sattachments' % prefix, title=_('Attachments'),
2284
                         element_type=SingleSelectWidgetWithOther,
2285
                         value=attachments,
2286
                         add_element_label=_('Add attachment'),
2287
                         element_kwargs={'render_br': False, 'options': attachments_options})
2288
            else:
2289
                form.add(WidgetList, '%sattachments' % prefix,
2290
                         title=_('Attachments (Python expressions)'),
2291
                         element_type=StringWidget,
2292
                         value=attachments,
2293
                         add_element_label=_('Add attachment'),
2294
                         element_kwargs={'render_br': False, 'size': 50},
2295
                         advanced=not(bool(attachments)))
2296

  
2297 2389
        if 'custom_from' in parameters:
2298 2390
            form.add(ComputedExpressionWidget, '%scustom_from' % prefix,
2299 2391
                     title=_('Custom From Address'), value=self.custom_from,
......
2381 2473
        if self.custom_from:
2382 2474
            email_from = self.compute(self.custom_from)
2383 2475

  
2384
        attachments = []
2385
        if self.attachments:
2386
            global_eval_dict = get_publisher().get_global_eval_dict()
2387
            local_eval_dict = get_publisher().substitutions.get_context_variables()
2388
            for attachment in self.attachments:
2389
                try:
2390
                    picklableupload = eval(attachment, global_eval_dict, local_eval_dict)
2391
                except:
2392
                    get_publisher().notify_of_exception(sys.exc_info(),
2393
                                                        context='[Sendmail/attachments]')
2394
                    continue
2395
                if not picklableupload:
2396
                    continue
2397
                try:
2398
                    picklableupload = FileField.convert_value_from_anything(picklableupload)
2399
                except ValueError:
2400
                    get_publisher().notify_of_exception(sys.exc_info(),
2401
                                                        context='[Sendmail/attachments]')
2402
                    continue
2403
                attachments.append(picklableupload)
2476
        attachments = [x for _,x in self.convert_attachments_to_uploads().items()]
2404 2477

  
2405 2478
        if len(addresses) > 1:
2406 2479
            emails.email(mail_subject, mail_body, email_rcpt=None,
2407
-