source: main/waeup.kofa/trunk/src/waeup/kofa/reports.py @ 16538

Last change on this file since 16538 was 14373, checked in by Henrik Bettermann, 8 years ago

Show report number (job_id) on report pdf slips.

  • Property svn:keywords set to Id
File size: 13.8 KB
RevLine 
[9344]1## $Id: reports.py 14373 2017-01-06 22:40:30Z henrik $
2##
3## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""Components for report generation.
19"""
20import grok
21import zc.async.interfaces
22from persistent.list import PersistentList
[9510]23from zope import schema
[9672]24from zope.component import getUtility, getUtilitiesFor
[9344]25from zope.component.hooks import setSite
26from zope.interface import implementer
27from zope.interface import Interface, Attribute
28from waeup.kofa.async import AsyncJob
29from waeup.kofa.interfaces import (
[9672]30    IJobManager, JOB_STATUS_MAP, IKofaPluggable, IKofaObject)
[9510]31from waeup.kofa.interfaces import MessageFactory as _
[9344]32from waeup.kofa.utils.helpers import now
33
34class IReport(Interface):
35    """A report.
36    """
[9510]37    args = Attribute("""The args passed to constructor""")
38
39    kwargs = Attribute("""The keywords passed to constructor""")
40
[9344]41    creation_dt = Attribute(
42        """Datetime when a report was created. The datetime should """
43        """reflect the point of time when the data was fetched, """
44        """not when any output was created.""")
45
[14373]46    def create_pdf(job_id):
[9344]47        """Generate a PDF copy.
48        """
49
[9510]50class IReportGenerator(IKofaObject):
[9344]51    """A report generator.
52    """
[9510]53    title = Attribute("""Human readable description of report type.""")
[9673]54
[9510]55    def generate(site, args=[], kw={}):
[9344]56        """Generate a report.
[9510]57
58        `args` and `kw` are the parameters needed to create a specific
59        report (if any).
[9344]60        """
61
62class IReportJob(zc.async.interfaces.IJob):
63
[9510]64    finished = schema.Bool(
65        title = u'`True` if the job finished.`',
66        default = False,
67        )
68
69    failed = schema.Bool(
[9642]70        title = u"`True` iff the job finished and didn't provide a report.",
[9510]71        default = None,
72        )
73
74    def __init__(site, generator_name):
75        """Create a report job via generator."""
76
[9344]77class IReportJobContainer(Interface):
78    """A component that contains (maybe virtually) report jobs.
79    """
80    def start_report_job(report_generator_name, user_id, args=[], kw={}):
81        """Start asynchronous report job.
82
83        `report_generator_name`
84            is the name of a report generator utility to be used.
85
86        `user_id`
87            is the ID of the user that triggers the report generation.
88
89        `args` and `kw`
90            args and keywords passed to the generators `generate()`
91            method.
92
93        The job_id is stored along with exporter name and user id in a
94        persistent list.
95
96        Returns the job ID of the job started.
97        """
98
99    def get_running_report_jobs(user_id=None):
100        """Get report jobs for user with `user_id` as list of tuples.
101
102        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
103        that order. The ``<generator_name>`` is the utility name of the
104        used report generator.
105
106        If `user_id` is ``None``, all running report jobs are returned.
107        """
108
109    def get_report_jobs_status(user_id=None):
110        """Get running/completed report jobs for `user_id` as list of tuples.
111
112        Each tuple holds ``<raw status>, <status translated>,
113        <generator title>`` in that order, where ``<status
114        translated>`` and ``<generator title>`` are translated strings
115        representing the status of the job and the human readable
116        title of the report generator used.
117        """
118
119    def delete_report_entry(entry):
120        """Delete the report job denoted by `entry`.
121
122        Removes `entry` from the local `running_report_jobs` list and
123        also removes the regarding job via the local job manager.
124
125        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
126        as created by :meth:`start_report_job` or returned by
127        :meth:`get_running_report_jobs`.
128        """
129
130    def report_entry_from_job_id(job_id):
131        """Get entry tuple for `job_id`.
132
133        Returns ``None`` if no such entry can be found.
134        """
135
[9510]136class IReportsContainer(grok.interfaces.IContainer, IReportJobContainer,
137                        IKofaObject):
138    """A grok container that holds report jobs.
139    """
140
[12900]141class HandleReports(grok.Permission):
142    """The HandleReports permission allows to add any kind of report
143    and to view and remove own reports, i.e. reports which were created by
144    the logged-in user.
145    """
146    grok.name('waeup.handleReports')
147
[12844]148class ManageReports(grok.Permission):
[12900]149    """The ManageReports permission allows to view, add and remove also
150    the reports of other users. It requires the permission to handle reports.
[9510]151    """
152    grok.name('waeup.manageReports')
153
[12900]154class ReportsOfficer(grok.Role):
[12901]155    """The Reports Officer has the permission to view, add and remove
[12900]156    **own** reports.
157    """
158    grok.name('waeup.ReportsOfficer')
159    grok.title(u'Reports Officer')
160    grok.permissions('waeup.handleReports')
161
[12844]162class ReportsManager(grok.Role):
[12901]163    """The Reports Manager has the permission to view, add and remove
[12900]164    **all** reports.
[12844]165    """
166    grok.name('waeup.ReportsManager')
167    grok.title(u'Reports Manager')
[12900]168    grok.permissions('waeup.handleReports', 'waeup.manageReports')
[12844]169
[9344]170def get_generators():
171    """Get available report generators.
172
173    Returns an iterator of tuples ``<NAME, GENERATOR>`` with ``NAME``
174    being the name under which the respective generator was
175    registered.
176    """
177    for name, util in getUtilitiesFor(IReportGenerator):
178        yield name, util
179    pass
180
181@implementer(IReport)
182class Report(object):
[9675]183    """A base for reports.
184    """
[9344]185    creation_dt = None
186
[9510]187    def __init__(self, args=[], kwargs={}):
188        self.args = args
189        self.kwargs = kwargs
[9344]190        self.creation_dt = now()
191
[14373]192    def create_pdf(self, job_id):
[9510]193        raise NotImplementedError()
[9344]194
195@implementer(IReportGenerator)
196class ReportGenerator(object):
[9675]197    """A base for report generators.
198    """
[9510]199    title = _("Unnamed Report")
[9674]200
[9510]201    def generate(self, site, args=[], kw={}):
[9344]202        result = Report()
203        return result
204
205def report_job(site, generator_name, args=[], kw={}):
206    """Get a generator and perform report creation.
207
208    `site`
209        is the site for which the report should be created.
210    `generator_name`
211        the global utility name under which the desired generator is
212        registered.
213    `args` and `kw`
214        Arguments and keywords to be passed to the `generate()` method of
215        the desired generator. While `args` should be a list, `kw` should
216        be a dictionary.
217    """
218    setSite(site)
219    generator = getUtility(IReportGenerator, name=generator_name)
220    report = generator.generate(site, *args, **kw)
221    return report
222
223@implementer(IReportJob)
224class AsyncReportJob(AsyncJob):
225    """An IJob that creates reports.
226
227    `AsyncReportJob` instances are regular `AsyncJob` instances with a
228    different constructor API. Instead of a callable to execute, you
229    must pass a `site`, some `generator_name`, and additional args and
230    keywords to create a report.
231
232    The real work is done when an instance of this class is put into a
233    queue. See :mod:`waeup.kofa.async` to learn more about
234    asynchronous jobs.
235
236    The `generator_name` must be the name under which an IReportGenerator
237    utility was registered with the ZCA.
238
239    The `site` must be a valid site  or ``None``.
240
241    The result of an `AsyncReportJob` is an IReport object.
242    """
243    def __init__(self, site, generator_name, args=[], kw={}):
[9510]244        self._generator_name = generator_name
[9344]245        super(AsyncReportJob, self).__init__(
[9510]246            report_job, site, generator_name, args=args, kw=kw)
[9344]247
[9510]248    @property
249    def finished(self):
250        """A job is marked `finished` if it is completed.
251
252        Please note: a finished report job does not neccessarily
253        provide an IReport result. See meth:`failed`.
254        """
255        return self.status == zc.async.interfaces.COMPLETED
256
257    @property
258    def failed(self):
259        """A report job is marked failed iff it is finished and the
260        result does not provide IReport.
261
262        While a job is unfinished, the `failed` status is ``None``.
263
264        Failed jobs normally provide a `traceback` to examine reasons.
265        """
266        if not self.finished:
267            return None
[9638]268        if not IReport.providedBy(getattr(self, 'result', None)):
[9510]269            return True
270        return False
271
[9344]272@implementer(IReportJobContainer)
273class ReportJobContainer(object):
274    """A mix-in that provides functionality for asynchronous report jobs.
275    """
276    running_report_jobs = PersistentList()
277
278    def start_report_job(self, generator_name, user_id, args=[], kw={}):
279        """Start asynchronous export job.
280
281        `generator_name`
282            is the name of a report generator utility to be used.
283
284        `user_id`
285            is the ID of the user that triggers the report generation.
286
287        `args` and `kw`
288            args and keywords passed to the generators `generate()`
289            method.
290
291        The job_id is stored along with exporter name and user id in a
292        persistent list.
293
[12583]294        Returns the job ID of the job started, `None` if the job could
295        not be started.
[9344]296        """
297        site = grok.getSite()
298        manager = getUtility(IJobManager)
299        job = AsyncReportJob(site, generator_name, args=args, kw=kw)
300        job_id = manager.put(job)
[12583]301        if job_id is not None:
302            # Make sure that the persisted list is stored in ZODB
303            self.running_report_jobs = PersistentList(self.running_report_jobs)
304            self.running_report_jobs.append((job_id, generator_name, user_id),)
[9344]305        return job_id
306
307    def get_running_report_jobs(self, user_id=None):
308        """Get report jobs for user with `user_id` as list of tuples.
309
310        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
311        that order. The ``<generator_name>`` is the utility name of the
312        used report generator.
313
314        If `user_id` is ``None``, all running report jobs are returned.
315        """
316        entries = []
317        to_delete = []
318        manager = getUtility(IJobManager)
319        for entry in self.running_report_jobs:
320            if user_id is not None and entry[2] != user_id:
321                continue
322            if manager.get(entry[0]) is None:
323                to_delete.append(entry)
324                continue
325            entries.append(entry)
326        if to_delete:
327            self.running_report_jobs = PersistentList(
328                [x for x in self.running_report_jobs if x not in to_delete])
329        return entries
330
331    def get_report_jobs_status(self, user_id=None):
332        """Get running/completed report jobs for `user_id` as list of tuples.
333
334        Each tuple holds ``<raw status>, <status translated>,
335        <generator title>`` in that order, where ``<status
336        translated>`` and ``<generator title>`` are translated strings
337        representing the status of the job and the human readable
338        title of the report generator used.
339        """
340        entries = self.get_running_report_jobs(user_id)
341        result = []
342        manager = getUtility(IJobManager)
343        for entry in entries:
344            job = manager.get(entry[0])
345            status, status_translated = JOB_STATUS_MAP[job.status]
346            generator = getUtility(IReportGenerator, name=entry[1])
347            generator_name = getattr(generator, 'title', 'unnamed')
348            result.append((status, status_translated, generator_name))
349        return result
350
351    def delete_report_entry(self, entry):
352        """Delete the report job denoted by `entry`.
353
354        Removes `entry` from the local `running_report_jobs` list and
355        also removes the regarding job via the local job manager.
356
357        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
358        as created by :meth:`start_report_job` or returned by
359        :meth:`get_running_report_jobs`.
360        """
361        manager = getUtility(IJobManager)
362        manager.remove(entry[0], self)
363        new_entries = [x for x in self.running_report_jobs
364                       if x != entry]
365        self.running_report_jobs = PersistentList(new_entries)
366        return
367
368    def report_entry_from_job_id(self, job_id):
369        """Get entry tuple for `job_id`.
370
371        Returns ``None`` if no such entry can be found.
372        """
373        for entry in self.running_report_jobs:
374            if entry[0] == job_id:
375                return entry
376        return None
[9510]377
378@implementer(IReportsContainer)
379class ReportsContainer(grok.Container, ReportJobContainer):
380    """A container for reports.
381    """
382
383@implementer(IKofaPluggable)
384class ReportsContainerPlugin(grok.GlobalUtility):
385    """A plugin that updates sites to contain a reports container.
386    """
387
388    grok.name('reports')
389
390    deprecated_attributes = []
391
392    def setup(self, site, name, logger):
393        """Add a reports container for `site`.
394
395        If there is such an object already, we install a fresh one.
396        """
397        if site.get('reports', None) is not None:
398            del site['reports']
399            logger.info('Removed reports container for site "%s"' % name)
400        return self.update(site, name, logger)
401
402    def update(self, site, name, logger):
403        """Install a reports container in `site`.
404
405        If one exists already, do nothing.
406        """
407        if site.get('reports', None) is not None:
408            return
409        site['reports'] = ReportsContainer()
410        logger.info('Added reports container for site "%s"' % name)
411        return
Note: See TracBrowser for help on using the repository browser.