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

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

Basic statistics for kofa. Still the functionality is hidden - but available :-)

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