Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework SIP Package generation and download #5837

Merged
merged 6 commits into from
Jul 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/HISTORY.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Changelog
---------------------

- Bump docxcompose version to 1.0.1 [njohner]
- Improve SIP package generation and download. [phgross]
- Fix qa tests. [lgraf]
- Disable properties action for teams. [deiferni]
- Add source vocabularies for workspace invitations and todo responsibles. [njohner]
Expand Down
14 changes: 14 additions & 0 deletions opengever/disposition/browser/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@
permission="opengever.disposition.DownloadSIPPackage"
/>

<browser:page
class=".ech0160.ECH0160StoreView"
for="opengever.disposition.interfaces.IDisposition"
name="ech0160_store"
permission="zope2.View"
/>

<browser:page
class=".ech0160.ECH0160DownloadView"
for="opengever.disposition.interfaces.IDisposition"
name="ech0160_download"
permission="opengever.disposition.DownloadSIPPackage"
/>

<browser:page
class=".overview.DispositionOverview"
for="opengever.disposition.interfaces.IDisposition"
Expand Down
34 changes: 34 additions & 0 deletions opengever/disposition/browser/ech0160.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from opengever.base.stream import TempfileStreamIterator
from opengever.disposition import _
from opengever.disposition.ech0160.sippackage import SIPPackage
from plone import api
from plone.namedfile.utils import set_headers
from plone.namedfile.utils import stream_data
from Products.Five import BrowserView
from pyxb.utils.domutils import BindingDOMSupport
from tempfile import TemporaryFile
Expand Down Expand Up @@ -34,3 +38,33 @@ def create_zipfile(self, package):
package.write_to_zipfile(zipfile)

return tmpfile


class ECH0160StoreView(BrowserView):
"""A view which generates and store's the SIP package as a blob file.
"""

def __call__(self):
self.context.store_sip_package()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This view will respond to a GET request. do you see a way to create a form which only responds to an i.e. POST, as it will modify data in the database.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do the maintenance-actions as discussed in a separate PR, I added a checkbox to the Main Issue.

Copy link
Contributor

@deiferni deiferni Jul 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have added a checkbox to the main issue in #5167.

msg = _('msg_sip_package_sucessfully_generated',
default=u'SIP Package generated successfully.')
api.portal.show_message(msg, request=self.request, type='info')
return self.request.RESPONSE.redirect(self.context.absolute_url())


class ECH0160DownloadView(BrowserView):
"""View which streams the existing SIP Package. Redirect and show status
message when SIP package does not exist.
"""

def __call__(self):
if not self.context.has_sip_package():
msg = _('msg_no_sip_package_generated',
default=u'No SIP Package generated for this disposition.')
api.portal.show_message(msg, request=self.request, type='error')
return self.request.RESPONSE.redirect(self.context.absolute_url())

sip_package = self.context.get_sip_package()
set_headers(sip_package, self.request.response,
u'{}.zip'.format(self.context.get_sip_name()))
return stream_data(sip_package)
11 changes: 10 additions & 1 deletion opengever/disposition/browser/overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def get_actions(self):
{'id': 'sip_download',
'label': _('label_dispositon_package_download',
default=u'Download disposition package'),
'url': '{}/ech0160_export'.format(self.context.absolute_url()),
'url': '{}/ech0160_download'.format(self.context.absolute_url()),
'visible': self.sip_download_available(),
'class': 'sip_download'},
{'id': 'removal_protocol',
Expand All @@ -88,6 +88,15 @@ def get_actions(self):
]

def sip_download_available(self):
if api.user.has_permission(
'opengever.disposition: Download SIP Package',
obj=self.context):

return self.context.has_sip_package()

phgross marked this conversation as resolved.
Show resolved Hide resolved
return None

def sip_store_available(self):
return api.user.has_permission(
'opengever.disposition: Download SIP Package',
obj=self.context)
Expand Down
46 changes: 45 additions & 1 deletion opengever/disposition/disposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from Acquisition import aq_parent
from collective import dexteritytextindexer
from datetime import date
from DateTime import DateTime
from opengever.activity import notification_center
from opengever.activity.roles import DISPOSITION_ARCHIVIST_ROLE
from opengever.activity.roles import DISPOSITION_RECORDS_MANAGER_ROLE
Expand All @@ -11,31 +12,39 @@
from opengever.base.source import SolrObjPathSourceBinder
from opengever.disposition import _
from opengever.disposition.appraisal import IAppraisal
from opengever.disposition.ech0160.sippackage import SIPPackage
from opengever.disposition.interfaces import IDisposition
from opengever.disposition.interfaces import IDuringDossierDestruction
from opengever.disposition.interfaces import IHistoryStorage
from opengever.dossier.base import DOSSIER_STATES_OFFERABLE
from opengever.dossier.behaviors.dossier import IDossier
from opengever.ogds.base.utils import get_current_admin_unit
from opengever.ogds.base.utils import ogds_service
from path import Path
from persistent.dict import PersistentDict
from persistent.list import PersistentList
from plone import api
from plone.autoform.directives import write_permission
from plone.dexterity.content import Container
from plone.namedfile.file import NamedBlobFile
from plone.supermodel import model
from pyxb.utils.domutils import BindingDOMSupport
from tempfile import TemporaryFile
from z3c.relationfield.schema import RelationChoice
from z3c.relationfield.schema import RelationList
from zExceptions import Unauthorized
from zipfile import ZIP_DEFLATED
from zipfile import ZipFile
from zope import schema
from zope.annotation import IAnnotations
from zope.annotation.interfaces import IAnnotations
from zope.component import getUtility
from zope.globalrequest import getRequest
from zope.i18n import translate
from zope.interface import alsoProvides
from zope.interface import implements
from zope.intid.interfaces import IIntIds

