This CKAN Extension demonstrates some common patterns for customising a CKAN instance. | This CKAN Extension demonstrates some common patterns for customising a CKAN instance. |
It comprises: | It comprises: |
* A CKAN Extension "plugin" at ``ckanext/exampletheme/__init__.py`` | * A CKAN Extension "plugin" at ``ckanext/exampletheme/__init__.py`` |
which, when loaded, overrides various settings in the core | which, when loaded, overrides various settings in the core |
``ini``-file to provide: | ``ini``-file to provide: |
* A path to local customisations of the core templates and stylesheets | * A path to local customisations of the core templates and stylesheets |
* A "stream filter" that replaces arbitrary strings in rendered templates | * A "stream filter" that replaces arbitrary strings in rendered templates |
* A "route" to override and extend the default behaviour of a core CKAN page | * A "route" to override and extend the default behaviour of a core CKAN page |
* A custom Pylons controller for overriding some core CKAN behaviour | * A custom Pylons controller for overriding some core CKAN behaviour |
* A custom Package edit form | * A custom Package edit form |
* Some simple template customisations | * Some simple template customisations |
Installation | |
============ | |
To install this package, from your CKAN virtualenv, run the following from your CKAN base folder (e.g. ``pyenv/``):: | |
pip install -e hg+https://bitbucket.org/okfn/ckanext-exampletheme#egg=ckanext-exampletheme | |
Then activate it by setting ``ckan.plugins = exampletheme`` in your main ``ini``-file. | |
Orientation | |
=========== | |
* Examine the source code, starting with ``ckanext/exampletheme/__init__.py`` | |
* To understand the nuts and bolts of this file, which is a CKAN | |
*Extension*, read in conjunction with the "Extension | |
documentation":http://packages.python.org/ckan/plugins.html | |
* One thing the extension does is set the values of | |
``extra_public_paths`` and ``extra_template_paths`` in the CKAN | |
config, which are "documented | |
here":http://packages.python.org/ckan/configuration.html#extra-template-paths | |
* These are set to point at directories within | |
`ckanext/exampletheme/theme/`` (in this package). Here, we override | |
the home page, provide some extra style with an ``extra.css``, and | |
customise the navigation and header of the main template in the file ``layout.html``. | |
The latter file is a great place to make global theme alterations. | |
It uses the _layout template_ pattern "described in the Genshi | |
documentation":http://genshi.edgewall.org/wiki/GenshiTutorial#AddingaLayoutTemplate. | |
This allows you to use Xpath selectors to override snippets of HTML | |
globally. | |
* The custom package edit form at ``package_form.py`` follows the | |
conventions in the "main CKAN | |
documentation":http://packages.python.org/ckan/forms.html | |
import os | import os |
from logging import getLogger | from logging import getLogger |
from genshi.filters.transform import Transformer | from genshi.filters.transform import Transformer |
from ckan.plugins import implements, SingletonPlugin | from ckan.plugins import implements, SingletonPlugin |
from ckan.plugins import IConfigurer | from ckan.plugins import IConfigurer |
from ckan.plugins import IGenshiStreamFilter | from ckan.plugins import IGenshiStreamFilter |
from ckan.plugins import IRoutes | from ckan.plugins import IRoutes |
log = getLogger(__name__) | log = getLogger(__name__) |
class ExampleThemePlugin(SingletonPlugin): | class ExampleThemePlugin(SingletonPlugin): |
"""This plugin demonstrates how a theme packaged as a CKAN | """This plugin demonstrates how a theme packaged as a CKAN |
extension might extend CKAN behaviour. | extension might extend CKAN behaviour. |
In this case, we implement three extension interfaces: | In this case, we implement three extension interfaces: |
- ``IConfigurer`` allows us to override configuration normally | - ``IConfigurer`` allows us to override configuration normally |
found in the ``ini``-file. Here we use it to specify the site | found in the ``ini``-file. Here we use it to specify the site |
title, and to tell CKAN to look in this package for templates | title, and to tell CKAN to look in this package for templates |
and resources that customise the core look and feel. | and resources that customise the core look and feel. |
- ``IGenshiStreamFilter`` allows us to filter and transform the | - ``IGenshiStreamFilter`` allows us to filter and transform the |
HTML stream just before it is rendered. In this case we use | HTML stream just before it is rendered. In this case we use |
it to rename "frob" to "foobar" | it to rename "frob" to "foobar" |
- ``IRoutes`` allows us to add new URLs, or override existing | - ``IRoutes`` allows us to add new URLs, or override existing |
URLs. In this example we use it to override the default | URLs. In this example we use it to override the default |
``/register`` behaviour with a custom controller | ``/register`` behaviour with a custom controller |
""" | """ |
implements(IConfigurer, inherit=True) | implements(IConfigurer, inherit=True) |
implements(IGenshiStreamFilter, inherit=True) | implements(IGenshiStreamFilter, inherit=True) |
implements(IRoutes, inherit=True) | implements(IRoutes, inherit=True) |
def update_config(self, config): | def update_config(self, config): |
"""This IConfigurer implementation causes CKAN to look in the | """This IConfigurer implementation causes CKAN to look in the |
```public``` and ```templates``` directories present in this | ```public``` and ```templates``` directories present in this |
package for any customisations. | package for any customisations. |
It also shows how to set the site title here (rather than in | It also shows how to set the site title here (rather than in |
the main site .ini file), and causes CKAN to use the | the main site .ini file), and causes CKAN to use the |
customised package form defined in ``package_form.py`` in this | customised package form defined in ``package_form.py`` in this |
directory. | directory. |
""" | """ |
here = os.path.dirname(__file__) | here = os.path.dirname(__file__) |
rootdir = os.path.dirname(os.path.dirname(here)) | rootdir = os.path.dirname(os.path.dirname(here)) |
our_public_dir = os.path.join(rootdir, 'ckanext', | our_public_dir = os.path.join(rootdir, 'ckanext', |
'exampletheme', 'theme', 'public') | 'exampletheme', 'theme', 'public') |
template_dir = os.path.join(rootdir, 'ckanext', | template_dir = os.path.join(rootdir, 'ckanext', |
'exampletheme', 'theme', | 'exampletheme', 'theme', |
'templates') | 'templates') |
# set our local template and resource overrides | # set our local template and resource overrides |
config['extra_public_paths'] = ','.join([our_public_dir, | config['extra_public_paths'] = ','.join([our_public_dir, |
config.get('extra_public_paths', '')]) | config.get('extra_public_paths', '')]) |
config['extra_template_paths'] = ','.join([template_dir, | config['extra_template_paths'] = ','.join([template_dir, |
config.get('extra_template_paths', '')]) | config.get('extra_template_paths', '')]) |
# set the title | # set the title |
config['ckan.site_title'] = "An example CKAN theme" | config['ckan.site_title'] = "An example CKAN theme" |
# set the customised package form (see ``setup.py`` for entry point) | # set the customised package form (see ``setup.py`` for entry point) |
config['package_form'] = "example_form" | config['package_form'] = "example_form" |
def filter(self, stream): | def filter(self, stream): |
"""Conform to IGenshiStreamFilter interface. | """Conform to IGenshiStreamFilter interface. |
This example filter renames 'frob' to 'foobar' (this string is | This example filter renames 'frob' to 'foobar' (this string is |
found in the custom ``home/index.html`` template provided as | found in the custom ``home/index.html`` template provided as |
part of the package). | part of the package). |
""" | """ |
stream = stream | Transformer('p[@id="examplething"]')\ | stream = stream | Transformer('//p[@id="examplething"]/text()')\ |
.substitute(r'frob', r'foobar') | .substitute(r'frob', r'foobar') |
return stream | return stream |
def before_map(self, map): | def before_map(self, map): |
"""This IRoutes implementation overrides the standard | """This IRoutes implementation overrides the standard |
``/user/register`` behaviour with a custom controller. You | ``/user/register`` behaviour with a custom controller. You |
might instead use it to provide a completely new page, for | might instead use it to provide a completely new page, for |
example. | example. |
Note that we have also provided a custom register form | Note that we have also provided a custom register form |
template at ``theme/templates/user/register.html``. | template at ``theme/templates/user/register.html``. |
""" | """ |
map.connect('/user/register', | map.connect('/user/register', |
controller='ckanext.exampletheme.controller:CustomUserController', | controller='ckanext.exampletheme.controller:CustomUserController', |
action='custom_register') | action='custom_register') |
return map | return map |
<html xmlns:py="http://genshi.edgewall.org/" | <html xmlns:py="http://genshi.edgewall.org/" |
xmlns:i18n="http://genshi.edgewall.org/i18n" | xmlns:i18n="http://genshi.edgewall.org/i18n" |
xmlns:xi="http://www.w3.org/2001/XInclude" | xmlns:xi="http://www.w3.org/2001/XInclude" |
py:strip=""> | py:strip=""> |
<py:def function="page_title">Home</py:def> | <py:def function="page_title">Home</py:def> |
<py:def function="optional_head"> | <py:def function="optional_head"> |
<style type="text/css"> | <style type="text/css"> |
#examplething { | #examplething { |
background-color: yellow; | background-color: yellow; |
padding: 10px; | padding: 10px; |
} | } |
</style> | </style> |
</py:def> | </py:def> |
<div py:match="content"> | <div py:match="content"> |
<h2>Welcome to Example Theme!</h2> | <h2>Welcome to Example Theme!</h2> |
<p> | |
This page left intentionally ugly | |
</p> | |
<p id="examplething"> | <p id="examplething"> |
This page left intentionally ugly | Here is the frob |
</p> | </p> |
</div> | </div> |
<xi:include href="layout.html" /> | <xi:include href="layout.html" /> |
</html> | </html> |
if [ $# -ne 1 ]; then | |
echo "Usage: `basename $0` {NewExtensionName}" | |
exit 65 | |
fi | |
NEWNAME=$1 | |
NEWNAME_LOWER="`echo $NEWNAME | awk '{print tolower($0)}'`" | |
echo $NEWNAME_LOWER | |
mv ckanext/exampletheme ckanext/$NEWNAME_LOWER | |
grep -rl ExampleTheme * | grep -v `basename $0` | xargs perl -pi -e "s/ExampleTheme/$NEWNAME/g" | |
grep -rl exampletheme * | grep -v `basename $0` | xargs perl -pi -e "s/exampletheme/$NEWNAME_LOWER/g" | |
cd .. | |
mv ckanext-exampletheme ckanext-$NEWNAME_LOWER |
from setuptools import setup, find_packages | from setuptools import setup, find_packages |
import sys, os | import sys, os |
version = '0.1' | version = '0.1' |
setup( | setup( |
name='ckanext-exampletheme', | name='ckanext-exampletheme', |
version=version, | version=version, |
description="Example themeb for customising CKAN", | description="Example themeb for customising CKAN", |
long_description="""\ | long_description="""\ |
""", | """, |
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers | classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers |
keywords='', | keywords='', |
author='Seb Bacon', | author='Seb Bacon', |
author_email='seb.bacon@gmail.com', | author_email='seb.bacon@gmail.com', |
url='', | url='', |
license='', | license='', |
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), |
namespace_packages=['ckanext', 'ckanext.exampletheme'], | namespace_packages=['ckanext', 'ckanext.exampletheme'], |
include_package_data=True, | include_package_data=True, |
zip_safe=False, | zip_safe=False, |
install_requires=[ | install_requires=[ |
# -*- Extra requirements: -*- | # -*- Extra requirements: -*- |
], | ], |
entry_points=\ | entry_points=\ |
""" | """ |
[ckan.plugins] | [ckan.plugins] |
datagm=ckanext.exampletheme:ExampleThemePlugin | exampletheme=ckanext.exampletheme:ExampleThemePlugin |
[ckan.forms] | [ckan.forms] |
example_form = ckanext.exampletheme.package_form:get_example_fieldset | example_form = ckanext.exampletheme.package_form:get_example_fieldset |
""", | """, |
) | ) |