~ubuntu-branches/ubuntu/precise/trac/precise

« back to all changes in this revision

Viewing changes to trac/ticket/report.py

  • Committer: Bazaar Package Importer
  • Author(s): Luis Matos
  • Date: 2008-07-13 23:46:20 UTC
  • mfrom: (1.1.13 upstream)
  • Revision ID: james.westby@ubuntu.com-20080713234620-13ynpdpkbaymfg1z
Tags: 0.11-2
* Re-added python-setup-tools to build dependences. Closes: #490320 #468705
* New upstream release Closes: 489727
* Added sugestion for other vcs support available: git bazaar mercurial 
* Added spamfilter plugin to sugests
* Moved packaging from python-support to python-central
* Added an entry to the NEWS about the cgi Closes: #490275
* Updated 10_remove_trac_suffix_from_title patch to be used in 0.11

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- coding: utf-8 -*-
2
2
#
3
 
# Copyright (C) 2003-2006 Edgewall Software
 
3
# Copyright (C) 2003-2008 Edgewall Software
4
4
# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
5
5
# Copyright (C) 2006 Christian Boos <cboos@neuf.fr>
6
6
# Copyright (C) 2006 Matthew Good <trac@matt-good.net>
16
16
#
17
17
# Author: Jonas Borgström <jonas@edgewall.com>
18
18
 
 
19
import csv
19
20
import re
20
21
from StringIO import StringIO
21
22
 
22
 
from trac import util
 
23
from genshi.builder import tag
 
24
 
 
25
from trac.config import IntOption
23
26
from trac.core import *
24
27
from trac.db import get_column_names
 
28
from trac.mimeview import Context
25
29
from trac.perm import IPermissionRequestor
 
30
from trac.resource import Resource, ResourceNotFound
26
31
from trac.util import sorted
27
 
from trac.util.datefmt import format_date, format_time, format_datetime, \
28
 
                               http_date
29
 
from trac.util.html import html
30
 
from trac.util.text import unicode_urlencode
31
 
from trac.web import IRequestHandler
32
 
from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
33
 
from trac.wiki import wiki_to_html, IWikiSyntaxProvider, Formatter
 
32
from trac.util.datefmt import format_datetime, format_time
 
33
from trac.util.presentation import Paginator
 
34
from trac.util.text import to_unicode, unicode_urlencode
 
35
from trac.util.translation import _
 
36
from trac.web.api import IRequestHandler, RequestDone
 
37
from trac.web.chrome import add_ctxtnav, add_link, add_stylesheet, \
 
38
                            INavigationContributor, Chrome
 
39
from trac.wiki import IWikiSyntaxProvider, WikiParser
 
40
 
34
41
 
35
42
class ReportModule(Component):
36
43
 
37
44
    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
38
45
               IWikiSyntaxProvider)
39
46
 
 
47
    items_per_page = IntOption('report', 'items_per_page', 100,
 
48
        """Number of tickets displayed per page in ticket reports,
 
49
        by default (''since 0.11'')""")
 
50
 
 
51
    items_per_page_rss = IntOption('report', 'items_per_page_rss', 0,
 
52
        """Number of tickets displayed in the rss feeds for reports
 
53
        (''since 0.11'')""")
 
54
    
40
55
    # INavigationContributor methods
41
56
 
42
57
    def get_active_navigation_item(self, req):
43
58
        return 'tickets'
44
59
 
45
60
    def get_navigation_items(self, req):
46
 
        if not req.perm.has_permission('REPORT_VIEW'):
47
 
            return
48
 
        yield ('mainnav', 'tickets',
49
 
               html.A('View Tickets', href=req.href.report()))
 
61
        if 'REPORT_VIEW' in req.perm:
 
62
            yield ('mainnav', 'tickets', tag.a(_('View Tickets'),
 
63
                                               href=req.href.report()))
50
64
 
51
65
    # IPermissionRequestor methods  
52
66
 
65
79
            return True
66
80
 
67
81
    def process_request(self, req):
68
 
        req.perm.assert_permission('REPORT_VIEW')
 
82
        req.perm.require('REPORT_VIEW')
69
83
 
70
84
        # did the user ask for any special report?
71
85
        id = int(req.args.get('id', -1))
72
 
        action = req.args.get('action', 'list')
 
86
        action = req.args.get('action', 'view')
73
87
 
74
88
        db = self.env.get_db_cnx()
75
89
 
 
90
        data = {}
76
91
        if req.method == 'POST':
77
92
            if action == 'new':
78
93
                self._do_create(req, db)
81
96
            elif action == 'edit':
82
97
                self._do_save(req, db, id)
83
98
        elif action in ('copy', 'edit', 'new'):
84
 
            self._render_editor(req, db, id, action == 'copy')
 
99
            template = 'report_edit.html'
 
100
            data = self._render_editor(req, db, id, action=='copy')
85
101
        elif action == 'delete':
86
 
            self._render_confirm_delete(req, db, id)
 
102
            template = 'report_delete.html'
 
103
            data = self._render_confirm_delete(req, db, id)
87
104
        else:
88
 
            resp = self._render_view(req, db, id)
89
 
            if not resp:
90
 
               return None
91
 
            template, content_type = resp
92
 
            if content_type:
93
 
               return resp
 
