Projet

Général

Profil

0001-workflow-added-attachment-support-for-RegisterCommen.patch

Michaël Bideau, 15 mai 2019 14:46

Télécharger (19,8 ko)

Voir les différences:

Subject: [PATCH] workflow: added attachment support for RegisterComment    
 misc: added a function to create a dict from a dict and a key prefix   
 tests: updated cases in misc and workflow to tests above new features

 tests/test_misc.py         |  15 +++++
 tests/test_workflows.py    |  98 +++++++++++++++++++++++++++++-
 wcs/qommon/evalutils.py    |  26 ++++++++
 wcs/wf/register_comment.py |  28 ++++++++-
 wcs/workflows.py           | 145 +++++++++++++++++++++++++--------------------
 5 files changed, 244 insertions(+), 68 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_dict_from_prefix():
445
    hello_word_b64 = base64.encodestring('hello world')
446

  
447
    d = evalutils.dict_from_prefix('var1', {})
448
    assert d == {}
449

  
450
    d = evalutils.dict_from_prefix('', {'k1':'v1'})
451
    assert d == {'k1':'v1'}
452

  
453
    d = evalutils.dict_from_prefix('k', {'k1':'v1', 'k2':'v2'})
454
    assert d == {'1':'v1', '2':'v2'}
455

  
456
    d = evalutils.dict_from_prefix('v', {'k1':'v1', 'k2':'v2'})
457
    assert d == {}
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
......
852 852
    formdata.evolution[-1]._display_parts = None
853 853
    assert formdata.evolution[-1].display_parts()[-1] == '<div><p>hello</p></div>'
854 854

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

  
858 858
    formdef = FormDef()
......
923 923
    assert url3 == url4
924 924

  
925 925

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

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

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

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

  
952
    pub.substitutions.feed(formdata)
953

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

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

  
962
    comment_text = 'File is attached to the form history'
963

  
964
    item = RegisterCommenterWorkflowStatusItem()
965
    item.attachments = ['form_var_backoffice_file1_raw']
966
    item.comment = comment_text
967
    item.perform(formdata)
968

  
969
    assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments'))) == 1
970
    for subdir in os.listdir(os.path.join(get_publisher().app_dir, 'attachments')):
971
        assert len(subdir) == 4
972
        assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments', subdir))) == 1
973

  
974
    assert len(formdata.evolution[-1].parts) == 2
975
    assert isinstance(formdata.evolution[-1].parts[0], AttachmentEvolutionPart)
976
    assert formdata.evolution[-1].parts[0].orig_filename == upload.orig_filename
977

  
978
    assert isinstance(formdata.evolution[-1].parts[1], JournalEvolutionPart)
979
    assert len(formdata.evolution[-1].parts[1].content) > 0
980
    comment_view = str(formdata.evolution[-1].parts[1].view())
981
    assert comment_view == '<p>%s</p>' % comment_text
982

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

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

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

  
1000
    item = RegisterCommenterWorkflowStatusItem()
1001
    item.attachments = ["utils.dict_from_prefix('%s_', locals())" % ws_response_varname]
1002
    item.comment = comment_text
1003
    item.perform(formdata)
1004

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

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

  
1014
    assert isinstance(formdata.evolution[-1].parts[1], JournalEvolutionPart)
1015
    assert len(formdata.evolution[-1].parts[1].content) > 0
1016
    comment_view = str(formdata.evolution[-1].parts[1].view())
1017
    assert comment_view == '<p>%s</p>' % comment_text
1018

  
1019

  
926 1020
def test_email(pub, emails):
927 1021
    pub.substitutions.feed(MockSubstitutionVariables())
928 1022

  
wcs/qommon/evalutils.py
22 22

  
23 23
import datetime
24 24
import time
25
import re
25 26

  
26 27
from .misc import get_as_datetime
27 28

  
......
136 137
        'content_type': content_type,
137 138
        'b64_content': base64.b64encode(content),
138 139
    }
140

  
141
def dict_from_prefix(prefix, in_dict):
142
    '''Return a dict based on a dict filtered by a key prefix.
143

  
144
       The prefix is removed from the key.
145

  
146
       Intent: meant to help build a PicklableUpload from a set
147
               of key/values stored in the workflow data.
148

  
149
       Note: to use this function in a context of a Python
150
             expression, you should pass the _wf_data_ using
151
             the function locals()
152

  
153
       Example: utils.dict_from_prefix('akey_', locals())
154
         Where: the workflow data contains the key/values:
155
                akey_filename     = <filename>
156
                akey_content_type = <mime_type>
157
                akey_b64_content  = <content base64 encoded>
158
           And: it produces a dict like the key/values are:
159
                filename     = wf_data['akey_filename']
160
                content_type = wf_data['akey_content_type']
161
                b64_content  = wf_data['akey_b64_content']
162
    '''
163
    return {re.sub(r'^%s' % prefix, '', k): v \
164
            for k,v in in_dict.items() if k.startswith('%s' % prefix)}