import os

DESTROY_PERMISSION = 'opengever.dossier: Destroy dossier'

Expand Down Expand Up @@ -293,3 +302,38 @@ def get_all_archivists(self):
archivists.append(principal)

return archivists

def store_sip_package(self):
self._sip_package = self.generate_sip_package()

def remove_sip_package(self):
self._sip_package = None

def generate_sip_package(self):
package = SIPPackage(self)
zip_file = self.create_zipfile(package)
zip_file.seek(0)
return NamedBlobFile(zip_file.read(), contentType='application/zip')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could pass a reference to the file instead of reading the file into memory yourself. That triggers an plone.namedfile.interfaces.IStorage adapter, which should be able to handle the file instance. If we are lucky it can be moved on the filesystem without reading the whole thing into memory. Not 100% sure but worth a try.

Copy link
Member Author

@phgross phgross Jul 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not found a way to pass in a file in to a NamedBlobFile, we would need to register a IStorage Adapter for __builtin__.file what I'm not sure is the right way. For now i did no change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed today, if it does not work by default i'm ok with not changing the current implementation 👍.


def create_zipfile(self, package):
tmpfile = TemporaryFile()
BindingDOMSupport.SetDefaultNamespace(u'http://bar.admin.ch/arelda/v4')
with ZipFile(tmpfile, 'w', ZIP_DEFLATED, True) as zipfile:
package.write_to_zipfile(zipfile)

return tmpfile

def has_sip_package(self):
return bool(self.get_sip_package())

def get_sip_package(self):
return getattr(self, '_sip_package', None)

def get_sip_name(self):
name = u'SIP_{}_{}'.format(
DateTime().strftime('%Y%m%d'),
api.portal.get().getId().upper())
if self.transfer_number:
name = u'{}_{}'.format(name, self.transfer_number)

return name
5 changes: 5 additions & 0 deletions opengever/disposition/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ def disposition_state_changed(context, event):

if event.action == 'disposition-transition-close':
context.destroy_dossiers()
context.remove_sip_package()

if event.action == 'disposition-transition-appraised-to-closed':
context.mark_dossiers_as_archived()
context.destroy_dossiers()
context.remove_sip_package()

if event.action == 'disposition-transition-dispose':
context.store_sip_package()

storage = IHistoryStorage(context)
storage.add(event.action,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-03-19 12:59+0000\n"
"POT-Creation-Date: 2019-07-25 14:32+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -273,6 +273,16 @@ msgstr "Angebot abgelehnt durch ${user}"
msgid "msg_disposition_updated"
msgstr "Aktualisiert durch ${user}"

#. Default: "No SIP Package generated for this disposition."
#: ./opengever/disposition/browser/ech0160.py
msgid "msg_no_sip_package_generated"
msgstr "Es wurde noch kein SIP Paket generiert für dieses Angebot."

#. Default: "SIP Package generated successfully."
#: ./opengever/disposition/browser/ech0160.py
msgid "msg_sip_package_sucessfully_generated"
msgstr "SIP Paket erfolgreich generiert."

#. Default: "Period"
#: ./opengever/disposition/browser/templates/overview.pt
msgid "period"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-03-19 12:59+0000\n"
"POT-Creation-Date: 2019-07-25 14:32+0000\n"
"PO-Revision-Date: 2018-05-22 10:09+0000\n"
"Last-Translator: Niklaus Johner <[email protected]>\n"
"Language-Team: French <https://translations.onegovgever.ch/projects/onegov-"
"gever/opengever-disposition/fr/>\n"
"Language: fr\n"
"Language-Team: French <https://translations.onegovgever.ch/projects/onegov-gever/opengever-disposition/fr/>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 2.13.1\n"
"Language-Code: en\n"
"Language-Name: English\n"
"Preferred-Encodings: utf-8 latin1\n"
"Domain: DOMAIN\n"
"Language: fr\n"
"X-Generator: Weblate 2.13.1\n"

#: ./opengever/disposition/browser/excel_export.py
msgid "The report could not been generated."
Expand Down Expand Up @@ -276,6 +275,16 @@ msgstr "Offre refusée par ${user}"
msgid "msg_disposition_updated"
msgstr "Actualisé par ${user}"

#. Default: "No SIP Package generated for this disposition."
#: ./opengever/disposition/browser/ech0160.py
msgid "msg_no_sip_package_generated"
msgstr ""

#. Default: "SIP Package generated successfully."
#: ./opengever/disposition/browser/ech0160.py
msgid "msg_sip_package_sucessfully_generated"
msgstr ""

#. Default: "Period"
#: ./opengever/disposition/browser/templates/overview.pt
msgid "period"
Expand Down
12 changes: 11 additions & 1 deletion opengever/disposition/locales/opengever.disposition.pot
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-03-19 12:59+0000\n"
"POT-Creation-Date: 2019-07-25 14:32+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -276,6 +276,16 @@ msgstr ""
msgid "msg_disposition_updated"
msgstr ""

#. Default: "No SIP Package generated for this disposition."
#: ./opengever/disposition/browser/ech0160.py
msgid "msg_no_sip_package_generated"
msgstr ""

#. Default: "SIP Package generated successfully."
#: ./opengever/disposition/browser/ech0160.py
msgid "msg_sip_package_sucessfully_generated"
msgstr ""

#. Default: "Period"
#: ./opengever/disposition/browser/templates/overview.pt
msgid "period"
Expand Down
37 changes: 37 additions & 0 deletions opengever/disposition/tests/test_disposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,40 @@ def test_reset_date_of_submission_for_dropped_dossiers(self, browser):
None, ILifeCycle(self.offered_dossier_to_destroy).date_of_submission)
self.assertEquals(
date.today(), ILifeCycle(self.expired_dossier).date_of_submission)

