Mercurial > genshi > mirror
diff examples/trac/trac/ticket/report.py @ 39:93b4dcbafd7b trunk
Copy Trac to main branch.
author | cmlenz |
---|---|
date | Mon, 03 Jul 2006 18:53:27 +0000 |
parents | |
children |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/examples/trac/trac/ticket/report.py @@ -0,0 +1,503 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2003-2006 Edgewall Software +# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com> +# Copyright (C) 2006 Christian Boos <cboos@neuf.fr> +# Copyright (C) 2006 Matthew Good <trac@matt-good.net> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.com/license.html. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://projects.edgewall.com/trac/. +# +# Author: Jonas Borgström <jonas@edgewall.com> + +import re +from StringIO import StringIO +import urllib + +from trac import util +from trac.core import * +from trac.db import get_column_names +from trac.perm import IPermissionRequestor +from trac.util import sorted +from trac.util.datefmt import format_date, format_time, format_datetime, \ + http_date +from trac.util.markup import html +from trac.web import IRequestHandler +from trac.web.chrome import add_link, add_stylesheet, INavigationContributor +from trac.wiki import wiki_to_html, IWikiSyntaxProvider, Formatter + + +class ReportModule(Component): + + implements(INavigationContributor, IPermissionRequestor, IRequestHandler, + IWikiSyntaxProvider) + + # INavigationContributor methods + + def get_active_navigation_item(self, req): + return 'tickets' + + def get_navigation_items(self, req): + if not req.perm.has_permission('REPORT_VIEW'): + return + yield ('mainnav', 'tickets', + html.A('View Tickets', href=req.href.report())) + + # IPermissionRequestor methods + + def get_permission_actions(self): + actions = ['REPORT_CREATE', 'REPORT_DELETE', 'REPORT_MODIFY', + 'REPORT_SQL_VIEW', 'REPORT_VIEW'] + return actions + [('REPORT_ADMIN', actions)] + + # IRequestHandler methods + + def match_request(self, req): + match = re.match(r'/report(?:/([0-9]+))?', req.path_info) + if match: + if match.group(1): + req.args['id'] = match.group(1) + return True + + def process_request(self, req): + req.perm.assert_permission('REPORT_VIEW') + + # did the user ask for any special report? + id = int(req.args.get('id', -1)) + action = req.args.get('action', 'list') + + db = self.env.get_db_cnx() + + if req.method == 'POST': + if action == 'new': + self._do_create(req, db) + elif action == 'delete': + self._do_delete(req, db, id) + elif action == 'edit': + self._do_save(req, db, id) + elif action in ('copy', 'edit', 'new'): + self._render_editor(req, db, id, action == 'copy') + elif action == 'delete': + self._render_confirm_delete(req, db, id) + else: + resp = self._render_view(req, db, id) + if not resp: + return None + template, content_type = resp + if content_type: + return resp + + if id != -1 or action == 'new': + add_link(req, 'up', req.href.report(), 'Available Reports') + + # Kludge: Reset session vars created by query module so that the + # query navigation links on the ticket page don't confuse the user + for var in ('query_constraints', 'query_time', 'query_tickets'): + if req.session.has_key(var): + del req.session[var] + + # Kludge: only show link to custom query if the query module is actually + # enabled + from trac.ticket.query import QueryModule + if req.perm.has_permission('TICKET_VIEW') and \ + self.env.is_component_enabled(QueryModule): + req.hdf['report.query_href'] = req.href.query() + + add_stylesheet(req, 'common/css/report.css') + return 'report.cs', None + + # Internal methods + + def _do_create(self, req, db): + req.perm.assert_permission('REPORT_CREATE') + + if req.args.has_key('cancel'): + req.redirect(req.href.report()) + + title = req.args.get('title', '') + query = req.args.get('query', '') + description = req.args.get('description', '') + cursor = db.cursor() + cursor.execute("INSERT INTO report (title,query,description) " + "VALUES (%s,%s,%s)", (title, query, description)) + id = db.get_last_id(cursor, 'report') + db.commit() + req.redirect(req.href.report(id)) + + def _do_delete(self, req, db, id): + req.perm.assert_permission('REPORT_DELETE') + + if req.args.has_key('cancel'): + req.redirect(req.href.report(id)) + + cursor = db.cursor() + cursor.execute("DELETE FROM report WHERE id=%s", (id,)) + db.commit() + req.redirect(req.href.report()) + + def _do_save(self, req, db, id): + """ + Saves report changes to the database + """ + req.perm.assert_permission('REPORT_MODIFY') + + if not req.args.has_key('cancel'): + title = req.args.get('title', '') + query = req.args.get('query', '') + description = req.args.get('description', '') + cursor = db.cursor() + cursor.execute("UPDATE report SET title=%s,query=%s,description=%s " + "WHERE id=%s", (title, query, description, id)) + db.commit() + req.redirect(req.href.report(id)) + + def _render_confirm_delete(self, req, db, id): + req.perm.assert_permission('REPORT_DELETE') + + cursor = db.cursor() + cursor.execute("SELECT title FROM report WHERE id = %s", (id,)) + row = cursor.fetchone() + if not row: + raise TracError('Report %s does not exist.' % id, + 'Invalid Report Number') + req.hdf['title'] = 'Delete Report {%s} %s' % (id, row[0]) + req.hdf['report'] = { + 'id': id, + 'mode': 'delete', + 'title': row[0], + 'href': req.href.report(id) + } + + def _render_editor(self, req, db, id, copy=False): + if id == -1: + req.perm.assert_permission('REPORT_CREATE') + title = query = description = '' + else: + req.perm.assert_permission('REPORT_MODIFY') + cursor = db.cursor() + cursor.execute("SELECT title,description,query FROM report " + "WHERE id=%s", (id,)) + row = cursor.fetchone() + if not row: + raise TracError('Report %s does not exist.' % id, + 'Invalid Report Number') + title = row[0] or '' + description = row[1] or '' + query = row[2] or '' + + if copy: + title += ' (copy)' + + if copy or id == -1: + req.hdf['title'] = 'Create New Report' + req.hdf['report.href'] = req.href.report() + req.hdf['report.action'] = 'new' + else: + req.hdf['title'] = 'Edit Report {%d} %s' % (id, title) + req.hdf['report.href'] = req.href.report(id) + req.hdf['report.action'] = 'edit' + + req.hdf['report.id'] = id + req.hdf['report.mode'] = 'edit' + req.hdf['report.title'] = title + req.hdf['report.sql'] = query + req.hdf['report.description'] = description + + def _render_view(self, req, db, id): + """ + uses a user specified sql query to extract some information + from the database and presents it as a html table. + """ + actions = {'create': 'REPORT_CREATE', 'delete': 'REPORT_DELETE', + 'modify': 'REPORT_MODIFY'} + for action in [k for k,v in actions.items() + if req.perm.has_permission(v)]: + req.hdf['report.can_' + action] = True + req.hdf['report.href'] = req.href.report(id) + + try: + args = self.get_var_args(req) + except ValueError,e: + raise TracError, 'Report failed: %s' % e + + title, description, sql = self.get_info(db, id, args) + + format = req.args.get('format') + if format == 'sql': + self._render_sql(req, id, title, description, sql) + return + + req.hdf['report.mode'] = 'list' + if id > 0: + title = '{%i} %s' % (id, title) + req.hdf['title'] = title + req.hdf['report.title'] = title + req.hdf['report.id'] = id + req.hdf['report.description'] = wiki_to_html(description, self.env, req) + if id != -1: + self.add_alternate_links(req, args) + + try: + cols, rows = self.execute_report(req, db, id, sql, args) + except Exception, e: + req.hdf['report.message'] = 'Report execution failed: %s' % e + return 'report.cs', None + + # Convert the header info to HDF-format + idx = 0 + for col in cols: + title=col.capitalize() + prefix = 'report.headers.%d' % idx + req.hdf['%s.real' % prefix] = col[0] + if title.startswith('__') and title.endswith('__'): + continue + elif title[0] == '_' and title[-1] == '_': + title = title[1:-1].capitalize() + req.hdf[prefix + '.fullrow'] = 1 + elif title[0] == '_': + continue + elif title[-1] == '_': + title = title[:-1] + req.hdf[prefix + '.breakrow'] = 1 + req.hdf[prefix] = title + idx = idx + 1 + + if req.args.has_key('sort'): + sortCol = req.args.get('sort') + colIndex = None + hiddenCols = 0 + for x in range(len(cols)): + colName = cols[x] + if colName == sortCol: + colIndex = x + if colName.startswith('__') and colName.endswith('__'): + hiddenCols += 1 + if colIndex != None: + k = 'report.headers.%d.asc' % (colIndex - hiddenCols) + asc = req.args.get('asc', None) + if asc: + asc = int(asc) # string '0' or '1' to int/boolean + else: + asc = 1 + req.hdf[k] = asc + def sortkey(row): + val = row[colIndex] + if isinstance(val, basestring): + val = val.lower() + return val + rows = sorted(rows, key=sortkey, reverse=(not asc)) + + # Get the email addresses of all known users + email_map = {} + for username, name, email in self.env.get_known_users(): + if email: + email_map[username] = email + + # Convert the rows and cells to HDF-format + row_idx = 0 + for row in rows: + col_idx = 0 + numrows = len(row) + for cell in row: + cell = unicode(cell) + column = cols[col_idx] + value = {} + # Special columns begin and end with '__' + if column.startswith('__') and column.endswith('__'): + value['hidden'] = 1 + elif (column[0] == '_' and column[-1] == '_'): + value['fullrow'] = 1 + column = column[1:-1] + req.hdf[prefix + '.breakrow'] = 1 + elif column[-1] == '_': + value['breakrow'] = 1 + value['breakafter'] = 1 + column = column[:-1] + elif column[0] == '_': + value['hidehtml'] = 1 + column = column[1:] + if column in ('ticket', 'id', '_id', '#', 'summary'): + id_cols = [idx for idx, col in enumerate(cols) + if col in ('ticket', 'id', '_id')] + if id_cols: + id_val = row[id_cols[0]] + value['ticket_href'] = req.href.ticket(id_val) + elif column == 'description': + desc = wiki_to_html(cell, self.env, req, db, + absurls=(format == 'rss')) + value['parsed'] = format == 'rss' and unicode(desc) or desc + elif column == 'reporter': + if cell.find('@') != -1: + value['rss'] = cell + elif cell in email_map: + value['rss'] = email_map[cell] + elif column == 'report': + value['report_href'] = req.href.report(cell) + elif column in ('time', 'date','changetime', 'created', 'modified'): + value['date'] = format_date(cell) + value['time'] = format_time(cell) + value['datetime'] = format_datetime(cell) + value['gmt'] = http_date(cell) + prefix = 'report.items.%d.%s' % (row_idx, unicode(column)) + req.hdf[prefix] = unicode(cell) + for key in value.keys(): + req.hdf[prefix + '.' + key] = value[key] + + col_idx += 1 + row_idx += 1 + req.hdf['report.numrows'] = row_idx + + if format == 'rss': + return 'report_rss.cs', 'application/rss+xml' + elif format == 'csv': + self._render_csv(req, cols, rows) + return None + elif format == 'tab': + self._render_csv(req, cols, rows, '\t') + return None + + return 'report.cs', None + + def add_alternate_links(self, req, args): + params = args + if req.args.has_key('sort'): + params['sort'] = req.args['sort'] + if req.args.has_key('asc'): + params['asc'] = req.args['asc'] + href = '' + if params: + href = '&' + urllib.urlencode(params) + add_link(req, 'alternate', '?format=rss' + href, 'RSS Feed', + 'application/rss+xml', 'rss') + add_link(req, 'alternate', '?format=csv' + href, + 'Comma-delimited Text', 'text/plain') + add_link(req, 'alternate', '?format=tab' + href, + 'Tab-delimited Text', 'text/plain') + if req.perm.has_permission('REPORT_SQL_VIEW'): + add_link(req, 'alternate', '?format=sql', 'SQL Query', + 'text/plain') + + def execute_report(self, req, db, id, sql, args): + sql, args = self.sql_sub_vars(req, sql, args) + if not sql: + raise TracError('Report %s has no SQL query.' % id) + if sql.find('__group__') == -1: + req.hdf['report.sorting.enabled'] = 1 + + self.log.debug('Executing report with SQL "%s" (%s)', sql, args) + + cursor = db.cursor() + cursor.execute(sql, args) + + # FIXME: fetchall should probably not be used. + info = cursor.fetchall() or [] + cols = get_column_names(cursor) + + db.rollback() + + return cols, info + + def get_info(self, db, id, args): + if id == -1: + # If no particular report was requested, display + # a list of available reports instead + title = 'Available Reports' + sql = 'SELECT id AS report, title FROM report ORDER BY report' + description = 'This is a list of reports available.' + else: + cursor = db.cursor() + cursor.execute("SELECT title,query,description from report " + "WHERE id=%s", (id,)) + row = cursor.fetchone() + if not row: + raise TracError('Report %d does not exist.' % id, + 'Invalid Report Number') + title = row[0] or '' + sql = row[1] + description = row[2] or '' + + return [title, description, sql] + + def get_var_args(self, req): + report_args = {} + for arg in req.args.keys(): + if not arg == arg.upper(): + continue + report_args[arg] = req.args.get(arg) + + # Set some default dynamic variables + if not report_args.has_key('USER'): + report_args['USER'] = req.authname + + return report_args + + def sql_sub_vars(self, req, sql, args): + values = [] + def add_value(aname): + try: + arg = args[aname] + except KeyError: + raise TracError("Dynamic variable '$%s' not defined." \ + % aname) + req.hdf['report.var.' + aname] = arg + values.append(arg) + + # simple parameter substitution + def repl(match): + add_value(match.group(1)) + return '%s' + + var_re = re.compile("'?[$]([A-Z]+)'?") + sql_io = StringIO() + + # break SQL into literals and non-literals to handle replacing + # variables within them with query parameters + for expr in re.split("('(?:[^']|(?:''))*')", sql): + sql_io.write(var_re.sub(repl, expr)) + return sql_io.getvalue(), values + + def _render_csv(self, req, cols, rows, sep=','): + req.send_response(200) + req.send_header('Content-Type', 'text/plain;charset=utf-8') + req.end_headers() + + req.write(sep.join(cols) + '\r\n') + for row in rows: + req.write(sep.join( + [unicode(c).replace(sep,"_") + .replace('\n',' ').replace('\r',' ') for c in row]) + '\r\n') + + def _render_sql(self, req, id, title, description, sql): + req.perm.assert_permission('REPORT_SQL_VIEW') + req.send_response(200) + req.send_header('Content-Type', 'text/plain;charset=utf-8') + req.end_headers() + + req.write('-- ## %s: %s ## --\n\n' % (id, title)) + if description: + req.write('-- %s\n\n' % '\n-- '.join(description.splitlines())) + req.write(sql) + + # IWikiSyntaxProvider methods + + def get_link_resolvers(self): + yield ('report', self._format_link) + + def get_wiki_syntax(self): + yield (r"!?\{(?P<it_report>%s\s*)\d+\}" % Formatter.INTERTRAC_SCHEME, + lambda x, y, z: self._format_link(x, 'report', y[1:-1], y, z)) + + def _format_link(self, formatter, ns, target, label, fullmatch=None): + intertrac = formatter.shorthand_intertrac_helper(ns, target, label, + fullmatch) + if intertrac: + return intertrac + report, args, fragment = formatter.split_link(target) + return html.A(label, href=formatter.href.report(report) + args, + class_='report')