wcs/wf/register_comment.py
21 21
from qommon.template import TemplateError
22 22
from qommon import get_logger
23 23

  
24
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
24
from wcs.workflows import (WorkflowStatusItem, register_item_class, template_on_formdata,
25
                           AttachmentEvolutionPart)
25 26

  
27
import sys
26 28

  
27 29
class JournalEvolutionPart: #pylint: disable=C1001
28 30
    content = None
......
84 86
                value=self.comment, cols=80, rows=10)
85 87

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

  
91
    def attach_uploads_to_formdata(self, formdata, uploads):
92
        if not formdata.evolution[-1].parts:
93
            formdata.evolution[-1].parts = []
94
        for upload in uploads:
95
            try:
96
                # useless but required to restore upload.fp from serialized state, needed by 'AttachmentEvolutionPart.from_upload()
97
                fp = upload.get_file_pointer()
98
                formdata.evolution[-1].add_part(AttachmentEvolutionPart.from_upload(upload))
99
            except:
100
                get_publisher().notify_of_exception(sys.exc_info(),
101
                                                    context='[comment/attachments]')
102
            continue
88 103

  
89 104
    def perform(self, formdata):
90 105
        if not formdata.evolution:
91 106
            return
107

  
108
        # process attachments first, they might be used in the comment
109
        # (with substitution vars)
110
        if self.attachments:
111
            uploads = self.convert_attachments_to_uploads()
112
            self.attach_uploads_to_formdata(formdata, uploads)
113
            formdata.store()  # store and invalidate cache, so references can be used in the comment message.
114

  
115
        # the comment can use attachments done above
92 116
        try:
93 117
            formdata.evolution[-1].add_part(JournalEvolutionPart(formdata, self.comment))
94 118
            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

  
......
1571 1574
    ok_in_global_action = True # means it can be used in a global action
1572 1575
    directory_name = None
1573 1576
    directory_class = None
1574
    support_substitution_variables = False
1577

  
1578
    support_substitution_variables = True
1579
    attachments = None
1580

  
1575 1581

  
1576 1582
    @classmethod
1577 1583
    def init(cls):
......
1651 1657
                     value=self.condition, size=40,
1652 1658
                     advanced=not(self.condition))
1653 1659

  
1660
        if 'attachments' in parameters:
1661
            attachments_options, attachments = self.get_attachments_options()
1662
            if len(attachments_options) > 1:
1663
                form.add(WidgetList, '%sattachments' % prefix, title=_('Attachments'),
1664
                         element_type=SingleSelectWidgetWithOther,
1665
                         value=attachments,
1666
                         add_element_label=_('Add attachment'),
1667
                         element_kwargs={'render_br': False, 'options': attachments_options})
1668
            else:
1669
                form.add(WidgetList, '%sattachments' % prefix,
1670
                         title=_('Attachments (Python expressions)'),
1671
                         element_type=StringWidget,
1672
                         value=attachments,
1673
                         add_element_label=_('Add attachment'),
1674
                         element_kwargs={'render_br': False, 'size': 50},
1675
                         advanced=not(bool(attachments)))
1676

  
1654 1677
    def get_parameters(self):
1655 1678
        return ('condition',)
1656 1679

  
......
1890 1913
            del odict['parent']
1891 1914
        return odict
1892 1915

  
1916
    def attachments_init_with_xml(self, elem, charset, include_id=False):
1917
        if elem is None:
1918
            self.attachments = None
1919
        else:
1920
            self.attachments = [item.text.encode(charset) for item in elem.findall('attachment')]
1921

  
1922
    def get_attachments_options(self):
1923
        attachments_options = [(None, '---', None)]
1924
        varnameless = []
1925
        for field in self.parent.parent.get_backoffice_fields():
1926
            if field.key != 'file':
1927
                continue
1928
            if field.varname:
1929
                codename = 'form_var_%s_raw' % field.varname
1930
            else:
1931
                codename = 'form_f%s' % field.id  # = form_fbo<n>
1932
                varnameless.append(codename)
1933
            attachments_options.append((codename, field.label, codename))
1934
        # filter: do not consider removed fields without varname
1935
        attachments = [attachment for attachment in self.attachments or []
1936
                       if ((not attachment.startswith('form_fbo')) or
1937
                           (attachment in varnameless))]
1938
        return attachments_options, attachments
1939

  
1940
    def convert_attachments_to_uploads(self):
1941
        uploads = []
1942

  
1943
        if self.attachments:
1944
            global_eval_dict = get_publisher().get_global_eval_dict()
1945
            local_eval_dict = get_publisher().substitutions.get_context_variables()
1946
            for attachment in self.attachments:
1947
                try:
1948
                    # execute any Python expression
1949
                    # and magically convert string like 'form_var_*_raw' to a PicklableUpload
1950
                    picklableupload = eval(attachment, global_eval_dict, local_eval_dict)
1951
                except:
1952
                    get_publisher().notify_of_exception(sys.exc_info(),
1953
                                                        context='[workflow/attachments]')
1954
                    continue
1955

  
1956
                if not picklableupload:
1957
                    continue
1958

  
1959
                try:
1960
                    # magically convert any value to a PicklableUpload
1961
                    # usualy a dict like one provided by qommon/evalutils:attachment()
