You are here: Home Articles Writing Trac plugins
Navigation
OpenID Log in

 

Writing Trac plugins

by Martin Aspeli last modified Oct 30, 2008 08:17 PM

Components, but not as you know them

Over the past few days, I've written a number of plugins for Trac 0.11. To my surprise, I found the experience rather enjoyable!

At work, we use Trac for project management, following agile/scrum principles. Hanno Schlichting pointed me at the excellent EstimationToolsPlugin (things in Trac land tend to have names in WikiWordCamelCase) by Joachim Hössler, which does nice burndown charts using Google's Chart API. I ended up patching this (hopefully the patch will be incorporated soon) so that it treats closed tickets as having 0 effort, even if the effort-remaining field is non-zero (we don't run down hours on our tickets - we simply estimate in story points and consider a work item down when the ticket is closed). 

This gave me an appetite, and I started digging a bit more. I've created several plugins since, including:

  • GoogleChartPlugin, which uses the Google Chart Wrapper Python library to graph the results of any SQL query.
  • WikiTableMacro, which allows you to execute a SQL query and output the results in a wiki page as a table.
  • TeamCalendarPlugin, which provides a simple page for team members to indicate which days they are working on the project.
  • SQLConstantsPlugin, which provides an administration console for maintaining any number of constants in the database.

Together, these plugins allow us to track and forecast performance, create complex reports, and visualise progress and financial data in tables and charts.

I'd heard bad things in the past about writing Trac plugins, but with 0.11 at least, it's a real breeze. The approach is similar to (and I assume, inspired by) Zope's component architecture, though it is much simpler. To create a plugin, you have to:

  • Create an egg.
  • Declare an entry point for your plugin. This is referenced in trac.ini when people load your plugins, somewhat analogous to ZCML slugs or site.zcml entries in Zope.
  • Write a class that implements one or more interfaces.

The interfaces are things like IPermissionRequestor (declares new permissions), INavigationProvider (adds new entries to the navigation) or IRequestHandler (can render a full page view, typically using Genshi templates). Each interface has a number of methods that you implement, which get called by Trac during the request cycle.

Here is an example, the GoogleChartMacro:

from trac.wiki.macros import WikiMacroBase
from trac.util.html import Markup

from GChartWrapper import GChart as GoogleChart

NON_API_KEYS = ['type', 'query', 'tuples']

class GChart(WikiMacroBase):
    """Create a chart using the Google Chart API.
     
    The GChart macro is initialised with keyword arguments much like a Python
    function. The two required arguments are 'query' (a SQL query) and 'type'
    (the chart type).  In addition, you can pass 'tuples=True' to have columns
    be sent as tuples, with each row a dataset. Otherwise, each full column will
    be used as one dataset.
    
    Other arguments are passed to the GChartWrapper plugin constructor.
     
    When rendered, the query will be executed. Each column will be sent to the
    Google Chart API as a dataset.
     
    Examples:
    {{{
        [[GChart(query="SELECT id FROM ticket", type="line")]]
        [[GChart(query="SELECT id, time FROM ticket", type="line", tuples=True)]]
    }}}
    """
    
    def render_macro(self, req, name, content):
        
        # XXX: This is a big security hole. Need to replace this with something that
        # only accepts simple key/value pairs.
        options = eval("dict(%s)" % content)
        
        query = options.get('query', None)
        ctype = options.get('type', None)
        tuples = options.get('tuples', False)
        
        if query is None or ctype is None:
            raise ValueError("Chart 'type' and 'query' parameters are required")
        
        for key in NON_API_KEYS:
            if key in options:
                del options[key]
        
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute(query)
        
        dataset = []
        
        for row in cursor:
            row_data = []
            for col in row:
                try:
                    row_data.append(float(col))
                except ValueError:
                    pass
            if not row_data:
                continue
            if tuples:
                dataset.append(row_data)
            else:
                for idx, data in enumerate(row_data):
                    if idx < len(dataset):
                        dataset[idx].append(data)
                    else:
                        dataset.append([data])
        
        chart = GoogleChart(ctype=ctype, dataset=dataset, **options)
        return Markup(chart.img())

That's pretty much it, save for registration code in setup.py.

I think Zope and Plone can learn a few things from this approach, especially since the Trac approach is not a million miles away from the way in which Zope and Plone are extended.

Trac is definitely simpler. Of course, Trac is an order of magnitude (or three) smaller than Zope and Plone in scope, and so requires a lot less infrastructure. However, browsing the Trac code, it is also very elegant and well structured. I didn't have much trouble understanding how the various pieces fit together when reading the code.

For writing plugins, there is no separate registration code (ZCML) - you just add an entry point that broadcasts your package to the Trac system, and Trac will scan your code for classes that subclass Component and implement various interfaces. This is of course very similar to the approach that Grok takes. 

Components plug into Trac via various fine-grained interfaces, which they opt into through the implements() directive. Each interface expects the class to implement one or two methods that are effectively callbacks.

Many parts of Zope or Plone expect this kind of configuration too, normally by registering utilities or adapters. However, these components are often more fine grained, and require separate registration. When I was writing Trac components, I would start with a simple component that provided one or two interfaces, and add more behaviours to it as I needed them.

In Zope, we often frown on multi-purpose components like this. Instead, we tend to write adapters that add behaviour. This is definitely more flexible, but for components that are effectively single-purpose "plug-ins" rather than more fine-grained and re-usable code, I think it is easier to keep everything in one class.

I also quite like the way Trac handles page requests. You basically implement a method like render_macro() or process_request(), perform updates and prepare data for the view, and then return the name of a Genshi template to render, along with the prepared data. Your component can also declare (via ITemplateProvider) a directory of templates and another directory of static resources, and it can add stylesheets to the head section of the standard page layout with a simple function call. See the TeamCalendarPlugin for an example.

This type of simple, declarative way to provide a directory full of templates and/or static resources, and a way to request required resources that are pulled into the head of the page automatically, are both things that could be made easier in Zope/Plone. Again, Grok leads the way here, with similar patterns that we could readily adopt.

Finally, I love TracHacks. It makes it dead simple to set up a new project with a wiki page with some pre-formatted text, tags-based classification and a pre-created Subversion location to check in the code. We should consider making a similar "quickstart" form to help people get code into the Collective. Perhaps we could even use the TracHacks code, since dev.plone.org runs Trac already?

If you are using Trac 0.11, and you haven't looked at the plugin architecture yet, I'd encourage you to do so. Combined with an understanding of the the Trac data model and the ability to create new tables, views and stored procedures in the MySQL database we run Trac on, they give me a great degree of confidence that I can make Trac work for us over the course of our project.

Document Actions
Plone Book
Professional Plone 4 Development

I am the author of a book called Professional Plone Development. You can read more about it here.

About this site

This Plone site is kindly hosted by: 

Six Feet Up