## $Id$ ## ## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## """Components for report generation. """ import grok import zc.async.interfaces from persistent.list import PersistentList from zope import schema from zope.component import ( getUtility, getUtilitiesFor, queryUtility) from zope.component.hooks import setSite from zope.interface import implementer from zope.interface import Interface, Attribute from waeup.kofa.async import AsyncJob from waeup.kofa.interfaces import ( IJobManager, JOB_STATUS_MAP, IKofaPluggable, IKofaObject, IKofaUtils) from waeup.kofa.interfaces import MessageFactory as _ from waeup.kofa.utils.helpers import now #: A status map that reflects the really interesting types of status #: for reports. #: #: For reports we want to know whether a job was finished #: and/or whether it failed. All the other possible states ('new', #: etc.) are not really interesting in that regard. STATUS_MAP = { 'unknown': _('unknown'), 'running': _('running'), 'finished': _('finished'), 'failed': _('FAILED'), } class IReport(Interface): """A report. """ args = Attribute("""The args passed to constructor""") kwargs = Attribute("""The keywords passed to constructor""") creation_dt = Attribute( """Datetime when a report was created. The datetime should """ """reflect the point of time when the data was fetched, """ """not when any output was created.""") title = schema.TextLine( title = u"A human readable short description for a report.", default = u'Untitled', ) description = schema.Text( title = u"A human readable text describing a report.", default = u'No description' ) def create_pdf(): """Generate a PDF copy. """ class IReportGenerator(IKofaObject): """A report generator. """ title = Attribute("""Human readable description of report type.""") def generate(site, args=[], kw={}): """Generate a report. `args` and `kw` are the parameters needed to create a specific report (if any). """ class IReportJob(zc.async.interfaces.IJob): finished = schema.Bool( title = u'`True` if the job finished.`', default = False, ) failed = schema.Bool( title = u"`True` iff the job finished and didn't provide a report.", default = None, ) description = schema.TextLine( title = u"""Textual representation of arguments and keywords.""", default = u"", ) report_status = schema.TextLine( title = u"""Translated status string.""", default = STATUS_MAP['unknown'], ) def __init__(site, generator_name): """Create a report job via generator.""" class IReportJobContainer(Interface): """A component that contains (maybe virtually) report jobs. """ def start_report_job(report_generator_name, user_id, args=[], kw={}): """Start asynchronous report job. `report_generator_name` is the name of a report generator utility to be used. `user_id` is the ID of the user that triggers the report generation. `args` and `kw` args and keywords passed to the generators `generate()` method. The job_id is stored along with exporter name and user id in a persistent list. Returns the job ID of the job started. """ def get_running_report_jobs(user_id=None): """Get report jobs for user with `user_id` as list of tuples. Each tuples holds ``, , `` in that order. The ```` is the utility name of the used report generator. If `user_id` is ``None``, all running report jobs are returned. """ def get_report_jobs_status(user_id=None): """Get running/completed report jobs for `user_id` as list of tuples. Each tuple holds ``, , `` in that order, where ```` and ```` are translated strings representing the status of the job and the human readable title of the report generator used. """ def get_report_jobs_description(user_id=None): """Get running/completed report jobs for `user_id` as list of tuples. The results contain enough information to render a status page or similar. Each tuple holds:: (``, , , , ``) in that order, with ```` The job id of the represented job. A string. ```` A human readable description of the report run. ```` The status of report jobs' status (translated) ```` Boolean indicating whether the job can be discarded. Only completed jobs can be discarded. ```` Boolean indicating whether the job result can be downloaded. This is only true if the job finished and didn't raised exceptions. If ``user_id`` is ``None``, all jobs are returned. """ def delete_report_entry(entry): """Delete the report job denoted by `entry`. Removes `entry` from the local `running_report_jobs` list and also removes the regarding job via the local job manager. `entry` is a tuple ``(, , )`` as created by :meth:`start_report_job` or returned by :meth:`get_running_report_jobs`. """ def report_entry_from_job_id(job_id): """Get entry tuple for `job_id`. Returns ``None`` if no such entry can be found. """ class IReportsContainer(grok.interfaces.IContainer, IReportJobContainer, IKofaObject): """A grok container that holds report jobs. """ class manageReportsPermission(grok.Permission): """A permission to manage reports. """ grok.name('waeup.manageReports') def get_generators(): """Get available report generators. Returns an iterator of tuples ```` with ``NAME`` being the name under which the respective generator was registered. """ for name, util in getUtilitiesFor(IReportGenerator): yield name, util pass @implementer(IReport) class Report(object): creation_dt = None @property def title(self): return _(u'A report') @property def description(self): return _(u'A dummy report') def __init__(self, args=[], kwargs={}): self.args = args self.kwargs = kwargs self.creation_dt = now() def create_pdf(self): raise NotImplementedError() def __repr__(self): return 'Report(args=%r, kwargs=%r)' % (self.args, self.kwargs) @implementer(IReportGenerator) class ReportGenerator(object): title = _("Unnamed Report") def generate(self, site, args=[], kw={}): result = Report() return result def report_job(site, generator_name, args=[], kw={}): """Get a generator and perform report creation. `site` is the site for which the report should be created. `generator_name` the global utility name under which the desired generator is registered. `args` and `kw` Arguments and keywords to be passed to the `generate()` method of the desired generator. While `args` should be a list, `kw` should be a dictionary. """ setSite(site) generator = getUtility(IReportGenerator, name=generator_name) report = generator.generate(site, *args, **kw) return report @implementer(IReportJob) class AsyncReportJob(AsyncJob): """An IJob that creates reports. `AsyncReportJob` instances are regular `AsyncJob` instances with a different constructor API. Instead of a callable to execute, you must pass a `site`, some `generator_name`, and additional args and keywords to create a report. The real work is done when an instance of this class is put into a queue. See :mod:`waeup.kofa.async` to learn more about asynchronous jobs. The `generator_name` must be the name under which an IReportGenerator utility was registered with the ZCA. The `site` must be a valid site or ``None``. The result of an `AsyncReportJob` is an IReport object. """ def __init__(self, site, generator_name, args=[], kw={}): self._generator_name = generator_name super(AsyncReportJob, self).__init__( report_job, site, generator_name, args=args, kw=kw) @property def finished(self): """A job is marked `finished` if it is completed. Please note: a finished report job does not neccessarily provide an IReport result. See meth:`failed`. """ return self.status == zc.async.interfaces.COMPLETED @property def failed(self): """A report job is marked failed iff it is finished and the result does not provide IReport. While a job is unfinished, the `failed` status is ``None``. Failed jobs normally provide a `traceback` to examine reasons. """ if not self.finished: return None if not IReport.providedBy(getattr(self, 'result', None)): return True return False @property def report_status(self): """The status of a report as translated string. """ if not self.finished: return STATUS_MAP['running'] if self.failed: return STATUS_MAP['failed'] return STATUS_MAP['finished'] @property def description(self): """A description gives a representation of the report to generate. The description contains the name of the report generator (trying to fetch the `name` attribute of the requested report generator) and the arguments and keywords passed in. Please note that this method is expensive! """ args = self.kwargs.get('args', []) kw = self.kwargs.get('kw', dict()) args = ', '.join(["%r" % x for x in args]) #kw = ', '.join(['%s = %r' % (key, val) for key, val in kw.items()]) kw = ', '.join(['%s' % val for val in kw.values()]) if len(args) and len(kw): str_repr = args + ', ' + kw else: str_repr = args + kw str_repr = '(' + str_repr + ')' try: generator = getUtility( IReportGenerator, name=self._generator_name) name = generator.title except: name = _('Unregistered Report Generator') return name + ' ' + str_repr @implementer(IReportJobContainer) class ReportJobContainer(object): """A mix-in that provides functionality for asynchronous report jobs. """ running_report_jobs = PersistentList() def start_report_job(self, generator_name, user_id, args=[], kw={}): """Start asynchronous export job. `generator_name` is the name of a report generator utility to be used. `user_id` is the ID of the user that triggers the report generation. `args` and `kw` args and keywords passed to the generators `generate()` method. The job_id is stored along with exporter name and user id in a persistent list. Returns the job ID of the job started. """ site = grok.getSite() manager = getUtility(IJobManager) job = AsyncReportJob(site, generator_name, args=args, kw=kw) job_id = manager.put(job) # Make sure that the persisted list is stored in ZODB self.running_report_jobs = PersistentList(self.running_report_jobs) self.running_report_jobs.append((job_id, generator_name, user_id),) return job_id def get_running_report_jobs(self, user_id=None): """Get report jobs for user with `user_id` as list of tuples. Each tuples holds ``, , `` in that order. The ```` is the utility name of the used report generator. If `user_id` is ``None``, all running report jobs are returned. """ entries = [] to_delete = [] manager = getUtility(IJobManager) for entry in self.running_report_jobs: if user_id is not None and entry[2] != user_id: continue if manager.get(entry[0]) is None: to_delete.append(entry) continue entries.append(entry) if to_delete: self.running_report_jobs = PersistentList( [x for x in self.running_report_jobs if x not in to_delete]) return entries def get_report_jobs_status(self, user_id=None): """Get running/completed report jobs for `user_id` as list of tuples. Each tuple holds ``, , `` in that order, where ```` and ```` are translated strings representing the status of the job and the human readable title of the report generator used. """ entries = self.get_running_report_jobs(user_id) result = [] manager = getUtility(IJobManager) for entry in entries: job = manager.get(entry[0]) status, status_translated = JOB_STATUS_MAP[job.status] generator = getUtility(IReportGenerator, name=entry[1]) generator_name = getattr(generator, 'title', 'unnamed') result.append((status, status_translated, generator_name)) return result def get_report_jobs_description(self, user_id=None): """Get running/completed report jobs fur `user_id` as list of tuples. The results contain enough information to render a status page or similar. Each tuple holds:: (``, , , , ``) in that order, with ```` The job id of the represented job. A string. ```` A human readable description of the report run. ```` The status of report jobs' status (translated) ```` Boolean indicating whether the job can be discarded. Only completed jobs can be discarded. ```` Boolean indicating whether the job result can be downloaded. This is only true if the job finished and didn't raised exceptions. ```` Datetime object indicating when the job was started. If ``user_id`` is ``None``, all jobs are returned. """ entries = self.get_running_report_jobs(user_id) result = [] for (job_id, gen_name, user_name) in entries: manager = getUtility(IJobManager) job = manager.get(job_id) status = job.report_status discardable = job.finished downloadable = job.finished and not job.failed starttime = getattr(job, 'begin_after', None) if starttime: starttime = starttime.astimezone(getUtility(IKofaUtils).tzinfo) starttime = starttime.strftime("%Y-%m-%d %H:%M:%S %Z") if not hasattr(job, 'description'): continue result.append((job_id, job.description, status, discardable, downloadable, starttime, user_name),) return result def delete_report_entry(self, entry): """Delete the report job denoted by `entry`. Removes `entry` from the local `running_report_jobs` list and also removes the regarding job via the local job manager. `entry` is a tuple ``(, , )`` as created by :meth:`start_report_job` or returned by :meth:`get_running_report_jobs`. """ manager = getUtility(IJobManager) manager.remove(entry[0], self) new_entries = [x for x in self.running_report_jobs if x != entry] self.running_report_jobs = PersistentList(new_entries) return def report_entry_from_job_id(self, job_id): """Get entry tuple for `job_id`. Returns ``None`` if no such entry can be found. """ for entry in self.running_report_jobs: if entry[0] == job_id: return entry return None @implementer(IReportsContainer) class ReportsContainer(grok.Container, ReportJobContainer): """A container for reports. """ @implementer(IKofaPluggable) class ReportsContainerPlugin(grok.GlobalUtility): """A plugin that updates sites to contain a reports container. """ grok.name('reports') deprecated_attributes = [] def setup(self, site, name, logger): """Add a reports container for `site`. If there is such an object already, we install a fresh one. """ if site.get('reports', None) is not None: del site['reports'] logger.info('Removed reports container for site "%s"' % name) return self.update(site, name, logger) def update(self, site, name, logger): """Install a reports container in `site`. If one exists already, do nothing. """ if site.get('reports', None) is not None: return site['reports'] = ReportsContainer() logger.info('Added reports container for site "%s"' % name) return