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":http://home.gna.org/pychart/ installed and if you use a Windows machine as server, you will need "GhostScript":http://sourceforge.net/projects/ghostscript/ too.
To '<tracker-home>/extensions' you add a file named 'piechart_action.py'. The content will be::
1 import time, random, os, binascii, pickle
2
3 try:
4 from subprocess import Popen, PIPE
5 except: ImportError
6
7 from roundup.cgi.actions import Action
8 from roundup.cgi import templating
9
10 class PieChartAction(Action):
11 def handle(self):
12 ''' Show piechart for current query results
13 '''
14 db = self.db
15
16 # needed to get the query data
17 request = templating.HTMLRequest(self.client)
18
19 # 'arg' will contain all the data that we need to pass to
20 # the sub piechart process
21 arg = {}
22
23 arg['tracker_home'] = db.config.TRACKER_HOME
24 arg['user'] = db.user.get(db.getuid(), 'username')
25 arg['classname'] = request.classname
26
27 if request.filterspec:
28 arg['filterspec'] = request.filterspec
29
30 if request.group:
31 arg['group'] = request.group
32
33 if request.search_text:
34 arg['search_text'] = request.search_text
35
36 # calculate a crc to be used as part of the temporary output filename
37 # used by the sub process
38 crc = binascii.crc32(' '.join([ '%s %s'%(option, value) \
for option, value in arg.items() ]), 0)
39 crc = binascii.crc32(''.join([ '%02d'%part \
for part in time.gmtime()[:6]]), crc)
40 crc = '%08X'%crc
41
42 temp_folder = os.path.normpath(os.path.join(db.config.TRACKER_HOME,
43 'temp'))
44
45 # create <tracker-home>/temp folder if it ain't not there
46 if not os.path.exists(temp_folder):
47 os.makedirs(temp_folder)
48
49 # create a temporary filename for the output file
50 image_file = os.path.normpath(os.path.join(temp_folder,
51 'pieChart_%s.png'%crc[-8:]))
52
53 # add the temporary output filename to the arguments passed
54 # to the sub process
55 arg['output_file'] = image_file
56
57 # build the command to run the sub process:
58 # this uses the python from your path; if this
59 # not the one required to run Roundup, use instead:
60 # '%s/bin/python %s'%(sys.prefix, ...
61 command = 'python %s'%(os.path.normpath(
62 os.path.join(db.config.TRACKER_HOME,
63 'extensions',
64 'piechart.py')))
65
66 # run the sub process (will create the piechart)
67 try: # try newer subprocess
68 p = Popen(command, shell=True, bufsize=8192,
69 stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
70 (stdin, stdout, stderr) = (p.stdin, p.stdout, p.stderr)
71 # Method exists, and was used.
72 except AttributeError:
73 # Method does not exist. What now?
74 # use os.popen3
75 stdin, stdout, stderr = os.popen3(command)
76
77 # pass values in 'arg' to sub process
78 pickle.dump(arg, stdin)
79 stdin.close()
80
81 # read any errors from error pipe
82 error = stderr.read()
83
84 stdout.close()
85 stderr.close()
86
87 image = None
88
89 # if there aren't any errors
90 if not error:
91 # read the piechart image
92 fp = os.open(image_file, os.O_RDONLY | getattr(os, 'O_BINARY', 0))
93 try:
94 length = os.fstat(fp)[6]
95 image = os.read(fp, length)
96 finally:
97 os.close(fp)
98
99 # remove the temporary piechart image
100 try:
101 os.unlink(image_file)
102 except:
103 pass
104
105 if not error and not image:
106 error = 'Received an empty pie chart'
107
108 if not error:
109 # display the image
110
111 headers = self.client.additional_headers
112 headers['Content-Type'] = 'image/png'
113 headers['Content-Disposition'] = 'inline; filename=pieChart.png'
114
115 self.client.header()
116
117 if self.client.env['REQUEST_METHOD'] == 'HEAD':
118 # all done, return a dummy string
119 return 'dummy'
120
121 # write the piechart to client
122 self.client.request.wfile.write(image)
123 else:
124 # we have an error
125
126 headers = self.client.additional_headers
127 headers['Content-Type'] = 'text/html'
128
129 self.client.header()
130
131 if self.client.env['REQUEST_METHOD'] == 'HEAD':
132 # all done, return a dummy string
133 return 'dummy'
134
135 # write the error to client
136 self.client.request.wfile.write('<pre>%s</pre>'%error)
137
138 return '\n'
139
140 def init(instance):
141 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":http://home.gna.org/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":http://home.gna.org/pychart/ and pass the output back to the action handler.
Here is the content of that stand-alone script (named 'piechart.py')::
1 import os, sys, re, time, types, pickle, random
2 import roundup.instance
3
4 from roundup import hyperdb
5
6 from pychart import *
7
8 log = []
9
10 def openTracker(tracker_home, user):
11 ''' open tracker and return a database instance
12 '''
13 if not tracker_home:
14 tracker_home = os.getenv('TRACKER_HOME')
15
16 if not os.path.exists(os.path.normpath('%s/config.ini'%tracker_home)):
17 sys.stderr.write('ERROR: TRACKER_HOME is not a falid path')
18 sys.exit(-1)
19
20 tracker = roundup.instance.open(tracker_home)
21 db = tracker.open(user)
22
23 return db
24
25
26 def createPieChart(tracker_home=None, user='anonymous', classname='issue',
27 filterspec={},
28 group=('+', 'status'),
29 search_text=None,
30 output_file=None):
31 ''' create piechart
32 '''
33 try:
34 db = openTracker(tracker_home, user)
35
36 try:
37 cl = db.getclass(classname)
38 log.append('cl=%s'%cl)
39
40 # full-text search
41 if search_text:
42 matches = db.indexer.search(re.findall(r'\b\w{2,25}\b',
43 search_text), cl)
44 else:
45 matches = None
46 log.append('matches=%s'%matches)
47
48 # some trackers do place the group settings in a list object
49 # for those we have to correct it, because the piechart.py
50 # script expects a tuple and not a tuple within a list
51 if len(group) == 1 and isinstance(group, types.ListType) \
and len(group[0]) == 2 and isinstance(group[0], types.TupleType):
52 # yep, the group tuple was placed in a list, now we correct it
53 group = group[0]
54 property = group[1]
55 log.append('property=%s'%property)
56
57 prop_type = cl.getprops()[property]
58 log.append('prop_type=%s'%prop_type)
59
60 if not isinstance(prop_type, hyperdb.Link) \
and not isinstance(prop_type, hyperdb.Multilink):
61 os.write(2, 'Piecharts can only be created on' \
'linked group properties!\n')
62 return
63
64 klass = db.getclass(prop_type.classname)
65 log.append('klass=%s'%klass)
66
67 # build a property dict, eg: { 'new':1, 'assigned':2 } in
68 # case of status
69 props = {}
70 chart = {}
71 issues = cl.filter(matches, filterspec, group)
72 log.append('issues=%s'%issues)
73 for nodeid in issues:
74 prop_ids = cl.get(nodeid, property)
75 if prop_ids:
76 if not isinstance(prop_ids, types.ListType):
77 prop_ids = [prop_ids]
78 for id in prop_ids:
79 prop = klass.get(id, klass.labelprop())
80 key = prop.replace('/', '-')
81 if props.has_key(key):
82 props[key] += 1
83 else:
84 props[key] = 1
85 else:
86 prop = '?'
87 if not props.has_key(prop):
88 props[prop] = 0
89 chart[prop] = (5, fill_style.white)
90 key = prop.replace('/', '-')
91 if props.has_key(key):
92 props[key] += 1
93 else:
94 props[key] = 1
95 log.append('props=%s'%props)
96
97 # create chart color/fill table
98 random.seed(0.5)
99 for key in props.keys():
100 col = color.T(r=random.random(),
101 g=random.random(),
102 b=random.random())
103 fill = fill_style._intern_color(fill_style.Plain(bgcolor=col))
104 chart[key] = (5, fill)
105
106 # create a sorted keylist
107 order = props.keys()
108 order.sort()
109 log.append('order=%s'%order)
110
111 # convert to structure accepted by PyChart
112 data = [ (key, props[key]) for key in order ]
113 log.append('data=%s'%data)
114
115 if len(data) > 0:
116 # format pie style
117 arc_offsets = []
118 fill_styles = []
119 offset = 10
120 for index, item in enumerate(data):
121 key = item[0]
122 data[index] = ('%s (%s)'%(item[0], item[1]), item[1])
123 if chart.has_key(key):
124 arc_offsets.append(offset)
125 fill_styles.append(chart[key][1])
126 else:
127 arc_offsets.append(offset)
128 fill_styles.append(fill_style.gray50)
129 offset += 10
130 if offset > 20:
131 offset = 10
132 log.append('data=%s'%data)
133
134 if output_file:
135 try:
136 os.unlink(output_file)
137 except:
138 pass
139
140 # now call PyChart for the chart generation
141 if output_file:
142 theme.output_file = output_file
143 theme.output_format = 'png'
144 theme.use_color = True
145 theme.default_font_size = 16
146 theme.reinitialize()
147 ar = area.T(size=(800, 750), legend=legend.T(),
148 x_grid_style=None, y_grid_style=None)
149 plot = pie_plot.T(data=data,
150 arc_offsets=arc_offsets,
151 fill_styles=fill_styles,
152 label_offset=40,
153 arrow_style=arrow.a3,
154 radius=200,
155 center=(400,300))
156 ar.add_plot(plot)
157 ar.draw()
158 else:
159 os.write(2, 'No data to display\n')
160 finally:
161 db.close()
162
163 except:
164 # in case of an error, write some additional info
165 # to the error pipe
166 os.write(2, 'Failed to create a pie chart due to' \
'a Python exception!\n')
167 os.write(2, '\n')
168 os.write(2, 'tracker_home : %s\n'%str(tracker_home))
169 os.write(2, 'user : %s\n'%str(user))
170 os.write(2, 'classname : %s\n'%str(classname))
171 os.write(2, 'filterspec : %s\n'%str(filterspec))
172 os.write(2, 'group : %s\n'%str(group))
173 os.write(2, 'search_text : %s\n'%str(search_text))
174 os.write(2, 'output_file : %s\n'%str(output_file))
175 os.write(2, '\n')
176 if log:
177 os.write(2, 'log:\n')
178 os.write(2, '\n'.join(log).replace('<', '<').replace('>', '>'))
179 os.write(2, '\n\n')
180 raise
181
182
183 def init(instance):
184 # this dummy 'init' is needed to fool roundup
185 pass
186
187 class devnull:
188 def write(self, *args):
189 pass
190 def writelines(self, *args):
191 pass
192 def flush(self, *args):
193 pass
194
195 if __name__ == '__main__':
196 arguments = {}
197
198 # throw away anything that might be written to stdout
199 # because the real stdout is a pipe that our caller does not
200 # read from and that therefore might block
201 sys.stdout = devnull()
202
203 # no arguments (must be passed through stdin by piechart action handler)
204 arguments = pickle.load(sys.stdin)
205 log.append('arguments=%s'%arguments)
206
207 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,<br> Marlon
History:
- 07.03.2007: added remark abour Python prefix, added stdout redirection to avoid deadlock -- Patrick Ohly
- 03.19.2012: use subprocess module if available otherwise use depricated os.popen3 -- John Rouillard
--