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

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

Cp. http://mathworld.wolfram.com/Iff.html

File size: 17.8 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
34#: A status map that reflects the really interesting types of status
35#: for reports.
36#:
37#: For reports we want to know whether a job was finished
38#: and/or whether it failed. All the other possible states ('new',
39#: etc.) are not really interesting in that regard.
40STATUS_MAP = {
41    'unknown': _('unknown'),
42    'running': _('running'),
43    'finished': _('finished'),
44    'failed': _('FAILED'),
45    }
46
47class IReport(Interface):
48    """A report.
49    """
50    args = Attribute("""The args passed to constructor""")
51
52    kwargs = Attribute("""The keywords passed to constructor""")
53
54    creation_dt = Attribute(
55        """Datetime when a report was created. The datetime should """
56        """reflect the point of time when the data was fetched, """
57        """not when any output was created.""")
58
59    title = schema.TextLine(
60        title = u"A human readable short description for a report.",
61        default = u'Untitled',
62        )
63
64    description = schema.Text(
65        title = u"A human readable text describing a report.",
66        default = u'No description'
67        )
68
69    def create_pdf():
70        """Generate a PDF copy.
71        """
72
73class IReportGenerator(IKofaObject):
74    """A report generator.
75    """
76    title = Attribute("""Human readable description of report type.""")
77    def generate(site, args=[], kw={}):
78        """Generate a report.
79
80        `args` and `kw` are the parameters needed to create a specific
81        report (if any).
82        """
83
84class IReportJob(zc.async.interfaces.IJob):
85
86    finished = schema.Bool(
87        title = u'`True` if the job finished.`',
88        default = False,
89        )
90
91    failed = schema.Bool(
92        title = u"`True` iff the job finished and didn't provide a report.",
93        default = None,
94        )
95
96    description = schema.TextLine(
97        title = u"""Textual representation of arguments and keywords.""",
98        default = u"",
99        )
100
101    report_status = schema.TextLine(
102        title = u"""Translated status string.""",
103        default = STATUS_MAP['unknown'],
104        )
105
106    def __init__(site, generator_name):
107        """Create a report job via generator."""
108
109class IReportJobContainer(Interface):
110    """A component that contains (maybe virtually) report jobs.
111    """
112    def start_report_job(report_generator_name, user_id, args=[], kw={}):
113        """Start asynchronous report job.
114
115        `report_generator_name`
116            is the name of a report generator utility to be used.
117
118        `user_id`
119            is the ID of the user that triggers the report generation.
120
121        `args` and `kw`
122            args and keywords passed to the generators `generate()`
123            method.
124
125        The job_id is stored along with exporter name and user id in a
126        persistent list.
127
128        Returns the job ID of the job started.
129        """
130
131    def get_running_report_jobs(user_id=None):
132        """Get report jobs for user with `user_id` as list of tuples.
133
134        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
135        that order. The ``<generator_name>`` is the utility name of the
136        used report generator.
137
138        If `user_id` is ``None``, all running report jobs are returned.
139        """
140
141    def get_report_jobs_status(user_id=None):
142        """Get running/completed report jobs for `user_id` as list of tuples.
143
144        Each tuple holds ``<raw status>, <status translated>,
145        <generator title>`` in that order, where ``<status
146        translated>`` and ``<generator title>`` are translated strings
147        representing the status of the job and the human readable
148        title of the report generator used.
149        """
150
151    def get_report_jobs_description(user_id=None):
152        """Get running/completed report jobs fur `user_id` as list of tuples.
153
154        The results contain enough information to render a status page
155        or similar. Each tuple holds::
156
157          (``<job_id>, <description>, <status_translated>,
158          <discardable>, <downloadable>``)
159
160        in that order, with
161
162        ``<job_id>``
163            The job id of the represented job. A string.
164
165        ``<description>``
166            A human readable description of the report run.
167
168        ``<status_translated>``
169            The status of report jobs' status (translated)
170
171        ``<discardable>``
172            Boolean indicating whether the job can be discarded.
173            Only completed jobs can be discarded.
174
175        ``<downloadable>``
176            Boolean indicating whether the job result can be
177            downloaded. This is only true if the job finished and
178            didn't raised exceptions.
179
180        If ``user_id`` is ``None``, all jobs are returned.
181        """
182
183    def delete_report_entry(entry):
184        """Delete the report job denoted by `entry`.
185
186        Removes `entry` from the local `running_report_jobs` list and
187        also removes the regarding job via the local job manager.
188
189        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
190        as created by :meth:`start_report_job` or returned by
191        :meth:`get_running_report_jobs`.
192        """
193
194    def report_entry_from_job_id(job_id):
195        """Get entry tuple for `job_id`.
196
197        Returns ``None`` if no such entry can be found.
198        """
199
200class IReportsContainer(grok.interfaces.IContainer, IReportJobContainer,
201                        IKofaObject):
202    """A grok container that holds report jobs.
203    """
204
205class manageReportsPermission(grok.Permission):
206    """A permission to manage reports.
207    """
208    grok.name('waeup.manageReports')
209
210def get_generators():
211    """Get available report generators.
212
213    Returns an iterator of tuples ``<NAME, GENERATOR>`` with ``NAME``
214    being the name under which the respective generator was
215    registered.
216    """
217    for name, util in getUtilitiesFor(IReportGenerator):
218        yield name, util
219    pass
220
221@implementer(IReport)
222class Report(object):
223    creation_dt = None
224
225    @property
226    def title(self):
227        return _(u'A report')
228
229    @property
230    def description(self):
231        return _(u'A dummy report')
232
233    def __init__(self, args=[], kwargs={}):
234        self.args = args
235        self.kwargs = kwargs
236        self.creation_dt = now()
237
238    def create_pdf(self):
239        raise NotImplementedError()
240
241    def __repr__(self):
242        return 'Report(args=%r, kwargs=%r)' % (self.args, self.kwargs)
243
244
245@implementer(IReportGenerator)
246class ReportGenerator(object):
247
248    title = _("Unnamed Report")
249    def generate(self, site, args=[], kw={}):
250        result = Report()
251        return result
252
253def report_job(site, generator_name, args=[], kw={}):
254    """Get a generator and perform report creation.
255
256    `site`
257        is the site for which the report should be created.
258    `generator_name`
259        the global utility name under which the desired generator is
260        registered.
261    `args` and `kw`
262        Arguments and keywords to be passed to the `generate()` method of
263        the desired generator. While `args` should be a list, `kw` should
264        be a dictionary.
265    """
266    setSite(site)
267    generator = getUtility(IReportGenerator, name=generator_name)
268    report = generator.generate(site, *args, **kw)
269    return report
270
271@implementer(IReportJob)
272class AsyncReportJob(AsyncJob):
273    """An IJob that creates reports.
274
275    `AsyncReportJob` instances are regular `AsyncJob` instances with a
276    different constructor API. Instead of a callable to execute, you
277    must pass a `site`, some `generator_name`, and additional args and
278    keywords to create a report.
279
280    The real work is done when an instance of this class is put into a
281    queue. See :mod:`waeup.kofa.async` to learn more about
282    asynchronous jobs.
283
284    The `generator_name` must be the name under which an IReportGenerator
285    utility was registered with the ZCA.
286
287    The `site` must be a valid site  or ``None``.
288
289    The result of an `AsyncReportJob` is an IReport object.
290    """
291    def __init__(self, site, generator_name, args=[], kw={}):
292        self._generator_name = generator_name
293        super(AsyncReportJob, self).__init__(
294            report_job, site, generator_name, args=args, kw=kw)
295
296    @property
297    def finished(self):
298        """A job is marked `finished` if it is completed.
299
300        Please note: a finished report job does not neccessarily
301        provide an IReport result. See meth:`failed`.
302        """
303        return self.status == zc.async.interfaces.COMPLETED
304
305    @property
306    def failed(self):
307        """A report job is marked failed iff it is finished and the
308        result does not provide IReport.
309
310        While a job is unfinished, the `failed` status is ``None``.
311
312        Failed jobs normally provide a `traceback` to examine reasons.
313        """
314        if not self.finished:
315            return None
316        if not IReport.providedBy(getattr(self, 'result', None)):
317            return True
318        return False
319
320    @property
321    def report_status(self):
322        """The status of a report as translated string.
323        """
324        if not self.finished:
325            return STATUS_MAP['running']
326        if self.failed:
327            return STATUS_MAP['failed']
328        return STATUS_MAP['finished']
329
330    @property
331    def description(self):
332        """A description gives a representation of the report to generate.
333
334        The description contains the name of the report generator
335        (trying to fetch the `name` attribute of the requested report
336        generator) and the arguments and keywords passed in.
337
338        Please note that this method is expensive!
339        """
340        args = self.kwargs.get('args', [])
341        kw = self.kwargs.get('kw', dict())
342        args = ', '.join(["%r" % x for x in args])
343        kw = ', '.join(['%s=%r' % (key, val) for key, val in kw.items()])
344        if len(args) and len(kw):
345            str_repr = args + ', ' + kw
346        else:
347            str_repr = args + kw
348        str_repr = '(' + str_repr + ')'
349        try:
350            generator = getUtility(
351                IReportGenerator, name=self._generator_name)
352            name = generator.title
353        except:
354            name = _('Unregistered Report Generator')
355        return name + ' ' + str_repr
356
357@implementer(IReportJobContainer)
358class ReportJobContainer(object):
359    """A mix-in that provides functionality for asynchronous report jobs.
360    """
361    running_report_jobs = PersistentList()
362
363    def start_report_job(self, generator_name, user_id, args=[], kw={}):
364        """Start asynchronous export job.
365
366        `generator_name`
367            is the name of a report generator utility to be used.
368
369        `user_id`
370            is the ID of the user that triggers the report generation.
371
372        `args` and `kw`
373            args and keywords passed to the generators `generate()`
374            method.
375
376        The job_id is stored along with exporter name and user id in a
377        persistent list.
378
379        Returns the job ID of the job started.
380        """
381        site = grok.getSite()
382        manager = getUtility(IJobManager)
383        job = AsyncReportJob(site, generator_name, args=args, kw=kw)
384        job_id = manager.put(job)
385        # Make sure that the persisted list is stored in ZODB
386        self.running_report_jobs = PersistentList(self.running_report_jobs)
387        self.running_report_jobs.append((job_id, generator_name, user_id),)
388        return job_id
389
390    def get_running_report_jobs(self, user_id=None):
391        """Get report jobs for user with `user_id` as list of tuples.
392
393        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
394        that order. The ``<generator_name>`` is the utility name of the
395        used report generator.
396
397        If `user_id` is ``None``, all running report jobs are returned.
398        """
399        entries = []
400        to_delete = []
401        manager = getUtility(IJobManager)
402        for entry in self.running_report_jobs:
403            if user_id is not None and entry[2] != user_id:
404                continue
405            if manager.get(entry[0]) is None:
406                to_delete.append(entry)
407                continue
408            entries.append(entry)
409        if to_delete:
410            self.running_report_jobs = PersistentList(
411                [x for x in self.running_report_jobs if x not in to_delete])
412        return entries
413
414    def get_report_jobs_status(self, user_id=None):
415        """Get running/completed report jobs for `user_id` as list of tuples.
416
417        Each tuple holds ``<raw status>, <status translated>,
418        <generator title>`` in that order, where ``<status
419        translated>`` and ``<generator title>`` are translated strings
420        representing the status of the job and the human readable
421        title of the report generator used.
422        """
423        entries = self.get_running_report_jobs(user_id)
424        result = []
425        manager = getUtility(IJobManager)
426        for entry in entries:
427            job = manager.get(entry[0])
428            status, status_translated = JOB_STATUS_MAP[job.status]
429            generator = getUtility(IReportGenerator, name=entry[1])
430            generator_name = getattr(generator, 'title', 'unnamed')
431            result.append((status, status_translated, generator_name))
432        return result
433
434    def get_report_jobs_description(self, user_id=None):
435        """Get running/completed report jobs fur `user_id` as list of tuples.
436
437        The results contain enough information to render a status page
438        or similar. Each tuple holds::
439
440          (``<job_id>, <description>, <status_translated>,
441          <discardable>, <downloadable>``)
442
443        in that order, with
444
445        ``<job_id>``
446            The job id of the represented job. A string.
447
448        ``<description>``
449            A human readable description of the report run.
450
451        ``<status_translated>``
452            The status of report jobs' status (translated)
453
454        ``<discardable>``
455            Boolean indicating whether the job can be discarded.
456            Only completed jobs can be discarded.
457
458        ``<downloadable>``
459            Boolean indicating whether the job result can be
460            downloaded. This is only true if the job finished and
461            didn't raised exceptions.
462
463        If ``user_id`` is ``None``, all jobs are returned.
464        """
465        entries = self.get_running_report_jobs(user_id)
466        result = []
467        for (job_id, gen_name, user_name) in entries:
468            manager = getUtility(IJobManager)
469            job = manager.get(job_id)
470            status = job.report_status
471            discardable = job.finished
472            downloadable = job.finished and not job.failed
473            if not hasattr(job, 'description'):
474                continue
475            result.append((job_id, job.description, status,
476                           discardable, downloadable),)
477        return result
478
479    def delete_report_entry(self, entry):
480        """Delete the report job denoted by `entry`.
481
482        Removes `entry` from the local `running_report_jobs` list and
483        also removes the regarding job via the local job manager.
484
485        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
486        as created by :meth:`start_report_job` or returned by
487        :meth:`get_running_report_jobs`.
488        """
489        manager = getUtility(IJobManager)
490        manager.remove(entry[0], self)
491        new_entries = [x for x in self.running_report_jobs
492                       if x != entry]
493        self.running_report_jobs = PersistentList(new_entries)
494        return
495
496    def report_entry_from_job_id(self, job_id):
497        """Get entry tuple for `job_id`.
498
499        Returns ``None`` if no such entry can be found.
500        """
501        for entry in self.running_report_jobs:
502            if entry[0] == job_id:
503                return entry
504        return None
505
506@implementer(IReportsContainer)
507class ReportsContainer(grok.Container, ReportJobContainer):
508    """A container for reports.
509    """
510
511@implementer(IKofaPluggable)
512class ReportsContainerPlugin(grok.GlobalUtility):
513    """A plugin that updates sites to contain a reports container.
514    """
515
516    grok.name('reports')
517
518    deprecated_attributes = []
519
520    def setup(self, site, name, logger):
521        """Add a reports container for `site`.
522
523        If there is such an object already, we install a fresh one.
524        """
525        if site.get('reports', None) is not None:
526            del site['reports']
527            logger.info('Removed reports container for site "%s"' % name)
528        return self.update(site, name, logger)
529
530    def update(self, site, name, logger):
531        """Install a reports container in `site`.
532
533        If one exists already, do nothing.
534        """
535        if site.get('reports', None) is not None:
536            return
537        site['reports'] = ReportsContainer()
538        logger.info('Added reports container for site "%s"' % name)
539        return
Note: See TracBrowser for help on using the repository browser.