105
            template, data, content_type = self._render_view(req, db, id)
 
106
            if content_type: # i.e. alternate format
 
107
               return template, data, content_type
94
108
 
95
109
        if id != -1 or action == 'new':
96
 
            add_link(req, 'up', req.href.report(), 'Available Reports')
97
 
 
98
 
            # Kludge: Reset session vars created by query module so that the
99
 
            # query navigation links on the ticket page don't confuse the user
100
 
            for var in ('query_constraints', 'query_time', 'query_tickets'):
101
 
                if req.session.has_key(var):
102
 
                    del req.session[var]
 
110
            add_ctxtnav(req, _('Available Reports'), href=req.href.report())
 
111
            add_link(req, 'up', req.href.report(), _('Available Reports'))
 
112
        else:
 
113
            add_ctxtnav(req, _('Available Reports'))
103
114
 
104
115
        # Kludge: only show link to custom query if the query module is actually
105
116
        # enabled
106
117
        from trac.ticket.query import QueryModule
107
 
        if req.perm.has_permission('TICKET_VIEW') and \
108
 
           self.env.is_component_enabled(QueryModule):
109
 
            req.hdf['report.query_href'] = req.href.query()
 
118
        if 'TICKET_VIEW' in req.perm and \
 
119
                self.env.is_component_enabled(QueryModule):
 
120
            add_ctxtnav(req, _('Custom Query'), href=req.href.query())
 
121
            data['query_href'] = req.href.query()
 
122
        else:
 
123
            data['query_href'] = None
110
124
 
111
125
        add_stylesheet(req, 'common/css/report.css')
112
 
        return 'report.cs', None
 
126
        return template, data, None
113
127
 
114
128
    # Internal methods
115
129
 
116
130
    def _do_create(self, req, db):
117
 
        req.perm.assert_permission('REPORT_CREATE')
 
131
        req.perm.require('REPORT_CREATE')
118
132
 
119
 
        if req.args.has_key('cancel'):
 
133
        if 'cancel' in req.args:
120
134
            req.redirect(req.href.report())
121
135
 
122
136
        title = req.args.get('title', '')
130
144
        req.redirect(req.href.report(id))
131
145
 
132
146
    def _do_delete(self, req, db, id):
133
 
        req.perm.assert_permission('REPORT_DELETE')
 
147
        req.perm.require('REPORT_DELETE')
134
148
 
135
 
        if req.args.has_key('cancel'):
 
149
        if 'cancel' in req.args:
136
150
            req.redirect(req.href.report(id))
137
151
 
138
152
        cursor = db.cursor()
141
155
        req.redirect(req.href.report())
142
156
 
143
157
    def _do_save(self, req, db, id):
144
 
        """
145
 
        Saves report changes to the database
146
 
        """
147
 
        req.perm.assert_permission('REPORT_MODIFY')
 
158
        """Save report changes to the database"""
 
159
        req.perm.require('REPORT_MODIFY')
148
160
 
149
 
        if not req.args.has_key('cancel'):
 
161
        if 'cancel' not in req.args:
150
162
            title = req.args.get('title', '')
151
163
            query = req.args.get('query', '')
152
164
            description = req.args.get('description', '')
157
169
        req.redirect(req.href.report(id))
158
170
 
159
171
    def _render_confirm_delete(self, req, db, id):
160
 
        req.perm.assert_permission('REPORT_DELETE')
 
172
        req.perm.require('REPORT_DELETE')
161
173
 
162
174
        cursor = db.cursor()
163
 
        cursor.execute("SELECT title FROM report WHERE id = %s", (id,))
164
 
        row = cursor.fetchone()
165
 
        if not row:
166
 
            raise TracError('Report %s does not exist.' % id,
167
 
                            'Invalid Report Number')
168
 
        req.hdf['title'] = 'Delete Report {%s} %s' % (id, row[0])
169
 
        req.hdf['report'] = {
170
 
            'id': id,
171
 
            'mode': 'delete',
172
 
            'title': row[0],
173
 
            'href': req.href.report(id)
174
 
        }
175
 
 
176
 
    def _render_editor(self, req, db, id, copy=False):
177
 
        if id == -1:
178
 
            req.perm.assert_permission('REPORT_CREATE')
179
 
            title = query = description = ''
 
175
        cursor.execute("SELECT title FROM report WHERE id=%s", (id,))
 
176
        for title, in cursor:
 
177
            return {'title': _('Delete Report {%(num)s} %(title)s', num=id,
 
178
                               title=title),
 
179
                    'action': 'delete',
 
180
                    'report': {'id': id, 'title': title}}
180
181
        else:
181
 
            req.perm.assert_permission('REPORT_MODIFY')
 
182
            raise TracError(_('Report %(num)s does not exist.', num=id),
 
183
                            _('Invalid Report Number'))
 
184
 
 
185
    def _render_editor(self, req, db, id, copy):
 
186
        if id != -1:
 
187
            req.perm.require('REPORT_MODIFY')
182
188
            cursor = db.cursor()
183
189
            cursor.execute("SELECT title,description,query FROM report "
184
190
                           "WHERE id=%s", (id,))
185
 
            row = cursor.fetchone()
186
 
            if not row:
187
 
                raise TracError('Report %s does not exist.' % id,
188
 
                                'Invalid Report Number')
