0001-workflow-added-attachment-support-for-RegisterCommen.patch
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 locales() |
|
152 | ||
153 |
Example: utils.dict_from_prefix('recepisse', locals()) |
|
154 |
Where: the workflow data contains the key/values: |
|
155 |
recepisse_filename = <filename> |
|
156 |
recepisse_content_type = <mime_type> |
|
157 |
recepisse_b64_content = <content base64 encoded> |
|
158 |
And: it produces a dict like the key/values are: |
|
159 |
filename = wf_data['recepisse_filename'] |
|
160 |
content_type = wf_data['recepisse_content_type'] |
|
161 |
b64_content = wf_data['recepisse_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 |
- |