Because managers and supervisors always want to see nice graphics, I picked Richard Jones's SimpleStatusCharts example and used it as a basis to create pie charts dependent on query results.
If you want to use this feature, make sure that your server has PyChart installed and if you use a Windows machine as server, you will need GhostScript too.
To 'piechart_action.py. The content will be:
import time, random, os, binascii, pickle
from roundup.cgi.actions import Action
from roundup.cgi import templating
class PieChartAction(Action):
def handle(self):
''' Show piechart for current query results
'''
db = self.db
# needed to get the query data
request = templating.HTMLRequest(self.client)
# 'arg' will contain all the data that we need to pass to
# the sub piechart process
arg = {}
arg['tracker_home'] = db.config.TRACKER_HOME
arg['user'] = db.user.get(db.getuid(), 'username')
arg['classname'] = request.classname
if request.filterspec:
arg['filterspec'] = request.filterspec
if request.group:
arg['group'] = request.group
if request.search_text:
arg['search_text'] = request.search_text
# calculate a crc to be used as part of the temporary output filename
# used by the sub process
crc = binascii.crc32(' '.join([ '%s %s'%(option, value) \
for option, value in arg.items() ]), 0)
crc = binascii.crc32(''.join([ '%02d'%part \
for part in time.gmtime()[:6]]), crc)
crc = '%08X'%crc
temp_folder = os.path.normpath(os.path.join(db.config.TRACKER_HOME,
'temp'))
# create <tracker-home>/temp folder if it ain't not there
if not os.path.exists(temp_folder):
os.makedirs(temp_folder)
# create a temporary filename for the output file
image_file = os.path.normpath(os.path.join(temp_folder,
'pieChart_%s.png'%crc[-8:]))
# add the temporary output filename to the arguments passed
# to the sub process
arg['output_file'] = image_file
# build the command to run the sub process:
# this uses the python from your path; if this
# not the one required to run Roundup, use instead:
# '%s/bin/python %s'%(sys.prefix, ...
command = 'python %s'%(os.path.normpath(
os.path.join(db.config.TRACKER_HOME,
'extensions',
'piechart.py')))
# run the sub process (will create the piechart)
stdin, stdout, stderr = os.popen3(command)
# pass values in 'arg' to sub process
pickle.dump(arg, stdin)
stdin.close()
# read any errors from error pipe
error = stderr.read()
stdout.close()
stderr.close()
image = None
# if there aren't any errors
if not error:
# read the piechart image
fp = os.open(image_file, os.O_RDONLY | getattr(os, 'O_BINARY', 0))
try:
length = os.fstat(fp)[6]
image = os.read(fp, length)
finally:
os.close(fp)
# remove the temporary piechart image
try:
os.unlink(image_file)
except:
pass
if not error and not image:
error = 'Received an empty pie chart'
if not error:
# display the image
headers = self.client.additional_headers
headers['Content-Type'] = 'image/png'
headers['Content-Disposition'] = 'inline; filename=pieChart.png'
self.client.header()
if self.client.env['REQUEST_METHOD'] == 'HEAD':
# all done, return a dummy string
return 'dummy'
# write the piechart to client
self.client.request.wfile.write(image)
else:
# we have an error
headers = self.client.additional_headers
headers['Content-Type'] = 'text/html'
self.client.header()
if self.client.env['REQUEST_METHOD'] == 'HEAD':
# all done, return a dummy string
return 'dummy'
# write the error to client
self.client.request.wfile.write('<pre>%s</pre>'%error)
return '\n'
def init(instance):
instance.registerAction('piechart', PieChartAction)
In that same directory you add a stand-alone script which will be called by the above action handler. This standalone script is needed because for some unknown reason PyChart fails to create a pie chart if it is called directly within a roundup session. The only solution was to create a child process which will call PyChart and pass the output back to the action handler.
Here is the content of that stand-alone script (named piechart.py):
import os, sys, re, time, types, pickle, random
import roundup.instance
from roundup import hyperdb
from pychart import *
log = []
def openTracker(tracker_home, user):
''' open tracker and return a database instance
'''
if not tracker_home:
tracker_home = os.getenv('TRACKER_HOME')
if not os.path.exists(os.path.normpath('%s/config.ini'%tracker_home)):
sys.stderr.write('ERROR: TRACKER_HOME is not a falid path')
sys.exit(-1)
tracker = roundup.instance.open(tracker_home)
db = tracker.open(user)
return db
def createPieChart(tracker_home=None, user='anonymous', classname='issue',
filterspec={},
group=('+', 'status'),
search_text=None,
output_file=None):
''' create piechart
'''
try:
db = openTracker(tracker_home, user)
try:
cl = db.getclass(classname)
log.append('cl=%s'%cl)
# full-text search
if search_text:
matches = db.indexer.search(re.findall(r'\b\w{2,25}\b',
search_text), cl)
else:
matches = None
log.append('matches=%s'%matches)
# some trackers do place the group settings in a list object
# for those we have to correct it, because the piechart.py
# script expects a tuple and not a tuple within a list
if len(group) == 1 and isinstance(group, types.ListType) \
and len(group[0]) == 2 and isinstance(group[0], types.TupleType):
# yep, the group tuple was placed in a list, now we correct it
group = group[0]
property = group[1]
log.append('property=%s'%property)
prop_type = cl.getprops()[property]
log.append('prop_type=%s'%prop_type)
if not isinstance(prop_type, hyperdb.Link) \
and not isinstance(prop_type, hyperdb.Multilink):
os.write(2, 'Piecharts can only be created on' \
'linked group properties!\n')
return
klass = db.getclass(prop_type.classname)
log.append('klass=%s'%klass)
# build a property dict, eg: { 'new':1, 'assigned':2 } in
# case of status
props = {}
chart = {}
issues = cl.filter(matches, filterspec, group)
log.append('issues=%s'%issues)
for nodeid in issues:
prop_ids = cl.get(nodeid, property)
if prop_ids:
if not isinstance(prop_ids, types.ListType):
prop_ids = [prop_ids]
for id in prop_ids:
prop = klass.get(id, klass.labelprop())
key = prop.replace('/', '-')
if props.has_key(key):
props[key] += 1
else:
props[key] = 1
else:
prop = '?'
if not props.has_key(prop):
props[prop] = 0
chart[prop] = (5, fill_style.white)
key = prop.replace('/', '-')
if props.has_key(key):
props[key] += 1
else:
props[key] = 1
log.append('props=%s'%props)
# create chart color/fill table
random.seed(0.5)
for key in props.keys():
col = color.T(r=random.random(),
g=random.random(),
b=random.random())
fill = fill_style._intern_color(fill_style.Plain(bgcolor=col))
chart[key] = (5, fill)
# create a sorted keylist
order = props.keys()
order.sort()
log.append('order=%s'%order)
# convert to structure accepted by PyChart
data = [ (key, props[key]) for key in order ]
log.append('data=%s'%data)
if len(data) > 0:
# format pie style
arc_offsets = []
fill_styles = []
offset = 10
for index, item in enumerate(data):
key = item[0]
data[index] = ('%s (%s)'%(item[0], item[1]), item[1])
if chart.has_key(key):
arc_offsets.append(offset)
fill_styles.append(chart[key][1])
else:
arc_offsets.append(offset)
fill_styles.append(fill_style.gray50)
offset += 10
if offset > 20:
offset = 10
log.append('data=%s'%data)
if output_file:
try:
os.unlink(output_file)
except:
pass
# now call PyChart for the chart generation
if output_file:
theme.output_file = output_file
theme.output_format = 'png'
theme.use_color = True
theme.default_font_size = 16
theme.reinitialize()
ar = area.T(size=(800, 750), legend=legend.T(),
x_grid_style=None, y_grid_style=None)
plot = pie_plot.T(data=data,
arc_offsets=arc_offsets,
fill_styles=fill_styles,
label_offset=40,
arrow_style=arrow.a3,
radius=200,
center=(400,300))
ar.add_plot(plot)
ar.draw()
else:
os.write(2, 'No data to display\n')
finally:
db.close()
except:
# in case of an error, write some additional info
# to the error pipe
os.write(2, 'Failed to create a pie chart due to' \
'a Python exception!\n')
os.write(2, '\n')
os.write(2, 'tracker_home : %s\n'%str(tracker_home))
os.write(2, 'user : %s\n'%str(user))
os.write(2, 'classname : %s\n'%str(classname))
os.write(2, 'filterspec : %s\n'%str(filterspec))
os.write(2, 'group : %s\n'%str(group))
os.write(2, 'search_text : %s\n'%str(search_text))
os.write(2, 'output_file : %s\n'%str(output_file))
os.write(2, '\n')
if log:
os.write(2, 'log:\n')
os.write(2, '\n'.join(log).replace('<', '<').replace('>', '>'))
os.write(2, '\n\n')
raise
def init(instance):
# this dummy 'init' is needed to fool roundup
pass
class devnull:
def write(self, *args):
pass
def writelines(self, *args):
pass
def flush(self, *args):
pass
if __name__ == '__main__':
arguments = {}
# throw away anything that might be written to stdout
# because the real stdout is a pipe that our caller does not
# read from and that therefore might block
sys.stdout = devnull()
# no arguments (must be passed through stdin by piechart action handler)
arguments = pickle.load(sys.stdin)
log.append('arguments=%s'%arguments)
createPieChart(**arguments)
Finally we need to call it somewhere. The simplest way is to add a link to the issue.index.html page like it has a link
for the csv_export action.
A typical pie chart link could look like:
<a tal:attributes="href python:request.indexargs_url('issue',
{'@action':'piechart'})" target="_blank" i18n:translate="">Show PieChart</a>
Regards,
Marlon
History:
- 07.03.2007: added remark abour Python prefix, added stdout redirection to avoid deadlock -- Patrick Ohly