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
Line 
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
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 ManageReports(grok.Permission):
142    """The ManageReports permission allows to view, add and remove reports.
143    """
144    grok.name('waeup.manageReports')
145
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
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):
166    """A base for reports.
167    """
168    creation_dt = None
169
170    def __init__(self, args=[], kwargs={}):
171        self.args = args
172        self.kwargs = kwargs
173        self.creation_dt = now()
174
175    def create_pdf(self):
176        raise NotImplementedError()
177
178@implementer(IReportGenerator)
179class ReportGenerator(object):
180    """A base for report generators.
181    """
182    title = _("Unnamed Report")
183
184    def generate(self, site, args=[], kw={}):
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={}):
227        self._generator_name = generator_name
228        super(AsyncReportJob, self).__init__(
229            report_job, site, generator_name, args=args, kw=kw)
230
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
251        if not IReport.providedBy(getattr(self, 'result', None)):
252            return True
253        return False
254
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
277        Returns the job ID of the job started, `None` if the job could
278        not be started.
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)
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),)
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
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.