source: main/waeup.ikoba/trunk/src/waeup/ikoba/reports.py @ 13100

Last change on this file since 13100 was 12986, checked in by Henrik Bettermann, 9 years ago

Remove quite old bug in doImport: Replace empty strings *and* lists with
ignore-markers in update *and* create mode.

  • Property svn:keywords set to Id
File size: 13.1 KB
RevLine 
[9344]1## $Id: reports.py 12986 2015-05-22 10:12:45Z 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
[9510]23from zope import schema
[9672]24from zope.component import getUtility, getUtilitiesFor
[9344]25from zope.component.hooks import setSite
26from zope.interface import implementer
27from zope.interface import Interface, Attribute
[11949]28from waeup.ikoba.async import AsyncJob
29from waeup.ikoba.interfaces import (
30    IJobManager, JOB_STATUS_MAP, IIkobaPluggable, IIkobaObject)
31from waeup.ikoba.interfaces import MessageFactory as _
32from waeup.ikoba.utils.helpers import now
[9344]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
46    def create_pdf():
47        """Generate a PDF copy.
48        """
49
[11949]50class IReportGenerator(IIkobaObject):
[9344]51    """A report generator.
52    """
[9510]53    title = Attribute("""Human readable description of report type.""")
[9673]54
[9510]55    def generate(site, args=[], kw={}):
[9344]56        """Generate a report.
[9510]57
58        `args` and `kw` are the parameters needed to create a specific
59        report (if any).
[9344]60        """
61
62class IReportJob(zc.async.interfaces.IJob):
63
[9510]64    finished = schema.Bool(
65        title = u'`True` if the job finished.`',
66        default = False,
67        )
68
69    failed = schema.Bool(
[9642]70        title = u"`True` iff the job finished and didn't provide a report.",
[9510]71        default = None,
72        )
73
74    def __init__(site, generator_name):
75        """Create a report job via generator."""
76
[9344]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
[9510]136class IReportsContainer(grok.interfaces.IContainer, IReportJobContainer,
[11949]137                        IIkobaObject):
[9510]138    """A grok container that holds report jobs.
139    """
140
[12986]141class ManageReports(grok.Permission):
142    """The ManageReports permission allows to view, add and remove reports.
[9510]143    """
144    grok.name('waeup.manageReports')
145
[12986]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
[9344]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):
[9675]166    """A base for reports.
167    """
[9344]168    creation_dt = None
169
[9510]170    def __init__(self, args=[], kwargs={}):
171        self.args = args
172        self.kwargs = kwargs
[9344]173        self.creation_dt = now()
174
175    def create_pdf(self):
[9510]176        raise NotImplementedError()
[9344]177
178@implementer(IReportGenerator)
179class ReportGenerator(object):
[9675]180    """A base for report generators.
181    """
[9510]182    title = _("Unnamed Report")
[9674]183
[9510]184    def generate(self, site, args=[], kw={}):
[9344]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
[11949]216    queue. See :mod:`waeup.ikoba.async` to learn more about
[9344]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={}):
[9510]227        self._generator_name = generator_name
[9344]228        super(AsyncReportJob, self).__init__(
[9510]229            report_job, site, generator_name, args=args, kw=kw)
[9344]230
[9510]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
[9638]251        if not IReport.providedBy(getattr(self, 'result', None)):
[9510]252            return True
253        return False
254
[9344]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.
278        """
279        site = grok.getSite()
280        manager = getUtility(IJobManager)
281        job = AsyncReportJob(site, generator_name, args=args, kw=kw)
282        job_id = manager.put(job)
283        # Make sure that the persisted list is stored in ZODB
284        self.running_report_jobs = PersistentList(self.running_report_jobs)
[9510]285        self.running_report_jobs.append((job_id, generator_name, user_id),)
[9344]286        return job_id
287
288    def get_running_report_jobs(self, user_id=None):
289        """Get report jobs for user with `user_id` as list of tuples.
290
291        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
292        that order. The ``<generator_name>`` is the utility name of the
293        used report generator.
294
295        If `user_id` is ``None``, all running report jobs are returned.
296        """
297        entries = []
298        to_delete = []
299        manager = getUtility(IJobManager)
300        for entry in self.running_report_jobs:
301            if user_id is not None and entry[2] != user_id:
302                continue
303            if manager.get(entry[0]) is None:
304                to_delete.append(entry)
305                continue
306            entries.append(entry)
307        if to_delete:
308            self.running_report_jobs = PersistentList(
309                [x for x in self.running_report_jobs if x not in to_delete])
310        return entries
311
312    def get_report_jobs_status(self, user_id=None):
313        """Get running/completed report jobs for `user_id` as list of tuples.
314
315        Each tuple holds ``<raw status>, <status translated>,
316        <generator title>`` in that order, where ``<status
317        translated>`` and ``<generator title>`` are translated strings
318        representing the status of the job and the human readable
319        title of the report generator used.
320        """
321        entries = self.get_running_report_jobs(user_id)
322        result = []
323        manager = getUtility(IJobManager)
324        for entry in entries:
325            job = manager.get(entry[0])
326            status, status_translated = JOB_STATUS_MAP[job.status]
327            generator = getUtility(IReportGenerator, name=entry[1])
328            generator_name = getattr(generator, 'title', 'unnamed')
329            result.append((status, status_translated, generator_name))
330        return result
331
332    def delete_report_entry(self, entry):
333        """Delete the report job denoted by `entry`.
334
335        Removes `entry` from the local `running_report_jobs` list and
336        also removes the regarding job via the local job manager.
337
338        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
339        as created by :meth:`start_report_job` or returned by
340        :meth:`get_running_report_jobs`.
341        """
342        manager = getUtility(IJobManager)
343        manager.remove(entry[0], self)
344        new_entries = [x for x in self.running_report_jobs
345                       if x != entry]
346        self.running_report_jobs = PersistentList(new_entries)
347        return
348
349    def report_entry_from_job_id(self, job_id):
350        """Get entry tuple for `job_id`.
351
352        Returns ``None`` if no such entry can be found.
353        """
354        for entry in self.running_report_jobs:
355            if entry[0] == job_id:
356                return entry
357        return None
[9510]358
359@implementer(IReportsContainer)
360class ReportsContainer(grok.Container, ReportJobContainer):
361    """A container for reports.
362    """
363
[11949]364@implementer(IIkobaPluggable)
[9510]365class ReportsContainerPlugin(grok.GlobalUtility):
366    """A plugin that updates sites to contain a reports container.
367    """
368
369    grok.name('reports')
370
371    deprecated_attributes = []
372
373    def setup(self, site, name, logger):
374        """Add a reports container for `site`.
375
376        If there is such an object already, we install a fresh one.
377        """
378        if site.get('reports', None) is not None:
379            del site['reports']
380            logger.info('Removed reports container for site "%s"' % name)
381        return self.update(site, name, logger)
382
383    def update(self, site, name, logger):
384        """Install a reports container in `site`.
385
386        If one exists already, do nothing.
387        """
388        if site.get('reports', None) is not None:
389            return
390        site['reports'] = ReportsContainer()
391        logger.info('Added reports container for site "%s"' % name)
392        return
Note: See TracBrowser for help on using the repository browser.