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

Last change on this file since 12850 was 12844, checked in by Henrik Bettermann, 10 years ago

Add ReportsManager? role.

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