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
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    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
56    def create_pdf():
57        """Generate a PDF copy.
58        """
59
60class IReportGenerator(IKofaObject):
61    """A report generator.
62    """
63    title = Attribute("""Human readable description of report type.""")
64    def generate(site, args=[], kw={}):
65        """Generate a report.
66
67        `args` and `kw` are the parameters needed to create a specific
68        report (if any).
69        """
70
71class IReportJob(zc.async.interfaces.IJob):
72
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
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
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
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
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
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
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
193    def __init__(self, args=[], kwargs={}):
194        self.args = args
195        self.kwargs = kwargs
196        self.creation_dt = now()
197
198    def create_pdf(self):
199        raise NotImplementedError()
200
201    def __repr__(self):
202        return 'Report(args=%r, kwargs=%r)' % (self.args, self.kwargs)
203
204
205@implementer(IReportGenerator)
206class ReportGenerator(object):
207
208    title = _("Unnamed Report")
209    def generate(self, site, args=[], kw={}):
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={}):
252        self._generator_name = generator_name
253        super(AsyncReportJob, self).__init__(
254            report_job, site, generator_name, args=args, kw=kw)
255
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:
300            generator = getUtility(
301                IReportGenerator, name=self._generator_name)
302            name = generator.title
303        except:
304            name = _('Unregistered Report Generator')
305        return name + ' ' + str_repr
306
307
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)
338        self.running_report_jobs.append((job_id, generator_name, user_id),)
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
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
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
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.