workflows: add possibility to attach files to notes in history (#32403)

This commit is contained in:
Michael Bideau 2019-05-15 14:44:18 +02:00 committed by Frédéric Péters
parent 0e62aa10fd
commit 8bf94c5e7a
5 changed files with 242 additions and 68 deletions

View File

@ -460,3 +460,18 @@ M. Francis Kuntz
assert not '<ul' in html
assert 'arabic simple' in html
assert 'M. Francis Kuntz' in html
def test_dict_from_prefix():
hello_word_b64 = base64.encodestring('hello world')
d = evalutils.dict_from_prefix('var1', {})
assert d == {}
d = evalutils.dict_from_prefix('', {'k1':'v1'})
assert d == {'k1':'v1'}
d = evalutils.dict_from_prefix('k', {'k1':'v1', 'k2':'v2'})
assert d == {'1':'v1', '2':'v2'}
d = evalutils.dict_from_prefix('v', {'k1':'v1', 'k2':'v2'})
assert d == {}

View File

@ -37,7 +37,7 @@ from wcs.wf.form import FormWorkflowStatusItem, WorkflowFormFieldsFormDef
from wcs.wf.jump import JumpWorkflowStatusItem, _apply_timeouts
from wcs.wf.timeout_jump import TimeoutWorkflowStatusItem
from wcs.wf.profile import UpdateUserProfileStatusItem
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem, JournalEvolutionPart
from wcs.wf.remove import RemoveWorkflowStatusItem
from wcs.wf.roles import AddRoleWorkflowStatusItem, RemoveRoleWorkflowStatusItem
from wcs.wf.wscall import WebserviceCallStatusItem
@ -923,6 +923,100 @@ def test_register_comment_attachment(pub):
assert url3 == url4
def test_register_comment_with_attachment_file(pub):
wf = Workflow(name='comment with attachments')
wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
wf.backoffice_fields_formdef.fields = [
FileField(id='bo1', label='bo field 1', type='file', varname='backoffice_file1'),
]
st1 = wf.add_status('Status1')
wf.store()
upload = PicklableUpload('test.jpeg', 'image/jpeg')
jpg = open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg')).read()
upload.receive([jpg])
formdef = FormDef()
formdef.name = 'baz'
formdef.fields = [
FileField(id='1', label='File', type='file', varname='frontoffice_file'),
]
formdef.workflow_id = wf.id
formdef.store()
formdata = formdef.data_class()()
formdata.data = {'1': upload}
formdata.just_created()
formdata.store()
pub.substitutions.feed(formdata)
setbo = SetBackofficeFieldsWorkflowStatusItem()
setbo.parent = st1
setbo.fields = [{'field_id': 'bo1', 'value': '=form_var_frontoffice_file_raw'}]
setbo.perform(formdata)
if os.path.exists(os.path.join(get_publisher().app_dir, 'attachments')):
shutil.rmtree(os.path.join(get_publisher().app_dir, 'attachments'))
comment_text = 'File is attached to the form history'
item = RegisterCommenterWorkflowStatusItem()
item.attachments = ['form_var_backoffice_file1_raw']
item.comment = comment_text
item.perform(formdata)
assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments'))) == 1
for subdir in os.listdir(os.path.join(get_publisher().app_dir, 'attachments')):
assert len(subdir) == 4
assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments', subdir))) == 1
assert len(formdata.evolution[-1].parts) == 2
assert isinstance(formdata.evolution[-1].parts[0], AttachmentEvolutionPart)
assert formdata.evolution[-1].parts[0].orig_filename == upload.orig_filename
assert isinstance(formdata.evolution[-1].parts[1], JournalEvolutionPart)
assert len(formdata.evolution[-1].parts[1].content) > 0
comment_view = str(formdata.evolution[-1].parts[1].view())
assert comment_view == '<p>%s</p>' % comment_text
if os.path.exists(os.path.join(get_publisher().app_dir, 'attachments')):
shutil.rmtree(os.path.join(get_publisher().app_dir, 'attachments'))
formdata.evolution[-1].parts = []
formdata.store()
ws_response_varname = 'ws_response_afile'
wf_data = {
'%s_filename' % ws_response_varname : 'hello.txt',
'%s_content_type' % ws_response_varname : 'text/plain',
'%s_b64_content' % ws_response_varname : base64.encodestring('hello world'),
}
formdata.update_workflow_data(wf_data)
formdata.store()
assert hasattr(formdata, 'workflow_data')
assert isinstance(formdata.workflow_data, dict)
item = RegisterCommenterWorkflowStatusItem()
item.attachments = ["utils.dict_from_prefix('%s_', locals())" % ws_response_varname]
item.comment = comment_text
item.perform(formdata)
assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments'))) == 1
for subdir in os.listdir(os.path.join(get_publisher().app_dir, 'attachments')):
assert len(subdir) == 4
assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments', subdir))) == 1
assert len(formdata.evolution[-1].parts) == 2
assert isinstance(formdata.evolution[-1].parts[0], AttachmentEvolutionPart)
assert formdata.evolution[-1].parts[0].orig_filename == 'hello.txt'
assert isinstance(formdata.evolution[-1].parts[1], JournalEvolutionPart)
assert len(formdata.evolution[-1].parts[1].content) > 0
comment_view = str(formdata.evolution[-1].parts[1].view())
assert comment_view == '<p>%s</p>' % comment_text
def test_email(pub, emails):
pub.substitutions.feed(MockSubstitutionVariables())

View File

@ -21,6 +21,7 @@ get_global_eval_dict.
"""
import datetime
import re
import time
from .misc import get_as_datetime
@ -136,3 +137,28 @@ def attachment(content, filename='', content_type='application/octet-stream'):
'content_type': content_type,
'b64_content': base64.b64encode(content),
}
def dict_from_prefix(prefix, in_dict):
'''Return a dict based on a dict filtered by a key prefix.
The prefix is removed from the key.
Intent: meant to help build a PicklableUpload from a set
of key/values stored in the workflow data.
Note: to use this function in a context of a Python
expression, you should pass the _wf_data_ using
the function locals()
Example: utils.dict_from_prefix('akey_', locals())
Where: the workflow data contains the key/values:
akey_filename = <filename>
akey_content_type = <mime_type>
akey_b64_content = <content base64 encoded>
And: it produces a dict like the key/values are:
filename = wf_data['akey_filename']
content_type = wf_data['akey_content_type']
b64_content = wf_data['akey_b64_content']
'''
return {k[len(prefix):]: v for k, v in in_dict.items() if k.startswith('%s' % prefix)}

View File

@ -21,8 +21,10 @@ from qommon.form import *
from qommon.template import TemplateError
from qommon import get_logger
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
from wcs.workflows import (WorkflowStatusItem, register_item_class, template_on_formdata,
AttachmentEvolutionPart)
import sys
class JournalEvolutionPart: #pylint: disable=C1001
content = None
@ -75,6 +77,7 @@ class RegisterCommenterWorkflowStatusItem(WorkflowStatusItem):
category = 'interaction'
comment = None
attachments = None
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
super(RegisterCommenterWorkflowStatusItem, self).add_parameters_widgets(
@ -84,11 +87,34 @@ class RegisterCommenterWorkflowStatusItem(WorkflowStatusItem):
value=self.comment, cols=80, rows=10)
def get_parameters(self):
return ('comment', 'condition')
return ('comment', 'attachments', 'condition')
def attach_uploads_to_formdata(self, formdata, uploads):
if not formdata.evolution[-1].parts:
formdata.evolution[-1].parts = []
for upload in uploads:
try:
# useless but required to restore upload.fp from serialized state,
# needed by AttachmentEvolutionPart.from_upload()
fp = upload.get_file_pointer()
formdata.evolution[-1].add_part(AttachmentEvolutionPart.from_upload(upload))
except:
get_publisher().notify_of_exception(sys.exc_info(),
context='[comment/attachments]')
continue
def perform(self, formdata):
if not formdata.evolution:
return
# process attachments first, they might be used in the comment
# (with substitution vars)
if self.attachments:
uploads = self.convert_attachments_to_uploads()
self.attach_uploads_to_formdata(formdata, uploads)
formdata.store() # store and invalidate cache, so references can be used in the comment message.
# the comment can use attachments done above
try:
formdata.evolution[-1].add_part(JournalEvolutionPart(formdata, self.comment))
formdata.store()

View File

@ -1662,6 +1662,23 @@ class WorkflowStatusItem(XmlSerialisable):
value=self.condition, size=40,
advanced=not(self.condition))
if 'attachments' in parameters:
attachments_options, attachments = self.get_attachments_options()
if len(attachments_options) > 1:
form.add(WidgetList, '%sattachments' % prefix, title=_('Attachments'),
element_type=SingleSelectWidgetWithOther,
value=attachments,
add_element_label=_('Add attachment'),
element_kwargs={'render_br': False, 'options': attachments_options})
else:
form.add(WidgetList, '%sattachments' % prefix,
title=_('Attachments (Python expressions)'),
element_type=StringWidget,
value=attachments,
add_element_label=_('Add attachment'),
element_kwargs={'render_br': False, 'size': 50},
advanced=not(bool(attachments)))
def get_parameters(self):
return ('condition',)
@ -1901,6 +1918,66 @@ class WorkflowStatusItem(XmlSerialisable):
del odict['parent']
return odict
def attachments_init_with_xml(self, elem, charset, include_id=False):
if elem is None:
self.attachments = None
else:
self.attachments = [item.text.encode(charset) for item in elem.findall('attachment')]
def get_attachments_options(self):
attachments_options = [(None, '---', None)]
varnameless = []
for field in self.parent.parent.get_backoffice_fields():
if field.key != 'file':
continue
if field.varname:
codename = 'form_var_%s_raw' % field.varname
else:
codename = 'form_f%s' % field.id.replace('-', '_') # = form_fbo<...>
varnameless.append(codename)
attachments_options.append((codename, field.label, codename))
# filter: do not consider removed fields without varname
attachments = [attachment for attachment in self.attachments or []
if ((not attachment.startswith('form_fbo')) or
(attachment in varnameless))]
return attachments_options, attachments
def convert_attachments_to_uploads(self):
uploads = []
if self.attachments:
global_eval_dict = get_publisher().get_global_eval_dict()
local_eval_dict = get_publisher().substitutions.get_context_variables()
for attachment in self.attachments:
if attachment.startswith('form_fbo') and '-' in attachment:
# detect varname-less backoffice fields that were set
# before #33366 was fixed, and fix them.
attachment = attachment.replace('-', '_')
try:
# execute any Python expression
# and magically convert string like 'form_var_*_raw' to a PicklableUpload
picklableupload = eval(attachment, global_eval_dict, local_eval_dict)
except:
get_publisher().notify_of_exception(sys.exc_info(),
context='[workflow/attachments]')
continue
if not picklableupload:
continue
try:
# convert any value to a PicklableUpload; it will ususally
# be a dict like one provided by qommon/evalutils:attachment()
picklableupload = FileField.convert_value_from_anything(picklableupload)
except ValueError:
get_publisher().notify_of_exception(sys.exc_info(),
context='[workflow/attachments]')
continue
uploads.append(picklableupload)
return uploads
class WorkflowStatusJumpItem(WorkflowStatusItem):
status = None
@ -2263,12 +2340,6 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
return super(SendmailWorkflowStatusItem, self)._get_role_id_from_xml(
elem, charset, include_id=include_id)
def attachments_init_with_xml(self, elem, charset, include_id=False):
if elem is None:
self.attachments = None
else:
self.attachments = [item.text.encode(charset) for item in elem.findall('attachment')]
def render_list_of_roles_or_emails(self, roles):
t = []
for r in roles:
@ -2298,24 +2369,6 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
def fill_admin_form(self, form):
self.add_parameters_widgets(form, self.get_parameters())
def get_attachments_options(self):
attachments_options = [(None, '---', None)]
varnameless = []
for field in self.parent.parent.get_backoffice_fields():
if field.key != 'file':
continue
if field.varname:
codename = 'form_var_%s_raw' % field.varname
else:
codename = 'form_f%s' % field.id.replace('-', '_') # = form_fbo<...>
varnameless.append(codename)
attachments_options.append((codename, field.label, codename))
# filter: do not consider removed fields without varname
attachments = [attachment for attachment in self.attachments or []
if ((not attachment.startswith('form_fbo')) or
(attachment in varnameless))]
return attachments_options, attachments
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
super(SendmailWorkflowStatusItem, self).add_parameters_widgets(
form, parameters, prefix=prefix, formdef=formdef)
@ -2336,23 +2389,6 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
value=self.body, cols=80, rows=10,
validation_function=ComputedExpressionWidget.validate_template)
if 'attachments' in parameters:
attachments_options, attachments = self.get_attachments_options()
if len(attachments_options) > 1:
form.add(WidgetList, '%sattachments' % prefix, title=_('Attachments'),
element_type=SingleSelectWidgetWithOther,
value=attachments,
add_element_label=_('Add attachment'),
element_kwargs={'render_br': False, 'options': attachments_options})
else:
form.add(WidgetList, '%sattachments' % prefix,
title=_('Attachments (Python expressions)'),
element_type=StringWidget,
value=attachments,
add_element_label=_('Add attachment'),
element_kwargs={'render_br': False, 'size': 50},
advanced=not(bool(attachments)))
if 'custom_from' in parameters:
form.add(ComputedExpressionWidget, '%scustom_from' % prefix,
title=_('Custom From Address'), value=self.custom_from,
@ -2440,30 +2476,7 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
if self.custom_from:
email_from = self.compute(self.custom_from)
attachments = []
if self.attachments:
global_eval_dict = get_publisher().get_global_eval_dict()
local_eval_dict = get_publisher().substitutions.get_context_variables()
for attachment in self.attachments:
if attachment.startswith('form_fbo') and '-' in attachment:
# detect varname-less backoffice fields that were set
# before #33366 was fixed, and fix them.
attachment = attachment.replace('-', '_')
try:
picklableupload = eval(attachment, global_eval_dict, local_eval_dict)
except:
get_publisher().notify_of_exception(sys.exc_info(),
context='[Sendmail/attachments]')
continue
if not picklableupload:
continue
try:
picklableupload = FileField.convert_value_from_anything(picklableupload)
except ValueError:
get_publisher().notify_of_exception(sys.exc_info(),
context='[Sendmail/attachments]')
continue
attachments.append(picklableupload)
attachments = self.convert_attachments_to_uploads()
if len(addresses) > 1:
emails.email(mail_subject, mail_body, email_rcpt=None,