189
 
            title = row[0] or ''
190
 
            description = row[1] or ''
191
 
            query = row[2] or ''
 
191
            for title, description, query in cursor:
 
192
                break
 
193
            else:
 
194
                raise TracError(_('Report %(num)s does not exist.', num=id),
 
195
                                _('Invalid Report Number'))
 
196
        else:
 
197
            req.perm.require('REPORT_CREATE')
 
198
            title = description = query = ''
 
199
 
 
200
        # an explicitly given 'query' parameter will override the saved query
 
201
        query = req.args.get('query', query)
192
202
 
193
203
        if copy:
194
204
            title += ' (copy)'
195
205
 
196
206
        if copy or id == -1:
197
 
            req.hdf['title'] = 'Create New Report'
198
 
            req.hdf['report.href'] = req.href.report()
199
 
            req.hdf['report.action'] = 'new'
 
207
            data = {'title': _('Create New Report'),
 
208
                    'action': 'new',
 
209
                    'error': None}
200
210
        else:
201
 
            req.hdf['title'] = 'Edit Report {%d} %s' % (id, title)
202
 
            req.hdf['report.href'] = req.href.report(id)
203
 
            req.hdf['report.action'] = 'edit'
 
211
            data = {'title': _('Edit Report {%(num)d} %(title)s', num=id,
 
212
                               title=title),
 
213
                    'action': 'edit',
 
214
                    'error': req.args.get('error')}
204
215
 
205
 
        req.hdf['report.id'] = id
206
 
        req.hdf['report.mode'] = 'edit'
207
 
        req.hdf['report.title'] = title
208
 
        req.hdf['report.sql'] = query
209
 
        req.hdf['report.description'] = description
 
216
        data['report'] = {'id': id, 'title': title,
 
217
                          'sql': query, 'description': description}
 
218
        return data
210
219
 
211
220
    def _render_view(self, req, db, id):
212
 
        """
213
 
        uses a user specified sql query to extract some information
214
 
        from the database and presents it as a html table.
215
 
        """
216
 
        actions = {'create': 'REPORT_CREATE', 'delete': 'REPORT_DELETE',
217
 
                   'modify': 'REPORT_MODIFY'}
218
 
        for action in [k for k,v in actions.items()
219
 
                       if req.perm.has_permission(v)]:
220
 
            req.hdf['report.can_' + action] = True
221
 
        req.hdf['report.href'] = req.href.report(id)
222
 
 
 
221
        """Retrieve the report results and pre-process them for rendering."""
223
222
        try:
224
223
            args = self.get_var_args(req)
225
224
        except ValueError,e:
226
 
            raise TracError, 'Report failed: %s' % e
227
 
 
228
 
        title, description, sql = self.get_info(db, id, args)
 
225
            raise TracError(_('Report failed: %(error)s', error=e))
 
226
 
 
227
        if id == -1:
 
228
            # If no particular report was requested, display
 
229
            # a list of available reports instead
 
230
            title = _('Available Reports')
 
231
            sql = ("SELECT id AS report, title, 'report' as _realm "
 
232
                   "FROM report ORDER BY report")
 
233
            description = _('This is a list of available reports.')
 
234
        else:
 
235
            cursor = db.cursor()
 
236
            cursor.execute("SELECT title,query,description from report "
 
237
                           "WHERE id=%s", (id,))
 
238
            for title, sql, description in cursor:
 
239
                break
 
240
            else:
 
241
                raise ResourceNotFound(
 
242
                    _('Report %(num)s does not exist.', num=id),
 
243
                    _('Invalid Report Number'))
 
244
 
 
245
        # If this is a saved custom query. redirect to the query module
 
246
        #
 
247
        # A saved query is either an URL query (?... or query:?...),
 
248
        # or a query language expression (query:...).
 
249
        #
 
250
        # It may eventually contain newlines, for increased clarity.
 
251
        #
 
252
        query = ''.join([line.strip() for line in sql.splitlines()])
 
253
        if query and (query[0] == '?' or query.startswith('query:?')):
 
254
            query = query[0] == '?' and query or query[6:]
 
255
            report_id = 'report=%s' % id
 
256
            if 'report=' in query:
 
257
                if not report_id in query:
 
258
                    err = _('When specified, the report number should be '
 
259
                            '"%(num)s".', num=id)
 
260
                    req.redirect(req.href.report(id, action='edit', error=err))
 
261
            else:
 
262
                if query[-1] != '?':
 
263
                    query += '&'
 
264
                query += report_id
 
265
            req.redirect(req.href.query() + query)
 
266
        elif query.startswith('query:'):
 
267
            try:
 
268
                from trac.ticket.query import Query, QuerySyntaxError
 
269
                query = Query.from_string(self.env, query[6:], report=id)
 
270
                req.redirect(query.get_href(req))
 
271
            except QuerySyntaxError, e:
 
272
                req.redirect(req.href.report(id, action='edit',
 
273
                                             error=to_unicode(e)))
229
274
 
230
275
        format = req.args.get('format')
231
276
        if format == 'sql':
232
 
            self._render_sql(req, id, title, description, sql)
233
 
            return
 
277
            self._send_sql(req, id, title, description, sql)
234
278
 
235
 
        req.hdf['report.mode'] = 'list'
236
279
        if id > 0:
