Skip to content

Commit

Permalink
Merge pull request #10 from seantis/issue_7
Browse files Browse the repository at this point in the history
Issue 7
  • Loading branch information
msom committed Mar 9, 2015
2 parents 7401a8f + 41a349e commit 3dfa6cf
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 45 deletions.
24 changes: 24 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@ Seantis Agencies

A directory of people for (government) agencies.

Views
-----

pdfexport
^^^^^^^^^
Called on a organzation.

Creates a PDF of the current organization and sub-organizations with portrait
and memberships. Redirects to the file if it already exists.

Use *force* (*/pdfexport?force=1*) to force the creation of the PDF.

pdfexport-agencies
^^^^^^^^^^^^^^^^^^
Called on the site root.

Exports - scheduled at 0:30 am - 1) all organizations and sub-organizations
with memberships to a PDF located at the root, 2) a PDF for each organisation
(calling */pdfexport* for every organization).

Use *force* (*/pdfexport-agencies?run=1&force=1*) to bypass the scheduler and
to force the creation of the PDFs.


Build Status
------------

Expand Down
5 changes: 4 additions & 1 deletion docs/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
Changelog
---------

0.6 (unreleased)
0.6 (2015-03-10)
~~~~~~~~~~~~~~~~

- Export the PDFs nightly using the clock server. Implements #7
[msom]

- Use unicode_collate_sortkey in membership sorting. Fixes #8
[msom]

Expand Down
17 changes: 16 additions & 1 deletion seantis/agencies/browser/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@
from plone.folder.interfaces import IExplicitOrdering
from plone.uuid.interfaces import IUUID
from zope.component import queryUtility
from zope.event import notify
from zope.interface import implements

from seantis.agencies.types import IOrganization
from seantis.agencies.browser.base import BaseView
from seantis.agencies.interfaces import IActivityEvent
from seantis.agencies.types import IOrganization
from seantis.plonetools import tools


class ResourceViewedEvent(object):
implements(IActivityEvent)


class OrganizationView(BaseView):

grok.require('zope2.View')
grok.context(IOrganization)
grok.name('view')

template = grok.PageTemplateFile('templates/organization.pt')
event_fired = False

def suborganizations(self):
return [
Expand All @@ -36,3 +44,10 @@ def memberships(self):
memberships = sorted(memberships, key=sortkey)

return memberships

def update(self, **kwargs):
super(OrganizationView, self).update(**kwargs)

if not self.event_fired:
notify(ResourceViewedEvent())
self.event_fired = True
196 changes: 163 additions & 33 deletions seantis/agencies/browser/pdfexport.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
import re
import tempfile

from datetime import datetime, date
from datetime import datetime, date, timedelta
from io import BytesIO
from pdfdocument.document import MarkupParagraph
from reportlab.lib.units import cm
from threading import Lock

from five import grok
from plone import api
from Products.CMFPlone.interfaces import IPloneSiteRoot
from plone.synchronize import synchronized
from zExceptions import NotFound
from zope.interface import Interface

Expand All @@ -22,7 +24,10 @@

from seantis.agencies import _
from seantis.agencies.types import IOrganization
from seantis.plonetools import tools
from seantis.plonetools import tools, unrestricted


PDF_EXPORT_FILENAME = u'exported_pdf.pdf'


class OrganizationsPdf(PDF):
Expand Down Expand Up @@ -188,56 +193,181 @@ def populate_organization(self, organization, level, last_child=False):
idx == len(children) - 1)


class PdfExportViewFull(grok.View):
class PdfExportView(grok.View):
""" View to create and store a PDF of the current organization and
sub-organizations with portrait and memberships. Redirects to the file if
it already exists.
grok.name('pdfexport-agencies')
grok.context(IPloneSiteRoot)
"""

grok.name('pdfexport')
grok.context(IOrganization)
grok.require('zope2.View')

template = None

def render(self):
filehandle = OrganizationsReport().build(self.context, self.request)
filename = PDF_EXPORT_FILENAME

filename = _(u'Organizations')
filename = codecs.utf_8_encode('filename="%s.pdf"' % filename)[0]
self.request.RESPONSE.setHeader('Content-disposition', filename)
self.request.RESPONSE.setHeader('Content-Type', 'application/pdf')
if filename in self.context and self.request.get('force') == '1':
self.context.manage_delObjects([filename])

response = filehandle.getvalue()
filehandle.seek(0, os.SEEK_END)
if filename not in self.context:
log.info(
u'creating pdf export of %s on the fly' % (
self.context.title
)
)

filesize = filehandle.tell()
filehandle.close()
report = OrganizationsReport(root=self.context, toc=False)
filehandle = report.build(self.context, self.request)

self.request.RESPONSE.setHeader('Content-Length', filesize)
self.context.invokeFactory(type_name='File', id=filename)
file = self.context.get(filename)
file.setContentType('application/pdf')
file.setExcludeFromNav(True)
file.setFilename(filename)
file.setFile(filehandle.getvalue())
file.reindexObject()

return response
self.response.redirect(filename)


