def _render_list(self, req): """Render the list of available reports.""" sort = req.args.get("sort", "report") asc = bool(int(req.args.get("asc", 1))) format = req.args.get("format") rows = self.env.db_query( """ SELECT id, title, description FROM report ORDER BY %s %s """ % ("title" if sort == "title" else "id", "" if asc else "DESC") ) if format == "rss": data = {"rows": rows} return "report_list.rss", data, "application/rss+xml" elif format == "csv": self._send_csv(req, ["report", "title", "description"], rows, mimetype="text/csv", filename="reports.csv") elif format == "tab": self._send_csv( req, ["report", "title", "description"], rows, "\t", mimetype="text/tab-separated-values", filename="reports.tsv", ) def report_href(**kwargs): return req.href.report(sort=req.args.get("sort"), asc="1" if asc else "0", **kwargs) add_link( req, "alternate", auth_link(req, report_href(format="rss")), _("RSS Feed"), "application/rss+xml", "rss" ) add_link(req, "alternate", report_href(format="csv"), _("Comma-delimited Text"), "text/plain") add_link(req, "alternate", report_href(format="tab"), _("Tab-delimited Text"), "text/plain") reports = [ ( id, title, description, "REPORT_MODIFY" in req.perm("report", id), "REPORT_DELETE" in req.perm("report", id), ) for id, title, description in rows ] data = {"reports": reports, "sort": sort, "asc": asc} return "report_list.html", data, None
def process_request(self, req): req.perm.require('MILESTONE_VIEW') show = req.args.getlist('show') if 'all' in show: show = ['completed'] milestones = Milestone.select(self.env, 'completed' in show) if 'noduedate' in show: milestones = [ m for m in milestones if m.due is not None or m.completed ] milestones = [ m for m in milestones if 'MILESTONE_VIEW' in req.perm(m.resource) ] stats = [] queries = [] for milestone in milestones: tickets = get_tickets_for_milestone(self.env, milestone=milestone.name, field='owner') tickets = apply_ticket_permissions(self.env, req, tickets) stat = get_ticket_stats(self.stats_provider, tickets) stats.append( milestone_stats_data(self.env, req, stat, milestone.name)) #milestone['tickets'] = tickets # for the iCalendar view if req.args.get('format') == 'ics': self._render_ics(req, milestones) return # FIXME should use the 'webcal:' scheme, probably username = None if req.authname and req.authname != 'anonymous': username = req.authname icshref = req.href.roadmap(show=show, user=username, format='ics') add_link(req, 'alternate', auth_link(req, icshref), _('iCalendar'), 'text/calendar', 'ics') data = { 'milestones': milestones, 'milestone_stats': stats, 'queries': queries, 'show': show, } add_stylesheet(req, 'common/css/roadmap.css') return 'roadmap.html', data, None
def _render_list(self, req): """Render the list of available reports.""" sort = req.args.get('sort', 'report') asc = bool(int(req.args.get('asc', 1))) format = req.args.get('format') rows = self.env.db_query( """ SELECT id, title, description FROM report ORDER BY %s %s """ % ('title' if sort == 'title' else 'id', '' if asc else 'DESC')) rows = [(id, title, description) for id, title, description in rows if 'REPORT_VIEW' in req.perm('report', id)] if format == 'rss': data = {'rows': rows} return 'report_list.rss', data, 'application/rss+xml' elif format == 'csv': self._send_csv(req, ['report', 'title', 'description'], rows, mimetype='text/csv', filename='reports.csv') elif format == 'tab': self._send_csv(req, ['report', 'title', 'description'], rows, '\t', mimetype='text/tab-separated-values', filename='reports.tsv') def report_href(**kwargs): return req.href.report(sort=req.args.get('sort'), asc='1' if asc else '0', **kwargs) add_link(req, 'alternate', auth_link(req, report_href(format='rss')), _('RSS Feed'), 'application/rss+xml', 'rss') add_link(req, 'alternate', report_href(format='csv'), _('Comma-delimited Text'), 'text/plain') add_link(req, 'alternate', report_href(format='tab'), _('Tab-delimited Text'), 'text/plain') reports = [(id, title, description, 'REPORT_MODIFY' in req.perm('report', id), 'REPORT_DELETE' in req.perm('report', id)) for id, title, description in rows] data = {'reports': reports, 'sort': sort, 'asc': asc} return 'report_list.html', data, None
def process_request(self, req): req.perm.require('MILESTONE_VIEW') show = req.args.getlist('show') if 'all' in show: show = ['completed'] milestones = Milestone.select(self.env, 'completed' in show) if 'noduedate' in show: milestones = [m for m in milestones if m.due is not None or m.completed] milestones = [m for m in milestones if 'MILESTONE_VIEW' in req.perm(m.resource)] stats = [] queries = [] for milestone in milestones: tickets = get_tickets_for_milestone( self.env, milestone=milestone.name, field='owner') tickets = apply_ticket_permissions(self.env, req, tickets) stat = get_ticket_stats(self.stats_provider, tickets) stats.append(milestone_stats_data(self.env, req, stat, milestone.name)) #milestone['tickets'] = tickets # for the iCalendar view if req.args.get('format') == 'ics': self._render_ics(req, milestones) return # FIXME should use the 'webcal:' scheme, probably username = None if req.authname and req.authname != 'anonymous': username = req.authname icshref = req.href.roadmap(show=show, user=username, format='ics') add_link(req, 'alternate', auth_link(req, icshref), _('iCalendar'), 'text/calendar', 'ics') data = { 'milestones': milestones, 'milestone_stats': stats, 'queries': queries, 'show': show, } add_stylesheet(req, 'common/css/roadmap.css') return 'roadmap.html', data, None
def _render_list(self, req): """Render the list of available reports.""" sort = req.args.get('sort', 'report') asc = req.args.getint('asc', 1, min=0, max=1) format = req.args.get('format') rows = [(report.id, report.title, report.description) for report in Report.select(self.env, sort, bool(asc)) if 'REPORT_VIEW' in req.perm(self.realm, report.id)] if format == 'rss': data = {'rows': rows} return 'report_list.rss', data, 'application/rss+xml' elif format == 'csv': self._send_csv(req, ['report', 'title', 'description'], rows, mimetype='text/csv', filename='reports.csv') elif format == 'tab': self._send_csv(req, ['report', 'title', 'description'], rows, '\t', mimetype='text/tab-separated-values', filename='reports.tsv') def report_href(**kwargs): return req.href.report(sort=req.args.get('sort'), asc=asc, **kwargs) add_link(req, 'alternate', auth_link(req, report_href(format='rss')), _('RSS Feed'), 'application/rss+xml', 'rss') add_link(req, 'alternate', report_href(format='csv'), _('Comma-delimited Text'), 'text/plain') add_link(req, 'alternate', report_href(format='tab'), _('Tab-delimited Text'), 'text/plain') reports = [(id, title, description, 'REPORT_MODIFY' in req.perm(self.realm, id), 'REPORT_DELETE' in req.perm(self.realm, id)) for id, title, description in rows] data = {'reports': reports, 'sort': sort, 'asc': asc} return 'report_list.html', data, None
def _render_list(self, req): """Render the list of available reports.""" sort = req.args.get('sort', 'report') asc = bool(int(req.args.get('asc', 1))) format = req.args.get('format') rows = self.env.db_query(""" SELECT id, title, description FROM report ORDER BY %s %s """ % ('title' if sort == 'title' else 'id', '' if asc else 'DESC')) rows = [(id, title, description) for id, title, description in rows if 'REPORT_VIEW' in req.perm('report', id)] if format == 'rss': data = {'rows': rows} return 'report_list.rss', data, 'application/rss+xml' elif format == 'csv': self._send_csv(req, ['report', 'title', 'description'], rows, mimetype='text/csv', filename='reports.csv') elif format == 'tab': self._send_csv(req, ['report', 'title', 'description'], rows, '\t', mimetype='text/tab-separated-values', filename='reports.tsv') def report_href(**kwargs): return req.href.report(sort=req.args.get('sort'), asc='1' if asc else '0', **kwargs) add_link(req, 'alternate', auth_link(req, report_href(format='rss')), _('RSS Feed'), 'application/rss+xml', 'rss') add_link(req, 'alternate', report_href(format='csv'), _('Comma-delimited Text'), 'text/plain') add_link(req, 'alternate', report_href(format='tab'), _('Tab-delimited Text'), 'text/plain') reports = [(id, title, description, 'REPORT_MODIFY' in req.perm('report', id), 'REPORT_DELETE' in req.perm('report', id)) for id, title, description in rows] data = {'reports': reports, 'sort': sort, 'asc': asc} return 'report_list.html', data, None
def process_request(self, req): req.perm.require('LOG_VIEW') mode = req.args.get('mode', 'stop_on_copy') path = req.args.get('path', '/') rev = req.args.get('rev') stop_rev = req.args.get('stop_rev') revs = req.args.get('revs') format = req.args.get('format') verbose = req.args.get('verbose') limit = int(req.args.get('limit') or self.default_log_limit) rm = RepositoryManager(self.env) reponame, repos, path = rm.get_repository_by_path(path) if not repos: raise ResourceNotFound( _("Repository '%(repo)s' not found", repo=reponame)) if reponame != repos.reponame: # Redirect alias qs = req.query_string req.redirect( req.href.log(repos.reponame or None, path) + ('?' + qs if qs else '')) normpath = repos.normalize_path(path) # if `revs` parameter is given, then we're restricted to the # corresponding revision ranges. # If not, then we're considering all revisions since `rev`, # on that path, in which case `revranges` will be None. revranges = None if revs: try: revranges = Ranges(revs) rev = revranges.b except ValueError: pass rev = unicode(repos.normalize_rev(rev)) display_rev = repos.display_rev # The `history()` method depends on the mode: # * for ''stop on copy'' and ''follow copies'', it's `Node.history()` # unless explicit ranges have been specified # * for ''show only add, delete'' we're using # `Repository.get_path_history()` cset_resource = repos.resource.child('changeset') show_graph = False if mode == 'path_history': def history(): for h in repos.get_path_history(path, rev): if 'CHANGESET_VIEW' in req.perm(cset_resource(id=h[1])): yield h elif revranges: def history(): prevpath = path expected_next_item = None ranges = list(revranges.pairs) ranges.reverse() for (a, b) in ranges: a = repos.normalize_rev(a) b = repos.normalize_rev(b) while not repos.rev_older_than(b, a) and b != a: node = get_existing_node(req, repos, prevpath, b) node_history = list(node.get_history(2)) p, rev, chg = node_history[0] if repos.rev_older_than(rev, a): break # simply skip, no separator if 'CHANGESET_VIEW' in req.perm(cset_resource(id=rev)): if expected_next_item: # check whether we're continuing previous range np, nrev, nchg = expected_next_item if rev != nrev: # no, we need a separator yield (np, nrev, None) yield node_history[0] prevpath = node_history[-1][0] # follow copy b = repos.previous_rev(rev) if len(node_history) > 1: expected_next_item = node_history[-1] else: expected_next_item = None if expected_next_item: yield (expected_next_item[0], expected_next_item[1], None) else: show_graph = path == '/' and not verbose \ and not repos.has_linear_changesets def history(): node = get_existing_node(req, repos, path, rev) for h in node.get_history(): if 'CHANGESET_VIEW' in req.perm(cset_resource(id=h[1])): yield h # -- retrieve history, asking for limit+1 results info = [] depth = 1 previous_path = normpath count = 0 for old_path, old_rev, old_chg in history(): if stop_rev and repos.rev_older_than(old_rev, stop_rev): break old_path = repos.normalize_path(old_path) item = { 'path': old_path, 'rev': old_rev, 'existing_rev': old_rev, 'change': old_chg, 'depth': depth, } if old_chg == Changeset.DELETE: item['existing_rev'] = repos.previous_rev(old_rev, old_path) if not (mode == 'path_history' and old_chg == Changeset.EDIT): info.append(item) if old_path and old_path != previous_path and \ not (mode == 'path_history' and old_path == normpath): depth += 1 item['depth'] = depth item['copyfrom_path'] = old_path if mode == 'stop_on_copy': break elif mode == 'path_history': depth -= 1 if old_chg is None: # separator entry stop_limit = limit else: count += 1 stop_limit = limit + 1 if count >= stop_limit: break previous_path = old_path if info == []: node = get_existing_node(req, repos, path, rev) if repos.rev_older_than(stop_rev, node.created_rev): # FIXME: we should send a 404 error here raise TracError( _( "The file or directory '%(path)s' doesn't " "exist at revision %(rev)s or at any previous revision.", path=path, rev=display_rev(rev)), _('Nonexistent path')) # Generate graph data graph = {} if show_graph: threads, vertices, columns = \ make_log_graph(repos, (item['rev'] for item in info)) graph.update(threads=threads, vertices=vertices, columns=columns, colors=self.graph_colors, line_width=0.04, dot_radius=0.1) add_script(req, 'common/js/excanvas.js', ie_if='IE') add_script(req, 'common/js/log_graph.js') add_script_data(req, graph=graph) def make_log_href(path, **args): link_rev = rev if rev == str(repos.youngest_rev): link_rev = None params = {'rev': link_rev, 'mode': mode, 'limit': limit} params.update(args) if verbose: params['verbose'] = verbose return req.href.log(repos.reponame or None, path, **params) if format in ('rss', 'changelog'): info = [i for i in info if i['change']] # drop separators if info and count > limit: del info[-1] elif info and count >= limit: # stop_limit reached, there _might_ be some more next_rev = info[-1]['rev'] next_path = info[-1]['path'] next_revranges = None if revranges: next_revranges = str(revranges.truncate(next_rev)) if next_revranges or not revranges: older_revisions_href = make_log_href(next_path, rev=next_rev, revs=next_revranges) add_link( req, 'next', older_revisions_href, _('Revision Log (restarting at %(path)s, rev. %(rev)s)', path=next_path, rev=display_rev(next_rev))) # only show fully 'limit' results, use `change == None` as a marker info[-1]['change'] = None revisions = [i['rev'] for i in info] changes = get_changes(repos, revisions, self.log) extra_changes = {} if format == 'changelog': for rev in revisions: changeset = changes[rev] cs = {} cs['message'] = wrap(changeset.message, 70, initial_indent='\t', subsequent_indent='\t') files = [] actions = [] for cpath, kind, chg, bpath, brev in changeset.get_changes(): files.append(bpath if chg == Changeset.DELETE else cpath) actions.append(chg) cs['files'] = files cs['actions'] = actions extra_changes[rev] = cs data = { 'context': web_context(req, 'source', path, parent=repos.resource), 'reponame': repos.reponame or None, 'repos': repos, 'path': path, 'rev': rev, 'stop_rev': stop_rev, 'display_rev': display_rev, 'revranges': revranges, 'mode': mode, 'verbose': verbose, 'limit': limit, 'items': info, 'changes': changes, 'extra_changes': extra_changes, 'graph': graph, 'wiki_format_messages': self.config['changeset'].getbool('wiki_format_messages') } if format == 'changelog': return 'revisionlog.txt', data, 'text/plain' elif format == 'rss': data['email_map'] = Chrome(self.env).get_email_map() data['context'] = web_context(req, 'source', path, parent=repos.resource, absurls=True) return 'revisionlog.rss', data, 'application/rss+xml' item_ranges = [] range = [] for item in info: if item['change'] is None: # separator if range: # start new range range.append(item) item_ranges.append(range) range = [] else: range.append(item) if range: item_ranges.append(range) data['item_ranges'] = item_ranges add_stylesheet(req, 'common/css/diff.css') add_stylesheet(req, 'common/css/browser.css') path_links = get_path_links(req.href, repos.reponame, path, rev) if path_links: data['path_links'] = path_links if path != '/': add_link(req, 'up', path_links[-2]['href'], _('Parent directory')) rss_href = make_log_href(path, format='rss', revs=revs, stop_rev=stop_rev) add_link(req, 'alternate', auth_link(req, rss_href), _('RSS Feed'), 'application/rss+xml', 'rss') changelog_href = make_log_href(path, format='changelog', revs=revs, stop_rev=stop_rev) add_link(req, 'alternate', changelog_href, _('ChangeLog'), 'text/plain') add_ctxtnav(req, _('View Latest Revision'), href=req.href.browser(repos.reponame or None, path)) if 'next' in req.chrome['links']: next = req.chrome['links']['next'][0] add_ctxtnav( req, tag.span(tag.a(_('Older Revisions'), href=next['href']), Markup(' →'))) return 'revisionlog.html', data, None
def process_request(self, req): req.perm('timeline').require('TIMELINE_VIEW') format = req.args.get('format') maxrows = req.args.getint('max', 50 if format == 'rss' else 0) lastvisit = req.session.as_int('timeline.lastvisit', 0) # indication of new events is unchanged when form is updated by user revisit = any(a in req.args for a in ['update', 'from', 'daysback', 'author']) if revisit: lastvisit = req.session.as_int('timeline.nextlastvisit', lastvisit) # Parse the from date and adjust the timestamp to the last second of # the day fromdate = datetime_now(req.tz) today = truncate_datetime(fromdate) yesterday = to_datetime(today.replace(tzinfo=None) - timedelta(days=1), req.tz) precisedate = precision = None if 'from' in req.args: # Acquire from date only from non-blank input reqfromdate = req.args.get('from').strip() if reqfromdate: try: precisedate = user_time(req, parse_date, reqfromdate) except TracError as e: add_warning(req, e) else: fromdate = precisedate.astimezone(req.tz) precision = req.args.get('precision', '') if precision.startswith('second'): precision = timedelta(seconds=1) elif precision.startswith('minute'): precision = timedelta(minutes=1) elif precision.startswith('hour'): precision = timedelta(hours=1) else: precision = None fromdate = to_datetime(datetime(fromdate.year, fromdate.month, fromdate.day, 23, 59, 59, 999999), req.tz) pref = req.session.as_int('timeline.daysback', self.default_daysback) default = 90 if format == 'rss' else pref daysback = req.args.as_int('daysback', default, min=1, max=self.max_daysback) authors = req.args.get('authors') if authors is None and format != 'rss': authors = req.session.get('timeline.authors') authors = (authors or '').strip() data = {'fromdate': fromdate, 'daysback': daysback, 'authors': authors, 'today': today, 'yesterday': yesterday, 'precisedate': precisedate, 'precision': precision, 'events': [], 'filters': [], 'abbreviated_messages': self.abbreviated_messages} available_filters = [] for event_provider in self.event_providers: with component_guard(self.env, req, event_provider): available_filters += (event_provider.get_timeline_filters(req) or []) # check the request or session for enabled filters, or use default filters = [f[0] for f in available_filters if f[0] in req.args] if not filters and format != 'rss': filters = [f[0] for f in available_filters if req.session.as_int('timeline.filter.' + f[0])] if not filters: filters = [f[0] for f in available_filters if len(f) == 2 or f[2]] # save the results of submitting the timeline form to the session if 'update' in req.args: for filter_ in available_filters: key = 'timeline.filter.%s' % filter_[0] if filter_[0] in req.args: req.session[key] = '1' elif key in req.session: del req.session[key] stop = fromdate start = to_datetime(stop.replace(tzinfo=None) - timedelta(days=daysback + 1), req.tz) # create author include and exclude sets include = set() exclude = set() for match in self._authors_pattern.finditer(authors): name = (match.group(2) or match.group(3) or match.group(4)).lower() if match.group(1): exclude.add(name) else: include.add(name) # gather all events for the given period of time events = [] for provider in self.event_providers: with component_guard(self.env, req, provider): for event in provider.get_timeline_events(req, start, stop, filters) or []: author = (event[2] or '').lower() if ((not include or author in include) and author not in exclude): events.append( self._event_data(req, provider, event, lastvisit)) # prepare sorted global list events = sorted(events, key=lambda e: e['datetime'], reverse=True) if maxrows: events = events[:maxrows] data['events'] = events if format == 'rss': rss_context = web_context(req, absurls=True) rss_context.set_hints(wiki_flavor='html', shorten_lines=False) data['context'] = rss_context return 'timeline.rss', data, {'content_type': 'application/rss+xml'} else: req.session.set('timeline.daysback', daysback, self.default_daysback) req.session.set('timeline.authors', authors, '') # store lastvisit if events and not revisit: lastviewed = to_utimestamp(events[0]['datetime']) req.session['timeline.lastvisit'] = max(lastvisit, lastviewed) req.session['timeline.nextlastvisit'] = lastvisit html_context = web_context(req) html_context.set_hints(wiki_flavor='oneliner', shorten_lines=self.abbreviated_messages) data['context'] = html_context add_stylesheet(req, 'common/css/timeline.css') rss_href = req.href.timeline([(f, 'on') for f in filters], daysback=90, max=50, authors=authors, format='rss') add_link(req, 'alternate', auth_link(req, rss_href), _('RSS Feed'), 'application/rss+xml', 'rss') Chrome(self.env).add_jquery_ui(req) for filter_ in available_filters: data['filters'].append({'name': filter_[0], 'label': filter_[1], 'enabled': filter_[0] in filters}) # Navigation to the previous/next period of 'daysback' days previous_start = fromdate.replace(tzinfo=None) - \ timedelta(days=daysback + 1) previous_start = format_date(previous_start, format='iso8601', tzinfo=req.tz) add_link(req, 'prev', req.href.timeline(from_=previous_start, authors=authors, daysback=daysback), _("Previous Period")) if today - fromdate > timedelta(days=0): next_start = fromdate.replace(tzinfo=None) + \ timedelta(days=daysback + 1) next_start = format_date(to_datetime(next_start, req.tz), format='iso8601', tzinfo=req.tz) add_link(req, 'next', req.href.timeline(from_=next_start, authors=authors, daysback=daysback), _("Next Period")) prevnext_nav(req, _("Previous Period"), _("Next Period")) return 'timeline.html', data
class TimelineModule(Component): implements(INavigationContributor, IPermissionRequestor, IRequestHandler, IRequestFilter, ITemplateProvider, IWikiSyntaxProvider) event_providers = ExtensionPoint(ITimelineEventProvider) default_daysback = IntOption( 'timeline', 'default_daysback', 30, """Default number of days displayed in the Timeline, in days. (''since 0.9.'')""") max_daysback = IntOption( 'timeline', 'max_daysback', 90, """Maximum number of days (-1 for unlimited) displayable in the Timeline. (''since 0.11'')""") abbreviated_messages = BoolOption( 'timeline', 'abbreviated_messages', True, """Whether wiki-formatted event messages should be truncated or not. This only affects the default rendering, and can be overriden by specific event providers, see their own documentation. (''Since 0.11'')""") _authors_pattern = re.compile(r'(-)?(?:"([^"]*)"|\'([^\']*)\'|([^\s]+))') # INavigationContributor methods def get_active_navigation_item(self, req): return 'timeline' def get_navigation_items(self, req): if 'TIMELINE_VIEW' in req.perm: yield ('mainnav', 'timeline', tag.a(_("Timeline"), href=req.href.timeline(), accesskey=2)) # IPermissionRequestor methods def get_permission_actions(self): return ['TIMELINE_VIEW'] # IRequestHandler methods def match_request(self, req): return req.path_info == '/timeline' def process_request(self, req): req.perm.assert_permission('TIMELINE_VIEW') format = req.args.get('format') maxrows = int(req.args.get('max', 50 if format == 'rss' else 0)) lastvisit = int(req.session.get('timeline.lastvisit', '0')) # indication of new events is unchanged when form is updated by user revisit = any(a in req.args for a in ['update', 'from', 'daysback', 'author']) if revisit: lastvisit = int( req.session.get('timeline.nextlastvisit', lastvisit)) # Parse the from date and adjust the timestamp to the last second of # the day fromdate = today = datetime.now(req.tz) precisedate = precision = None if 'from' in req.args: # Acquire from date only from non-blank input reqfromdate = req.args['from'].strip() if reqfromdate: precisedate = user_time(req, parse_date, reqfromdate) fromdate = precisedate precision = req.args.get('precision', '') if precision.startswith('second'): precision = timedelta(seconds=1) elif precision.startswith('minute'): precision = timedelta(minutes=1) elif precision.startswith('hour'): precision = timedelta(hours=1) else: precision = None fromdate = fromdate.replace(hour=23, minute=59, second=59, microsecond=999999) daysback = as_int(req.args.get('daysback'), 90 if format == 'rss' else None) if daysback is None: daysback = as_int(req.session.get('timeline.daysback'), None) if daysback is None: daysback = self.default_daysback daysback = max(0, daysback) if self.max_daysback >= 0: daysback = min(self.max_daysback, daysback) authors = req.args.get('authors') if authors is None and format != 'rss': authors = req.session.get('timeline.authors') authors = (authors or '').strip() data = { 'fromdate': fromdate, 'daysback': daysback, 'authors': authors, 'today': user_time(req, format_date, today), 'yesterday': user_time(req, format_date, today - timedelta(days=1)), 'precisedate': precisedate, 'precision': precision, 'events': [], 'filters': [], 'abbreviated_messages': self.abbreviated_messages, 'lastvisit': lastvisit } available_filters = [] for event_provider in self.event_providers: available_filters += event_provider.get_timeline_filters(req) or [] # check the request or session for enabled filters, or use default filters = [f[0] for f in available_filters if f[0] in req.args] if not filters and format != 'rss': filters = [ f[0] for f in available_filters if req.session.get('timeline.filter.' + f[0]) == '1' ] if not filters: filters = [f[0] for f in available_filters if len(f) == 2 or f[2]] # save the results of submitting the timeline form to the session if 'update' in req.args: for filter in available_filters: key = 'timeline.filter.%s' % filter[0] if filter[0] in req.args: req.session[key] = '1' elif key in req.session: del req.session[key] stop = fromdate start = stop - timedelta(days=daysback + 1) # create author include and exclude sets include = set() exclude = set() for match in self._authors_pattern.finditer(authors): name = (match.group(2) or match.group(3) or match.group(4)).lower() if match.group(1): exclude.add(name) else: include.add(name) # gather all events for the given period of time events = [] for provider in self.event_providers: try: for event in provider.get_timeline_events( req, start, stop, filters) or []: # Check for 0.10 events author = (event[2 if len(event) < 6 else 4] or '').lower() if (not include or author in include) \ and not author in exclude: events.append(self._event_data(provider, event)) except Exception, e: # cope with a failure of that provider self._provider_failure(e, req, provider, filters, [f[0] for f in available_filters]) # prepare sorted global list events = sorted(events, key=lambda e: e['date'], reverse=True) if maxrows: events = events[:maxrows] data['events'] = events if format == 'rss': data['email_map'] = Chrome(self.env).get_email_map() rss_context = web_context(req, absurls=True) rss_context.set_hints(wiki_flavor='html', shorten_lines=False) data['context'] = rss_context return 'timeline.rss', data, 'application/rss+xml' else: req.session.set('timeline.daysback', daysback, self.default_daysback) req.session.set('timeline.authors', authors, '') # store lastvisit if events and not revisit: lastviewed = to_utimestamp(events[0]['date']) req.session['timeline.lastvisit'] = max(lastvisit, lastviewed) req.session['timeline.nextlastvisit'] = lastvisit html_context = web_context(req) html_context.set_hints(wiki_flavor='oneliner', shorten_lines=self.abbreviated_messages) data['context'] = html_context add_stylesheet(req, 'common/css/timeline.css') rss_href = req.href.timeline([(f, 'on') for f in filters], daysback=90, max=50, authors=authors, format='rss') add_link(req, 'alternate', auth_link(req, rss_href), _('RSS Feed'), 'application/rss+xml', 'rss') Chrome(self.env).add_jquery_ui(req) for filter_ in available_filters: data['filters'].append({ 'name': filter_[0], 'label': filter_[1], 'enabled': filter_[0] in filters }) # Navigation to the previous/next period of 'daysback' days previous_start = format_date(fromdate - timedelta(days=daysback + 1), format='%Y-%m-%d', tzinfo=req.tz) add_link( req, 'prev', req.href.timeline(from_=previous_start, authors=authors, daysback=daysback), _('Previous Period')) if today - fromdate > timedelta(days=0): next_start = format_date(fromdate + timedelta(days=daysback + 1), format='%Y-%m-%d', tzinfo=req.tz) add_link( req, 'next', req.href.timeline(from_=next_start, authors=authors, daysback=daysback), _('Next Period')) prevnext_nav(req, _('Previous Period'), _('Next Period')) return 'timeline.html', data, None
data['context'] = web_context(req, report_resource, absurls=True) return 'report.rss', data, 'application/rss+xml' elif format == 'csv': filename = 'report_%s.csv' % id if id else 'report.csv' self._send_csv(req, cols, authorized_results, mimetype='text/csv', filename=filename) elif format == 'tab': filename = 'report_%s.tsv' % id if id else 'report.tsv' self._send_csv(req, cols, authorized_results, '\t', mimetype='text/tab-separated-values', filename=filename) else: p = page if max is not None else None add_link(req, 'alternate', auth_link(req, report_href(format='rss', page=None)), _('RSS Feed'), 'application/rss+xml', 'rss') add_link(req, 'alternate', report_href(format='csv', page=p), _('Comma-delimited Text'), 'text/plain') add_link(req, 'alternate', report_href(format='tab', page=p), _('Tab-delimited Text'), 'text/plain') if 'REPORT_SQL_VIEW' in req.perm('report', id): add_link(req, 'alternate', req.href.report(id=id, format='sql'), _('SQL Query'), 'text/plain') # reuse the session vars of the query module so that # the query navigation links on the ticket can be used to # navigate report results as well try: req.session['query_tickets'] = \
def _render_view(self, req, id): """Retrieve the report results and pre-process them for rendering.""" r = Report(self.env, id) title, description, sql = r.title, r.description, r.query # If this is a saved custom query, redirect to the query module # # A saved query is either an URL query (?... or query:?...), # or a query language expression (query:...). # # It may eventually contain newlines, for increased clarity. # query = ''.join(line.strip() for line in sql.splitlines()) if query and (query[0] == '?' or query.startswith('query:?')): query = query if query[0] == '?' else query[6:] report_id = 'report=%s' % id if 'report=' in query: if report_id not in query: err = _('When specified, the report number should be ' '"%(num)s".', num=id) req.redirect(req.href.report(id, action='edit', error=err)) else: if query[-1] != '?': query += '&' query += report_id req.redirect(req.href.query() + quote_query_string(query)) elif query.startswith('query:'): from trac.ticket.query import Query, QuerySyntaxError try: query = Query.from_string(self.env, query[6:], report=id) except QuerySyntaxError as e: req.redirect(req.href.report(id, action='edit', error=to_unicode(e))) else: req.redirect(query.get_href(req.href)) format = req.args.get('format') if format == 'sql': self._send_sql(req, id, title, description, sql) title = '{%i} %s' % (id, title) report_resource = Resource(self.realm, id) req.perm(report_resource).require('REPORT_VIEW') context = web_context(req, report_resource) page = req.args.getint('page', 1) default_max = {'rss': self.items_per_page_rss, 'csv': 0, 'tab': 0}.get(format, self.items_per_page) max = req.args.getint('max') limit = as_int(max, default_max, min=0) # explict max takes precedence offset = (page - 1) * limit sort_col = req.args.get('sort', '') asc = req.args.getint('asc', 0, min=0, max=1) args = {} def report_href(**kwargs): """Generate links to this report preserving user variables, and sorting and paging variables. """ params = args.copy() if sort_col: params['sort'] = sort_col if page != 1: params['page'] = page if max != default_max: params['max'] = max params.update(kwargs) params['asc'] = 1 if params.get('asc', asc) else None return req.href.report(id, params) data = {'action': 'view', 'report': {'id': id, 'resource': report_resource}, 'context': context, 'title': title, 'description': description, 'max': limit, 'args': args, 'show_args_form': False, 'message': None, 'paginator': None, 'report_href': report_href} try: args = self.get_var_args(req) sql = self.get_default_var_args(args, sql) except ValueError as e: data['message'] = _("Report failed: %(error)s", error=e) return 'report_view.html', data, None data.update({'args': args, 'title': sub_vars(title, args), 'description': sub_vars(description or '', args)}) try: res = self.execute_paginated_report(req, id, sql, args, limit, offset) except TracError as e: data['message'] = _("Report failed: %(error)s", error=e) else: if len(res) == 2: e, sql = res data['message'] = \ tag_("Report execution failed: %(error)s %(sql)s", error=tag.pre(exception_to_unicode(e)), sql=tag(tag.hr(), tag.pre(sql, style="white-space: pre"))) if data['message']: return 'report_view.html', data, None cols, results, num_items, missing_args, limit_offset = res need_paginator = limit > 0 and limit_offset need_reorder = limit_offset is None results = [list(row) for row in results] numrows = len(results) paginator = None if need_paginator: paginator = Paginator(results, page - 1, limit, num_items) data['paginator'] = paginator if paginator.has_next_page: add_link(req, 'next', report_href(page=page + 1), _('Next Page')) if paginator.has_previous_page: add_link(req, 'prev', report_href(page=page - 1), _('Previous Page')) pagedata = [] shown_pages = paginator.get_shown_pages(21) for p in shown_pages: pagedata.append([report_href(page=p), None, str(p), _('Page %(num)d', num=p)]) fields = ['href', 'class', 'string', 'title'] paginator.shown_pages = [dict(zip(fields, p)) for p in pagedata] paginator.current_page = {'href': None, 'class': 'current', 'string': str(paginator.page + 1), 'title': None} numrows = paginator.num_items # Place retrieved columns in groups, according to naming conventions # * _col_ means fullrow, i.e. a group with one header # * col_ means finish the current group and start a new one field_labels = TicketSystem(self.env).get_ticket_field_labels() header_groups = [[]] for idx, col in enumerate(cols): if col in field_labels: title = field_labels[col] else: title = col.strip('_').capitalize() header = { 'col': col, 'title': title, 'hidden': False, 'asc': None, } if col == sort_col: if asc: data['asc'] = asc data['sort'] = sort_col header['asc'] = bool(asc) if not paginator and need_reorder: # this dict will have enum values for sorting # and will be used in sortkey(), if non-empty: sort_values = {} if sort_col in ('status', 'resolution', 'priority', 'severity'): # must fetch sort values for that columns # instead of comparing them as strings with self.env.db_query as db: for name, value in db( "SELECT name, %s FROM enum WHERE type=%%s" % db.cast('value', 'int'), (sort_col,)): sort_values[name] = value def sortkey(row): val = row[idx] # check if we have sort_values, then use them as keys. if sort_values: return sort_values.get(val) # otherwise, continue with string comparison: if isinstance(val, basestring): val = val.lower() return val results = sorted(results, key=sortkey, reverse=not asc) header_group = header_groups[-1] if col.startswith('__') and col.endswith('__'): # __col__ header['hidden'] = True elif col[0] == '_' and col[-1] == '_': # _col_ header_group = [] header_groups.append(header_group) header_groups.append([]) elif col[0] == '_': # _col header['hidden'] = True elif col[-1] == '_': # col_ header_groups.append([]) header_group.append(header) # Structure the rows and cells: # - group rows according to __group__ value, if defined # - group cells the same way headers are grouped chrome = Chrome(self.env) row_groups = [] authorized_results = [] prev_group_value = None for row_idx, result in enumerate(results): col_idx = 0 cell_groups = [] row = {'cell_groups': cell_groups} realm = TicketSystem.realm parent_realm = '' parent_id = '' email_cells = [] for header_group in header_groups: cell_group = [] for header in header_group: value = cell_value(result[col_idx]) cell = {'value': value, 'header': header, 'index': col_idx} col = header['col'] col_idx += 1 # Detect and create new group if col == '__group__' and value != prev_group_value: prev_group_value = value # Brute force handling of email in group by header row_groups.append( (value and chrome.format_author(req, value), [])) # Other row properties row['__idx__'] = row_idx if col in self._html_cols: row[col] = value if col in ('report', 'ticket', 'id', '_id'): row['id'] = value # Special casing based on column name col = col.strip('_') if col in ('reporter', 'cc', 'owner'): email_cells.append(cell) elif col == 'realm': realm = value elif col == 'parent_realm': parent_realm = value elif col == 'parent_id': parent_id = value cell_group.append(cell) cell_groups.append(cell_group) if parent_realm: resource = Resource(realm, row.get('id'), parent=Resource(parent_realm, parent_id)) else: resource = Resource(realm, row.get('id')) # FIXME: for now, we still need to hardcode the realm in the action if resource.realm.upper() + '_VIEW' not in req.perm(resource): continue authorized_results.append(result) if email_cells: for cell in email_cells: emails = chrome.format_emails(context.child(resource), cell['value']) result[cell['index']] = cell['value'] = emails row['resource'] = resource if row_groups: row_group = row_groups[-1][1] else: row_group = [] row_groups = [(None, row_group)] row_group.append(row) data.update({'header_groups': header_groups, 'row_groups': row_groups, 'numrows': numrows}) if format == 'rss': data['context'] = web_context(req, report_resource, absurls=True) return 'report.rss', data, 'application/rss+xml' elif format == 'csv': filename = 'report_%s.csv' % id if id else 'report.csv' self._send_csv(req, cols, authorized_results, mimetype='text/csv', filename=filename) elif format == 'tab': filename = 'report_%s.tsv' % id if id else 'report.tsv' self._send_csv(req, cols, authorized_results, '\t', mimetype='text/tab-separated-values', filename=filename) else: p = page if max is not None else None add_link(req, 'alternate', auth_link(req, report_href(format='rss', page=None)), _('RSS Feed'), 'application/rss+xml', 'rss') add_link(req, 'alternate', report_href(format='csv', page=p), _('Comma-delimited Text'), 'text/plain') add_link(req, 'alternate', report_href(format='tab', page=p), _('Tab-delimited Text'), 'text/plain') if 'REPORT_SQL_VIEW' in req.perm(self.realm, id): add_link(req, 'alternate', req.href.report(id=id, format='sql'), _('SQL Query'), 'text/plain') # reuse the session vars of the query module so that # the query navigation links on the ticket can be used to # navigate report results as well try: req.session['query_tickets'] = \ ' '.join(str(int(row['id'])) for rg in row_groups for row in rg[1]) req.session['query_href'] = \ req.session['query_href'] = report_href() # Kludge: we have to clear the other query session # variables, but only if the above succeeded for var in ('query_constraints', 'query_time'): if var in req.session: del req.session[var] except (ValueError, KeyError): pass if set(data['args']) - {'USER'}: data['show_args_form'] = True # Add values of all select-type ticket fields for autocomplete. fields = TicketSystem(self.env).get_ticket_fields() arg_values = {} for arg in set(data['args']) - {'USER'}: attrs = fields.by_name(arg.lower()) if attrs and 'options' in attrs: arg_values[attrs['name']] = attrs['options'] if arg_values: add_script_data(req, arg_values=arg_values) Chrome(self.env).add_jquery_ui(req) if missing_args: add_warning(req, _( 'The following arguments are missing: %(args)s', args=", ".join(missing_args))) return 'report_view.html', data, None
if events and not revisit: lastviewed = to_utimestamp(events[0]['date']) req.session['timeline.lastvisit'] = max(lastvisit, lastviewed) req.session['timeline.nextlastvisit'] = lastvisit html_context = web_context(req) html_context.set_hints(wiki_flavor='oneliner', shorten_lines=self.abbreviated_messages) data['context'] = html_context add_stylesheet(req, 'common/css/timeline.css') rss_href = req.href.timeline([(f, 'on') for f in filters], daysback=90, max=50, authors=authors, format='rss') add_link(req, 'alternate', auth_link(req, rss_href), _('RSS Feed'), 'application/rss+xml', 'rss') Chrome(self.env).add_jquery_ui(req) for filter_ in available_filters: data['filters'].append({ 'name': filter_[0], 'label': filter_[1], 'enabled': filter_[0] in filters }) # Navigation to the previous/next period of 'daysback' days previous_start = fromdate.replace(tzinfo=None) - \ timedelta(days=daysback + 1) previous_start = format_date(previous_start, format='iso8601',