1962
                    picklableupload = FileField.convert_value_from_anything(picklableupload)
1963
                except ValueError:
1964
                    get_publisher().notify_of_exception(sys.exc_info(),
1965
                                                        context='[workflow/attachments]')
1966
                    continue
1967

  
1968
                uploads.append(picklableupload)
1969

  
1970
        return uploads
1971

  
1893 1972

  
1894 1973
class WorkflowStatusJumpItem(WorkflowStatusItem):
1895 1974
    status = None
......
2230 2309
    description = N_('Email')
2231 2310
    key = 'sendmail'
2232 2311
    category = 'interaction'
2233
    support_substitution_variables = True
2234 2312

  
2235 2313
    to = []
2236 2314
    subject = None
2237 2315
    body = None
2238 2316
    custom_from = None
2239
    attachments = None
2240 2317

  
2241 2318
    comment = None
2242 2319

  
......
2252 2329
        return super(SendmailWorkflowStatusItem, self)._get_role_id_from_xml(
2253 2330
                elem, charset, include_id=include_id)
2254 2331

  
2255
    def attachments_init_with_xml(self, elem, charset, include_id=False):
2256
        if elem is None:
2257
            self.attachments = None
2258
        else:
2259
            self.attachments = [item.text.encode(charset) for item in elem.findall('attachment')]
2260

  
2261 2332
    def render_list_of_roles_or_emails(self, roles):
2262 2333
        t = []
2263 2334
        for r in roles:
......
2287 2358
    def fill_admin_form(self, form):
2288 2359
        self.add_parameters_widgets(form, self.get_parameters())
2289 2360

  
2290
    def get_attachments_options(self):
2291
        attachments_options = [(None, '---', None)]
2292
        varnameless = []
2293
        for field in self.parent.parent.get_backoffice_fields():
2294
            if field.key != 'file':
2295
                continue
2296
            if field.varname:
2297
                codename = 'form_var_%s_raw' % field.varname
2298
            else:
2299
                codename = 'form_f%s' % field.id  # = form_fbo<n>
2300
                varnameless.append(codename)
2301
            attachments_options.append((codename, field.label, codename))
2302
        # filter: do not consider removed fields without varname
2303
        attachments = [attachment for attachment in self.attachments or []
2304
                       if ((not attachment.startswith('form_fbo')) or
2305
                           (attachment in varnameless))]
2306
        return attachments_options, attachments
2307

  
2308 2361
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
2309 2362
        super(SendmailWorkflowStatusItem, self).add_parameters_widgets(
2310 2363
                form, parameters, prefix=prefix, formdef=formdef)
......
2325 2378
                     value=self.body, cols=80, rows=10,
2326 2379
                     validation_function=ComputedExpressionWidget.validate_template)
2327 2380

  
2328
        if 'attachments' in parameters:
2329
            attachments_options, attachments = self.get_attachments_options()
2330
            if len(attachments_options) > 1:
2331
                form.add(WidgetList, '%sattachments' % prefix, title=_('Attachments'),
2332
                         element_type=SingleSelectWidgetWithOther,
2333
                         value=attachments,
2334
                         add_element_label=_('Add attachment'),
2335
                         element_kwargs={'render_br': False, 'options': attachments_options})
2336
            else:
2337
                form.add(WidgetList, '%sattachments' % prefix,
2338
                         title=_('Attachments (Python expressions)'),
2339
                         element_type=StringWidget,
2340
                         value=attachments,
2341
                         add_element_label=_('Add attachment'),
2342
                         element_kwargs={'render_br': False, 'size': 50},
2343
                         advanced=not(bool(attachments)))
2344

  
2345 2381
        if 'custom_from' in parameters:
2346 2382
            form.add(ComputedExpressionWidget, '%scustom_from' % prefix,
2347 2383
                     title=_('Custom From Address'), value=self.custom_from,
......
2429 2465
        if self.custom_from:
2430 2466
            email_from = self.compute(self.custom_from)
2431 2467

  
2432
        attachments = []
2433
        if self.attachments:
2434
            global_eval_dict = get_publisher().get_global_eval_dict()
2435
            local_eval_dict = get_publisher().substitutions.get_context_variables()
2436
            for attachment in self.attachments:
2437
                try:
2438
                    picklableupload = eval(attachment, global_eval_dict, local_eval_dict)
2439
                except:
2440
                    get_publisher().notify_of_exception(sys.exc_info(),
2441
                                                        context='[Sendmail/attachments]')
2442
                    continue
2443
                if not picklableupload:
2444
                    continue
2445
                try:
2446
                    picklableupload = FileField.convert_value_from_anything(picklableupload)
2447
                except ValueError:
2448
                    get_publisher().notify_of_exception(sys.exc_info(),
2449
                                                        context='[Sendmail/attachments]')
2450
                    continue
2451
                attachments.append(picklableupload)
2468
        attachments = self.convert_attachments_to_uploads()
2452 2469

  
2453 2470
        if len(addresses) > 1:
2454 2471
            emails.email(mail_subject, mail_body, email_rcpt=None,
2455
-