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

Last change on this file since 9590 was 9510, checked in by uli, 12 years ago

More report related components we need for UI.
Also make ReportJobs? more convenient to use from other components.

File size: 13.9 KB
Line 
1## $Id$
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, queryUtility
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    def generate(site, args=[], kw={}):
55        """Generate a report.
56
57        `args` and `kw` are the parameters needed to create a specific
58        report (if any).
59        """
60
61class IReportJob(zc.async.interfaces.IJob):
62
63    finished = schema.Bool(
64        title = u'`True` if the job finished.`',
65        default = False,
66        )
67
68    failed = schema.Bool(
69        title = u"`True` iff the job finished and didn't provide a report.",
70        default = None,
71        )
72
73    description = schema.TextLine(
74        title = u"""Textual representation of arguments and keywords.""",
75        default = u"",
76        )
77
78    def __init__(site, generator_name):
79        """Create a report job via generator."""
80
81class IReportJobContainer(Interface):
82    """A component that contains (maybe virtually) report jobs.
83    """
84    def start_report_job(report_generator_name, user_id, args=[], kw={}):
85        """Start asynchronous report job.
86
87        `report_generator_name`
88            is the name of a report generator utility to be used.
89
90        `user_id`
91            is the ID of the user that triggers the report generation.
92
93        `args` and `kw`
94            args and keywords passed to the generators `generate()`
95            method.
96
97        The job_id is stored along with exporter name and user id in a
98        persistent list.
99
100        Returns the job ID of the job started.
101        """
102
103    def get_running_report_jobs(user_id=None):
104        """Get report jobs for user with `user_id` as list of tuples.
105
106        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
107        that order. The ``<generator_name>`` is the utility name of the
108        used report generator.
109
110        If `user_id` is ``None``, all running report jobs are returned.
111        """
112
113    def get_report_jobs_status(user_id=None):
114        """Get running/completed report jobs for `user_id` as list of tuples.
115
116        Each tuple holds ``<raw status>, <status translated>,
117        <generator title>`` in that order, where ``<status
118        translated>`` and ``<generator title>`` are translated strings
119        representing the status of the job and the human readable
120        title of the report generator used.
121        """
122
123    def delete_report_entry(entry):
124        """Delete the report job denoted by `entry`.
125
126        Removes `entry` from the local `running_report_jobs` list and
127        also removes the regarding job via the local job manager.
128
129        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
130        as created by :meth:`start_report_job` or returned by
131        :meth:`get_running_report_jobs`.
132        """
133
134    def report_entry_from_job_id(job_id):
135        """Get entry tuple for `job_id`.
136
137        Returns ``None`` if no such entry can be found.
138        """
139
140class IReportsContainer(grok.interfaces.IContainer, IReportJobContainer,
141                        IKofaObject):
142    """A grok container that holds report jobs.
143    """
144
145class manageReportsPermission(grok.Permission):
146    """A permission to manage reports.
147    """
148    grok.name('waeup.manageReports')
149
150def get_generators():
151    """Get available report generators.
152
153    Returns an iterator of tuples ``<NAME, GENERATOR>`` with ``NAME``
154    being the name under which the respective generator was
155    registered.
156    """
157    for name, util in getUtilitiesFor(IReportGenerator):
158        yield name, util
159    pass
160
161@implementer(IReport)
162class Report(object):
163    creation_dt = None
164
165    def __init__(self, args=[], kwargs={}):
166        self.args = args
167        self.kwargs = kwargs
168        self.creation_dt = now()
169
170    def create_pdf(self):
171        raise NotImplementedError()
172
173@implementer(IReportGenerator)
174class ReportGenerator(object):
175
176    title = _("Unnamed Report")
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(self.result):
245            return True
246        return False
247
248    @property
249    def description(self):
250        """A description gives a representation of the report to generate.
251
252        The description contains the name of the report generator
253        (trying to fetch the `name` attribute of the requested report
254        generator) and the arguments and keywords passed in.
255
256        Please note that this method is expensive!
257        """
258        args = self.kwargs.get('args', [])
259        kw = self.kwargs.get('kw', dict())
260        args = ', '.join(["%r" % x for x in args])
261        kw = ', '.join(['%s=%r' % (key, val) for key, val in kw.items()])
262        if len(args) and len(kw):
263            str_repr = args + ', ' + kw
264        else:
265            str_repr = args + kw
266        str_repr = '(' + str_repr + ')'
267        try:
268            generator = queryUtility(
269                IReportGenerator, name=self._generator_name)
270            name = generator.name
271        except:
272            name = _('Invalid Report Generator')
273        return name + ' ' + str_repr
274
275
276@implementer(IReportJobContainer)
277class ReportJobContainer(object):
278    """A mix-in that provides functionality for asynchronous report jobs.
279    """
280    running_report_jobs = PersistentList()
281
282    def start_report_job(self, generator_name, user_id, args=[], kw={}):
283        """Start asynchronous export job.
284
285        `generator_name`
286            is the name of a report generator utility to be used.
287
288        `user_id`
289            is the ID of the user that triggers the report generation.
290
291        `args` and `kw`
292            args and keywords passed to the generators `generate()`
293            method.
294
295        The job_id is stored along with exporter name and user id in a
296        persistent list.
297
298        Returns the job ID of the job started.
299        """
300        site = grok.getSite()
301        manager = getUtility(IJobManager)
302        job = AsyncReportJob(site, generator_name, args=args, kw=kw)
303        job_id = manager.put(job)
304        # Make sure that the persisted list is stored in ZODB
305        self.running_report_jobs = PersistentList(self.running_report_jobs)
306        self.running_report_jobs.append((job_id, generator_name, user_id),)
307        return job_id
308
309    def get_running_report_jobs(self, user_id=None):
310        """Get report jobs for user with `user_id` as list of tuples.
311
312        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
313        that order. The ``<generator_name>`` is the utility name of the
314        used report generator.
315
316        If `user_id` is ``None``, all running report jobs are returned.
317        """
318        entries = []
319        to_delete = []
320        manager = getUtility(IJobManager)
321        for entry in self.running_report_jobs:
322            if user_id is not None and entry[2] != user_id:
323                continue
324            if manager.get(entry[0]) is None:
325                to_delete.append(entry)
326                continue
327            entries.append(entry)
328        if to_delete:
329            self.running_report_jobs = PersistentList(
330                [x for x in self.running_report_jobs if x not in to_delete])
331        return entries
332
333    def get_report_jobs_status(self, user_id=None):
334        """Get running/completed report jobs for `user_id` as list of tuples.
335
336        Each tuple holds ``<raw status>, <status translated>,
337        <generator title>`` in that order, where ``<status
338        translated>`` and ``<generator title>`` are translated strings
339        representing the status of the job and the human readable
340        title of the report generator used.
341        """
342        entries = self.get_running_report_jobs(user_id)
343        result = []
344        manager = getUtility(IJobManager)
345        for entry in entries:
346            job = manager.get(entry[0])
347            status, status_translated = JOB_STATUS_MAP[job.status]
348            generator = getUtility(IReportGenerator, name=entry[1])
349            generator_name = getattr(generator, 'title', 'unnamed')
350            result.append((status, status_translated, generator_name))
351        return result
352
353    def delete_report_entry(self, entry):
354        """Delete the report job denoted by `entry`.
355
356        Removes `entry` from the local `running_report_jobs` list and
357        also removes the regarding job via the local job manager.
358
359        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
360        as created by :meth:`start_report_job` or returned by
361        :meth:`get_running_report_jobs`.
362        """
363        manager = getUtility(IJobManager)
364        manager.remove(entry[0], self)
365        new_entries = [x for x in self.running_report_jobs
366                       if x != entry]
367        self.running_report_jobs = PersistentList(new_entries)
368        return
369
370    def report_entry_from_job_id(self, job_id):
371        """Get entry tuple for `job_id`.
372
373        Returns ``None`` if no such entry can be found.
374        """
375        for entry in self.running_report_jobs:
376            if entry[0] == job_id:
377                return entry
378        return None
379
380@implementer(IReportsContainer)
381class ReportsContainer(grok.Container, ReportJobContainer):
382    """A container for reports.
383    """
384
385@implementer(IKofaPluggable)
386class ReportsContainerPlugin(grok.GlobalUtility):
387    """A plugin that updates sites to contain a reports container.
388    """
389
390    grok.name('reports')
391
392    deprecated_attributes = []
393
394    def setup(self, site, name, logger):
395        """Add a reports container for `site`.
396
397        If there is such an object already, we install a fresh one.
398        """
399        if site.get('reports', None) is not None:
400            del site['reports']
401            logger.info('Removed reports container for site "%s"' % name)
402        return self.update(site, name, logger)
403
404    def update(self, site, name, logger):
405        """Install a reports container in `site`.
406
407        If one exists already, do nothing.
408        """
409        if site.get('reports', None) is not None:
410            return
411        site['reports'] = ReportsContainer()
412        logger.info('Added reports container for site "%s"' % name)
413        return
Note: See TracBrowser for help on using the repository browser.