Projet

Général

Profil

0001-forms-add-direct-download-of-files-from-workflow-for.patch

Frédéric Péters, 01 avril 2022 09:36

Télécharger (18,5 ko)

Voir les différences:

Subject: [PATCH] forms: add direct download of files from workflow forms
 (#59672)

 tests/form_pages/test_all.py | 89 ++++++++++++++++++++++++++++++++++++
 wcs/fields.py                |  2 +
 wcs/formdata.py              | 21 +++++++++
 wcs/formdef.py               | 30 ++----------
 wcs/forms/common.py          | 40 ++++++++++------
 wcs/qommon/misc.py           | 11 +++++
 wcs/qommon/upload_storage.py |  9 ++++
 wcs/variables.py             | 26 ++++++++---
 wcs/wf/form.py               | 12 +++--
 9 files changed, 192 insertions(+), 48 deletions(-)
tests/form_pages/test_all.py
9059 9059
    assert not formdata.workflow_data
9060 9060

  
9061 9061

  
9062
def test_workflow_form_file_access(pub):
9063
    FormDef.wipe()
9064
    Workflow.wipe()
9065
    BlockDef.wipe()
9066

  
9067
    user = create_user(pub)
9068

  
9069
    block = BlockDef()
9070
    block.name = 'foobar'
9071
    block.fields = [
9072
        fields.FileField(id='123', required=True, label='Test', type='file', varname='test'),
9073
    ]
9074
    block.store()
9075

  
9076
    wf = Workflow(name='test')
9077
    status = wf.add_status('New', 'st1')
9078
    next_status = wf.add_status('Next', 'st2')
9079

  
9080
    status.items = []
9081
    display_form = FormWorkflowStatusItem()
9082
    display_form.id = '_display_form'
9083
    display_form.by = ['_submitter']
9084
    display_form.varname = 'blah'
9085
    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
9086
    display_form.formdef.fields = [
9087
        fields.BlockField(id='1', label='test', type='block:foobar', varname='fooblock', max_items=3),
9088
        fields.FileField(id='2', label='test2', type='file', varname='file'),
9089
    ]
9090
    status.items.append(display_form)
9091
    display_form.parent = status
9092

  
9093
    jump = JumpOnSubmitWorkflowStatusItem()
9094
    jump.id = '_jump'
9095
    jump.status = next_status.id
9096
    status.items.append(jump)
9097
    jump.parent = status
9098

  
9099
    register_comment = RegisterCommenterWorkflowStatusItem()
9100
    register_comment.id = '_register'
9101
    register_comment.comment = '''<p>
9102
        <a href="{{ form_workflow_form_blah_var_fooblock_0_test_url }}" id="t1">1st file in block</a>
9103
        <a href="{{ form_workflow_form_blah_var_fooblock_1_test_url }}" id="t2">2nd file in block</a>
9104
        <a href="{{ form_workflow_form_blah_0_var_fooblock_0_test_url }}" id="t3">again 1st file in block</a>
9105
        <a href="{{ form_workflow_form_blah_0_var_file_url }}" id="t4">file field</a>
9106
    </p>'''
9107
    next_status.items.append(register_comment)
9108
    register_comment.parent = next_status
9109

  
9110
    wf.store()
9111

  
9112
    formdef = create_formdef()
9113
    formdef.workflow_id = wf.id
9114
    formdef.fields = []
9115
    formdef.store()
9116

  
9117
    formdef.data_class().wipe()
9118

  
9119
    formdata = formdef.data_class()()
9120
    formdata.user_id = user.id
9121
    formdata.just_created()
9122
    formdata.store()
9123

  
9124
    app = login(get_app(pub), username='foo', password='foo')
9125
    resp = app.get(formdata.get_url(backoffice=False))
9126
    resp.form['fblah_1$element0$f123$file'] = Upload('test1.txt', b'foobar1', 'text/plain')
9127
    resp = resp.form.submit('fblah_1$add_element')
9128
    resp.form['fblah_1$element1$f123$file'] = Upload('test2.txt', b'foobar2', 'text/plain')
9129
    resp.form['fblah_2$file'] = Upload('test3.txt', b'foobar3', 'text/plain')
9130
    resp = resp.form.submit('submit').follow()
9131
    assert app.get(resp.pyquery('#t1').attr.href).body == b'foobar1'
9132
    assert app.get(resp.pyquery('#t2').attr.href).body == b'foobar2'
9133
    assert app.get(resp.pyquery('#t3').attr.href).body == b'foobar1'
9134
    assert app.get(resp.pyquery('#t4').attr.href).body == b'foobar3'
9135
    app.get(resp.pyquery('#t4').attr.href + 'X', status=404)  # wrong URL, unknown file
9136

  
9137
    # unlogged user
9138
    assert '/login' in get_app(pub).get(resp.pyquery('#t1').attr.href).location
9139

  
9140
    # other user
9141
    user = pub.user_class()
9142
    user.name = 'Second user'
9143
    user.store()
9144
    account = PasswordAccount(id='foo2')
9145
    account.set_password('foo2')
9146
    account.user_id = user.id
9147
    account.store()
9148
    login(get_app(pub), username='foo2', password='foo2').get(resp.pyquery('#t1').attr.href, status=403)
9149

  
9150

  
9062 9151
def test_rich_commentable_action(pub):
9063 9152
    create_user(pub)
9064 9153

  
wcs/fields.py
1484 1484
        return self.get_view_value(value, include_image_thumbnail=False, max_len=max_len, **kwargs)
1485 1485

  
1486 1486
    def get_download_query_string(self, **kwargs):
1487
        if kwargs.get('file_value'):
1488
            return 'hash=%s' % kwargs.get('file_value').file_digest()
1487 1489
        if kwargs.get('parent_field'):
1488 1490
            return 'f=%s$%s$%s' % (kwargs['parent_field'].id, kwargs['parent_field_index'], self.id)
1489 1491
        return 'f=%s' % self.id
wcs/formdata.py
17 17
import collections
18 18
import copy
19 19
import datetime
20
import itertools
20 21
import json
21 22
import re
22 23
import sys
......
393 394
            empty &= self.data.get(key) is None
394 395
        return empty
395 396

  
397
    def get_all_file_data(self):
398
        from wcs.wf.form import WorkflowFormEvolutionPart
399

  
400
        for field_data in itertools.chain((self.data or {}).values(), (self.workflow_data or {}).values()):
401
            if misc.is_upload(field_data):
402
                yield field_data
403
            elif isinstance(field_data, dict) and isinstance(field_data.get('data'), list):
404
                for subfield_rowdata in field_data.get('data'):
405
                    if isinstance(subfield_rowdata, dict):
406
                        for block_field_data in subfield_rowdata.values():
407
                            if misc.is_upload(block_field_data):
408
                                yield block_field_data
409
        for part in self.iter_evolution_parts():
410
            if misc.is_attachment(part):
411
                yield part
412
            elif isinstance(part, WorkflowFormEvolutionPart):
413
                for field_data in (part.data or {}).values():
414
                    if misc.is_upload(field_data):
415
                        yield field_data
416

  
396 417
    @classmethod
397 418
    def get_actionable_count(cls, user_roles):
398 419
        if get_publisher().is_using_postgresql():
wcs/formdef.py
42 42
from .qommon.admin.emails import EmailsDirectory
43 43
from .qommon.cron import CronJob
44 44
from .qommon.form import Form, HtmlWidget, UploadedFile
45
from .qommon.misc import JSONEncoder, get_as_datetime, simplify, xml_node_text
45
from .qommon.misc import JSONEncoder, get_as_datetime, is_attachment, is_upload, simplify, xml_node_text
46 46
from .qommon.publisher import get_publisher_class
47 47
from .qommon.storage import Equal, StorableObject, StrictNotEqual, fix_key
48 48
from .qommon.substitution import Substitutions
......
2064 2064
    known_filenames.update([x for x in glob.glob(os.path.join(publisher.app_dir, 'uploads/*'))])
2065 2065
    known_filenames.update([x for x in glob.glob(os.path.join(publisher.app_dir, 'attachments/*/*'))])
2066 2066

  
2067
    def is_upload(obj):
2068
        # we can't use isinstance() because obj can be a
2069
        # wcs.qommon.form.PicklableUpload or a qommon.form.PicklableUpload
2070
        return obj.__class__.__name__ == 'PicklableUpload'
2071

  
2072
    def is_attachment(obj):
2073
        return obj.__class__.__name__ == 'AttachmentEvolutionPart'
2074

  
2075 2067
    def accumulate_filenames():
2076 2068
        from wcs.carddef import CardDef
2077
        from wcs.wf.form import WorkflowFormEvolutionPart
2078 2069

  
2079 2070
        for formdef in FormDef.select(ignore_migration=True) + CardDef.select(ignore_migration=True):
2080 2071
            for option_data in (formdef.workflow_options or {}).values():
2081 2072
                if is_upload(option_data):
2082 2073
                    yield option_data.get_fs_filename()
2083 2074
            for formdata in formdef.data_class().select_iterator(ignore_errors=True, itersize=200):
2084
                for field_data in itertools.chain(
2085
                    (formdata.data or {}).values(), (formdata.workflow_data or {}).values()
2086
                ):
2075
                for field_data in formdata.get_all_file_data():
2087 2076
                    if is_upload(field_data):
2088 2077
                        yield field_data.get_fs_filename()
2089
                    elif isinstance(field_data, dict) and isinstance(field_data.get('data'), list):
2090
                        for subfield_rowdata in field_data.get('data'):
2091
                            if isinstance(subfield_rowdata, dict):
2092
                                for block_field_data in subfield_rowdata.values():
2093
                                    if is_upload(block_field_data):
2094
                                        yield block_field_data.get_fs_filename()
2095
                for part in formdata.iter_evolution_parts():
2096
                    if is_attachment(part):
2097
                        yield part.filename
2098
                    elif isinstance(part, WorkflowFormEvolutionPart):
2099
                        for field_data in (part.data or {}).values():
2100
                            if is_upload(field_data):
2101
                                yield field_data.get_fs_filename()
2078
                    elif is_attachment(field_data):
2079
                        yield field_data.filename
2102 2080
        for user in publisher.user_class.select():
2103 2081
            for field_data in (user.form_data or {}).values():
2104 2082
                if is_upload(field_data):
wcs/forms/common.py
76 76
            redirect_url = sign_url_auto_orig(redirect_url)
77 77
            return redirect(redirect_url)
78 78

  
79
        return self.serve_file(file, thumbnail=self.thumbnails)
80

  
81
    @classmethod
82
    def serve_file(cls, file, thumbnail=False):
79 83
        response = get_response()
80 84

  
81
        if self.thumbnails:
85
        if thumbnail:
82 86
            if file.can_thumbnail():
83 87
                try:
84
                    thumbnail = misc.get_thumbnail(file.get_fs_filename(), content_type=file.content_type)
88
                    content = misc.get_thumbnail(file.get_fs_filename(), content_type=file.content_type)
85 89
                    response.set_content_type('image/png')
86
                    return thumbnail
90
                    return content
87 91
                except misc.ThumbnailError:
88 92
                    raise errors.TraversalError()
89 93
            else:
......
696 700
    def download(self):
697 701
        if not is_url_signed():
698 702
            self.check_receiver()
699
        try:
700
            fn = get_request().form['f']
701
            if '$' in fn:
702
                # path to block field contents
703
                fn2, idx, sub = fn.split('$', 2)
704
                file = self.filled.data[fn2]['data'][int(idx)][sub]
705
            else:
706
                file = self.filled.data[fn]
707
        except (KeyError, ValueError):
708
            raise errors.TraversalError()
703
        file = None
704
        if get_request().form and get_request().form.get('hash'):
705
            # look in all known formdata files for file with given hash
706
            file_digest = get_request().form.get('hash')
707
            for field_data in self.filled.get_all_file_data():
708
                if not hasattr(field_data, 'file_digest'):
709
                    continue
710
                if field_data.file_digest() == file_digest:
711
                    return FileDirectory.serve_file(field_data)
712
        elif get_request().form and get_request().form.get('f'):
713
            try:
714
                fn = get_request().form['f']
715
                if '$' in fn:
716
                    # path to block field contents
717
                    fn2, idx, sub = fn.split('$', 2)
718
                    file = self.filled.data[fn2]['data'][int(idx)][sub]
719
                else:
720
                    file = self.filled.data[fn]
721
            except (KeyError, ValueError):
722
                pass
709 723

  
710 724
        if not hasattr(file, 'content_type'):
711 725
            raise errors.TraversalError()
wcs/qommon/misc.py
1033 1033
    return value
1034 1034

  
1035 1035

  
1036
def is_upload(obj):
1037
    # we can't use isinstance() because obj can be a
1038
    # wcs.qommon.form.PicklableUpload or a qommon.form.PicklableUpload
1039
    return obj.__class__.__name__ == 'PicklableUpload'
1040

  
1041

  
1042
def is_attachment(obj):
1043
    # ditto
1044
    return obj.__class__.__name__ == 'AttachmentEvolutionPart'
1045

  
1046

  
1036 1047
class QLookupRedirect:
1037 1048
    """
1038 1049
    Class to use to interrupt a _q_lookup method and redirect.
wcs/qommon/upload_storage.py
53 53
            self.fp = io.BytesIO(self.data)
54 54
            del self.data
55 55

  
56
    def file_digest(self):
57
        if getattr(self, 'qfilename', None):
58
            # last file part is created using misc.file_digest()
59
            return self.qfilename.split('/')[-1]
60
        return None
61

  
56 62
    def get_file(self):
57 63
        # quack like UploadedFile
58 64
        return self.get_file_pointer()
......
180 186
        self.frontoffice_redirect = bool(frontoffice_redirect == 'true')
181 187
        self.backoffice_redirect = bool(backoffice_redirect == 'true')
182 188

  
189
    def file_digest(self):
190
        return None
191

  
183 192
    def save_tempfile(self, upload):
184 193
        if getattr(upload, 'storage_attrs', None):
185 194
            # upload is already a remote PicklableUpload, it does not
wcs/variables.py
671 671

  
672 672

  
673 673
class LazyFormDataVar:
674
    def __init__(self, fields, data, formdata=None):
674
    def __init__(self, fields, data, formdata=None, base_formdata=None):
675 675
        self._fields = fields
676 676
        self._data = data or {}
677 677
        self._formdata = formdata
678
        self._base_formdata = base_formdata
678 679

  
679 680
    def inspect_keys(self):
680 681
        return self.varnames.keys()
......
701 702
        return self._varnames
702 703

  
703 704
    def get_field_kwargs(self, field):
704
        return {'data': self._data, 'field': field, 'formdata': self._formdata}
705
        return {
706
            'data': self._data,
707
            'field': field,
708
            'formdata': self._formdata,
709
            'base_formdata': self._base_formdata,
710
        }
705 711

  
706 712
    def __getitem__(self, key):
707 713
        try:
......
761 767

  
762 768

  
763 769
class LazyFieldVar:
764
    def __init__(self, data, field, formdata=None, **kwargs):
770
    def __init__(self, data, field, formdata=None, base_formdata=None, **kwargs):
765 771
        self._data = data
766 772
        self._field = field
767 773
        self._formdata = formdata
774
        self._base_formdata = base_formdata
768 775
        self._field_kwargs = kwargs
769 776

  
770 777
    @property
......
1124 1131
class LazyFieldVarFile(LazyFieldVar):
1125 1132
    def inspect_keys(self):
1126 1133
        keys = ['raw']
1127
        if hasattr(self._formdata, 'get_file_base_url'):
1134
        if hasattr(self._formdata, 'get_file_base_url') or self._base_formdata:
1128 1135
            keys.append('url')
1129 1136
        return keys
1130 1137

  
1131 1138
    @property
1132 1139
    def url(self):
1133
        if not hasattr(self._formdata, 'get_file_base_url'):
1140
        if 'url' not in self.inspect_keys():
1134 1141
            return None
1142
        if self._base_formdata:
1143
            return self._field.get_download_url(formdata=self._base_formdata, file_value=self.raw)
1135 1144
        return self._field.get_download_url(formdata=self._formdata, **self._field_kwargs)
1136 1145

  
1137 1146

  
1138 1147
class LazyBlockDataVar(LazyFormDataVar):
1139
    def __init__(self, fields, data, formdata=None, parent_field=None, parent_field_index=0):
1148
    def __init__(
1149
        self, fields, data, formdata=None, parent_field=None, parent_field_index=0, base_formdata=None
1150
    ):
1140 1151
        super().__init__(fields, data, formdata=formdata)
1141 1152
        self.parent_field = parent_field
1142 1153
        self.parent_field_index = parent_field_index
1154
        self.base_formdata = base_formdata
1143 1155

  
1144 1156
    def get_field_kwargs(self, field):
1145 1157
        kwargs = super().get_field_kwargs(field)
1146 1158
        kwargs['parent_field'] = self.parent_field
1147 1159
        kwargs['parent_field_index'] = self.parent_field_index
1160
        kwargs['base_formdata'] = self.base_formdata
1148 1161
        return kwargs
1149 1162

  
1150 1163

  
......
1174 1187
            formdata=self._formdata,
1175 1188
            parent_field=self._field,
1176 1189
            parent_field_index=int(key),
1190
            base_formdata=self._base_formdata,
1177 1191
        )
1178 1192

  
1179 1193
    def __len__(self):
wcs/wf/form.py
321 321
            if not isinstance(part, WorkflowFormEvolutionPart):
322 322
                continue
323 323
            if part.varname == varname and part.data:
324
                wfform_formdatas.append(LazyFormDataWorkflowFormsItem(part))
324
                wfform_formdatas.append(LazyFormDataWorkflowFormsItem(part, base_formdata=self._formdata))
325 325
        if wfform_formdatas:
326 326
            return LazyFormDataWorkflowFormsItems(wfform_formdatas)
327 327
        raise AttributeError(varname)
......
362 362

  
363 363

  
364 364
class LazyFormDataWorkflowFormsItem:
365
    def __init__(self, part):
365
    def __init__(self, part, base_formdata):
366 366
        self._part = part
367 367
        self.data = part.data
368
        self.base_formdata = base_formdata
368 369

  
369 370
    def inspect_keys(self):
370 371
        return ['var']
......
372 373
    @property
373 374
    def var(self):
374 375
        # pass self as formdata, it will be used to access self.data in LazyFieldVarBlock
375
        return LazyFormDataVar(self._part.formdef.get_all_fields(), self._part.data, formdata=self)
376
        return LazyFormDataVar(
377
            self._part.formdef.get_all_fields(),
378
            self._part.data,
379
            formdata=self,
380
            base_formdata=self.base_formdata,
381
        )
376
-