wiki:EggCookingTutorialTrac0.11

Version 18 (modified by cpowell@…, 12 years ago) (diff)

AttributeError: 'HelloWorldPlugin' object has no attribute 'get_htdocs_dirs

Basic Egg cooking (Hello World 1)

Since Trac 0.11, Genshi is used as the new template engine. Some APIs have also been changed.

This tutorial shows how to make an egg, and load an egg in Trac. In the advanced parts you'll learn how to serve templates and static content from an egg.

You should be familiar with component architecture and plugin development. This plugin is based on the example in the plugin development article. Here we extend it a bit further.

Required items

Directories

To develop a plugin you need to create a few directories to keep things together.

So let's create the following directories:

./helloworld-plugin/
./helloworld-plugin/helloworld/

Main plugin

The first step is to generate the main module for this plugin. We will construct a simple plugin that will display "Hello world!" on the screen when accessed through the /helloworld URL. The plugin also provides a "Hello" button that is, by default, rendered on the far right in the main navigation bar.

So create helloworld.py in ./helloworld-plugin/helloworld/:

# Helloworld plugin

import re

from genshi.builder import tag

from trac.core import *
from trac.web import IRequestHandler
from trac.web.chrome import INavigationContributor

class HelloWorldPlugin(Component):
    implements(INavigationContributor, IRequestHandler)

    # INavigationContributor methods
    def get_active_navigation_item(self, req):
        return 'helloworld'
    
    def get_navigation_items(self, req):
        yield ('mainnav', 'helloworld',
               tag.a('Hello World', href=req.href.helloworld()))
    
    # IRequestHandler methods
    def match_request(self, req):
        return re.match(r'/helloworld(?:_trac)?(?:/.*)?$', req.path_info)
    
    def process_request(self, req):
        content = 'Hello World'
        req.send_response(200)
        req.send_header('Content-Type', 'text/plain')
        req.send_header('Content-Length', len('content')
        req.end_headers()
        req.write(content)

To better understand how that works, read the INavigationContributor and IRequestHandler interface specifications.

Make it a module

To make the plugin a module, you simply create an __init__.py in ./helloworld-plugin/helloworld/:

# Helloworld module
from helloworld import *

Make it an egg

Now it's time to make it an egg. For that we need a chicken called setup.py in ./helloworld-plugin/:

from setuptools import find_packages, setup

# name can be any name.  This name will be used to create .egg file.
# name that is used in packages is the one that is used in the trac.ini file.
# use package name as entry_points
setup(
    name='Trachelloworld', version='1.1',
    packages=find_packages(exclude=['*.tests*']),
    entry_points = """
        [trac.plugins]
        helloworld = helloworld
    """,
)

You don't need to add special egg metadata in Trac 0.11.

First deployment

Now try to build the plugin. Run the command python setup.py bdist_egg in the directory where you created it. If everything went OK you should have a .egg file in ./dist directory. Copy this .egg file to /[your trac env]/plugins directory.

During development you can run the command python setup.py develop in the directory where you created it. This way you don't need to do it every time you change the code. Check TracDev/PluginDevelopment for more deployment options.

Edit the conf/trac.ini file by adding helloworld.* = enabled in the [components] section.

Copy this .egg file to /[your trac env]/plugins directory. Restart the trac server. If you're using mod_python you have to restart Apache.

Now you should see a Hello World link on the far right in the main navigation bar when accessing your site. Click it.

Aftermath

Now you have successfully created your first egg. You can continue further to learn how to use templates in your plugins, and make its output look like other Trac pages.

Cook even better eggs (Hello World 2)

After you read Basic Egg Cooking and created your first egg, it's time to make it a bit better.

First we integrate our output to other Trac layout in the form of a Genshi template.

Adding a template

To have a template we need a directory and of course the template itself. We will keep the same simple "Hello world!" text, but this time we will integrate our fine words into a Trac layout.

For that we need to create one additional directory:

./helloworld-plugin/helloworld/templates/

In that directory create a file called helloworld.html:

<!DOCTYPE html
    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include href="layout.html" />
  <head>
    <title>Helloworld</title>
  </head>

  <body>
    <div id="ctxtnav" class="nav"></div>

    <div id="content" class="helloworld">
      <h1>Hello World!</h1>
    </div>
  </body>
</html>

Now you have created the template for the plugin.

Tell Trac where template is

Trac doesn't know where your template is so you have to tell it. This is done by implementing the ITemplateProvider interface in helloworld.py.

So you change a few lines as following:

Line 7 is changed from

from trac.web.chrome import INavigationContributor

to

from trac.web.chrome import INavigationContributor, ITemplateProvider

Line 10 is changed from

    implements(INavigationContributor, IRequestHandler)
    implements(INavigationContributor, IRequestHandler, ITemplateProvider)

Starting from line 24 the old process_request method is replaced by

    def process_request(self, req):
        data = {}        
        # This tuple is for Genshi (template_name, data, content_type)
        # Without data the trac layout will not appear.
        return 'helloworld.html', data, None

And near the end of the file you need to define where your template is located

    # ITemplateProvider methods
    # Used to add the plugin's templates and htdocs 
    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

    def get_htdocs_dirs(self):
        return []

Complete version of helloworld.py:

# Helloworld plugin

import re

from genshi.builder import tag

from trac.core import *
from trac.web import IRequestHandler
from trac.web.chrome import INavigationContributor, ITemplateProvider

class HelloWorldPlugin(Component):
    implements(INavigationContributor, IRequestHandler, ITemplateProvider)

    # INavigationContributor methods
    def get_active_navigation_item(self, req):
        return 'helloworld'

    def get_navigation_items(self, req):
        yield ('mainnav', 'helloworld',
               tag.a('Hello World', href=req.href.helloworld()))

    # IRequestHandler methods
    def match_request(self, req):
        return re.match(r'/helloworld(?:_trac)?(?:/.*)?$', req.path_info)

    def process_request(self, req):
        data = {}        
        # This tuple is for Genshi (template_name, data, content_type)
        # Without data the trac layout will not appear.
        return 'helloworld.html', data, None

    # ITemplateProvider methods
    # Used to add the plugin's templates and htdocs 
    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

Copy the template to the egg

Finally you have to include the new template directory in the egg.

So change setup.py to be like:

from setuptools import find_packages, setup

# name can be any name.  This name will be used to create the .egg file.
# name that is used in packages is the one that is used in the trac.ini file.
# use package name as entry_points
setup(
    name='Trachelloworld', version='1.1',
    packages=find_packages(exclude=['*.tests*']),
    entry_points = """
        [trac.plugins]
        helloworld = helloworld
    """,
    package_data={'helloworld': ['templates/*.html']},
)

Building and deploying

Building and deployment goes exactly the same as it did in the previous section First Deployment.

A "Permission denied: '/root/.python-eggs'" error indicates that the plugin cache is being written to a location its process does not have access to. See TracPlugins for details.

Now you should see a big "Hello world!" integrated into your Trac layout when you press that fancy button in the main navigation bar.

Aftermath

Now that you have added a basic template for your plugin let's add the final twist, putting some static content like a stylesheet and an image. Continue to the next section to cook a high-end egg.

Cooking high-end eggs (Hello World 3)

Now you have a pretty neat plugin already but let's add that final twist and serve some static content like a stylesheet and an image.

An important thing to check

The first step is to ensure that your trac.ini doesn't have htdocs_location set, otherwise Trac can't serve static data.

Directory Listing for Hello World 3

helloworld3-plugin
|-- helloworld
|   |-- __init__.py
|   |-- helloworld.py
|   |-- htdocs
|   |   |-- css
|   |   |   `-- helloworld.css
|   |   `-- images
|   |       `-- helloworld3.png
|   `-- templates
|       `-- helloworld.html
`-- setup.py

5 directories, 6 files

More directories

Since we don't have enough directories in our simple plugin let's create some more:

./helloworld-plugin/helloworld/htdocs/
./helloworld-plugin/helloworld/htdocs/css/
./helloworld-plugin/helloworld/htdocs/images/

Style is everything

We want to use our own styles to give some color to our fine content. For that we need a CSS file.

Create helloworld.css in ./helloworld-plugin/helloworld/htdocs/css/:

div.helloworld h1 {
        color: red;
}

An image says more than a thousand words

Images are always nice.

Put a small image named helloworld.jpg in ./helloworld-plugin/helloworld/htdocs/images/.

Note: Since it's not practical to show JPEG contents here you should find one image by yourself somewhere.

The egg grows

Natural eggs don't dron, Python ones do.

Modify setup.py to include our static data:

from setuptools import find_packages, setup

# name can be any name.  This name will be used to create .egg file.
# name that is used in packages is the one that is used in the trac.ini file.
# use package name as entry_points
setup(
    name='Trachelloworld', version='1.1',
    packages=find_packages(exclude=['*.tests*']),
    entry_points = """
        [trac.plugins]
        helloworld = helloworld
    """,
    package_data={'helloworld': ['templates/*.html', 
                                 'htdocs/css/*.css', 
                                 'htdocs/images/*']},
)

Let Trac know about our static content

Trac doesn't know where our stylesheet and image are located. So you have to let Trac now.

Add the following code near the end of helloworld.py which implements get_htdocs_dir() to define the static data path information for this plugin with identical prefix 'hw'.

    def get_htdocs_dirs(self):
        """Return a list of directories with static resources (such as style
        sheets, images, etc.)

        Each item in the list must be a `(prefix, abspath)` tuple. The
        `prefix` part defines the path in the URL that requests to these
        resources are prefixed with.

        The `abspath` is the absolute path to the directory containing the
        resources on the local file system.
        """
        from pkg_resources import resource_filename
        return [('hw', resource_filename(__name__, 'htdocs'))]

Remember to load the stylesheet

To make Trac to load our stylesheet you need to modify process_request method starting from line 24 to the following:

    def process_request(self, req):
        data = {}
        add_stylesheet(req, 'hw/css/helloworld.css')
        # This tuple is for Genshi (template_name, data, content_type)
        # Without data the trac layout will not appear.
        return 'helloworld.html', data, None

Note that the prefix path 'hw/' that was specified by get_htdocs_dirs() should be used.

And also add an import of add_stylesheet at the beginning of helloworld.py.

from trac.web.chrome import INavigationContributor, ITemplateProvider, \
        add_stylesheet

Note: When you develop a Component that implements IAdminPanelProvider you have to insert add_stylesheet(...) to the method render_admin_panel. You don't need to implement IRequestHandler.

A complete version of our code

The whole of final code is here:

# Helloworld plugin
import re

from genshi.builder import tag

from trac.core import *
from trac.web import IRequestHandler
from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet

class HelloworldModule(Component):
    implements(INavigationContributor, ITemplateProvider, IRequestHandler)

    # INavigationContributor methods
    def get_active_navigation_item(self, req):
        return 'helloworld'

    def get_navigation_items(self, req):
        yield ('mainnav', 'helloworld',
               tag.a('Hello World', href=req.href.helloworld()))

    # IRequestHandler methods
    def match_request(self, req):
        return re.match(r'/helloworld(?:_trac)?(?:/.*)?$', req.path_info)

    def process_request(self, req):
        data = {}
        add_stylesheet(req, 'hw/css/helloworld.css')
        # This tuple is for Genshi (template_name, data, content_type)
        # Without data the trac layout will not appear.
        return 'helloworld.html', data, None

    # ITemplateProvider methods
    # Used to add the plugin's templates and htdocs 
    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]
    
    def get_htdocs_dirs(self):
        """Return a list of directories with static resources (such as style
        sheets, images, etc.)

        Each item in the list must be a `(prefix, abspath)` tuple. The
        `prefix` part defines the path in the URL that requests to these
        resources are prefixed with.

        The `abspath` is the absolute path to the directory containing the
        resources on the local file system.
        """
        from pkg_resources import resource_filename
        return [('hw', resource_filename(__name__, 'htdocs'))]    

Back to images

We need to add our image to the template to see it.

Our new helloworld.html:

<!DOCTYPE html
    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include href="layout.html" />
  <head>
    <title>Helloworld</title>
  </head>

  <body>
    <div id="ctxtnav" class="nav"></div>

    <div id="content" class="helloworld">
      <h1>Hello World!</h1>
      <img src="${href.chrome('/hw/images/helloworld.jpg')}" />
    </div>
  </body>
</html>

Compilation and deployment

Now you should be familiar with both, so make an egg and deploy it.

Click and see... Hello world! with your pretty own image.

Aftermath

Now you have successfully completed a simple plugin that uses its own template and serves some static data.

Remove the plugin

TBC.

TagIt(khundeen)?