class ContainerWidget(WidgetBase): """Embed widgets positioned according to the rules defined by a layout. """ def get_widget_params(self, name): """Return a dictionary containing arguments specification for the widget with specified name. """ return { 'layout': { 'desc': """Name of layout used to arrange widgets""", 'required': True, }, 'schema': { 'desc': """Widgets and position (in JSON)""", 'required': True, 'type': JsonField() }, 'show_captions': { 'desc': """Show widget titles""", 'default': False, }, 'title': { 'desc': """User-defined title""", }, } get_widget_params = pretty_wrapper(get_widget_params, check_widget_name) def render_widget(self, name, context, options): """Count ocurrences of values assigned to given ticket field. """ dbsys = DashboardSystem(self.env) req = context.req params = ('layout', 'schema', 'show_captions', 'title') layout, schema, show_captions, title = \ self.bind_params(name, options, *params) lp = dbsys.resolve_layout(layout) dbmod = DashboardModule(self.env) layout_data = lp.expand_layout(layout, context, { 'schema': schema, 'embed': True }) widgets = dbmod.expand_widget_data(req, schema) return layout_data['template'], \ { 'title' : title, 'data' : dict( context=context, layout=schema, widgets=widgets, title='', default={ 'height' : dbmod.default_widget_height or None } ), }, \ context render_widget = pretty_wrapper(render_widget, check_widget_name)
class TicketQueryWidget(WidgetBase): """Display tickets matching a TracQuery using a grid """ def get_widget_params(self, name): """Return a dictionary containing arguments specification for the widget with specified name. """ return { 'query': { 'desc': """Query string""", 'required': True, }, 'max': { 'default': 0, 'desc': """Limit the number of results displayed""", 'type': int, }, 'page': { 'desc': """Page number""", 'type': int, }, 'title': { 'desc': """Widget title""", }, } get_widget_params = pretty_wrapper(get_widget_params, check_widget_name) def render_widget(self, name, context, options): """Execute custom query and render data using a grid """ data = None req = context.req try: params = ('query', 'max', 'page', 'title') qstr, maxrows, page, title = self.bind_params( name, options, *params) fakereq = dummy_request(self.env, req.authname) fakereq.args = args = parse_qs(qstr) fakereq.arg_list = [] for k, v in args.items(): # Patch for 0.13 fakereq.arg_list.extend((k, _v) for _v in v) try: if len(v) == 1: args[k] = v[0] except TypeError: pass args.update({'page': page, 'max': maxrows}) qrymdl = self.env[QueryModule] if qrymdl is None: raise TracError('Query module not available (disabled?)') data = qrymdl.process_request(fakereq)[1] except TracError, exc: if data is not None: exc.title = data.get('title', 'TracQuery') raise else:
class TicketRelationsWidget(WidgetBase): """Display ticket relations. """ def get_widget_params(self, name): """Return a dictionary containing arguments specification for the widget with specified name. """ return { 'tid': { 'desc': """Source ticket id""", 'type': int }, 'max': { 'desc': """Limit the number of relations displayed""", 'type': int }, } get_widget_params = pretty_wrapper(get_widget_params, check_widget_name) def render_widget(self, name, context, options): """Gather list of relations and render data in compact view """ title = _('Related tickets') params = ('tid', 'max') tid, max_ = self.bind_params(name, options, *params) ticket = Ticket(self.env, tid) data = { 'ticket': ticket, 'relations': \ RelationManagementModule(self.env).get_ticket_relations(ticket), 'get_resource_shortname': get_resource_shortname, 'get_resource_summary': get_resource_summary, } return 'widget_relations.html', { 'title': title, 'data': data, }, context render_widget = pretty_wrapper(render_widget, check_widget_name)
class TicketReportWidget(WidgetBase): """Display tickets in saved report using a grid """ def get_widget_params(self, name): """Return a dictionary containing arguments specification for the widget with specified name. """ return { 'id': { 'desc': """Report number""", 'required': True, 'type': int, }, 'page': { 'default': 1, 'desc': """Retrieve results in given page.""", 'type': int, }, 'user': { 'desc': """Render the report for a given user.""", }, } get_widget_params = pretty_wrapper(get_widget_params, check_widget_name) def render_widget(self, name, context, options): """Execute stored report and render data using a grid """ data = None req = context.req try: params = ('id', 'page', 'user') rptid, page, user = self.bind_params(name, options, *params) user = user or req.authname fakereq = dummy_request(self.env, req.authname) fakereq.args = {'page': page, 'user': user} del fakereq.redirect # raise RequestDone as usual rptmdl = self.env[ReportModule] if rptmdl is None: raise TracError('Report module not available (disabled?)') if trac_version < trac_tags[0]: args = fakereq, self.env.get_db_cnx(), rptid else: args = fakereq, rptid data = rptmdl._render_view(*args)[1] except ResourceNotFound, exc: raise InvalidIdentifier(unicode(exc)) except RequestDone: raise TracError('Cannot execute report. Redirection needed')
'header' : h, 'index' : hidx, 'value' : t[h['col']] } \ for hidx, h in enumerate(headers)]], 'id' : t['id'], 'resource' : Resource('ticket', t['id']) } for t in tickets]) \ for group_value, tickets in data['groups'] ])) return 'widget_grid.html', \ { 'title' : title or _('Custom Query'), 'data' : data, 'ctxtnav' : [ tag.a(_('More'), href=more_link_href)], 'altlinks' : fakereq.chrome.get('links', {}).get('alternate') }, \ qryctx render_widget = pretty_wrapper(render_widget, check_widget_name) #-------------------------------------- # Query functions and methods #-------------------------------------- def exec_query(env, req, qstr='status!=closed'): """ Perform a ticket query, returning a list of ticket ID's. """ return Query.from_string(env, qstr).execute(req)
class ProductWidget(WidgetBase): """Display products available to the user. """ def get_widget_params(self, name): """Return a dictionary containing arguments specification for the widget with specified name. """ return { 'max': {'desc': """Limit the number of products displayed""", 'type': int}, 'cols': {'desc': """Number of columns""", 'type': int} } get_widget_params = pretty_wrapper(get_widget_params, check_widget_name) COMMON_QUERY = 'order=priority&status=!closed&col=id&col=summary' \ '&col=owner&col=type&col=status&col=priority&col=product' def _get_product_info(self, product, href, resource, max_): penv = ProductEnvironment(self.env, product.prefix) results = [] # some queries return a list/tuple, some a generator, # hence count() to get the result length def count(iter_): try: return len(iter_) except TypeError: return sum(1 for _ in iter_) query = resource['type'].select(penv) for q in itertools.islice(query, max_): q.url = href(resource['name'], q.name) \ if resource.get('hrefurl') \ else Query.from_string(penv, '%s=%s&%s&col=%s' % (resource['name'], q.name, self.COMMON_QUERY, resource['name']) ).get_href(href) q.ticket_count = penv.db_query(""" SELECT COUNT(*) FROM ticket WHERE ticket.%s='%s' AND ticket.status <> 'closed' """ % (resource['name'], q.name))[0][0] results.append(q) # add a '(No <milestone/component/version>)' entry if there are # tickets without an assigned resource in the product ticket_count = penv.db_query( """SELECT COUNT(*) FROM ticket WHERE %s='' AND status <> 'closed'""" % (resource['name'],))[0][0] if ticket_count != 0: q = resource['type'](penv) q.name = '(No %s)' % (resource['name'],) q.url = Query.from_string(penv, 'status=!closed&col=id&col=summary&col=owner' '&col=status&col=priority&order=priority&%s=' % (resource['name'],) ).get_href(href) q.ticket_count = ticket_count results.append(q) results.sort(key=lambda x: x.ticket_count, reverse=True) # add a link to the resource list if there are # more than max resources defined if count(query) > max_: q = resource['type'](penv) q.name = _('... more') q.ticket_count = None q.url = href(resource['name']) if resource.get('hrefurl') \ else href.dashboard() results.append(q) return results def render_widget(self, name, context, options): """Gather product list and render data in compact view """ data = {} req = context.req title = '' params = ('max', 'cols') max_, cols = self.bind_params(name, options, *params) if not isinstance(self.env, ProductEnvironment): for p in Product.select(self.env): if 'PRODUCT_VIEW' in req.perm(Neighborhood('product', p.prefix)): penv = ProductEnvironment(self.env, p.prefix) phref = ProductEnvironment.resolve_href(penv, self.env) for resource in ( {'type': Milestone, 'name': 'milestone', 'hrefurl': True}, {'type': Component, 'name': 'component'}, {'type': Version, 'name': 'version'}, ): setattr(p, resource['name'] + 's', self._get_product_info(p, phref, resource, max_)) p.owner_link = Query.from_string(self.env, 'status!=closed&col=id&col=summary&col=owner' '&col=status&col=priority&order=priority' '&group=product&owner=%s' % (p._data['owner'] or '', ) ).get_href(phref) p.href = phref() data.setdefault('product_list', []).append(p) title = _('Products') data['colseq'] = itertools.cycle(xrange(cols - 1, -1, -1)) if cols \ else itertools.repeat(1) return 'widget_product.html', { 'title': title, 'data': data, 'ctxtnav': [tag.a(_('More'), href=req.href('products'))], }, context render_widget = pretty_wrapper(render_widget, check_widget_name)
'value' : t[h['col']] } \ for hidx, h in enumerate(headers)]], 'id' : t['id'], 'resource' : Resource('ticket', t['id']) } for t in tickets]) \ for group_value, tickets in data['groups'] ])) return 'widget_grid.html', \ { 'title' : title or _('Custom Query'), 'data' : data, 'ctxtnav' : [ tag.a(_('More'), href=query.get_href(req.href))], 'altlinks' : fakereq.chrome.get('links', {}).get('alternate') }, \ qryctx render_widget = pretty_wrapper(render_widget, check_widget_name) #-------------------------------------- # Query functions and methods #-------------------------------------- def exec_query(env, req, qstr='status!=closed'): """ Perform a ticket query, returning a list of ticket ID's. """ return Query.from_string(env, qstr).execute(req)
class TicketFieldValuesWidget(WidgetBase): """Display a tag cloud representing frequency of values assigned to ticket fields. """ DASH_ITEM_HREF_MAP = { 'milestone': ('milestone', ), } def get_widget_params(self, name): """Return a dictionary containing arguments specification for the widget with specified name. """ return { 'field': { 'desc': """Target ticket field. """ """Required if no group in `query`.""", }, 'query': { 'desc': """TracQuery used to filter target tickets.""", }, 'title': { 'desc': """Widget title""", }, 'verbose': { 'desc': """Show frequency next to each value""", 'default': False, 'type': bool, }, 'threshold': { 'desc': """Filter items having smaller frequency""", 'type': int, }, 'max': { 'default': 0, 'desc': """Limit the number of items displayed""", 'type': int }, 'view': { 'desc': """Display mode. Should be one of the following - `list` : Unordered value list (default) - `cloud` : Similar to tag cloud """, 'default': 'list', 'type': EnumField('list', 'cloud', 'table', 'compact'), }, } get_widget_params = pretty_wrapper(get_widget_params, check_widget_name) def render_widget(self, name, context, options): """Count ocurrences of values assigned to given ticket field. """ req = context.req params = ('field', 'query', 'verbose', 'threshold', 'max', 'title', 'view') fieldnm, query, verbose, threshold, maxitems, title, view = \ self.bind_params(name, options, *params) field_maps = { 'type': { 'admin_url': 'type', 'title': 'Types', }, 'status': { 'admin_url': None, 'title': 'Statuses', }, 'priority': { 'admin_url': 'priority', 'title': 'Priorities', }, 'milestone': { 'admin_url': 'milestones', 'title': 'Milestones', }, 'component': { 'admin_url': 'components', 'title': 'Components', }, 'version': { 'admin_url': 'versions', 'title': 'Versions', }, 'severity': { 'admin_url': 'severity', 'title': 'Severities', }, 'resolution': { 'admin_url': 'resolution', 'title': 'Resolutions', }, } _field = [] def check_field_name(): if fieldnm is None: raise InvalidWidgetArgument('field', 'Missing ticket field') tsys = self.env[TicketSystem] if tsys is None: raise TracError(_('Error loading ticket system (disabled?)')) for field in tsys.get_ticket_fields(): if field['name'] == fieldnm: _field.append(field) break else: if fieldnm in field_maps: admin_suffix = field_maps.get(fieldnm)['admin_url'] if 'TICKET_ADMIN' in req.perm and admin_suffix is not None: hint = _( 'You can add one or more ' '<a href="%(url)s">here</a>.', url=req.href.admin('ticket', admin_suffix)) else: hint = _( 'Contact your administrator for further details') return 'widget_alert.html', \ { 'title' : Markup(_('%(field)s', field=field_maps[fieldnm]['title'])), 'data' : dict(msgtype='info', msglabel="Note", msgbody=Markup(_('''No values are defined for ticket field <em>%(field)s</em>. %(hint)s''', field=fieldnm, hint=hint)) ) }, context else: raise InvalidWidgetArgument( 'field', 'Unknown ticket field %s' % (fieldnm, )) return None if query is None: data = check_field_name() if data is not None: return data field = _field[0] if field.get('custom'): sql = "SELECT COALESCE(value, ''), count(COALESCE(value, ''))" \ " FROM ticket_custom " \ " WHERE name='%(name)s' GROUP BY COALESCE(value, '')" else: sql = "SELECT COALESCE(%(name)s, ''), " \ "count(COALESCE(%(name)s, '')) FROM ticket " \ "GROUP BY COALESCE(%(name)s, '')" sql = sql % field # TODO : Implement threshold and max db_query = req.perm.env.db_query \ if isinstance(req.perm.env, ProductEnvironment) \ else req.perm.env.db_direct_query with db_query as db: cursor = db.cursor() cursor.execute(sql) items = cursor.fetchall() QUERY_COLS = [ 'id', 'summary', 'owner', 'type', 'status', 'priority' ] item_link = lambda item: req.href.query(col=QUERY_COLS + [fieldnm], **{fieldnm: item[0]}) else: query = Query.from_string(self.env, query, group=fieldnm) if query.group is None: data = check_field_name() if data is not None: return data raise InvalidWidgetArgument( 'field', 'Invalid ticket field for ticket groups') fieldnm = query.group sql, v = query.get_sql() sql = "SELECT COALESCE(%(name)s, '') , count(COALESCE(%(name)s, ''))"\ "FROM (%(sql)s) AS foo GROUP BY COALESCE(%(name)s, '')" % \ { 'name' : fieldnm, 'sql' : sql } db = self.env.get_db_cnx() try: cursor = db.cursor() cursor.execute(sql, v) items = cursor.fetchall() finally: cursor.close() query_href = query.get_href(req.href) item_link= lambda item: query_href + \ '&' + unicode_urlencode([(fieldnm, item[0])]) if fieldnm in self.DASH_ITEM_HREF_MAP: def dash_item_link(item): if item[0]: args = self.DASH_ITEM_HREF_MAP[fieldnm] + (item[0], ) return req.href(*args) else: return item_link(item) else: dash_item_link = item_link if title is None: heading = _(fieldnm.capitalize()) else: heading = None return 'widget_cloud.html', \ { 'title' : title, 'data' : dict( bounds=minmax(items, lambda x: x[1]), item_link=dash_item_link, heading=heading, items=items, verbose=verbose, view=view, ), }, \ context render_widget = pretty_wrapper(render_widget, check_widget_name)
class TicketGroupStatsWidget(WidgetBase): """Display progress bar illustrating statistics gathered on a group of tickets. """ def get_widget_params(self, name): """Return a dictionary containing arguments specification for the widget with specified name. """ return { 'query': { 'default': 'status!=closed', 'desc': """Query string""", }, 'stats_provider': { 'desc': """Name of the component implementing `ITicketGroupStatsProvider`, which is used to collect statistics on groups of tickets.""", 'default': 'DefaultTicketGroupStatsProvider' }, 'skin': { 'desc': """Look and feel of the progress bar""", 'type': EnumField('info', 'success', 'warning', 'danger', 'info-stripped', 'success-stripped', 'warning-stripped', 'danger-stripped') }, 'title': { 'desc': """Widget title""", }, 'legend': { 'desc': """Text on top of the progress bar""", }, 'desc': { 'desc': """Descriptive (wiki) text""", }, 'view': { 'desc': """Display mode to render progress info""", 'type': EnumField('compact', 'standard') }, } get_widget_params = pretty_wrapper(get_widget_params, check_widget_name) def render_widget(self, name, context, options): """Prepare ticket stats """ req = context.req params = ('query', 'stats_provider', 'skin', 'title', 'legend', 'desc', 'view') qstr, pnm, skin, title, legend, desc, view = \ self.bind_params(name, options, *params) statsp = resolve_ep_class(ITicketGroupStatsProvider, self, pnm, default=RoadmapModule( self.env).stats_provider) if skin is not None: skin = (skin or '').split('-', 2) tickets = exec_query(self.env, req, qstr) tickets = apply_ticket_permissions(self.env, req, tickets) stat = get_ticket_stats(statsp, tickets) add_stylesheet(req, 'dashboard/css/bootstrap.css') add_stylesheet(req, 'dashboard/css/bootstrap-responsive.css') add_stylesheet(req, 'dashboard/css/roadmap.css') return 'widget_progress.html', \ { 'title' : title, 'data' : dict( desc=desc, legend=legend, bar_styles=skin, stats=stat, view=view, ), }, \ context render_widget = pretty_wrapper(render_widget, check_widget_name)
class TimelineWidget(WidgetBase): """Display activity feed. """ default_count = IntOption( 'widget_activity', 'limit', 25, """Maximum number of items displayed by default""", doc_domain='bhdashboard') event_filters = ExtensionPoint(ITimelineEventsFilter) _filters_map = None @property def filters_map(self): """Quick access to timeline events filters to be applied for a given timeline provider. """ if self._filters_map is None: self._filters_map = {} for _filter in self.event_filters: providers = _filter.supported_providers() if providers is None: providers = [None] for p in providers: self._filters_map.setdefault(p, []).append(_filter) return self._filters_map def get_widget_params(self, name): """Return a dictionary containing arguments specification for the widget with specified name. """ return { 'from': { 'desc': """Display events before this date""", 'type': DateField(), # TODO: Custom datetime format }, 'daysback': { 'desc': """Event time window""", 'type': int, }, 'precision': { 'desc': """Time precision""", 'type': EnumField('second', 'minute', 'hour') }, 'doneby': { 'desc': """Filter events related to user""", }, 'filters': { 'desc': """Event filters""", 'type': ListField() }, 'max': { 'desc': """Limit the number of events displayed""", 'type': int }, 'realm': { 'desc': """Resource realm. Used to filter events""", }, 'id': { 'desc': """Resource ID. Used to filter events""", }, } get_widget_params = pretty_wrapper(get_widget_params, check_widget_name) def render_widget(self, name, context, options): """Gather timeline events and render data in compact view """ data = None req = context.req try: timemdl = self.env[TimelineModule] admin_page = tag.a(_("administration page."), title=_("Plugin Administration Page"), href=req.href.admin('general/plugin')) if timemdl is None: return 'widget_alert.html', { 'title': _("Activity"), 'data': { 'msglabel': _("Warning"), 'msgbody': tag_( "The TimelineWidget is disabled because the " "Timeline component is not available. " "Is the component disabled? " "You can enable from the %(page)s", page=admin_page), 'dismiss': False, } }, context params = ('from', 'daysback', 'doneby', 'precision', 'filters', 'max', 'realm', 'id') start, days, user, precision, filters, count, realm, rid = \ self.bind_params(name, options, *params) if context.resource.realm == 'ticket': if days is None: # calculate a long enough time daysback ticket = Ticket(self.env, context.resource.id) ticket_age = datetime.now(utc) - ticket.time_created days = ticket_age.days + 1 if count is None: # ignore short count for ticket feeds count = 0 if count is None: count = self.default_count fakereq = dummy_request(self.env, req.authname) fakereq.args = { 'author': user or '', 'daysback': days or '', 'max': count, 'precision': precision, 'user': user } if filters: fakereq.args.update(dict((k, True) for k in filters)) if start is not None: fakereq.args['from'] = start.strftime('%x %X') wcontext = context.child() if (realm, rid) != (None, None): # Override rendering context resource = Resource(realm, rid) if resource_exists(self.env, resource) or \ realm == rid == '': wcontext = context.child(resource) wcontext.req = req else: self.log.warning("TimelineWidget: Resource %s not found", resource) # FIXME: Filter also if existence check is not conclusive ? if resource_exists(self.env, wcontext.resource): module = FilteredTimeline(self.env, wcontext) self.log.debug('Filtering timeline events for %s', wcontext.resource) else: module = timemdl data = module.process_request(fakereq)[1] except TracError, exc: if data is not None: exc.title = data.get('title', _('Activity')) raise else:
class TimelineWidget(WidgetBase): """Display activity feed. """ default_count = IntOption('widget_activity', 'limit', 25, """Maximum number of items displayed by default""") def get_widget_params(self, name): """Return a dictionary containing arguments specification for the widget with specified name. """ return { 'from' : { 'desc' : """Display events before this date""", 'type' : DateField(), # TODO: Custom datetime format }, 'daysback' : { 'desc' : """Event time window""", 'type' : int, }, 'precision' : { 'desc' : """Time precision""", 'type' : EnumField('second', 'minute', 'hour') }, 'doneby' : { 'desc' : """Filter events related to user""", }, 'filters' : { 'desc' : """Event filters""", 'type' : ListField() }, 'max' : { 'desc' : """Limit the number of events displayed""", 'type' : int }, } get_widget_params = pretty_wrapper(get_widget_params, check_widget_name) def render_widget(self, name, context, options): """Gather timeline events and render data in compact view """ data = None req = context.req try: params = ('from', 'daysback', 'doneby', 'precision', 'filters', \ 'max') start, days, user, precision, filters, count = \ self.bind_params(name, options, *params) if count is None: count = self.default_count fakereq = dummy_request(self.env, req.authname) fakereq.args = { 'author' : user or '', 'daysback' : days or '', 'max' : count, 'precision' : precision, 'user' : user } if start is not None: fakereq.args['from'] = start.strftime('%x %X') timemdl = self.env[TimelineModule] if timemdl is None : raise TracError('Timeline module not available (disabled?)') data = timemdl.process_request(fakereq)[1] except TracError, exc: if data is not None: exc.title = data.get('title', 'TracReports') raise else: