Skip to content
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
packages = ['ticketgraph'],
package_data = { 'ticketgraph' : [ 'htdocs/*.*', 'templates/*.*' ] },

author = 'Colin Snover',
author_email = 'tracplugins@zetafleet.com',
author = 'Fabrizio Parrella',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacing authorship information like this is not appropriate.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I initially meant to create a new plugin all together that is why I did change it. reverting that change

author_email = 'fabrizio@bibivu.com',
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, based on Colin Snover version.',
license = 'MIT',
keywords = 'trac plugin ticket statistics graph',
classifiers = [
Expand Down
153 changes: 115 additions & 38 deletions ticketgraph/htdocs/ticketgraph.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,117 @@
var plot = null;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not expose globals.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case this is necessary because the plot var is used at a later time , but I am open to suggestions on how to handle this differently. I am pushing new changes now

var line_tick = 86400000;
if(typeof stack_graph!=='undefined'&&stack_graph==true){
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow the code style convention used by this code.

line_tick /= 3.5;
for(var k in closedTickets){
closedTickets[k][0] += line_tick+5000000;
}
for(var k in workedTickets){
workedTickets[k][0] += (line_tick+5000000)*2;
}
}
$(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 graph = $('#placeholder').width(800).height(500),
barSettings = { show: true, barWidth: line_tick, align: 'center', stack: false};
plot = $.plot($('#placeholder'),
[
{
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
},
{
data: openTickets,
label: 'Open tickets',
yaxis: 2,
lines: { show: true, steps: false },
bars: {show: false},
shadowSize: 0,
color: '#333',
idx: 4
}
],
{
series:{
bars: barSettings
},
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>';
}
}
});

$("<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();
}
});
// setTimeout(function(){tracGraphTogglePlot(2);},500);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean up your commented out code before you submit a pull request please.

});
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 hour = a.getHours();
// var min = a.getMinutes();
// var sec = a.getSeconds();
var time = month + ' ' + date + ', ' + year;
return time;
}
function tracGraphTogglePlot(seriesIdx){
var someData = plot.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);
}
plot.setData(someData);
// plot.setupGrid();
plot.draw();
}
39 changes: 25 additions & 14 deletions ticketgraph/templates/ticketgraph.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,29 @@
<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>
<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>
45 changes: 35 additions & 10 deletions ticketgraph/ticketgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,71 +46,96 @@ def process_request(self, req):
today = datetime.datetime.combine(datetime.date.today(), datetime.time(tzinfo=utc))

days = int(req.args.get('days', 30))
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': {}
}

# number of created tickets for the time period, grouped by day (ms)
cursor.execute('SELECT COUNT(DISTINCT id), FLOOR(time / 86400000000) * 86400000 ' \
# cursor.execute('SELECT COUNT(DISTINCT id), FLOOR(time / 86400000000) * 86400000 ' \
cursor.execute('SELECT COUNT(DISTINCT id),( UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(time/1000000)))*1000) ' \
'AS date FROM ticket WHERE time BETWEEN %s AND %s ' \
'GROUP BY date ORDER BY date ASC', (ts_start, ts_end))
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), 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))
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 ' \
'GROUP BY date ORDER BY date ASC', (ts_start, ts_end))
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), 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))
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\'')

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

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['openTickets_ts'] = open_ts-86400000
data['openTickets_tickets'] = open_tickets
data['openTickets_days'] = days

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