Roundup Wiki

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('<', '&lt;').replace('>', '&gt;'))
 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

--