Skip to content
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
author = 'Colin Snover',
author_email = '[email protected]',
description = 'Graphs Trac tickets over time',
long_description = 'A Trac plugin that displays a visual graph of ticket changes over time.',
long_description = 'A Trac plugin that displays a visual graph of ticket changes over time, modified by Fabrizio Parrella ([email protected]).',
license = 'MIT',
keywords = 'trac plugin ticket statistics graph',
classifiers = [
Expand Down
157 changes: 119 additions & 38 deletions ticketgraph/htdocs/ticketgraph.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,121 @@
var tracGraphPlot = null;
$(document).ready(function() {
var graph = $('#placeholder').width(640).height(400),
barSettings = { show: true, barWidth: 86400000 };
$.plot($('#placeholder'),
[
{
data: closedTickets,
label: 'Closed tickets',
bars: barSettings,
color: 1
},
{
data: openedTickets,
label: 'New tickets',
bars: barSettings,
color: 2,
stack: true
},
{
data: reopenedTickets,
label: 'Reopened tickets',
bars: barSettings,
color: 3,
stack: true
},
{
data: openTickets,
label: 'Open tickets',
yaxis: 2,
lines: { show: true },
color: 0
}
],
{
xaxis: { mode: 'time', minTickSize: [1, "day"] },
yaxis: { min: 0, label: 'Tickets' },
y2axis: { min: 0 },
legend: { position: 'nw' }
});
var lineTick = 86400000;
if(typeof stack_graph !== 'undefined' && stack_graph == true){
lineTick /= 3.5;
for(var k in closedTickets){
closedTickets[k][0] += lineTick+5000000;
}
for(var k in workedTickets){
workedTickets[k][0] += (lineTick+5000000)*2;
}
}
$('#owner')[0].options.add(new Option("All","%"));
for(var i in users){
if(users[i][1]==""||users[i][1]==null){
users[i][1] = users[i][0];
}
$('#owner')[0].options.add(new Option(users[i][1],users[i][0]));
}
$('#owner').val(owner);
var graph = $('#placeholder').width(800).height(500);
var data = [
{
data: openedTickets,
label: 'New tickets',
color: '#66cd00',
stack: true,
idx: 0
},
{
data: reopenedTickets,
label: 'Reopened tickets',
color: '#458b00',
stack: true,
idx: 1
},
{
data: closedTickets,
label: 'Closed tickets',
color: '#8b0000',
idx: 2
}, {
data: workedTickets,
label: 'Worked tickets',
color: '#45458b',
idx: 3
}
];
if(owner==="" || owner==="%"){
data.push({
data: openTickets,
label: 'Open tickets',
yaxis: 2,
lines: { show: true, steps: false },
bars: {show: false},
shadowSize: 0,
color: '#333',
idx: 4
});
}
var options = {
series:{
bars: { show: true, barWidth: lineTick, align: 'center', stack: false}
},
xaxis: { mode: 'time', minTickSize: [1, "day"] },
grid: { hoverable: true },
yaxis: { label: 'Tickets' },
y2axis: { min: 0 },
legend: {
container:$("#legend-container"),
position: 'ne',
labelFormatter: function(label, series){
return '<a href="#" onClick="tracGraphTogglePlot('+series.idx+'); return false;">'+label+'</a>';
}
}
};
tracGraphPlot = $.plot($('#placeholder'), data, options);

$("<div id='tooltip'></div>").css({
position: "absolute",
display: "none",
border: "1px solid #fdd",
padding: "2px",
"background-color": "#fee",
opacity: 0.80
}).appendTo("body");


$("#placeholder").bind("plothover", function (event, pos, item) {
if (item) {
var x = item.datapoint[0],
y = Math.abs(item.datapoint[1]);
$("#tooltip").html(tracGraphTimeConverter(x) + '<br />' + item.series.label + " : " + y)
.css({top: item.pageY+5, left: item.pageX+5})
.fadeIn(200);
} else {
$("#tooltip").hide();
}
});
});
function tracGraphTimeConverter(timestamp){
var a = new Date(timestamp);
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var year = a.getFullYear();
var month = months[a.getMonth()];
var date = a.getDate();
var time = month + ' ' + date + ', ' + year;
return time;
}
function tracGraphTogglePlot(seriesIdx){
var someData = tracGraphPlot.getData();
if(typeof someData[seriesIdx].data_old === 'undefined'){
someData[seriesIdx].data_old=someData[seriesIdx].data;
someData[seriesIdx].data=[];
}else{
someData[seriesIdx].data=someData[seriesIdx].data_old;
delete(someData[seriesIdx].data_old);
}
tracGraphPlot.setData(someData);
tracGraphPlot.draw();
}
41 changes: 27 additions & 14 deletions ticketgraph/templates/ticketgraph.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,31 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="layout.html" />
<head>
<title>Ticket Graph</title>
${Markup('&lt;!--[if lt IE 7]&gt;')}
<script type="text/javascript" src="${href.chrome('ticketgraph/excanvas.min.js')}"></script>
${Markup('&lt;![endif]--&gt;')}
</head>
<body>
<h1>Ticket Graph</h1>
<div id="content">
<h2>Last ${days} days</h2>
<div id="placeholder"></div>
</div>
</body>
<xi:include href="layout.html" />
<head>
<title>Ticket Graph</title>
${Markup('&lt;!--[if lt IE 7]&gt;')}
<script type="text/javascript" src="${href.chrome('ticketgraph/excanvas.min.js')}"></script>
${Markup('&lt;![endif]--&gt;')}
</head>
<body>
<h1>Ticket Graph</h1>
<div id="content">
<h2>Last ${days} days</h2>
<p>
<form method="get" id="ticketgraph">
Days Back:
<select name="days">
<option py:for="option in [30,60,90,180,365]" value="$option" selected="${option == days or None}">$option</option>
</select>
User:
<select name="owner" id="owner"></select>
<label><input type="checkbox" name="sg" id="graph-sg" value="1" checked="${sg or None}" />Stack Graph</label>
<input type="submit" value="Graph" />
</form>
</p>
<div id="legend-container"></div>
<div id="placeholder"></div>
</div>
</body>
</html>
79 changes: 62 additions & 17 deletions ticketgraph/ticketgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,71 +46,116 @@ def process_request(self, req):
today = datetime.datetime.combine(datetime.date.today(), datetime.time(tzinfo=utc))

days = int(req.args.get('days', 30))
owner = req.args.get('owner', "%")
stack_graph={};
stack_graph['stack_graph'] = bool(int(req.args.get('sg', 0)))
# These are in microseconds; the data returned is in milliseconds
# because it gets passed to flot
ts_start = to_utimestamp(today - datetime.timedelta(days=days))
ts_end = to_utimestamp(today) + 86400000000;
ts_utc_delta = math.ceil((datetime.datetime.utcnow()-datetime.datetime.now()).total_seconds())*1000;

db = self.env.get_read_db()
cursor = db.cursor()

series = {
'openedTickets': {},
'closedTickets': {},
'workedTickets': {},
'reopenedTickets': {},
'openTickets': {}
}
args = [ts_start, ts_end];
tix_args = [ts_start, ts_end];
where = '';
tix_where = '';
if owner=="" :
owner = "%"

if owner!="%" :
where += 'AND author LIKE %s ';
tix_where += 'AND reporter LIKE %s ';
args.append(owner)
tix_args.append(owner)


# number of created tickets for the time period, grouped by day (ms)
cursor.execute('SELECT COUNT(DISTINCT id), FLOOR(time / 86400000000) * 86400000 ' \
'AS date FROM ticket WHERE time BETWEEN %s AND %s ' \
'GROUP BY date ORDER BY date ASC', (ts_start, ts_end))
cursor.execute('SELECT COUNT(DISTINCT id),( UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(time/1000000)))*1000) ' \
'AS date FROM ticket WHERE time BETWEEN %s AND %s ' + tix_where +
'GROUP BY date ORDER BY date ASC', tix_args)
for count, timestamp in cursor:
series['openedTickets'][float(timestamp)] = float(count)

# number of reopened tickets for the time period, grouped by day (ms)
cursor.execute('SELECT COUNT(DISTINCT ticket), FLOOR(time / 86400000000) * 86400000 ' \
cursor.execute('SELECT COUNT(DISTINCT ticket), UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(time/1000000)))*1000 ' \
'AS date FROM ticket_change WHERE field = \'status\' AND newvalue = \'reopened\' ' \
'AND time BETWEEN %s AND %s ' \
'GROUP BY date ORDER BY date ASC', (ts_start, ts_end))
'AND time BETWEEN %s AND %s ' + where +
'GROUP BY date ORDER BY date ASC', args)
for count, timestamp in cursor:
series['reopenedTickets'][float(timestamp)] = float(count)

# number of worked tickets for the time period, grouped by day (ms)
cursor.execute('SELECT COUNT(DISTINCT ticket), UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(time/1000000)))*1000 ' \
'AS date FROM ticket_change WHERE ' \
'time BETWEEN %s AND %s ' + where +
'GROUP BY date ORDER BY date ASC', args)
for count, timestamp in cursor:
series['workedTickets'][float(timestamp)] = float((1 if stack_graph['stack_graph'] else -1)*count)


# number of closed tickets for the time period, grouped by day (ms)
cursor.execute('SELECT COUNT(DISTINCT ticket), FLOOR(time / 86400000000) * 86400000 ' \
cursor.execute('SELECT COUNT(DISTINCT ticket), UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(time/1000000)))*1000 ' \
'AS date FROM ticket_change WHERE field = \'status\' AND newvalue = \'closed\' ' \
'AND time BETWEEN %s AND %s ' \
'GROUP BY date ORDER BY date ASC', (ts_start, ts_end))
'AND time BETWEEN %s AND %s ' + where +
'GROUP BY date ORDER BY date ASC', args)
for count, timestamp in cursor:
series['closedTickets'][float(timestamp)] = float(count)
series['closedTickets'][float(timestamp)] = float((1 if stack_graph['stack_graph'] else -1)*count)

# number of open tickets at the end of the reporting period
cursor.execute('SELECT COUNT(*) FROM ticket WHERE status <> \'closed\'')
if owner!='%' :
cursor.execute('SELECT COUNT(*) FROM ticket WHERE status <> \'closed\' AND owner LIKE %s ', [owner])
else:
cursor.execute('SELECT COUNT(*) FROM ticket WHERE status <> \'closed\' ')


open_tickets = cursor.fetchone()[0]
open_ts = math.floor(ts_end / 1000)
open_ts = math.floor((ts_end) / 1000)+ts_utc_delta

# series['openTickets'][open_ts] = open_tickets

while open_ts >= math.floor(ts_start / 1000):
if open_ts in series['closedTickets']:
open_tickets += series['closedTickets'][open_ts]
open_tickets += (1 if stack_graph['stack_graph'] else -1)*series['closedTickets'][open_ts]
if open_ts in series['openedTickets']:
open_tickets -= series['openedTickets'][open_ts]
if open_ts in series['reopenedTickets']:
open_tickets -= series['reopenedTickets'][open_ts]

series['openTickets'][open_ts] = open_tickets
open_ts -= 86400000
series['openTickets'][open_ts-86400000] = open_tickets

new_ts_utc_delta = math.ceil((datetime.datetime.utcfromtimestamp(open_ts/1000)-datetime.datetime.fromtimestamp(open_ts/1000)).total_seconds())*1000;

open_ts -= 86400000 + ts_utc_delta - new_ts_utc_delta
ts_utc_delta = new_ts_utc_delta

data = {}
for i in series:
keys = series[i].keys()
keys.sort()
data[i] = [ (k, series[i][k]) for k in keys ]

data['owner'] = owner
data['users']={}
i=0;
for user in self.env.get_known_users():
data['users'][i] = user
i=i+1

add_script(req, 'ticketgraph/jquery.flot.min.js')
# add_script(req, 'http://people.iola.dk/olau/flot/jquery.flot.js')
add_script(req, 'ticketgraph/jquery.flot.stack.min.js')
add_script(req, 'ticketgraph/ticketgraph.js')
add_script_data(req, data)
add_script_data(req, stack_graph)

return 'ticketgraph.html', { 'days': days }, None

return 'ticketgraph.html', { 'days': days, 'sg': stack_graph['stack_graph'] }, None