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/plugin.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 | Installation |
============ | ============ |
To install this package, from your CKAN virtualenv, run the following from your CKAN base folder (e.g. ``pyenv/``):: | 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 | 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. | Then activate it by setting ``ckan.plugins = exampletheme`` in your main ``ini``-file. |
Orientation | Orientation |
=========== | =========== |
* Examine the source code, starting with ``ckanext/exampletheme/__init__.py`` | * Examine the source code, starting with ``ckanext/exampletheme/plugin.py`` |
* To understand the nuts and bolts of this file, which is a CKAN | * To understand the nuts and bolts of this file, which is a CKAN |
*Extension*, read in conjunction with the "Extension | *Extension*, read in conjunction with the "Extension |
documentation":http://packages.python.org/ckan/plugins.html | documentation":http://packages.python.org/ckan/plugins.html |
* One thing the extension does is set the values of | * One thing the extension does is set the values of |
``extra_public_paths`` and ``extra_template_paths`` in the CKAN | ``extra_public_paths`` and ``extra_template_paths`` in the CKAN |
config, which are "documented | config, which are "documented |
here":http://packages.python.org/ckan/configuration.html#extra-template-paths | here":http://packages.python.org/ckan/configuration.html#extra-template-paths |
* These are set to point at directories within | * These are set to point at directories within |
`ckanext/exampletheme/theme/`` (in this package). Here, we override | `ckanext/exampletheme/theme/`` (in this package). Here, we override |
the home page, provide some extra style with an ``extra.css``, and | 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``. | 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. | The latter file is a great place to make global theme alterations. |
It uses the _layout template_ pattern "described in the Genshi | It uses the _layout template_ pattern "described in the Genshi |
documentation":http://genshi.edgewall.org/wiki/GenshiTutorial#AddingaLayoutTemplate. | documentation":http://genshi.edgewall.org/wiki/GenshiTutorial#AddingaLayoutTemplate. |
This allows you to use Xpath selectors to override snippets of HTML | This allows you to use Xpath selectors to override snippets of HTML |
globally. | globally. |
* The custom package edit form at ``package_form.py`` follows the | * The custom package edit form at ``package_form.py`` follows the |
conventions in the "main CKAN | conventions in the "main CKAN |
documentation":http://packages.python.org/ckan/forms.html | documentation":http://packages.python.org/ckan/forms.html |
import os | # package |
from logging import getLogger | |
from genshi.filters.transform import Transformer | |
from ckan.plugins import implements, SingletonPlugin | |
from ckan.plugins import IConfigurer | |
from ckan.plugins import IGenshiStreamFilter | |
from ckan.plugins import IRoutes | |
log = getLogger(__name__) | |
class ExampleThemePlugin(SingletonPlugin): | |
"""This plugin demonstrates how a theme packaged as a CKAN | |
extension might extend CKAN behaviour. | |
In this case, we implement three extension interfaces: | |
- ``IConfigurer`` allows us to override configuration normally | |
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 | |
and resources that customise the core look and feel. | |
- ``IGenshiStreamFilter`` allows us to filter and transform the | |
HTML stream just before it is rendered. In this case we use | |
it to rename "frob" to "foobar" | |
- ``IRoutes`` allows us to add new URLs, or override existing | |
URLs. In this example we use it to override the default | |
``/register`` behaviour with a custom controller | |
""" | |
implements(IConfigurer, inherit=True) | |
implements(IGenshiStreamFilter, inherit=True) | |
implements(IRoutes, inherit=True) | |
def update_config(self, config): | |
"""This IConfigurer implementation causes CKAN to look in the | |
```public``` and ```templates``` directories present in this | |
package for any customisations. | |
It also shows how to set the site title here (rather than in | |
the main site .ini file), and causes CKAN to use the | |
customised package form defined in ``package_form.py`` in this | |
directory. | |
""" | |
here = os.path.dirname(__file__) | |
rootdir = os.path.dirname(os.path.dirname(here)) | |
our_public_dir = os.path.join(rootdir, 'ckanext', | |
'exampletheme', 'theme', 'public') | |
template_dir = os.path.join(rootdir, 'ckanext', | |
'exampletheme', 'theme', | |
'templates') | |
# set our local template and resource overrides | |
config['extra_public_paths'] = ','.join([our_public_dir, | |
config.get('extra_public_paths', '')]) | |
config['extra_template_paths'] = ','.join([template_dir, | |
config.get('extra_template_paths', '')]) | |
# set the title | |
config['ckan.site_title'] = "An example CKAN theme" | |
# set the customised package form (see ``setup.py`` for entry point) | |
config['package_form'] = "example_form" | |
def filter(self, stream): | |
"""Conform to IGenshiStreamFilter interface. | |
This example filter renames 'frob' to 'foobar' (this string is | |
found in the custom ``home/index.html`` template provided as | |
part of the package). | |
""" | |
stream = stream | Transformer('//p[@id="examplething"]/text()')\ | |
.substitute(r'frob', r'foobar') | |
return stream | |
def before_map(self, map): | |
"""This IRoutes implementation overrides the standard | |
``/user/register`` behaviour with a custom controller. You | |
might instead use it to provide a completely new page, for | |
example. | |
Note that we have also provided a custom register form | |
template at ``theme/templates/user/register.html``. | |
""" | |
map.connect('/user/register', | |
controller='ckanext.exampletheme.controller:CustomUserController', | |
action='custom_register') | |
return map | |
from sqlalchemy.util import OrderedDict | from sqlalchemy.util import OrderedDict |
from pylons.i18n import _ | from pylons.i18n import _ |
from ckan.forms import common | from ckan.forms import common |
from ckan.forms import package | from ckan.forms import package |
# Setup the fieldset | # Setup the fieldset |
def build_example_form(is_admin=False, | def build_example_form(is_admin=False, |
user_editable_groups=None, | user_editable_groups=None, |
**kwargs): | **kwargs): |
"""Customise the core CKAN dataset editing form by adding a new | """Customise the core CKAN dataset editing form by adding a new |
field "temporal coverage", and changing the layout of the core | field "temporal coverage", and changing the layout of the core |
fields. | fields. |
""" | """ |
# Restrict fields | # Restrict fields |
builder = package.build_package_form( | builder = package.build_package_form( |
user_editable_groups=user_editable_groups) | user_editable_groups=user_editable_groups) |
# Extra fields | # Extra fields |
builder.add_field(common.DateRangeExtraField('temporal_coverage')) | builder.add_field(common.DateRangeExtraField('temporal_coverage')) |
# Layout | # Layout |
field_groups = OrderedDict([ | field_groups = OrderedDict([ |
(_('Basic information'), ['title', 'name', 'url', | (_('Customised Basic information'), ['title', 'name', 'url', |
'notes', 'tags']), | 'notes', 'tags']), |
(_('Details'), ['author', 'author_email', 'groups', | (_('Details'), ['author', 'author_email', 'groups', |
'maintainer', 'maintainer_email', | 'maintainer', 'maintainer_email', |
'license_id', 'temporal_coverage' ]), | 'license_id', 'temporal_coverage' ]), |
(_('Resources'), ['resources']), | (_('Resources'), ['resources']), |
]) | ]) |
builder.set_displayed_fields(field_groups) | builder.set_displayed_fields(field_groups) |
return builder | return builder |
def get_example_fieldset(is_admin=False, user_editable_groups=None, **kwargs): | def get_example_fieldset(is_admin=False, user_editable_groups=None, **kwargs): |
return build_example_form(is_admin=is_admin, | return build_example_form(is_admin=is_admin, |
user_editable_groups=user_editable_groups, | user_editable_groups=user_editable_groups, |
**kwargs).get_fieldset() | **kwargs).get_fieldset() |
import os | |
from logging import getLogger | |
from genshi.filters.transform import Transformer | |
from ckan.plugins import implements, SingletonPlugin | |
from ckan.plugins import IConfigurer | |
from ckan.plugins import IGenshiStreamFilter | |
from ckan.plugins import IRoutes | |
log = getLogger(__name__) | |
class ExampleThemePlugin(SingletonPlugin): | |
"""This plugin demonstrates how a theme packaged as a CKAN | |
extension might extend CKAN behaviour. | |
In this case, we implement three extension interfaces: | |
- ``IConfigurer`` allows us to override configuration normally | |
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 | |
and resources that customise the core look and feel. | |
- ``IGenshiStreamFilter`` allows us to filter and transform the | |
HTML stream just before it is rendered. In this case we use | |
it to rename "frob" to "foobar" | |
- ``IRoutes`` allows us to add new URLs, or override existing | |
URLs. In this example we use it to override the default | |
``/register`` behaviour with a custom controller | |
""" | |
implements(IConfigurer, inherit=True) | |
implements(IGenshiStreamFilter, inherit=True) | |
implements(IRoutes, inherit=True) | |
def update_config(self, config): | |
"""This IConfigurer implementation causes CKAN to look in the | |
```public``` and ```templates``` directories present in this | |
package for any customisations. | |
It also shows how to set the site title here (rather than in | |
the main site .ini file), and causes CKAN to use the | |
customised package form defined in ``package_form.py`` in this | |
directory. | |
""" | |
here = os.path.dirname(__file__) | |
rootdir = os.path.dirname(os.path.dirname(here)) | |
our_public_dir = os.path.join(rootdir, 'ckanext', | |
'exampletheme', 'theme', 'public') | |
template_dir = os.path.join(rootdir, 'ckanext', | |
'exampletheme', 'theme', | |
'templates') | |
# set our local template and resource overrides | |
config['extra_public_paths'] = ','.join([our_public_dir, | |
config.get('extra_public_paths', '')]) | |
config['extra_template_paths'] = ','.join([template_dir, | |
config.get('extra_template_paths', '')]) | |
# set the title | |
config['ckan.site_title'] = "An example CKAN theme" | |
# set the customised package form (see ``setup.py`` for entry point) | |
config['package_form'] = "example_form" | |
def filter(self, stream): | |
"""Conform to IGenshiStreamFilter interface. | |
This example filter renames 'frob' to 'foobar' (this string is | |
found in the custom ``home/index.html`` template provided as | |
part of the package). | |
""" | |
stream = stream | Transformer('//p[@id="examplething"]/text()')\ | |
.substitute(r'frob', r'foobar') | |
return stream | |
def before_map(self, map): | |
"""This IRoutes implementation overrides the standard | |
``/user/register`` behaviour with a custom controller. You | |
might instead use it to provide a completely new page, for | |
example. | |
Note that we have also provided a custom register form | |
template at ``theme/templates/user/register.html``. | |
""" | |
# Note that when we set up the route, we must use the form | |
# that gives it a name (i.e. in this case, 'register'), so it | |
# works correctly with the url_for helper:: | |
# h.url_for('register') | |
map.connect('register', | |
'/user/register', | |
controller='ckanext.exampletheme.controller:CustomUserController', | |
action='custom_register') | |
map.connect('/package/new', controller='package_formalchemy', action='new') | |
map.connect('/package/edit/{id}', controller='package_formalchemy', action='edit') | |
return map | |
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] |
exampletheme=ckanext.exampletheme:ExampleThemePlugin | exampletheme=ckanext.exampletheme.plugin:ExampleThemePlugin |
[ckan.forms] | [ckan.forms] |
example_form = ckanext.exampletheme.package_form:get_example_fieldset | example_form = ckanext.exampletheme.package_form:get_example_fieldset |
""", | """, |
) | ) |