237
280
            title = '{%i} %s' % (id, title)
238
 
        req.hdf['title'] = title
239
 
        req.hdf['report.title'] = title
240
 
        req.hdf['report.id'] = id
241
 
        req.hdf['report.description'] = wiki_to_html(description, self.env, req)
242
 
        if id != -1:
243
 
            self.add_alternate_links(req, args)
 
281
 
 
282
        report_resource = Resource('report', id)
 
283
        context = Context.from_request(req, report_resource)
 
284
        data = {'action': 'view', 'title': title,
 
285
                'report': {'id': id, 'resource': report_resource},
 
286
                'context': context,
 
287
                'title': title, 'description': description,
 
288
                'args': args, 'message': None, 'paginator':None}
 
289
 
 
290
        page = int(req.args.get('page', '1'))
 
291
        limit = self.items_per_page
 
292
        if req.args.get('format', '') == 'rss':
 
293
            limit = self.items_per_page_rss
 
294
        offset = (page - 1) * limit
 
295
        user = req.args.get('USER', None)
244
296
 
245
297
        try:
246
 
            cols, rows = self.execute_report(req, db, id, sql, args)
 
298
            cols, results, num_items = self.execute_paginated_report(
 
299
                    req, db, id, sql, args, limit, offset)
 
300
            results = [list(row) for row in results]
 
301
            numrows = len(results)
 
302
 
247
303
        except Exception, e:
248
 
            req.hdf['report.message'] = 'Report execution failed: %s' % e
249
 
            return 'report.cs', None
250
 
 
251
 
        # Convert the header info to HDF-format
252
 
        idx = 0
253
 
        for col in cols:
254
 
            title=col.capitalize()
255
 
            prefix = 'report.headers.%d' % idx
256
 
            req.hdf['%s.real' % prefix] = col
257
 
            if title.startswith('__') and title.endswith('__'):
258
 
                continue
259
 
            elif title[0] == '_' and title[-1] == '_':
260
 
                title = title[1:-1].capitalize()
261
 
                req.hdf[prefix + '.fullrow'] = 1
262
 
            elif title[0] == '_':
263
 
                continue
264
 
            elif title[-1] == '_':
265
 
                title = title[:-1]
266
 
                req.hdf[prefix + '.breakrow'] = 1
267
 
            req.hdf[prefix] = title
268
 
            idx = idx + 1
269
 
 
270
 
        if req.args.has_key('sort'):
271
 
            sortCol = req.args.get('sort')
272
 
            colIndex = None
273
 
            hiddenCols = 0
274
 
            for x in range(len(cols)):
275
 
                colName = cols[x]
276
 
                if colName == sortCol:
277
 
                    colIndex = x
278
 
                if colName.startswith('__') and colName.endswith('__'):
279
 
                    hiddenCols += 1
280
 
            if colIndex != None:
281
 
                k = 'report.headers.%d.asc' % (colIndex - hiddenCols)
282
 
                asc = req.args.get('asc', None)
283
 
                if asc:
284
 
                    asc = int(asc) # string '0' or '1' to int/boolean
285
 
                else:
286
 
                    asc = 1
287
 
                req.hdf[k] = asc
288
 
                def sortkey(row):
289
 
                    val = row[colIndex]
290
 
                    if isinstance(val, basestring):
291
 
                        val = val.lower()
292
 
                    return val
293
 
                rows = sorted(rows, key=sortkey, reverse=(not asc))
 
304
            data['message'] = _('Report execution failed: %(error)s',
 
305
                                error=to_unicode(e))
 
306
            return 'report_view.html', data, None
 
307
        paginator = None
 
308
        if id != -1 and limit > 0:
 
309
            asc = req.args.get('asc', None)
 
310
            sort_col = req.args.get('sort', None)
 
311
            paginator = Paginator(results, page - 1, limit, num_items)
 
312
            data['paginator'] = paginator
 
313
            if paginator.has_next_page:
 
314
                next_href = req.href.report(id, asc=asc, sort=sort_col,
 
315
                                            USER=user, page=page + 1)
 
316
                add_link(req, 'next', next_href, _('Next Page'))
 
317
            if paginator.has_previous_page:
 
318
                prev_href = req.href.report(id, asc=asc, sort=sort_col,
 
319
                                            USER=user, page=page - 1)
 
320
                add_link(req, 'prev', prev_href, _('Previous Page'))
 
321
 
 
322
            pagedata = []
 
323
            shown_pages = paginator.get_shown_pages(21)
 
324
            for p in shown_pages:
 
325
                pagedata.append([req.href.report(id, asc=asc, sort=sort_col, 
 
326
                                                 USER=user, page=p),
 
327
                                 None, str(p), _('Page %(num)d', num=p)])          
 
328
            fields = ['href', 'class', 'string', 'title']
 
329
            paginator.shown_pages = [dict(zip(fields, p)) for p in pagedata]
 
330
            paginator.current_page = {'href': None, 'class': 'current',
 
331
                                    'string': str(paginator.page + 1),
 
332
                                    'title': None}
 
333
            numrows = paginator.num_items
 
334
 
 
335
        sort_col = req.args.get('sort', '')
 
336
        asc = req.args.get('asc', 1)
 
337
        asc = bool(int(asc)) # string '0' or '1' to int/boolean
 
