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

Last change on this file since 17055 was 14373, checked in by Henrik Bettermann, 8 years ago

Show report number (job_id) on report pdf slips.

  • Property svn:keywords set to Id
File size: 13.8 KB
Line 
1## $Id: reports.py 14373 2017-01-06 22:40:30Z 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
23from zope import schema
24from zope.component import getUtility, getUtilitiesFor
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(job_id):
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
55    def generate(site, args=[], kw={}):
56        """Generate a report.
57
58        `args` and `kw` are the parameters needed to create a specific
59        report (if any).
60        """
61
62class IReportJob(zc.async.interfaces.IJob):
63
64    finished = schema.Bool(
65        title = u'`True` if the job finished.`',
66        default = False,
67        )
68
69    failed = schema.Bool(
70        title = u"`True` iff the job finished and didn't provide a report.",
71        default = None,
72        )
73
74    def __init__(site, generator_name):
75        """Create a report job via generator."""
76
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
136class IReportsContainer(grok.interfaces.IContainer, IReportJobContainer,
137                        IKofaObject):
138    """A grok container that holds report jobs.
139    """
140
141class HandleReports(grok.Permission):
142    """The HandleReports permission allows to add any kind of report
143    and to view and remove own reports, i.e. reports which were created by
144    the logged-in user.
145    """
146    grok.name('waeup.handleReports')
147
148class ManageReports(grok.Permission):
149    """The ManageReports permission allows to view, add and remove also
150    the reports of other users. It requires the permission to handle reports.
151    """
152    grok.name('waeup.manageReports')
153
154class ReportsOfficer(grok.Role):
155    """The Reports Officer has the permission to view, add and remove
156    **own** reports.
157    """
158    grok.name('waeup.ReportsOfficer')
159    grok.title(u'Reports Officer')
160    grok.permissions('waeup.handleReports')
161
162class ReportsManager(grok.Role):
163    """The Reports Manager has the permission to view, add and remove
164    **all** reports.
165    """
166    grok.name('waeup.ReportsManager')
167    grok.title(u'Reports Manager')
168    grok.permissions('waeup.handleReports', '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    """A base for reports.
184    """
185    creation_dt = None
186
187    def __init__(self, args=[], kwargs={}):
188        self.args = args
189        self.kwargs = kwargs
190        self.creation_dt = now()
191
192    def create_pdf(self, job_id):
193        raise NotImplementedError()
194
195@implementer(IReportGenerator)
196class ReportGenerator(object):
197    """A base for report generators.
198    """
199    title = _("Unnamed Report")
200
201    def generate(self, site, args=[], kw={}):
202        result = Report()
203        return result
204
205def report_job(site, generator_name, args=[], kw={}):
206    """Get a generator and perform report creation.
207
208    `site`
209        is the site for which the report should be created.
210    `generator_name`
211        the global utility name under which the desired generator is
212        registered.
213    `args` and `kw`
214        Arguments and keywords to be passed to the `generate()` method of
215        the desired generator. While `args` should be a list, `kw` should
216        be a dictionary.
217    """
218    setSite(site)
219    generator = getUtility(IReportGenerator, name=generator_name)
220    report = generator.generate(site, *args, **kw)
221    return report
222
223@implementer(IReportJob)
224class AsyncReportJob(AsyncJob):
225    """An IJob that creates reports.
226
227    `AsyncReportJob` instances are regular `AsyncJob` instances with a
228    different constructor API. Instead of a callable to execute, you
229    must pass a `site`, some `generator_name`, and additional args and
230    keywords to create a report.
231
232    The real work is done when an instance of this class is put into a
233    queue. See :mod:`waeup.kofa.async` to learn more about
234    asynchronous jobs.
235
236    The `generator_name` must be the name under which an IReportGenerator
237    utility was registered with the ZCA.
238
239    The `site` must be a valid site  or ``None``.
240
241    The result of an `AsyncReportJob` is an IReport object.
242    """
243    def __init__(self, site, generator_name, args=[], kw={}):
244        self._generator_name = generator_name
245        super(AsyncReportJob, self).__init__(
246            report_job, site, generator_name, args=args, kw=kw)
247
248    @property
249    def finished(self):
250        """A job is marked `finished` if it is completed.
251
252        Please note: a finished report job does not neccessarily
253        provide an IReport result. See meth:`failed`.
254        """
255        return self.status == zc.async.interfaces.COMPLETED
256
257    @property
258    def failed(self):
259        """A report job is marked failed iff it is finished and the
260        result does not provide IReport.
261
262        While a job is unfinished, the `failed` status is ``None``.
263
264        Failed jobs normally provide a `traceback` to examine reasons.
265        """
266        if not self.finished:
267            return None
268        if not IReport.providedBy(getattr(self, 'result', None)):
269            return True
270        return False
271
272@implementer(IReportJobContainer)
273class ReportJobContainer(object):
274    """A mix-in that provides functionality for asynchronous report jobs.
275    """
276    running_report_jobs = PersistentList()
277
278    def start_report_job(self, generator_name, user_id, args=[], kw={}):
279        """Start asynchronous export job.
280
281        `generator_name`
282            is the name of a report generator utility to be used.
283
284        `user_id`
285            is the ID of the user that triggers the report generation.
286
287        `args` and `kw`
288            args and keywords passed to the generators `generate()`
289            method.
290
291        The job_id is stored along with exporter name and user id in a
292        persistent list.
293
294        Returns the job ID of the job started, `None` if the job could
295        not be started.
296        """
297        site = grok.getSite()
298        manager = getUtility(IJobManager)
299        job = AsyncReportJob(site, generator_name, args=args, kw=kw)
300        job_id = manager.put(job)
301        if job_id is not None:
302            # Make sure that the persisted list is stored in ZODB
303            self.running_report_jobs = PersistentList(self.running_report_jobs)
304            self.running_report_jobs.append((job_id, generator_name, user_id),)
305        return job_id
306
307    def get_running_report_jobs(self, user_id=None):
308        """Get report jobs for user with `user_id` as list of tuples.
309
310        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
311        that order. The ``<generator_name>`` is the utility name of the
312        used report generator.
313
314        If `user_id` is ``None``, all running report jobs are returned.
315        """
316        entries = []
317        to_delete = []
318        manager = getUtility(IJobManager)
319        for entry in self.running_report_jobs:
320            if user_id is not None and entry[2] != user_id:
321                continue
322            if manager.get(entry[0]) is None:
323                to_delete.append(entry)
324                continue
325            entries.append(entry)
326        if to_delete:
327            self.running_report_jobs = PersistentList(
328                [x for x in self.running_report_jobs if x not in to_delete])
329        return entries
330
331    def get_report_jobs_status(self, user_id=None):
332        """Get running/completed report jobs for `user_id` as list of tuples.
333
334        Each tuple holds ``<raw status>, <status translated>,
335        <generator title>`` in that order, where ``<status
336        translated>`` and ``<generator title>`` are translated strings
337        representing the status of the job and the human readable
338        title of the report generator used.
339        """
340        entries = self.get_running_report_jobs(user_id)
341        result = []
342        manager = getUtility(IJobManager)
343        for entry in entries:
344            job = manager.get(entry[0])
345            status, status_translated = JOB_STATUS_MAP[job.status]
346            generator = getUtility(IReportGenerator, name=entry[1])
347            generator_name = getattr(generator, 'title', 'unnamed')
348            result.append((status, status_translated, generator_name))
349        return result
350
351    def delete_report_entry(self, entry):
352        """Delete the report job denoted by `entry`.
353
354        Removes `entry` from the local `running_report_jobs` list and
355        also removes the regarding job via the local job manager.
356
357        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
358        as created by :meth:`start_report_job` or returned by
359        :meth:`get_running_report_jobs`.
360        """
361        manager = getUtility(IJobManager)
362        manager.remove(entry[0], self)
363        new_entries = [x for x in self.running_report_jobs
364                       if x != entry]
365        self.running_report_jobs = PersistentList(new_entries)
366        return
367
368    def report_entry_from_job_id(self, job_id):
369        """Get entry tuple for `job_id`.
370
371        Returns ``None`` if no such entry can be found.
372        """
373        for entry in self.running_report_jobs:
374            if entry[0] == job_id:
375                return entry
376        return None
377
378@implementer(IReportsContainer)
379class ReportsContainer(grok.Container, ReportJobContainer):
380    """A container for reports.
381    """
382
383@implementer(IKofaPluggable)
384class ReportsContainerPlugin(grok.GlobalUtility):
385    """A plugin that updates sites to contain a reports container.
386    """
387
388    grok.name('reports')
389
390    deprecated_attributes = []
391
392    def setup(self, site, name, logger):
393        """Add a reports container for `site`.
394
395        If there is such an object already, we install a fresh one.
396        """
397        if site.get('reports', None) is not None:
398            del site['reports']
399            logger.info('Removed reports container for site "%s"' % name)
400        return self.update(site, name, logger)
401
402    def update(self, site, name, logger):
403        """Install a reports container in `site`.
404
405        If one exists already, do nothing.
406        """
407        if site.get('reports', None) is not None:
408            return
409        site['reports'] = ReportsContainer()
410        logger.info('Added reports container for site "%s"' % name)
411        return
Note: See TracBrowser for help on using the repository browser.