@browsing
def test_sip_package_is_genarated_and_stored_on_dispose(self, browser):
self.login(self.records_manager, browser)

self.set_workflow_state('disposition-state-appraised', self.disposition)

browser.open(self.disposition, view='overview')
browser.click_on('disposition-transition-dispose')

self.assertEquals(['Item state changed.'], info_messages())
self.assertTrue(self.disposition.has_sip_package())

# Download is possible
self.assertIn(
'Download disposition package', browser.css('ul.actions li').text)

@browsing
def test_sip_package_is_removed_on_close(self, browser):
self.login(self.records_manager, browser)

self.disposition.store_sip_package()
self.set_workflow_state('disposition-state-appraised', self.disposition)
browser.open(self.disposition, view='overview')

browser.click_on('disposition-transition-dispose')
self.assertTrue(self.disposition.has_sip_package())

with self.login(self.archivist, browser=browser):
browser.open(self.disposition, view='overview')
browser.click_on('disposition-transition-archive')

browser.open(self.disposition, view='overview')
browser.click_on('disposition-transition-close')

self.assertEquals(['Item state changed.'], info_messages())
self.assertFalse(self.disposition.has_sip_package())
50 changes: 50 additions & 0 deletions opengever/disposition/tests/test_ech0160export.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from datetime import datetime
from ftw.testbrowser import browsing
from ftw.testbrowser.pages.statusmessages import error_messages
from ftw.testbrowser.pages.statusmessages import info_messages
from ftw.testing import freeze
from opengever.testing import IntegrationTestCase
import os


class TesteCH0160Deployment(IntegrationTestCase):
Expand All @@ -22,3 +25,50 @@ def test_returns_zip_file_stream(self, browser):
self.assertEquals(
'inline; filename="SIP_20160611_PLONE_10xy.zip"',
self.request.response.headers.get('content-disposition'))


class TestECH0160StoreView(IntegrationTestCase):

@browsing
def test_generates_sip_package_and_stores_it_as_a_blob_on_the_filesystem(self, browser):
self.login(self.records_manager, browser=browser)

self.set_workflow_state('disposition-state-disposed', self.disposition)
self.disposition.transfer_number = "10xy"
self.assertFalse(self.disposition.has_sip_package())

with freeze(datetime(2016, 6, 11)):
browser.open(self.disposition, view='ech0160_store')

self.assertEquals(
['SIP Package generated successfully.'], info_messages())
self.assertTrue(self.disposition.has_sip_package())


class TestECH0160DownloadView(IntegrationTestCase):

@browsing
def test_shows_status_message_when_no_zip_is_stored(self, browser):
self.login(self.archivist, browser=browser)

self.set_workflow_state('disposition-state-disposed', self.disposition)
browser.open(self.disposition, view='ech0160_download')
self.assertEquals([u'No SIP Package generated for this disposition.'],
error_messages())

@browsing
def test_streams_zip_when_sip_package_is_stored(self, browser):
self.login(self.archivist, browser=browser)

self.set_workflow_state('disposition-state-disposed', self.disposition)
self.disposition.transfer_number = "10xy"
self.disposition.store_sip_package()

with freeze(datetime(2016, 6, 11)):
browser.open(self.disposition, view='ech0160_download')

self.assertEquals(
'application/zip', browser.headers.get('content-type'))
self.assertEquals(
"attachment; filename*=UTF-8''SIP_20160611_PLONE_10xy.zip",
browser.headers.get('content-disposition'))
Loading