class PdfExportView(grok.View):
class PdfExportScheduler(object):
""" Schedules the creation of all PDFs nightly at 0:30.
grok.name('pdfexport')
grok.context(IOrganization)
"""

_lock = Lock()

def __init__(self):
self.next_run = 0
self.running = False

def get_next_run(self, now=None):
if now is None:
now = datetime.now()

# Schedule next run tomorrow at 0:30
at_hours = 0
at_minutes = 30
days = 1
if now.hour < (at_hours + 1) and now.minute < at_minutes:
days = 0
next_run = datetime(now.year, now.month, now.day)
next_run = next_run + timedelta(
days=days, hours=at_hours, minutes=at_minutes
)

return next_run

@synchronized(_lock)
def run(self, context, request, force, now=None):

result = False

if self.running:
log.info(u'already exporting')
return

if now is None:
now = datetime.now()

if os.getenv('seantis_agencies_export', False) == 'true':
if not self.next_run:
self.next_run = self.get_next_run(now)

if (now > self.next_run) or force:
self.running = True
try:
self.next_run = self.get_next_run(now)
self.export_single_pdfs(context, request)
self.export_full_pdf(context, request)
result = True
finally:
self.running = False

return result

def export_full_pdf(self, context, request):
filename = codecs.utf_8_encode('%s.pdf' % context.title)[0]

log.info('begin exporting to %s' % (filename))

filehandle = OrganizationsReport().build(context, request)

if filename in context:
context.manage_delObjects([filename])

context.invokeFactory(type_name='File', id=filename)
file = context.get(filename)
file.setContentType('application/pdf')
file.setExcludeFromNav(True)
file.setFilename(filename)
file.setFile(filehandle.getvalue())
file.reindexObject()

log.info('exported to %s' % (filename))

def export_single_pdf(self, context, request):
filename = PDF_EXPORT_FILENAME

log.info(u'creating pdf export of %s' % (context.title))

report = OrganizationsReport(root=context, toc=False)
filehandle = report.build(context, request)

if filename in context:
context.manage_delObjects([filename])

context.invokeFactory(type_name='File', id=filename)
file = context.get(filename)
file.setContentType('application/pdf')
file.setExcludeFromNav(True)
file.setFilename(filename)
file.setFile(filehandle.getvalue())
file.reindexObject()

children = [o.getObject() for o in context.suborganizations()]
for child in children:
self.export_single_pdf(child, request)

def export_single_pdfs(self, context, request):
catalog = api.portal.get_tool('portal_catalog')
folder_path = '/'.join(context.getPhysicalPath())
organizations = catalog(
path={'query': folder_path, 'depth': 1},
portal_type='seantis.agencies.organization',
sort_on='getObjPositionInParent'
)

for organization in [o.getObject() for o in organizations]:
self.export_single_pdf(organization, request)


export_scheduler = PdfExportScheduler()


class PdfExportViewFull(grok.View):
""" View to invoke the creation of all PDFs nightly
"""

grok.name('pdfexport-agencies')
grok.context(IPloneSiteRoot)
grok.require('zope2.View')

template = None

def render(self):
report = OrganizationsReport(root=self.context, toc=False)
filehandle = report.build(self.context, self.request)
self.request.response.setHeader("Content-type", "text/plain")

filename = self.context.title
filename = codecs.utf_8_encode('filename="%s.pdf"' % filename)[0]
self.request.RESPONSE.setHeader('Content-disposition', filename)
self.request.RESPONSE.setHeader('Content-Type', 'application/pdf')
result = False

response = filehandle.getvalue()
filehandle.seek(0, os.SEEK_END)
force = self.request.get('force') == '1'
with unrestricted.run_as('Manager'):
result = export_scheduler.run(self.context, self.request, force)

filesize = filehandle.tell()
filehandle.close()

self.request.RESPONSE.setHeader('Content-Length', filesize)

return response
if result:
return u'PDFs exported'
else:
return u'PDFs not exported'
5 changes: 5 additions & 0 deletions seantis/agencies/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@

class ISeantisAgenciesSpecific(Interface):
""" Browser Layer for seantis.agencies. """


class IActivityEvent(Interface):
""" Event triggered when a seantis.agencies.organization resource
is first viewed."""
10 changes: 10 additions & 0 deletions seantis/agencies/maintenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from five import grok
from zope.component.hooks import getSite

from seantis.plonetools import async
from seantis.agencies.interfaces import IActivityEvent


@grok.subscribe(IActivityEvent)
def on_resource_viewed(event):
async.register('/%s/pdfexport-agencies' % getSite().id, 60 * 60)
2 changes: 1 addition & 1 deletion seantis/agencies/profiles/default/metadata.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<metadata>
<version>1003</version>
<version>1004</version>
<dependencies>
<dependency>profile-plone.app.dexterity:default</dependency>
<dependency>profile-seantis.plonetools:default</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<property name="allowed_content_types">
<element value="seantis.agencies.membership" />
<element value="seantis.agencies.organization" />
<element value="File" />
</property>

<!-- schema interface -->
Expand Down
Loading

0 comments on commit 3dfa6cf

Please sign in to comment.