blob: 3b6855441a9dadb1abf19ed20967c6a0757fc566 [file] [log] [blame]
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
r"""Project dashboard for Apache(TM) Bloodhound
Widgets displaying ticket data.
"""
from itertools import imap, islice
from urllib import urlencode
from genshi.builder import tag
from genshi.core import Markup
from trac.core import implements, TracError
from trac.ticket.api import TicketSystem
from trac.ticket.query import Query
from trac.ticket.roadmap import apply_ticket_permissions, get_ticket_stats, \
ITicketGroupStatsProvider, RoadmapModule
from trac.util.translation import _
from trac.web.chrome import add_stylesheet
from bhdashboard.api import DateField, EnumField, InvalidWidgetArgument, \
ListField
from bhdashboard.widgets.query import exec_query
from bhdashboard.util import WidgetBase, check_widget_name, \
dummy_request, merge_links, minmax, \
pretty_wrapper, resolve_ep_class, \
trac_version, trac_tags
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(_('''There is no value 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 = self.env.get_db_cnx()
try :
cursor = db.cursor()
cursor.execute(sql)
items = cursor.fetchall()
finally:
cursor.close()
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 + \
'&' + 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/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)