338
 
 
339
        # Place retrieved columns in groups, according to naming conventions
 
340
        #  * _col_ means fullrow, i.e. a group with one header
 
341
        #  * col_ means finish the current group and start a new one
 
342
        header_groups = [[]]
 
343
        for idx, col in enumerate(cols):
 
344
            header = {
 
345
                'col': col,
 
346
                'title': col.strip('_').capitalize(),
 
347
                'hidden': False,
 
348
                'asc': False
 
349
            }
 
350
 
 
351
            if col == sort_col:
 
352
                header['asc'] = asc
 
353
                if not paginator:
 
354
                    # this dict will have enum values for sorting
 
355
                    # and will be used in sortkey(), if non-empty:
 
356
                    sort_values = {}
 
357
                    if sort_col in ['status', 'resolution', 'priority', 
 
358
                                    'severity']:
 
359
                        # must fetch sort values for that columns
 
360
                        # instead of comparing them as strings
 
361
                        if not db:
 
362
                            db = self.env.get_db_cnx()
 
363
                        cursor = db.cursor()
 
364
                        cursor.execute("SELECT name," + 
 
365
                                       db.cast('value', 'int') + 
 
366
                                       " FROM enum WHERE type=%s", (sort_col,))
 
367
                        for name, value in cursor:
 
368
                            sort_values[name] = value
 
369
 
 
370
                    def sortkey(row):
 
371
                        val = row[idx]
 
372
                        # check if we have sort_values, then use them as keys.
 
373
                        if sort_values:
 
374
                            return sort_values.get(val)
 
375
                        # otherwise, continue with string comparison:
 
376
                        if isinstance(val, basestring):
 
377
                            val = val.lower()
 
378
                        return val
 
379
                    results = sorted(results, key=sortkey, reverse=(not asc))
 
380
 
 
381
            header_group = header_groups[-1]
 
382
 
 
383
            if col.startswith('__') and col.endswith('__'): # __col__
 
384
                header['hidden'] = True
 
385
            elif col[0] == '_' and col[-1] == '_':          # _col_
 
386
                header_group = []
 
387
                header_groups.append(header_group)
 
388
                header_groups.append([])
 
389
            elif col[0] == '_':                             # _col
 
390
                header['hidden'] = True
 
391
            elif col[-1] == '_':                            # col_
 
392
                header_groups.append([])
 
393
            header_group.append(header)
 
394
 
 
395
        # Structure the rows and cells:
 
396
        #  - group rows according to __group__ value, if defined
 
397
        #  - group cells the same way headers are grouped
 
398
        row_groups = []
 
399
        prev_group_value = None
 
400
        for row_idx, result in enumerate(results):
 
401
            col_idx = 0
 
402
            cell_groups = []
 
403
            row = {'cell_groups': cell_groups}
 
404
            realm = 'ticket'
 
405
            email_cells = []
 
406
            for header_group in header_groups:
 
407
                cell_group = []
 
408
                for header in header_group:
 
409
                    value = unicode(result[col_idx])
 
410
                    cell = {'value': value, 'header': header, 'index': col_idx}
 
411
                    col = header['col']
 
412
                    col_idx += 1
 
413
                    # Detect and create new group
 
414
                    if col == '__group__' and value != prev_group_value:
 
415
                        prev_group_value = value
 
416
                        # Brute force handling of email in group by header
 
417
                        row_groups.append(
 
418
                            (Chrome(self.env).format_author(req, value), []) )
 
419
                    # Other row properties
 
420
                    row['__idx__'] = row_idx
 
421
                    if col in ('__style__', '__color__',
 
422
                               '__fgcolor__', '__bgcolor__'):
 
423
                        row[col] = value
 
424
                    if col in ('report', 'ticket', 'id', '_id'):
 
425
                        row['id'] = value
 
426
                    # Special casing based on column name
 
427
                    col = col.strip('_')
 
428
                    if col in ('reporter', 'cc', 'owner'):
 
429
                        email_cells.append(cell)
 
430
                    elif col == 'realm':
 
431
                        realm = value
 
432
                    cell_group.append(cell)
 
433
                cell_groups.append(cell_group)
 
434
            resource = Resource(realm, row.get('id'))
 
435
            # FIXME: for now, we still need to hardcode the realm in the action
 
436
            if resource.realm.upper()+'_VIEW' not in req.perm(resource):
 
437
                continue
 
438
            if email_cells:
 
439
                for cell in email_cells:
 
440
                    emails = Chrome(self.env).format_emails(context(resource),
 
441
                                                            cell['value'])
 
442
                    result[cell['index']] = cell['value'] = emails
 
443
            row['resource'] = resource
 
444
            if row_groups:
 
445
                row_group = row_groups[-1][1]
 
446
            else:
 
447
                row_group = []
 
448
                row_groups = [(None, row_group)]
 
449
            row_group.append(row)
294
450
 
295
451
        # Get the email addresses of all known users
296
452
        email_map = {}
297
 
        for username, name, email in self.env.get_known_users():
298
 
            if email:
299
 
                email_map[username] = email
300
 
 
301
 
        # Convert the rows and cells to HDF-format
302
 
        row_idx = 0
303
 
        for row in rows:
304
 
            col_idx = 0
305
 
            numrows = len(row)
