From 713ec0b0d49344b8af3f18e00b96477bd0f33eba Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Fri, 8 Apr 2016 12:56:43 +0200 Subject: [PATCH 1/2] add option to push attached document to portfolio (#10506) --- tests/test_form_pages.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++ wcs/file_validation.py | 47 +++++++++++++++++++++++++++++- wcs/wf/attachment.py | 13 ++++++++- wcs/wf/export_to_model.py | 24 ++++++++++----- 4 files changed, 149 insertions(+), 9 deletions(-) diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py index cbd1097..5111f34 100644 --- a/tests/test_form_pages.py +++ b/tests/test_form_pages.py @@ -1913,6 +1913,80 @@ def test_formdata_generated_document_odt_to_pdf_download(pub): assert resp.content_type == 'application/pdf' assert resp.body.startswith('%PDF-') + +@pytest.mark.skipif(transform_to_pdf is None, reason='libreoffice not found') +def test_formdata_generated_document_odt_to_pdf_download_push_to_portfolio(pub, fargo_url, + fargo_secret, caplog): + create_user(pub) + pub.cfg['debug'] = {'logger': True} + pub.write_cfg() + wf = Workflow(name='status') + st1 = wf.add_status('Status1', 'st1') + export_to = ExportToModel() + export_to.label = 'create doc' + export_to.varname = 'created_doc' + template_filename = os.path.join(os.path.dirname(__file__), 'template.odt') + template = open(template_filename).read() + upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream') + upload.fp = StringIO.StringIO() + upload.fp.write(template) + upload.fp.seek(0) + export_to.model_file = UploadedFile(pub.app_dir, None, upload) + export_to.id = '_export_to' + export_to.by = ['_submitter'] + export_to.convert_to_pdf = True + export_to.push_to_portfolio = True + st1.items.append(export_to) + export_to.parent = st1 + wf.store() + + formdef = create_formdef() + formdef.workflow_id = wf.id + formdef.fields = [] + formdef.store() + formdef.data_class().wipe() + + resp = login(get_app(pub), username='foo', password='foo').get('/test/') + resp = resp.forms[0].submit('submit') + assert 'Check values then click submit.' in resp.body + resp = resp.forms[0].submit('submit') + assert resp.status_int == 302 + form_location = resp.location + resp = resp.follow() + assert 'The form has been recorded' in resp.body + + with mock.patch('wcs.file_validation.http_post_request') as http_post_request: + http_post_request.return_value = None, 200, 'null', None + resp = resp.form.submit('button_export_to') + assert http_post_request.call_count == 1 + assert ('file template.pdf pushed to portfolio of foo@localhost' + == caplog.records()[-1].message) + + resp = resp.follow() # $form/$id/create_doc + resp = resp.follow() # $form/$id/create_doc/ + assert resp.content_type == 'application/pdf' + assert 'PDF' in resp.body + + export_to.attach_to_history = True + wf.store() + + resp = login(get_app(pub), username='foo', password='foo').get(form_location) + with mock.patch('wcs.file_validation.http_post_request') as http_post_request: + http_post_request.return_value = None, 200, 'null', None + resp = resp.form.submit('button_export_to') + assert http_post_request.call_count == 1 + assert ('file template.pdf pushed to portfolio of foo@localhost' + == caplog.records()[-1].message) + assert resp.location == form_location + resp = resp.follow() # back to form page + + resp = resp.click('template.pdf') + assert resp.location.endswith('/template.pdf') + resp = resp.follow() + assert resp.content_type == 'application/pdf' + assert resp.body.startswith('%PDF-') + + def test_formdata_form_file_download(pub): create_user(pub) wf = Workflow(name='status') diff --git a/wcs/file_validation.py b/wcs/file_validation.py index db5c0d9..5d9bb87 100644 --- a/wcs/file_validation.py +++ b/wcs/file_validation.py @@ -18,10 +18,11 @@ import json import urlparse import hashlib import urllib +import base64 from qommon import get_logger from qommon.misc import http_get_page, json_loads, http_post_request, ConnectionError -from quixote import get_publisher, get_request +from quixote import get_publisher, get_request, get_response from wcs.api_utils import get_secret_and_orig, sign_url @@ -57,6 +58,20 @@ def fargo_post_json(url, payload): return status, json_loads(response_payload) +# Allow doing a signed POST in an afterjob, as fargo_url() does not work if no request is in +# context; so we do it in the constructor. +class fargo_post_json_async(object): + def __init__(self, url, payload): + self.url = fargo_url(url) + self.payload = payload + + def __call__(self): + headers = {'Content-Type': 'application/json'} + response, status, response_payload, auth_header = http_post_request( + self.url, json.dumps(self.payload), headers=headers) + return status, json_loads(response_payload) + + def sha256_of_upload(upload): return hashlib.sha256(upload.get_content()).hexdigest() @@ -147,3 +162,33 @@ def validate(filled, field, upload): upload.metadata = response['data'] filled.data['%s_structured' % field.id] = upload.metadata filled.store() + + +def push_document(user, filename, stream): + if not user: + return + charset = get_publisher().site_charset + payload = {} + if user.name_identifiers: + payload['user_nameid'] = unicode(user.name_identifiers[0], 'ascii') + elif user.email: + payload['user_email'] = unicode(user.email, 'ascii') + payload['origin'] = get_request().get_server().split(':')[0] + payload['file_name'] = unicode(filename, charset) + stream.seek(0) + payload['file_b64_content'] = base64.b64encode(stream.read()) + async_post = fargo_post_json_async('/api/documents/push/', payload) + + def afterjob(job): + status = 0 + status, resp = async_post() + if status == 200: + get_logger().info('file %s pushed to portfolio of %s' + % (filename, user.display_name)) + else: + get_logger().error('failed %s failed to be pushed to portfolio of %s' + % (filename, user.display_name)) + + get_response().add_after_job( + N_('Sending file %s in portfolio of %s') % (filename, user.display_name), + afterjob) diff --git a/wcs/wf/attachment.py b/wcs/wf/attachment.py index 0a359a0..cc9eb80 100644 --- a/wcs/wf/attachment.py +++ b/wcs/wf/attachment.py @@ -21,6 +21,7 @@ from wcs.workflows import * from qommon.errors import * from wcs.forms.common import FormStatusPage, FileDirectory +from wcs.file_validation import has_file_validation, push_document def lookup_wf_attachment(self, filename): # supports for URLs such as /$formdata/$id/files/attachment/test.txt @@ -79,6 +80,7 @@ class AddAttachmentWorkflowStatusItem(WorkflowStatusItem): by = [] backoffice_info_text = None varname = None + push_to_portfolio = False @classmethod def init(cls): @@ -112,11 +114,16 @@ class AddAttachmentWorkflowStatusItem(WorkflowStatusItem): if self.required: form.set_error('attachment%s' % self.id, _('Missing file')) return + if self.push_to_portfolio: + push_document(formdata.get_user(), f.base_filename, f.fp) evo.add_part(AttachmentEvolutionPart.from_upload(f, varname=self.varname)) def get_parameters(self): - return ('by', 'required', 'title', 'display_title', 'button_label', + parameters = ('by', 'required', 'title', 'display_title', 'button_label', 'display_button', 'hint', 'backoffice_info_text', 'varname') + if has_file_validation(): + parameters += ('push_to_portfolio',) + return parameters def add_parameters_widgets(self, form, parameters, prefix='', formdef=None): if 'by' in parameters: @@ -146,6 +153,10 @@ class AddAttachmentWorkflowStatusItem(WorkflowStatusItem): if 'varname' in parameters: form.add(VarnameWidget, '%svarname' % prefix, title=_('Variable Name'), value=self.varname) + if 'push_to_portfolio' in parameters: + form.add(CheckboxWidget, '%spush_to_portfolio' % prefix, + title=_('Push generated file to portfolio'), + value=self.push_to_portfolio) register_item_class(AddAttachmentWorkflowStatusItem) diff --git a/wcs/wf/export_to_model.py b/wcs/wf/export_to_model.py index f7c2aa7..d0f2d88 100644 --- a/wcs/wf/export_to_model.py +++ b/wcs/wf/export_to_model.py @@ -37,6 +37,7 @@ import qommon from wcs.fields import SubtitleField, TitleField, CommentField, PageField from wcs.workflows import (WorkflowStatusItem, AttachmentEvolutionPart, template_on_formdata, register_item_class) +from wcs.file_validation import has_file_validation, push_document try: @@ -179,6 +180,7 @@ class ExportToModel(WorkflowStatusItem): backoffice_info_text = None varname = None convert_to_pdf = False + push_to_portfolio = False def render_as_line(self): if self.label: @@ -207,15 +209,18 @@ class ExportToModel(WorkflowStatusItem): if not evo.comment: evo.comment = _('Form exported in a model') in_backoffice = get_request() and get_request().is_in_backoffice() + outstream = self.apply_template_to_formdata(formdata) + filename = self.model_file.base_filename + content_type = self.model_file.content_type + if self.convert_to_pdf: + filename = filename.rsplit('.', 1)[0] + '.pdf' + content_type = 'application/pdf' + if self.push_to_portfolio: + push_document(formdata.get_user(), filename, outstream) if self.attach_to_history: - filename = self.model_file.base_filename - content_type = self.model_file.content_type - if self.convert_to_pdf: - filename = filename.rsplit('.', 1)[0] + '.pdf' - content_type = 'application/pdf' evo.add_part(AttachmentEvolutionPart( filename, - self.apply_template_to_formdata(formdata), + outstream, content_type=content_type, varname=self.varname)) return formdata.get_url(backoffice=in_backoffice) @@ -309,6 +314,10 @@ class ExportToModel(WorkflowStatusItem): form.add(CheckboxWidget, '%sconvert_to_pdf' % prefix, title=_('Convert generated file to PDF'), value=self.convert_to_pdf) + if 'push_to_portfolio' in parameters: + form.add(CheckboxWidget, '%spush_to_portfolio' % prefix, + title=_('Push generated file to portfolio'), + value=self.push_to_portfolio) def get_directory_name(self): return qommon.misc.simplify(self.label or 'export_to_model', space='_') @@ -369,12 +378,13 @@ class ExportToModel(WorkflowStatusItem): outstream.seek(0) return outstream - def get_parameters(self): parameters = ('by', 'label', 'model_file', 'attach_to_history', 'backoffice_info_text', 'varname') if transform_to_pdf is not None: parameters += ('convert_to_pdf',) + if has_file_validation(): + parameters += ('push_to_portfolio',) return parameters def model_file_export_to_xml(self, xml_item, charset, include_id=False): -- 2.1.4