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 '/extensions' you add a file named 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('<', '&lt;').replace('>', '&gt;'))
                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: