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

Last change on this file since 12768 was 12583, checked in by uli, 10 years ago

Do not store empty jobs.

  • Property svn:keywords set to Id
File size: 12.9 KB
RevLine 
[9344]1## $Id: reports.py 12583 2015-02-10 14:33:23Z uli $
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
46    def create_pdf():
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
141class manageReportsPermission(grok.Permission):
142    """A permission to manage reports.
143    """
144    grok.name('waeup.manageReports')
145
[9344]146def get_generators():
147    """Get available report generators.
148
149    Returns an iterator of tuples ``<NAME, GENERATOR>`` with ``NAME``
150    being the name under which the respective generator was
151    registered.
152    """
153    for name, util in getUtilitiesFor(IReportGenerator):
154        yield name, util
155    pass
156
157@implementer(IReport)
158class Report(object):
[9675]159    """A base for reports.
160    """
[9344]161    creation_dt = None
162
[9510]163    def __init__(self, args=[], kwargs={}):
164        self.args = args
165        self.kwargs = kwargs
[9344]166        self.creation_dt = now()
167
168    def create_pdf(self):
[9510]169        raise NotImplementedError()
[9344]170
171@implementer(IReportGenerator)
172class ReportGenerator(object):
[9675]173    """A base for report generators.
174    """
[9510]175    title = _("Unnamed Report")
[9674]176
[9510]177    def generate(self, site, args=[], kw={}):
[9344]178        result = Report()
179        return result
180
181def report_job(site, generator_name, args=[], kw={}):
182    """Get a generator and perform report creation.
183
184    `site`
185        is the site for which the report should be created.
186    `generator_name`
187        the global utility name under which the desired generator is
188        registered.
189    `args` and `kw`
190        Arguments and keywords to be passed to the `generate()` method of
191        the desired generator. While `args` should be a list, `kw` should
192        be a dictionary.
193    """
194    setSite(site)
195    generator = getUtility(IReportGenerator, name=generator_name)
196    report = generator.generate(site, *args, **kw)
197    return report
198
199@implementer(IReportJob)
200class AsyncReportJob(AsyncJob):
201    """An IJob that creates reports.
202
203    `AsyncReportJob` instances are regular `AsyncJob` instances with a
204    different constructor API. Instead of a callable to execute, you
205    must pass a `site`, some `generator_name`, and additional args and
206    keywords to create a report.
207
208    The real work is done when an instance of this class is put into a
209    queue. See :mod:`waeup.kofa.async` to learn more about
210    asynchronous jobs.
211
212    The `generator_name` must be the name under which an IReportGenerator
213    utility was registered with the ZCA.
214
215    The `site` must be a valid site  or ``None``.
216
217    The result of an `AsyncReportJob` is an IReport object.
218    """
219    def __init__(self, site, generator_name, args=[], kw={}):
[9510]220        self._generator_name = generator_name
[9344]221        super(AsyncReportJob, self).__init__(
[9510]222            report_job, site, generator_name, args=args, kw=kw)
[9344]223
[9510]224    @property
225    def finished(self):
226        """A job is marked `finished` if it is completed.
227
228        Please note: a finished report job does not neccessarily
229        provide an IReport result. See meth:`failed`.
230        """
231        return self.status == zc.async.interfaces.COMPLETED
232
233    @property
234    def failed(self):
235        """A report job is marked failed iff it is finished and the
236        result does not provide IReport.
237
238        While a job is unfinished, the `failed` status is ``None``.
239
240        Failed jobs normally provide a `traceback` to examine reasons.
241        """
242        if not self.finished:
243            return None
[9638]244        if not IReport.providedBy(getattr(self, 'result', None)):
[9510]245            return True
246        return False
247
[9344]248@implementer(IReportJobContainer)
249class ReportJobContainer(object):
250    """A mix-in that provides functionality for asynchronous report jobs.
251    """
252    running_report_jobs = PersistentList()
253
254    def start_report_job(self, generator_name, user_id, args=[], kw={}):
255        """Start asynchronous export job.
256
257        `generator_name`
258            is the name of a report generator utility to be used.
259
260        `user_id`
261            is the ID of the user that triggers the report generation.
262
263        `args` and `kw`
264            args and keywords passed to the generators `generate()`
265            method.
266
267        The job_id is stored along with exporter name and user id in a
268        persistent list.
269
[12583]270        Returns the job ID of the job started, `None` if the job could
271        not be started.
[9344]272        """
273        site = grok.getSite()
274        manager = getUtility(IJobManager)
275        job = AsyncReportJob(site, generator_name, args=args, kw=kw)
276        job_id = manager.put(job)
[12583]277        if job_id is not None:
278            # Make sure that the persisted list is stored in ZODB
279            self.running_report_jobs = PersistentList(self.running_report_jobs)
280            self.running_report_jobs.append((job_id, generator_name, user_id),)
[9344]281        return job_id
282
283    def get_running_report_jobs(self, user_id=None):
284        """Get report jobs for user with `user_id` as list of tuples.
285
286        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
287        that order. The ``<generator_name>`` is the utility name of the
288        used report generator.
289
290        If `user_id` is ``None``, all running report jobs are returned.
291        """
292        entries = []
293        to_delete = []
294        manager = getUtility(IJobManager)
295        for entry in self.running_report_jobs:
296            if user_id is not None and entry[2] != user_id:
297                continue
298            if manager.get(entry[0]) is None:
299                to_delete.append(entry)
300                continue
301            entries.append(entry)
302        if to_delete:
303            self.running_report_jobs = PersistentList(
304                [x for x in self.running_report_jobs if x not in to_delete])
305        return entries
306
307    def get_report_jobs_status(self, user_id=None):
308        """Get running/completed report jobs for `user_id` as list of tuples.
309
310        Each tuple holds ``<raw status>, <status translated>,
311        <generator title>`` in that order, where ``<status
312        translated>`` and ``<generator title>`` are translated strings
313        representing the status of the job and the human readable
314        title of the report generator used.
315        """
316        entries = self.get_running_report_jobs(user_id)
317        result = []
318        manager = getUtility(IJobManager)
319        for entry in entries:
320            job = manager.get(entry[0])
321            status, status_translated = JOB_STATUS_MAP[job.status]
322            generator = getUtility(IReportGenerator, name=entry[1])
323            generator_name = getattr(generator, 'title', 'unnamed')
324            result.append((status, status_translated, generator_name))
325        return result
326
327    def delete_report_entry(self, entry):
328        """Delete the report job denoted by `entry`.
329
330        Removes `entry` from the local `running_report_jobs` list and
331        also removes the regarding job via the local job manager.
332
333        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
334        as created by :meth:`start_report_job` or returned by
335        :meth:`get_running_report_jobs`.
336        """
337        manager = getUtility(IJobManager)
338        manager.remove(entry[0], self)
339        new_entries = [x for x in self.running_report_jobs
340                       if x != entry]
341        self.running_report_jobs = PersistentList(new_entries)
342        return
343
344    def report_entry_from_job_id(self, job_id):
345        """Get entry tuple for `job_id`.
346
347        Returns ``None`` if no such entry can be found.
348        """
349        for entry in self.running_report_jobs:
350            if entry[0] == job_id:
351                return entry
352        return None
[9510]353
354@implementer(IReportsContainer)
355class ReportsContainer(grok.Container, ReportJobContainer):
356    """A container for reports.
357    """
358
359@implementer(IKofaPluggable)
360class ReportsContainerPlugin(grok.GlobalUtility):
361    """A plugin that updates sites to contain a reports container.
362    """
363
364    grok.name('reports')
365
366    deprecated_attributes = []
367
368    def setup(self, site, name, logger):
369        """Add a reports container for `site`.
370
371        If there is such an object already, we install a fresh one.
372        """
373        if site.get('reports', None) is not None:
374            del site['reports']
375            logger.info('Removed reports container for site "%s"' % name)
376        return self.update(site, name, logger)
377
378    def update(self, site, name, logger):
379        """Install a reports container in `site`.
380
381        If one exists already, do nothing.
382        """
383        if site.get('reports', None) is not None:
384            return
385        site['reports'] = ReportsContainer()
386        logger.info('Added reports container for site "%s"' % name)
387        return
Note: See TracBrowser for help on using the repository browser.