306
 
            for cell in row:
307
 
                cell = unicode(cell)
308
 
                column = cols[col_idx]
309
 
                value = {}
310
 
                # Special columns begin and end with '__'
311
 
                if column.startswith('__') and column.endswith('__'):
312
 
                    value['hidden'] = 1
313
 
                elif (column[0] == '_' and column[-1] == '_'):
314
 
                    value['fullrow'] = 1
315
 
                    column = column[1:-1]
316
 
                    req.hdf[prefix + '.breakrow'] = 1
317
 
                elif column[-1] == '_':
318
 
                    value['breakrow'] = 1
319
 
                    value['breakafter'] = 1
320
 
                    column = column[:-1]
321
 
                elif column[0] == '_':
322
 
                    value['hidehtml'] = 1
323
 
                    column = column[1:]
324
 
                if column in ('ticket', 'id', '_id', '#', 'summary'):
325
 
                    id_cols = [idx for idx, col in enumerate(cols)
326
 
                               if col in ('ticket', 'id', '_id')]
327
 
                    if id_cols:
328
 
                        id_val = row[id_cols[0]]
329
 
                        value['ticket_href'] = req.href.ticket(id_val)
330
 
                elif column == 'description':
331
 
                    desc = wiki_to_html(cell, self.env, req, db,
332
 
                                        absurls=(format == 'rss'))
333
 
                    value['parsed'] = format == 'rss' and unicode(desc) or desc
334
 
                elif column == 'reporter':
335
 
                    if cell.find('@') != -1:
336
 
                        value['rss'] = cell
337
 
                    elif cell in email_map:
338
 
                        value['rss'] = email_map[cell]
339
 
                elif column == 'report':
340
 
                    value['report_href'] = req.href.report(cell)
341
 
                elif column in ('time', 'date','changetime', 'created', 'modified'):
342
 
                    if cell == 'None':
343
 
                        value['date'] = value['time'] = cell
344
 
                        value['datetime'] = value['gmt'] = cell
345
 
                    else:
346
 
                        value['date'] = format_date(cell)
347
 
                        value['time'] = format_time(cell)
348
 
                        value['datetime'] = format_datetime(cell)
349
 
                        value['gmt'] = http_date(cell)
350
 
                prefix = 'report.items.%d.%s' % (row_idx, unicode(column))
351
 
                req.hdf[prefix] = unicode(cell)
352
 
                for key in value.keys():
353
 
                    req.hdf[prefix + '.' + key] = value[key]
354
 
 
355
 
                col_idx += 1
356
 
            row_idx += 1
357
 
        req.hdf['report.numrows'] = row_idx
 
453
        if Chrome(self.env).show_email_addresses:
 
454
            for username, name, email in self.env.get_known_users():
 
455
                if email:
 
456
                    email_map[username] = email
 
457
 
 
458
        data.update({'header_groups': header_groups,
 
459
                     'row_groups': row_groups,
 
460
                     'numrows': numrows,
 
461
                     'sorting_enabled': len(row_groups)==1,
 
462
                     'email_map': email_map})
 
463
 
 
464
        if id:
 
465
            self.add_alternate_links(req, args)
358
466
 
359
467
        if format == 'rss':
360
 
            return 'report_rss.cs', 'application/rss+xml'
 
468
            data['context'] = Context.from_request(req, report_resource,
 
469
                                                   absurls=True)
 
470
            return 'report.rss', data, 'application/rss+xml'
361
471
        elif format == 'csv':
362
472
            filename = id and 'report_%s.csv' % id or 'report.csv'
363
 
            self._render_csv(req, cols, rows, mimetype='text/csv',
364
 
                             filename=filename)
365
 
            return None
 
473
            self._send_csv(req, cols, results, mimetype='text/csv',
 
474
                           filename=filename)
366
475
        elif format == 'tab':
367
476
            filename = id and 'report_%s.tsv' % id or 'report.tsv'
368
 
            self._render_csv(req, cols, rows, '\t',
369
 
                             mimetype='text/tab-separated-values',
370
 
                             filename=filename)
371
 
            return None
372
 
 
373
 
        return 'report.cs', None
 
477
            self._send_csv(req, cols, results, '\t',
 
478
                           mimetype='text/tab-separated-values',
 
479
                           filename=filename)
 
480
        else:
 
481
            if id != -1:
 
482
                # reuse the session vars of the query module so that
 
483
                # the query navigation links on the ticket can be used to 
 
484
                # navigate report results as well
 
485
                try:
 
486
                    req.session['query_tickets'] = \
 
487
                        ' '.join([str(int(row['id']))
 
488
                                  for rg in row_groups for row in rg[1]])
 
489
                    #FIXME: I am not sure the extra args are necessary
 
490
                    req.session['query_href'] = \
 
491
                        req.href.report(id, asc=not asc and '0' or None, 
 
492
                                        sort=sort_col, USER=user, page=page)
 
493
                    # Kludge: we have to clear the other query session
 
494
                    # variables, but only if the above succeeded 
 
495
                    for var in ('query_constraints', 'query_time'):
 
496
                        if var in req.session:
 
497
                            del req.session[var]
 
498
                except (ValueError, KeyError):
 
499
                    pass
 
500
            return 'report_view.html', data, None
374
501
 
