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

Last change on this file since 12725 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
Line 
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
23from zope import schema
24from zope.component import getUtility, getUtilitiesFor
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 (
30    IJobManager, JOB_STATUS_MAP, IKofaPluggable, IKofaObject)
31from waeup.kofa.interfaces import MessageFactory as _
32from waeup.kofa.utils.helpers import now
33
34class IReport(Interface):
35    """A report.
36    """
37    args = Attribute("""The args passed to constructor""")
38
39    kwargs = Attribute("""The keywords passed to constructor""")
40
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
50class IReportGenerator(IKofaObject):
51    """A report generator.
52    """
53    title = Attribute("""Human readable description of report type.""")
54
55    def generate(site, args=[], kw={}):
56        """Generate a report.
57
58        `args` and `kw` are the parameters needed to create a specific
59        report (if any).
60        """
61
62class IReportJob(zc.async.interfaces.IJob):
63
64    finished = schema.Bool(
65        title = u'`True` if the job finished.`',
66        default = False,
67        )
68
69    failed = schema.Bool(
70        title = u"`True` iff the job finished and didn't provide a report.",
71        default = None,
72        )
73
74    def __init__(site, generator_name):
75        """Create a report job via generator."""
76
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
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
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):
159    """A base for reports.
160    """
161    creation_dt = None
162
163    def __init__(self, args=[], kwargs={}):
164        self.args = args
165        self.kwargs = kwargs
166        self.creation_dt = now()
167
168    def create_pdf(self):
169        raise NotImplementedError()
170
171@implementer(IReportGenerator)
172class ReportGenerator(object):
173    """A base for report generators.
174    """
175    title = _("Unnamed Report")
176
177    def generate(self, site, args=[], kw={}):
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={}):
220        self._generator_name = generator_name
221        super(AsyncReportJob, self).__init__(
222            report_job, site, generator_name, args=args, kw=kw)
223
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
244        if not IReport.providedBy(getattr(self, 'result', None)):
245            return True
246        return False
247
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
270        Returns the job ID of the job started, `None` if the job could
271        not be started.
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)
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),)
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
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.