[[TOC(EggCookingTutorialTrac0.11)]] = Basic Egg cooking = 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 [trac:TracDev/ComponentArchitecture component architecture] and [trac:TracDev/PluginDevelopment plugin development]. This plugin is based on the example in the plugin development article. Here we extend it a bit further. == Required items == * ''setuptools>=0.6b1''. For instructions and files see [http://peak.telecommunity.com/DevCenter/EasyInstall#installing-easy-install EasyInstall] page. * '''Trac 0.11'''. Download it from the [http://projects.edgewall.com/trac/wiki/TracDownload TracDownload] page. * '''Genshi>=0.4'''. Download it from the [http://genshi.edgewall.org/wiki/Download GenshiDownload] page. == Directories == To develop a plugin you need to create a few directories to keep things together. So let's create 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/'': {{{ #!python # 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): req.send_response(200) req.send_header('Content-Type', 'text/plain') req.end_headers() req.write('Hello world!') }}} To help understand how that works, read the [http://trac.edgewall.org/browser/trunk/trac/web/chrome.py#L108 INavigationContributor] and [http://trac.edgewall.org/browser/trunk/trac/web/api.py#L517 IRequestHandler] interface specifications. == Make it a module == To make the plugin a module, you simply create an ''_''____''_init_''____''_.py'' in ''./helloworld-plugin/helloworld/'': {{{ #!python # 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/'': {{{ #!python 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 [http://peak.telecommunity.com/DevCenter/EggFormats#project-metadata 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 [trac:TracDev/PluginDevelopment] for more deployment options. Edit the conf/trac.ini file by adding {{{helloworld.* = enabled}}} in the {{{[components]}}} section. Restart the trac server. If you're using mod_python you have to restart Apache. Now you should see ''Hello World'' link at far right in 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 = After you read [wiki:EggCookingTutorialTrac0.11#BasicEggcooking 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 form of Genshi template. == Adding 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 new file ''helloworld.html'': {{{ #!text/html Helloworld

Hello World!

}}} 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 few lines as following: Line 7 is changed from {{{ #!python from trac.web.chrome import INavigationContributor }}} to {{{ #!python from trac.web.chrome import INavigationContributor, ITemplateProvider }}} Line 10 is changed from {{{ #!python implements(INavigationContributor, IRequestHandler) }}} {{{ #!python implements(INavigationContributor, IRequestHandler, ITemplateProvider) }}} Starting from line 24 old ''process_request'' method is replaced by {{{ #!python 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 to end of file you need to tell where your template is located {{{ #!python # 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')] }}} Complete version of ''helloworld.py'': {{{ #!python # 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 template to egg == Finally you have to include the new template directory in an egg. So change ''setup.py'' to be like: {{{ #!python 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']}, ) }}} == Building and deploying == Building and deployment goes exactly the same as it did in the previous section [wiki:EggCookingTutorialTrac0.11#Firstdeployment First Deployment]. 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 next section to cook high-end egg. = Cooking high-end eggs = Now you have pretty neat plugin already but let's add final twist and serve some static content like stylesheet and image. == Important thing to check == First step is to ensure that your ''trac.ini'' doesn't have ''htdocs_location'' set otherwise Trac can't serve static data. == More directories == Since we don't have enough directories in our simple plugin let's make few more: {{{ ./helloworld-plugin/helloworld/htdocs/ ./helloworld-plugin/helloworld/htdocs/css/ ./helloworld-plugin/helloworld/htdocs/images/ }}} == Style is everything == We want to use our own stylesheet to give some color to our fine content. For that we need cascaded stylesheet. Create ''helloworld.css'' in ''./helloworld-plugin/helloworld/htdocs/css/'': {{{ #!text/css div.helloworld h1 { color: red; } }}} == Image tells more than thousand words == Images are always nice. Put small image named ''helloworld.jpg'' in ''./helloworld-plugin/helloworld/htdocs/images/''. Note: Since it's not practical to show jpg contents here you should find one image by yourself somewhere. == Egg grows == Even natural eggs doesn't grow Python eggs does. Modify ''setup.py'' to include our static data: {{{ #!python 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/*']}, ) }}} == Tell it to Trac too == Trac doesn't know where our fine stylesheet and image is located. So you have to tell it to Trac. Add the following code at the tail in file ''helloworld.py'' which implements {{{get_htdocs_dir()}}} to tell the static data path information for this plugin with identical prefix 'hw'. {{{ #!python 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 stylesheet == To make Trac to load our stylesheet you need to modify ''process_request'' method starting from line 24 to following: {{{ #!python 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 prefix path 'hw/' specified by {{{get_htdocs_dirs()}}} should be used. And also import {{{add_stylesheet()}}} at the beginning of ''helloworld.py''. {{{ #!python from trac.web.chrome import INavigationContributor, ITemplateProvider, \ add_stylesheet }}} == Complete version of code == The whole of final code is here: {{{ #!python # 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 template to see it. Our new ''helloworld.cs'': {{{ #!text/html Helloworld

Hello World!

}}} == 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 nice simple plugin that uses it's own template and serves some static data. [[TagIt(khundeen)]]