375
502
    def add_alternate_links(self, req, args):
376
503
        params = args
377
 
        if req.args.has_key('sort'):
 
504
        if 'sort' in req.args:
378
505
            params['sort'] = req.args['sort']
379
 
        if req.args.has_key('asc'):
 
506
        if 'asc' in req.args:
380
507
            params['asc'] = req.args['asc']
381
508
        href = ''
382
509
        if params:
383
510
            href = '&' + unicode_urlencode(params)
384
 
        add_link(req, 'alternate', '?format=rss' + href, 'RSS Feed',
 
511
        add_link(req, 'alternate', '?format=rss' + href, _('RSS Feed'),
385
512
                 'application/rss+xml', 'rss')
386
513
        add_link(req, 'alternate', '?format=csv' + href,
387
 
                 'Comma-delimited Text', 'text/plain')
 
514
                 _('Comma-delimited Text'), 'text/plain')
388
515
        add_link(req, 'alternate', '?format=tab' + href,
389
 
                 'Tab-delimited Text', 'text/plain')
390
 
        if req.perm.has_permission('REPORT_SQL_VIEW'):
391
 
            add_link(req, 'alternate', '?format=sql', 'SQL Query',
 
516
                 _('Tab-delimited Text'), 'text/plain')
 
517
        if 'REPORT_SQL_VIEW' in req.perm:
 
518
            add_link(req, 'alternate', '?format=sql', _('SQL Query'),
392
519
                     'text/plain')
393
520
 
394
521
    def execute_report(self, req, db, id, sql, args):
395
 
        sql, args = self.sql_sub_vars(req, sql, args, db)
 
522
        """Execute given sql report (0.10 backward compatibility method)
 
523
        
 
524
        :see: ``execute_paginated_report``
 
525
        """
 
526
        return self.execute_paginated_report(req, db, id, sql, args)[:2]
 
527
 
 
528
    def execute_paginated_report(self, req, db, id, sql, args, 
 
529
                                 limit=0, offset=0):
 
530
        sql, args = self.sql_sub_vars(sql, args, db)
396
531
        if not sql:
397
 
            raise TracError('Report %s has no SQL query.' % id)
398
 
        if sql.find('__group__') == -1:
399
 
            req.hdf['report.sorting.enabled'] = 1
400
 
 
401
 
        self.log.debug('Executing report with SQL "%s" (%s)', sql, args)
402
 
 
 
532
            raise TracError(_('Report %(num)s has no SQL query.', num=id))
 
533
        self.log.debug('Executing report with SQL "%s"' % sql)
 
534
        self.log.debug('Request args: %r' % req.args)
403
535
        cursor = db.cursor()
 
536
 
 
537
        num_items = 0
 
538
        if id != -1 and limit > 0:
 
539
            # The number of tickets is obtained.
 
540
            count_sql = 'SELECT COUNT(*) FROM (' + sql + ') AS tab'
 
541
            cursor.execute(count_sql, args)
 
542
            self.log.debug("Query SQL(Get num items): " + count_sql)
 
543
            for row in cursor:
 
544
                pass
 
545
            num_items = row[0]
 
546
    
 
547
            # The column name is obtained.
 
548
            get_col_name_sql = 'SELECT * FROM ( ' + sql + ' ) AS tab LIMIT 1'
 
549
            cursor.execute(get_col_name_sql, args)
 
550
            self.env.log.debug("Query SQL(Get col names): " + get_col_name_sql)
 
551
            cols = get_column_names(cursor)
 
552
 
 
553
            sort_col = req.args.get('sort', '')
 
554
            self.log.debug("Columns %r, Sort column %s" % (cols, sort_col))
 
555
            order_cols = []
 
556
            if sort_col:
 
557
                if '__group__' in cols:
 
558
                    order_cols.append('__group__')
 
559
                if sort_col in cols:
 
560
                    order_cols.append(sort_col)
 
561
                else:
 
562
                    raise TracError(_('Query parameter "sort=%(sort_col)s" '
 
563
                                      ' is invalid', sort_col=sort_col))
 
564
 
 
565
            # The report-query results is obtained
 
566
            asc = req.args.get('asc', '1')
 
567
            asc_str = asc == '1' and 'ASC' or 'DESC'
 
568
            order_by = ''
 
569
            if len(order_cols) != 0:
 
570
                order = ', '.join(order_cols)
 
571
                order_by = " ".join([' ORDER BY', order, asc_str])
 
572
            sql = " ".join(['SELECT * FROM (', sql, ') AS tab', order_by])
 
573
            sql =" ".join([sql, 'LIMIT', str(limit), 'OFFSET', str(offset)])
 
574
            self.log.debug("Query SQL: " + sql)
404
575
        cursor.execute(sql, args)
405
 
 
406
576
        # FIXME: fetchall should probably not be used.
407
577
        info = cursor.fetchall() or []
408
578
        cols = get_column_names(cursor)
409
579
 
410
580
        db.rollback()
411
581
 
412
 
        return cols, info
413
 
 
414
 
    def get_info(self, db, id, args):
415
 
        if id == -1:
416
 
            # If no particular report was requested, display
417
 
            # a list of available reports instead
418
 
            title = 'Available Reports'
419
 
            sql = 'SELECT id AS report, title FROM report ORDER BY report'
