0001-forms-add-direct-download-of-files-from-workflow-for.patch
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 |
- |