420
 
            description = 'This is a list of reports available.'
421
 
        else:
422
 
            cursor = db.cursor()
423
 
            cursor.execute("SELECT title,query,description from report "
424
 
                           "WHERE id=%s", (id,))
425
 
            row = cursor.fetchone()
426
 
            if not row:
427
 
                raise TracError('Report %d does not exist.' % id,
428
 
                                'Invalid Report Number')
429
 
            title = row[0] or ''
430
 
            sql = row[1]
431
 
            description = row[2] or ''
432
 
 
433
 
        return [title, description, sql]
 
582
        return cols, info, num_items
434
583
 
435
584
    def get_var_args(self, req):
436
585
        report_args = {}
440
589
            report_args[arg] = req.args.get(arg)
441
590
 
442
591
        # Set some default dynamic variables
443
 
        if not report_args.has_key('USER'):
 
592
        if 'USER' not in report_args:
444
593
            report_args['USER'] = req.authname
445
594
 
446
595
        return report_args
447
596
 
448
 
    def sql_sub_vars(self, req, sql, args, db=None):
 
597
    def sql_sub_vars(self, sql, args, db=None):
449
598
        if db is None:
450
599
            db = self.env.get_db_cnx()
451
600
        values = []
453
602
            try:
454
603
                arg = args[aname]
455
604
            except KeyError:
456
 
                raise TracError("Dynamic variable '$%s' not defined." \
457
 
                                % aname)
458
 
            req.hdf['report.var.' + aname] = arg
 
605
                raise TracError(_("Dynamic variable '%(name)s' not defined.",
 
606
                                  name='$%s' % aname))
459
607
            values.append(arg)
460
608
 
461
609
        var_re = re.compile("[$]([A-Z]+)")
488
636
                sql_io.write(var_re.sub(repl, expr))
489
637
        return sql_io.getvalue(), values
490
638
 
491
 
    def _render_csv(self, req, cols, rows, sep=',', mimetype='text/plain',
492
 
                    filename=None):
 
639
    def _send_csv(self, req, cols, rows, sep=',', mimetype='text/plain',
 
640
                  filename=None):
493
641
        req.send_response(200)
494
642
        req.send_header('Content-Type', mimetype + ';charset=utf-8')
495
643
        if filename:
496
644
            req.send_header('Content-Disposition', 'filename=' + filename)
497
645
        req.end_headers()
498
646
 
499
 
        req.write(sep.join(cols) + '\r\n')
 
647
        def iso_time(t):
 
648
            return format_time(t, 'iso8601')
 
649
 
 
650
        def iso_datetime(dt):
 
651
            return format_datetime(dt, 'iso8601')
 
652
 
 
653
        col_conversions = {
 
654
            'time': iso_time,
 
655
            'datetime': iso_datetime,
 
656
            'changetime': iso_datetime,
 
657
            'date': iso_datetime,
 
658
            'created': iso_datetime,
 
659
            'modified': iso_datetime,
 
660
        }
 
661
 
 
662
        converters = [col_conversions.get(c.strip('_'), unicode) for c in cols]
 
663
 
 
664
        writer = csv.writer(req, delimiter=sep)
 
665
        writer.writerow([unicode(c).encode('utf-8') for c in cols])
500
666
        for row in rows:
501
 
            req.write(sep.join(
502
 
                [unicode(c).replace(sep,"_")
503
 
                 .replace('\n',' ').replace('\r',' ') for c in row]) + '\r\n')
504
 
 
505
 
    def _render_sql(self, req, id, title, description, sql):
506
 
        req.perm.assert_permission('REPORT_SQL_VIEW')
 
667
            row = list(row)
 
668
            for i in xrange(len(row)):
 
669
                row[i] = converters[i](row[i]).encode('utf-8')
 
670
            writer.writerow(row)
 
671
 
 
672
        raise RequestDone
 
673
 
 
674
    def _send_sql(self, req, id, title, description, sql):
 
675
        req.perm.require('REPORT_SQL_VIEW')
507
676
        req.send_response(200)
508
677
        req.send_header('Content-Type', 'text/plain;charset=utf-8')
509
678
        if id:
515
684
        if description:
516
685
            req.write('-- %s\n\n' % '\n-- '.join(description.splitlines()))
517
686
        req.write(sql)
 
687
        raise RequestDone
518
688
        
519
689
    # IWikiSyntaxProvider methods
520
690
    
522
692
        yield ('report', self._format_link)
523
693
 
524
694
    def get_wiki_syntax(self):
525
 
        yield (r"!?\{(?P<it_report>%s\s*)\d+\}" % Formatter.INTERTRAC_SCHEME,
 
695
        yield (r"!?\{(?P<it_report>%s\s*)\d+\}" % WikiParser.INTERTRAC_SCHEME,
526
696
               lambda x, y, z: self._format_link(x, 'report', y[1:-1], y, z))
527
697
 
528
698
    def _format_link(self, formatter, ns, target, label, fullmatch=None):
531
701
        if intertrac:
532
702
            return intertrac
533
703
        report, args, fragment = formatter.split_link(target)
534
 
        return html.A(label, href=formatter.href.report(report) + args,
535
 
                      class_='report')
 
704
        return tag.a(label, href=formatter.href.report(report) + args,
 
705
                     class_='report')