Customizing CKAN’s templates
CKAN pages are generated from Jinja2 template files. This tutorial will walk
you through the process of writing your own template files to modify and
replace the default ones, and change the layout and content of CKAN pages.
Creating a CKAN extension
A CKAN theme is simply a CKAN plugin that contains some custom templates and
static files, so before getting started on our CKAN theme we’ll have to create
an extension and plugin. For a detailed explanation of the steps below, see
Writing extensions tutorial.
Use the paster create command to create an empty extension:
. /usr/lib/ckan/default/bin/activate
cd /usr/lib/ckan/default/src
paster --plugin=ckan create -t ckanext ckanext-example_theme
Create the file ckanext-example_theme/ckanext/example_theme/plugin.py with the following contents:
import ckan.plugins as plugins
class ExampleThemePlugin(plugins.SingletonPlugin):
'''An example theme plugin.
'''
pass
Edit the entry_points in ckanext-example_theme/setup.py to look like this:
entry_points='''
[ckan.plugins]
example_theme=ckanext.example_theme.plugin:ExampleThemePlugin
''',
Run python setup.py develop:
cd ckanext-example_theme
python setup.py develop
Add the plugin to the ckan.plugins setting in your /etc/ckan/default/development.ini
file:
ckan.plugins = stats text_view recline_view example_theme
Start CKAN in the development web server:
$ paster serve --reload /etc/ckan/default/development.ini
Starting server in PID 13961.
serving on 0.0.0.0:5000 view at http://127.0.0.1:5000
Open the CKAN front page in your web browser. If your plugin is in the
ckan.plugins setting and CKAN starts without crashing, then your
plugin is installed and CKAN can find it. Of course, your plugin doesn’t
do anything yet.
Replacing a default template file
Every CKAN page is generated by rendering a particular template. For each
page of a CKAN site there’s a corresponding template file. For example the
front page is generated from the
ckan/templates/home/index.html
file, the /about page is generated from
ckan/templates/home/about.html,
the datasets page at /dataset is generated from
ckan/templates/package/search.html,
etc.
To customize pages, our plugin needs to register its own custom template
directory containing template files that override the default ones.
Edit the ckanext-example_theme/ckanext/example_theme/plugin.py file that we created earlier, so that it looks like
this:
'''plugin.py
'''
import ckan.plugins as plugins
import ckan.plugins.toolkit as toolkit
class ExampleThemePlugin(plugins.SingletonPlugin):
'''An example theme plugin.
'''
# Declare that this class implements IConfigurer.
plugins.implements(plugins.IConfigurer)
def update_config(self, config):
# Add this plugin's templates dir to CKAN's extra_template_paths, so
# that CKAN will use this plugin's custom templates.
# 'templates' is the path to the templates dir, relative to this
# plugin.py file.
toolkit.add_template_directory(config, 'templates')
This new code does a few things:
It imports CKAN’s plugins toolkit
module:
import ckan.plugins.toolkit as toolkit
The plugins toolkit is a Python module containing core functions, classes
and exceptions for CKAN plugins to use. For more about the plugins toolkit,
see Writing extensions tutorial.
It calls implements() to declare that it
implements the IConfigurer plugin
interface:
plugins.implements(plugins.IConfigurer)
This tells CKAN that our
ExampleThemePlugin class
implements the methods declared in the
IConfigurer interface. CKAN will call
these methods of our plugin class at the appropriate times.
It implements the
update_config() method, which
is the only method declared in the
IConfigurer interface:
def update_config(self, config):
# Add this plugin's templates dir to CKAN's extra_template_paths, so
# that CKAN will use this plugin's custom templates.
# 'templates' is the path to the templates dir, relative to this
# plugin.py file.
toolkit.add_template_directory(config, 'templates')
CKAN will call this method when it starts up, to give our plugin a chance to
modify CKAN’s configuration settings. Our
update_config()
method calls add_template_directory() to
register its custom template directory with CKAN.
This tells CKAN to look for template files in ckanext-example_theme/ckanext/example_theme/templates whenever
it renders a page. Any template file in this directory that has the same
name as one of CKAN’s default template files, will be used instead of the
default file.
Now, let’s customize the CKAN front page. We first need to discover which
template file CKAN uses to render the front page, so we can replace it.
Set debug to true in your /etc/ckan/default/development.ini file:
[DEFAULT]
# WARNING: *THIS SETTING MUST BE SET TO FALSE ON A PRODUCTION ENVIRONMENT*
debug = true
The first template file listed is the one we’re interested in:
Template name: home/index.html
Template path: /usr/lib/ckan/default/src/ckan/ckan/templates/home/index.html
This tells us that home/index.html is the root template file used to render
the front page. The debug footer appears at the bottom of every CKAN page, and
can always be used to find the page’s template files, and other information
about the page.
Note
Most CKAN pages are rendered from multiple template files.
The first file listed in the debug footer is the root template file of the
page. All other template files used to render the page (listed further down
in the debug footer) are either included by the root file, or included by
another file that is included by the root file.
To figure out which template file renders a particular part of the page you
have to inspect the
source code of the template files,
starting with the root file.
Now let’s override home/index.html using our plugins’ custom templates
directory. Create the ckanext-example_theme/ckanext/example_theme/templates directory, create a home directory
inside the templates directory, and create an empty index.html file
inside the home directory:
ckanext-example_theme/
ckanext/
example_theme/
templates/
home/
index.html <-- An empty file.
If you now restart the development web server (kill the server using Ctrl-c,
then run the paster serve command again) and reload the CKAN front page
in your web browser, you should see an empty page, because we’ve replaced the
template file for the front page with an empty file.
Note
If you run paster serve with the --reload option, then it isn’t
usually necessary to restart the server after editing a Python file,
a template file, your CKAN config file, or any other CKAN file. If you’ve
added a new file or directory, however, you need to restart the server
manually.
Jinja2
CKAN template files are written in the Jinja2 templating language. Jinja
template files, such as our index.html file, are simply text files that,
when processed, generate any text-based output format such as HTML,
XML, CSV, etc. Most of the template files in CKAN generate HTML.
We’ll introduce some Jinja2 basics below. Jinja2 templates have many more
features than these, for full details see the
Jinja2 docs.
Expressions and variables
Jinja2 expressions are snippets of code between {{ ... }} delimiters,
when a template is rendered any expressions are evaluated and replaced with
the resulting value.
The simplest use of an expression is to display the value of a variable, for
example {{ foo }} in a template file will be replaced with the value of the
variable foo when the template is rendered.
CKAN makes a number of global variables available to all templates. One such
variable is app_globals, which can be used to access certain global
attributes including some of the settings from your CKAN config file. For
example, to display the value of the ckan.site_title setting from your
config file you would put this code in any template file:
<p>The title of this site is: {{ app_globals.site_title }}.</p>
Note
If a template tries to render a variable or attribute that doesn’t exist,
rather than crashing or giving an error message, the Jinja2 expression
simply evaluates to nothing (an empty string). For example, these Jinja2
expressions will output nothing:
{{ app_globals.an_attribute_that_does_not_exist }}
{{ a_variable_that_does_not_exist }}
If, on the other hand, you try to render an attribute of a variable that
doesn’t exist, then Jinja2 will crash. For example, this Jinja2 expression
will crash with an
UndefinedError: 'a_variable_that_does_not_exist' is undefined:
{{ a_variable_that_does_not_exist.an_attribute_that_does_not_exist }}
See the Jinja2 variables docs
for details.
Extending templates with {% ckan_extends %}
CKAN provides a custom Jinja tag {% ckan_extends %} that we can use to
declare that our home/index.html template extends the default
home/index.html template, instead of completely replacing it.
Edit the empty index.html file you just created, and add one line:
If you now reload the CKAN front page in your browser, you should see the
normal front page appear again. When CKAN processes our index.html file,
the {% ckan_extends %} tag tells it to process the default
home/index.html file first.
Replacing template blocks with {% block %}
Jinja templates can contain blocks that child templates can override. For
example, CKAN’s default home/layout1.html template (one of the files used
to render the CKAN front page) has a block that contains the Jinja and HTML
code for the “featured group” that appears on the front page:
{% block featured_group %}
{% snippet 'home/snippets/featured_group.html' %}
{% endblock %}
Note
This code calls a template snippet that contains the
actual Jinja and HTML code for the featured group, more on snippets later.
Note
The CKAN front page supports a number of different layouts: layout1,
layout2, layout3, etc. The layout can be chosen by a sysadmin using the
admin page. This tutorial assumes your CKAN is set to
use the first (default) layout.
When a custom template file extends one of CKAN’s default template files using
{% ckan_extends %}, it can replace any of the blocks from the default
template with its own code by using {% block %}. Create the file
ckanext-example_theme/ckanext/example_theme/templates/home/layout1.html with these contents:
{% ckan_extends %}
{% block featured_group %}
Hello block world!
{% endblock %}
This file extends the default layout1.html template, and overrides the
featured_group block. Restart the development web server and reload the
CKAN front page in your browser. You should see that the featured groups
section of the page has been replaced, but the rest of the page remains intact.
Note
Most template files in CKAN contain multiple blocks.
To find out what blocks a template has, and which block renders a particular
part of the page, you have to look at the
source code of the default template files.
Extending parent blocks with Jinja’s {{ super() }}
If you want to add some code to a block but don’t want to replace the entire
block, you can use Jinja’s {{ super() }} tag:
{% ckan_extends %}
{% block featured_group %}
<p>This paragraph will be added to the top of the
<code>featured_group</code> block.</p>
{# Insert the contents of the original featured_group block: #}
{{ super() }}
<p>This paragraph will be added to the bottom of the
<code>featured_group</code> block.</p>
{% endblock %}
When the child block above is rendered, Jinja will replace the
{{ super() }} tag with the contents of the parent block.
The {{ super() }} tag can be placed anywhere in the block.
Template helper functions
Now let’s put some interesting content into our custom template block.
One way for templates to get content out of CKAN is by calling CKAN’s
template helper functions.
For example, let’s replace the featured group on the front page with an
activity stream of the site’s recently created, updated and deleted datasets.
Change the code in ckanext-example_theme/ckanext/example_theme/templates/home/layout1.html to this:
{% ckan_extends %}
{% block featured_group %}
{{ h.recently_changed_packages_activity_stream(limit=4) }}
{% endblock %}
Reload the CKAN front page in your browser and you should see a new activity
stream:
To call a template helper function we use a Jinja2 expression (code wrapped
in {{ ... }} brackets), and we use the global variable h
(available to all templates) to access the helper:
{{ h.recently_changed_packages_activity_stream(limit=4) }}
To see what other template helper functions are available, look at the
template helper functions reference docs.
Adding your own template helper functions
Plugins can add their own template helper functions by implementing CKAN’s
ITemplateHelpers plugin interface.
(see Writing extensions tutorial for a detailed explanation of CKAN plugins and
plugin interfaces).
Let’s add another item to our custom front page: a list of the most “popular”
groups on the site (the groups with the most datasets). We’ll add a custom
template helper function to select the groups to be shown. First, in our
plugin.py file we need to implement
ITemplateHelpers and provide our helper
function. Change the contents of plugin.py to look like this:
import ckan.plugins as plugins
import ckan.plugins.toolkit as toolkit
def most_popular_groups():
'''Return a sorted list of the groups with the most datasets.'''
# Get a list of all the site's groups from CKAN, sorted by number of
# datasets.
groups = toolkit.get_action('group_list')(
data_dict={'sort': 'packages desc', 'all_fields': True})
# Truncate the list to the 10 most popular groups only.
groups = groups[:10]
return groups
class ExampleThemePlugin(plugins.SingletonPlugin):
'''An example theme plugin.
'''
plugins.implements(plugins.IConfigurer)
# Declare that this plugin will implement ITemplateHelpers.
plugins.implements(plugins.ITemplateHelpers)
def update_config(self, config):
# Add this plugin's templates dir to CKAN's extra_template_paths, so
# that CKAN will use this plugin's custom templates.
toolkit.add_template_directory(config, 'templates')
def get_helpers(self):
'''Register the most_popular_groups() function above as a template
helper function.
'''
# Template helper function names should begin with the name of the
# extension they belong to, to avoid clashing with functions from
# other extensions.
return {'example_theme_most_popular_groups': most_popular_groups}
We’ve added a number of new features to plugin.py. First, we defined a
function to get the most popular groups from CKAN:
def most_popular_groups():
'''Return a sorted list of the groups with the most datasets.'''
# Get a list of all the site's groups from CKAN, sorted by number of
# datasets.
groups = toolkit.get_action('group_list')(
data_dict={'sort': 'packages desc', 'all_fields': True})
# Truncate the list to the 10 most popular groups only.
groups = groups[:10]
return groups
This function calls one of CKAN’s action functions to get the groups from
CKAN. See Writing extensions tutorial for more about action functions.
Next, we called implements() to declare that our class
now implements ITemplateHelpers:
plugins.implements(plugins.ITemplateHelpers)
Finally, we implemented the
get_helpers() method from
ITemplateHelpers to register our function
as a template helper:
def get_helpers(self):
'''Register the most_popular_groups() function above as a template
helper function.
'''
# Template helper function names should begin with the name of the
# extension they belong to, to avoid clashing with functions from
# other extensions.
return {'example_theme_most_popular_groups': most_popular_groups}
Now that we’ve registered our helper function, we need to call it from our
template. As with CKAN’s default template helpers, templates access custom
helpers via the global variable h.
Edit ckanext-example_theme/ckanext/example_theme/templates/home/layout1.html to look like this:
{% ckan_extends %}
{% block featured_group %}
{{ h.recently_changed_packages_activity_stream(limit=4) }}
{% endblock %}
{% block featured_organization %}
{# Show a list of the site's most popular groups. #}
<h3>Most popular groups</h3>
<ul>
{% for group in h.example_theme_most_popular_groups() %}
<li>{{ group.display_name }}</li>
{% endfor %}
</ul>
{% endblock %}
Now reload your CKAN front page in your browser. You should see the featured
organization section replaced with a list of the most popular groups:
Simply displaying a list of group titles isn’t very good. We want the groups to
be hyperlinked to their pages, and also to show some other information about
the group such as its description and logo image. To display our groups nicely,
we’ll use CKAN’s template snippets...
Template snippets
Template snippets are small snippets of template code that, just like helper
functions, can be called from any template file. To call a snippet, you use
another of CKAN’s custom Jinja2 tags: {% snippet %}. CKAN comes with a
selection of snippets, which you can find in the various snippets
directories in ckan/templates/,
such as ckan/templates/snippets/
and ckan/templates/package/snippets/.
For a complete list of the default snippets available to templates, see
Template snippets reference.
ckan/templates/group/snippets/group_list.html is a snippet that renders a
list of groups nicely (it’s used to render the groups on CKAN’s /group page
and on user dashboard pages, for example):
{#
Display a grid of group items.
groups - A list of groups.
Example:
{% snippet "group/snippets/group_list.html" %}
#}
{% block group_list %}
<ul class="media-grid" data-module="media-grid">
{% block group_list_inner %}
{% for group in groups %}
{% snippet "group/snippets/group_item.html", group=group, position=loop.index %}
{% endfor %}
{% endblock %}
</ul>
{% endblock %}
(As you can see, this snippet calls another snippet, group_item.html, to
render each individual group.)
Let’s change our ckanext-example_theme/ckanext/example_theme/templates/home/layout1.html file to call this snippet:
{% ckan_extends %}
{% block featured_group %}
{{ h.recently_changed_packages_activity_stream(limit=4) }}
{% endblock %}
{% block featured_organization %}
<h3>Most popular groups</h3>
{# Call the group_list.html snippet. #}
{% snippet 'group/snippets/group_list.html',
groups=h.example_theme_most_popular_groups() %}
{% endblock %}
Here we pass two arguments to the {% snippet %} tag:
{% snippet 'group/snippets/group_list.html',
groups=h.example_theme_most_popular_groups() %}
the first argument is the name of the snippet file to call. The second
argument, separated by a comma, is the list of groups to pass into the snippet.
After the filename you can pass any number of variables into a snippet, and
these will all be available to the snippet code as top-level global variables.
As in the group_list.html docstring above, each snippet’s docstring
should document the parameters it requires.
If you reload your CKAN front page in your web browser now, you should see
the most popular groups rendered in the same style as the list of groups on
the /groups page:
This style isn’t really what we want for our front page, each group is too big.
To render the groups in a custom style, we can define a custom snippet...
Adding your own template snippets
Just as plugins can add their own template helper functions, they can also add
their own snippets. To add template snippets, all a plugin needs to do is add a
snippets directory in its templates directory, and start adding files.
The snippets will be callable from other templates immediately.
Note
For CKAN to find your plugins’ snippets directories, you should already have
added your plugin’s custom template directory to CKAN, see Replacing a default template file.
Let’s create a custom snippet to display our most popular groups, we’ll put
the <h3>Most popular groups</h3> heading into the snippet and make it nice
and modular, so that we can reuse the whole thing on different parts of the
site if we want to.
Create a new directory ckanext-example_theme/ckanext/example_theme/templates/snippets containing a file named
example_theme_most_popular_groups.html with these contents:
{#
Renders a list of the site's most popular groups.
groups - the list of groups to render
#}
<h3>Most popular groups</h3>
<ul>
{% for group in groups %}
<li>
<a href="{{ h.url_for('group_read', action='read', id=group.name) }}">
<h3>{{ group.display_name }}</h3>
</a>
{% if group.description %}
<p>
{{ h.markdown_extract(group.description, extract_length=80) }}
</p>
{% else %}
<p>{{ _('This group has no description') }}</p>
{% endif %}
{% if group.packages %}
<strong>{{ ungettext('{num} Dataset', '{num} Datasets', group.packages).format(num=group.packages) }}</strong>
{% else %}
<span>{{ _('0 Datasets') }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
Note
As in the example above, a snippet should have a docstring at the top of the
file that briefly documents what the snippet does and what parameters it
requires. See Snippets should have docstrings.
This code uses a Jinja2 for loop to render each of the groups, and calls a
number of CKAN’s template helper functions:
- To hyperlink each group’s name to the group’s page, it calls
url_for().
- If the group has a description, it calls
markdown_extract() to render the description
nicely.
- If the group doesn’t have a description, it uses the _() function to
mark the 'This group has no description' message for translation.
When the page is rendered in a user’s web browser, this string will be shown
in the user’s language (if there’s a translation of the string into that
language).
- When rendering the group’s number of datasets, it uses the
ungettext() function to mark the message for translation with
localized handling of plural forms.
The code also accesses the attributes of each group: {{ group.name }}`,
``{{ group.display_name }}, {{ group.description }},
{{ group.packages }}, etc. To see what attributes a group or any other CKAN
object (packages/datasets, organizations, users...) has, you can use
CKAN’s API to inspect the object. For example to find out what
attributes a group has, call the group_show()
function.
Now edit your ckanext-example_theme/ckanext/example_theme/templates/home/layout1.html file and change it to use our new snippet instead
of the default one:
{% ckan_extends %}
{% block featured_group %}
{{ h.recently_changed_packages_activity_stream(limit=4) }}
{% endblock %}
{% block featured_organization %}
{% snippet 'snippets/example_theme_most_popular_groups.html',
groups=h.example_theme_most_popular_groups() %}
{% endblock %}
Restart the development web server and reload the CKAN front page and you
should see the most popular groups rendered differently:
Warning
Default snippets can be overridden.
If a plugin adds a snippet with the same name as one of CKAN’s default
snippets, the plugin’s snippet will override the default snippet wherever
the default snippet is used.
Also if two plugins both have snippets with the same name, one of the
snippets will override the other.
To avoid unintended conflicts, we recommend that snippet filenames begin
with the name of the extension they belong to, e.g.
snippets/example_theme_*.html.
See Snippet filenames should begin with the name of the extension.
Note
Snippets don’t have access to the global template context variable,
c (see Variables and functions available to templates).
Snippets can access other global variables such as h,
app_globals and request, as well as any variables
explicitly passed into the snippet by the parent template when it calls the
snippet with a {% snippet %} tag.
Accessing custom config settings from templates
Not all CKAN config settings are available to templates via
app_globals. In particular, if an extension wants to use its own
custom config setting, this setting will not be available. If you need to
access a custom config setting from a template, you can do so by wrapping the
config setting in a helper function.
Todo
I’m not sure if making config settings available to templates like this is
a very good idea. Is there an alternative best practice?
Let’s add a config setting, show_most_popular_groups, to enable or disable
the most popular groups on the front page. First, add a new helper function to
plugin.py to wrap the config setting.
import pylons.config as config
import ckan.plugins as plugins
import ckan.plugins.toolkit as toolkit
def show_most_popular_groups():
'''Return the value of the most_popular_groups config setting.
To enable showing the most popular groups, add this line to the
[app:main] section of your CKAN config file::
ckan.example_theme.show_most_popular_groups = True
Returns ``False`` by default, if the setting is not in the config file.
:rtype: boolean
'''
value = config.get('ckan.example_theme.show_most_popular_groups', False)
value = toolkit.asbool(value)
return value
def most_popular_groups():
'''Return a sorted list of the groups with the most datasets.'''
# Get a list of all the site's groups from CKAN, sorted by number of
# datasets.
groups = toolkit.get_action('group_list')(
data_dict={'sort': 'packages desc', 'all_fields': True})
# Truncate the list to the 10 most popular groups only.
groups = groups[:10]
return groups
class ExampleThemePlugin(plugins.SingletonPlugin):
'''An example theme plugin.
'''
plugins.implements(plugins.IConfigurer)
# Declare that this plugin will implement ITemplateHelpers.
plugins.implements(plugins.ITemplateHelpers)
def update_config(self, config):
# Add this plugin's templates dir to CKAN's extra_template_paths, so
# that CKAN will use this plugin's custom templates.
toolkit.add_template_directory(config, 'templates')
def get_helpers(self):
'''Register the most_popular_groups() function above as a template
helper function.
'''
# Template helper function names should begin with the name of the
# extension they belong to, to avoid clashing with functions from
# other extensions.
return {'example_theme_most_popular_groups': most_popular_groups,
'example_theme_show_most_popular_groups':
show_most_popular_groups,
}
def show_most_popular_groups():
'''Return the value of the most_popular_groups config setting.
To enable showing the most popular groups, add this line to the
[app:main] section of your CKAN config file::
ckan.example_theme.show_most_popular_groups = True
Returns ``False`` by default, if the setting is not in the config file.
:rtype: boolean
'''
value = config.get('ckan.example_theme.show_most_popular_groups', False)
value = toolkit.asbool(value)
return value
Now we can call this helper function from our layout1.html template:
{% block featured_organization %}
{% if h.example_theme_show_most_popular_groups() %}
{% snippet 'snippets/example_theme_most_popular_groups.html' %}
{% else %}
{{ super() }}
{% endif %}
{% endblock %}
If the user sets this config setting to True in their CKAN config file,
then the most popular groups will be displayed on the front page, otherwise
the block will fall back to its default contents.
Adding static files
You may need to add some custom static files to your CKAN site and use them
from your templates, for example image files, PDF files, or any other static
files that should be returned as-is by the webserver (as opposed to Jinja
template files, which CKAN renders before returning them to the user).
By adding a directory to CKAN’s extra_public_paths config setting,
a plugin can make a directory of static files available to be used or linked to
by templates. Let’s add a static image file, and change the home page template
to use our file as the promoted image on the front page.
First, create a public directory in your extension with a
promoted-image.jpg file in it:
ckanext-example_theme/
ckanext/
example_theme/
public/
promoted-image.jpg
promoted-image.jpg should be a 420x220px JPEG image file. You could use
this image file for example:
Then in plugin.py, register your public directory with CKAN by calling
the add_public_directory() function. Add this
line to the update_config()
function:
def update_config(self, config):
# Add this plugin's templates dir to CKAN's extra_template_paths, so
# that CKAN will use this plugin's custom templates.
toolkit.add_template_directory(config, 'templates')
# Add this plugin's public dir to CKAN's extra_public_paths, so
# that CKAN will use this plugin's custom static files.
toolkit.add_public_directory(config, 'public')
If you now browse to 127.0.0.1:5000/promoted-image.jpg,
you should see your image file.
To replace the image on the front page with your custom image, we need to
override the promoted.html template snippet. Create the following directory
and file:
ckanext-example_theme/
ckanext/
example_theme/
templates/
home/
snippets/
promoted.html
Edit your new promoted.html snippet, and insert these contents:
{% ckan_extends %}
{% block home_image_caption %}
{{ _("CKAN's data previewing tool has many powerful features") }}
{% endblock %}
{# Replace the promoted image. #}
{% block home_image_content %}
<a class="media-image" href="#">
<img src="/promoted-image.jpg" alt="Featured image"
width="420" height="220" />
</a>
{% endblock %}
After calling {% ckan_extends %} to declare that it extends (rather than
completely replaces) the default promoted.html snippet, this custom snippet
overrides two of promoted.html‘s template blocks. The first block replaces
the caption text of the promoted image. The second block replaces the <img>
tag itself, pointing it at our custom static image file:
{% block home_image_content %}
<a class="media-image" href="#">
<img src="/promoted-image.jpg" alt="Featured image"
width="420" height="220" />
</a>
{% endblock %}
If you now restart the development web server and reload the CKAN front page
in your browser, you should see the promoted image replaced with our custom
one:
Customizing CKAN’s CSS
See also
There’s nothing special about CSS in CKAN, once you’ve got started with
editing CSS in CKAN (by following the tutorial below), then you just use the
usual tools and techniques to explore and hack the CSS. We recommend using
your browser’s web development tools to explore and experiment with the CSS,
then using any good text editor to edit your extension’s CSS files as
needed. For example:
- Firefox developer tools
- These include a Page Inspector and a Style Editor
- Firebug
- Another web development toolkit for Firefox
- Chrome developer tools
- Tools for inspecting and editing CSS in Google Chrome
- Mozilla Developer Network’s CSS section
- A good collection of CSS documentation and tutorials
Extensions can add their own CSS files to modify or extend CKAN’s default CSS.
Create an example_theme.css file in your extension’s public directory:
ckanext-example_theme/
ckanext/
example_theme/
public/
example_theme.css
Add this CSS into the example_theme.css file, to change the color of CKAN’s
“account masthead” (the bar across the top of the site that shows the logged-in
user’s account info):
.account-masthead {
background-color: rgb(40, 40, 40);
}
If you restart the development web server you should be able to open this file
at http://127.0.0.1:5000/example_theme.css in a web browser.
To make CKAN use our custom CSS we need to override the base.html template,
this is the base template which the templates for all CKAN pages extend, so if
we include a CSS file in this base template then the file will be included in
every page of your CKAN site. Create the file:
ckanext-example_theme/
ckanext/
example_theme/
templates/
base.html
and put this Jinja code in it:
{% ckan_extends %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="/example_theme.css" />
{% endblock %}
The default base.html template defines a styles block which can be
extended to link to custom CSS files (any code in the styles block will appear
in the <head> of the HTML page).
Restart the development web server and reload the CKAN page in your browser,
and you should see the background color of the account masthead change:
This custom color should appear on all pages of your CKAN site.
Now that we have CKAN using our CSS file, we can add more CSS rules to the file
and customize CKAN’s CSS as much as we want.
Let’s add a bit more code to our example_theme.css file. This CSS
implements a partial imitation of the datahub.io theme
(circa 2013):
/* =====================================================
The "account masthead" bar across the top of the site
===================================================== */
.account-masthead {
background-color: rgb(40, 40, 40);
}
/* The "bubble" containing the number of new notifications. */
.account-masthead .account .notifications a span {
background-color: black;
}
/* The text and icons in the user account info. */
.account-masthead .account ul li a {
color: rgba(255, 255, 255, 0.6);
}
/* The user account info text and icons, when the user's pointer is hovering
over them. */
.account-masthead .account ul li a:hover {
color: rgba(255, 255, 255, 0.7);
background-color: black;
}
/* ========================================================================
The main masthead bar that contains the site logo, nav links, and search
======================================================================== */
.masthead {
background-color: #3d3d3d;
}
/* The "navigation pills" in the masthead (the links to Datasets,
Organizations, etc) when the user's pointer hovers over them. */
.masthead .navigation .nav-pills li a:hover {
background-color: rgb(48, 48, 48);
color: white;
}
/* The "active" navigation pill (for example, when you're on the /dataset page
the "Datasets" link is active). */
.masthead .navigation .nav-pills li.active a {
background-color: rgb(74, 74, 74);
}
/* The "box shadow" effect that appears around the search box when it
has the keyboard cursor's focus. */
.masthead input[type="text"]:focus {
-webkit-box-shadow: inset 0px 0px 2px 0px rgba(0, 0, 0, 0.7);
box-shadow: inset 0px 0px 2px 0px rgba(0, 0, 0, 0.7);
}
/* ===========================================
The content in the middle of the front page
=========================================== */
/* Remove the "box shadow" effect around various boxes on the page. */
.box {
box-shadow: none;
}
/* Remove the borders around the "Welcome to CKAN" and "Search Your Data"
boxes. */
.hero .box {
border: none;
}
/* Change the colors of the "Search Your Data" box. */
.homepage .module-search .module-content {
color: rgb(68, 68, 68);
background-color: white;
}
/* Change the background color of the "Popular Tags" box. */
.homepage .module-search .tags {
background-color: rgb(61, 61, 61);
}
/* Remove some padding. This makes the bottom edges of the "Welcome to CKAN"
and "Search Your Data" boxes line up. */
.module-content:last-child {
padding-bottom: 0px;
}
.homepage .module-search {
padding: 0px;
}
/* Add a border line between the top and bottom halves of the front page. */
.homepage [role="main"] {
border-top: 1px solid rgb(204, 204, 204);
}
/* ====================================
The footer at the bottom of the site
==================================== */
.site-footer,
body {
background-color: rgb(40, 40, 40);
}
/* The text in the footer. */
.site-footer,
.site-footer label,
.site-footer small {
color: rgba(255, 255, 255, 0.6);
}
/* The link texts in the footer. */
.site-footer a {
color: rgba(255, 255, 255, 0.6);
}
Adding CSS and JavaScript files using Fanstatic
If you’re adding CSS files to your theme, you can add them
using Fanstatic rather than the simple
extra_public_paths method described in Adding static files.
If you’re adding a JavaScript module, you must use Fanstatic.
Using Fanstatic to add JavaScript and CSS files takes advantage
of Fanstatic’s features, such as automatically serving minified files in
production, caching and bundling files together to reduce page load times,
specifying dependencies between files so that the files a page needs (and only
the files it needs) are always loaded, and other tricks to optimize page load
times.
Note
CKAN will only serve *.js and *.css files as Fanstatic resources,
other types of static files (eg. image files, PDF files) must be added
using the extra_public_paths method described in Adding static files.
Adding a custom JavaScript or CSS file to CKAN using Fanstatic is simple.
We’ll demonstrate by changing our previous custom CSS example (see Customizing CKAN’s CSS)
to serve the CSS file with Fanstatic.
First, create a fanstatic directory in your extension and move the CSS
file from public into fanstatic:
ckanext-example_theme/
ckanext/
example_theme/
public/
promoted-image.jpg
fanstatic/
example_theme.css
Use CKAN’s add_resource() function to
register your fanstatic directory with CKAN. Edit the update_config()
method in your plugin.py file:
def update_config(self, config):
# Add this plugin's templates dir to CKAN's extra_template_paths, so
# that CKAN will use this plugin's custom templates.
toolkit.add_template_directory(config, 'templates')
# Add this plugin's public dir to CKAN's extra_public_paths, so
# that CKAN will use this plugin's custom static files.
toolkit.add_public_directory(config, 'public')
# Register this plugin's fanstatic directory with CKAN.
# Here, 'fanstatic' is the path to the fanstatic directory
# (relative to this plugin.py file), and 'example_theme' is the name
# that we'll use to refer to this fanstatic directory from CKAN
# templates.
toolkit.add_resource('fanstatic', 'example_theme')
Finally, edit your extension’s templates/base.html file and use CKAN’s
custom Jinja2 tag {% resource %} instead of the normal <link> tag to
import the file:
{% ckan_extends %}
{% block styles %}
{{ super() }}
{# Import example_theme.css using Fanstatic.
'example_theme/' is the name that the example_theme/fanstatic directory
was registered with when the toolkit.add_resource() function was called.
'example_theme.css' is the path to the CSS file, relative to the root of
the fanstatic directory. #}
{% resource 'example_theme/example_theme.css' %}
{% endblock %}
Note
You can put {% resource %} tags anywhere in any template, and Fanstatic
will insert the necessary <style> and <script> tags to include your
CSS and JavaScript files and their dependencies in the right places in
the HTML output (CSS files in the HTML <head>, JavaScript files at
the bottom of the page).
Resources will not be included on the line where the {% resource %}
tag is.
Note
A config file can be used to configure how Fanstatic should serve each resource
file (whether or not to bundle files, what order to include files in, whether
to include files at the top or bottom of the page, dependencies between files,
etc.) See Resources for details.
Customizing CKAN’s JavaScript
JavaScript code in CKAN is broken down into modules: small, independent units
of JavaScript code. CKAN themes can add JavaScript features by providing their
own modules. This tutorial will explain the main concepts involved in CKAN
JavaScript modules and walk you through the process of adding custom modules
to themes.
See also
This tutorial assumes a basic understanding of CKAN plugins and templating,
see:
See also
This tutorial assumes a basic understanding of JavaScript and jQuery,
see:
Overview
The idea behind CKAN’s JavaScript modules is to keep the code simple and
easy to test, debug and maintain, by breaking it down into small,
independent modules. JavaScript modules in CKAN don’t share global
variables, and don’t call each other’s code.
These JavaScript modules are attached to HTML elements in the page, and enhance
the functionality of those elements. The idea is that an HTML element with a
JavaScript module attached should still be fully functional even if JavaScript
is completely disabled (e.g. because the user’s web browser doesn’t support
JavaScript). The user experience may not be quite as nice without JavaScript,
but the functionality should still be there. This is a programming technique
known as graceful degradation, and is a basic tenet of web accessibility.
In the sections below, we’ll walk you through the steps to add a new JavaScript
feature to CKAN - dataset info popovers. We’ll add an info button to each
dataset on the datasets page which, when clicked, opens a popover containing
some extra information and user actions related to the dataset:
Initializing a JavaScript module
To get CKAN to call some custom JavaScript code, we need to:
Implement a JavaScript module, and register it with CKAN.
Create the file ckanext-example_theme/ckanext/example_theme/fanstatic/example_theme_popover.js, with these
contents:
// Enable JavaScript's strict mode. Strict mode catches some common
// programming errors and throws exceptions, prevents some unsafe actions from
// being taken, and disables some confusing and bad JavaScript features.
"use strict";
ckan.module('example_theme_popover', function ($, _) {
return {
initialize: function () {
console.log("I've been initialized for element: ", this.el);
}
};
});
This bit of JavaScript calls the ckan.module() function to register a
new JavaScript module with CKAN. ckan.module() takes two arguments: the
name of the module being registered ('example_theme_popover' in this
example) and a function that returns the module itself. The function takes
two arguments, which we’ll look at later. The module is just a JavaScript
object with a single attribute, initialize, whose value is a function
that CKAN will call to initialize the module. In this example, the
initialize function just prints out a confirmation message - this
JavaScript module doesn’t do anything interesting yet.
Note
Each JavaScript module’s initialize() function is called on
DOM ready.
Include the JavaScript module in a page, using Fanstatic, and apply it to
one or more HTML elements on that page. We’ll override CKAN’s
package_item.html template snippet to insert our module whenever a
package is rendered as part of a list of packages (for example, on the
dataset search page). Create the file
ckanext-example_theme/ckanext/example_theme/templates/snippets/package_item.html with these
contents:
{% ckan_extends %}
{% block content %}
{{ super() }}
{# Use Fanstatic to include our custom JavaScript module.
A <script> tag for the module will be inserted in the right place at the
bottom of the page. #}
{% resource 'example_theme/example_theme_popover.js' %}
{# Apply our JavaScript module to an HTML element. The data-module attribute,
which can be applied to any HTML element, tells CKAN to initialize an
instance of the named JavaScript module for the element.
The initialize() method of our module will be called with this HTML
element as its this.el object. #}
<button data-module="example_theme_popover"
class="btn"
href="#">
<i class="icon-info-sign"></i>
</button>
{% endblock %}
If you now restart the development server and open
http://127.0.0.1:5000/dataset in your web browser, you should see an
extra info button next to each dataset shown. If you open a
JavaScript console in your browser, you should see the message that your
module has printed out.
See also
Most web browsers come with built-in developer tools including a
JavaScript console that lets you see text printed by JavaScript code
to console.log(), a JavaScript debugger, and more. For example:
If you have more than one dataset on your page, you’ll see the module’s
message printed once for each dataset. The package_item.html template
snippet is rendered once for each dataset that’s shown in the list, so your
<button> element with the data-module="example_theme_popover"
attribute is rendered once for each dataset, and CKAN creates a new instance
of your JavaScript module for each of these <button> elements. If you
view the source of your page, however, you’ll see that
example_theme_popover.js is only included with a <script> tag once.
Fanstatic is smart enough to deduplicate resources.
Note
JavaScript modules must be included as Fanstatic resources,
you can’t add them to a public directory and include them using your
own <script> tags.
this.options and this.el
Now let’s start to make our JavaScript module do something useful: show a
Bootstrap popover
with some extra info about the dataset when the user clicks on the info button.
First, we need our Jinja template to pass some of the dataset’s fields to our
JavaScript module as options. Change package_item.html to look like
this:
{% ckan_extends %}
{% block content %}
{{ super() }}
{% resource 'example_theme/example_theme_popover.js' %}
{# Apply our JavaScript module to an HTML <button> element.
The additional data-module-* attributes are options that will be passed
to the JavaScript module. #}
<button data-module="example_theme_popover"
data-module-title="{{ package.title }}"
data-module-license="{{ package.license_title }}"
data-module-num_resources="{{ package.num_resources }}">
<i class="icon-info-sign"></i>
</button>
{% endblock %}
This adds some data-module-* attributes to our <button> element, e.g.
data-module-title="{{ package.title }}" ({{ package.title }} is a
Jinja2 expression that evaluates to the
title of the dataset, CKAN passes the Jinja2 variable package to our
template).
Warning
Although HTML 5 treats any attribute named data-* as a data attribute,
only attributes named data-module-* will be passed as options to a CKAN
JavaScript module. So we have to named our parameters
data-module-title etc., not just data-title.
Now let’s make use of these options in our JavaScript module. Change
example_theme_popover.js to look like this:
"use strict";
/* example_theme_popover
*
* This JavaScript module adds a Bootstrap popover with some extra info about a
* dataset to the HTML element that the module is applied to. Users can click
* on the HTML element to show the popover.
*
* title - the title of the dataset
* license - the title of the dataset's copyright license
* num_resources - the number of resources that the dataset has.
*
*/
ckan.module('example_theme_popover', function ($, _) {
return {
initialize: function () {
// Access some options passed to this JavaScript module by the calling
// template.
var num_resources = this.options.num_resources;
var license = this.options.license;
// Format a simple string with the number of resources and the license,
// e.g. "3 resources, Open Data Commons Attribution License".
var content = 'NUM resources, LICENSE'
.replace('NUM', this.options.num_resources)
.replace('LICENSE', this.options.license)
// Add a Bootstrap popover to the HTML element (this.el) that this
// JavaScript module was initialized on.
this.el.popover({title: this.options.title,
content: content,
placement: 'left'});
}
};
});
Note
It’s best practice to add a docstring to the top of a JavaScript module,
as in the example above, briefly documenting what the module does and what
options it takes. See JavaScript modules should have docstrings.
Any data-module-* attributes on the HTML element are passed into the
JavaScript module in the object this.options:
var num_resources = this.options.num_resources;
var license = this.options.license;
A JavaScript module can access the HTML element that it was applied to
through the this.el variable. To add a popover to our info button, we call
Bootstap’s popover() function on the element, passing in an options object
with some of the options that Bootstrap’s popovers accept:
// Add a Bootstrap popover to the HTML element (this.el) that this
// JavaScript module was initialized on.
this.el.popover({title: this.options.title,
content: content,
placement: 'left'});
Default values for options
Default values for JavaScript module options can be provided by adding an
options object to the module. If the HTML element doesn’t have a
data-module-* attribute for an option, then the default will be used
instead. For example...
Todo
Think of an example to do using default values.
Ajax, event handling and CKAN’s JavaScript sandbox
So far, we’ve used simple JavaScript string formatting to put together the
contents of our popover. If we want the popover to contain much more complex
HTML we really need to render a template for it, using the full power of
Jinja2 templates and CKAN’s
template helper functions. Let’s edit our
plugin to use a Jinja2 template to render the contents of the popups nicely.
First, edit package_item.html to make it pass a few more parameters to the
JavaScript module using data-module-* attributes:
{% ckan_extends %}
{% block content %}
{{ super() }}
{% resource 'example_theme/example_theme_popover.js' %}
{% resource 'example_theme/example_theme_popover.css' %}
<button data-module="example_theme_popover"
data-module-id="{{ package.id }}"
data-module-title="{{ package.title }}"
data-module-license_title="{{ package.license_title }}"
data-module-num_resources="{{ package.num_resources }}">
<i class="icon-info-sign"></i>
</button>
{% endblock %}
We’ve also added a second {% resource %} tag to the snippet above, to
include a custom CSS file. We’ll see the contents of that CSS file later.
Next, we need to add a new template snippet to our extension that will be used
to render the contents of the popovers. Create this
example_theme_popover.html file:
ckanext-example_theme/
ckanext/
example_theme/
templates/
ajax_snippets/
example_theme_popover.html
and put these contents in it:
{# The contents for a dataset popover.
id - the id of the dataset
num_resources - the dataset's number of resources
license_title - the dataset's license title
#}
<div class="context-info">
<div class="nums">
<dl>
<dt>{{ _('Followers') }}</dt>
<dd>{{ h.get_action('dataset_follower_count', {'id': id}) }}</dd>
<dt>{{ _('Resources') }}</dt>
<dd>{{ num_resources }}</dd>
</dl>
</div>
<div class="license">
<dl>
<dt>License</dt>
<dd>{{ license_title }}</dd>
</dl>
</div>
<div class="clearfix"></div>
{{ h.follow_button('dataset', id) }}
<a class="btn go-to-dataset"
href="{{ h.url_for(controller='package', action='read', id=id) }}">
<i class="icon-circle-arrow-right"></i>
Go to dataset
</a>
</div>
This is a Jinja2 template that renders some nice looking contents for a
popover, containing a few bits of information about a dataset. It uses a number
of CKAN’s Jinja2 templating features, including marking user-visible strings
for translation and calling template helper functions. See Customizing CKAN’s templates
for details about Jinja2 templating in CKAN.
Note
The new template file has to be in a templates/ajax_snippets/ directory
so that we can use the template from our JavaScript code using
CKAN’s getTemplate() function. Only templates
from ajax_snippets directories are available from the
getTemplate() function.
Next, edit fanstatic/example_theme_popover.js as shown below.
There’s a lot going on in this new JavaScript code, including:
Using Bootstrap’s popover API
to show and hide popovers, and set their contents.
Using jQuery’s event handling API
to get our functions to be called when the user clicks on a button.
Using a function from CKAN’s JavaScript sandbox.
The sandbox is a JavaScript object, available to all JavaScript modules
as this.sandbox, that contains a collection of useful functions and
variables.
this.sandbox.client is a CKAN API client written in JavaScript, that
should be used whenever a JavaScript module needs to talk to the CKAN API,
instead of modules doing their own HTTP requests.
this.sandbox.client.getTemplate() is a function that sends an
asynchronous (ajax) HTTP request (i.e. send an HTTP request from
JavaScript and receive the response in JavaScript, without causing
the browser to reload the whole page) to CKAN asking for a template snippet
to be rendered.
Hopefully the liberal commenting in the code below makes it clear enough what’s
going on:
"use strict";
ckan.module('example_theme_popover', function ($, _) {
return {
initialize: function () {
// proxyAll() ensures that whenever an _on*() function from this
// JavaScript module is called, the `this` variable in the function will
// be this JavaScript module object.
//
// You probably want to call proxyAll() like this in the initialize()
// function of most modules.
//
// This is a shortcut function provided by CKAN, it wraps jQuery's
// proxy() function: http://api.jquery.com/jQuery.proxy/
$.proxyAll(this, /_on/);
// Add a Bootstrap popover to the button. Since we don't have the HTML
// from the snippet yet, we just set the content to "Loading..."
this.el.popover({title: this.options.title, html: true,
content: 'Loading...', placement: 'left'});
// Add an event handler to the button, when the user clicks the button
// our _onClick() function will be called.
this.el.on('click', this._onClick);
},
// Whether or not the rendered snippet has already been received from CKAN.
_snippetReceived: false,
_onClick: function(event) {
// Send an ajax request to CKAN to render the popover.html snippet.
// We wrap this in an if statement because we only want to request
// the snippet from CKAN once, not every time the button is clicked.
if (!this._snippetReceived) {
this.sandbox.client.getTemplate('example_theme_popover.html',
this.options,
this._onReceiveSnippet);
this._snippetReceived = true;
}
},
// CKAN calls this function when it has rendered the snippet, and passes
// it the rendered HTML.
_onReceiveSnippet: function(html) {
// Replace the popover with a new one that has the rendered HTML from the
// snippet as its contents.
this.el.popover('destroy');
this.el.popover({title: this.options.title, html: true,
content: html, placement: 'left'});
this.el.popover('show');
},
};
});
Finally, we need some custom CSS to make the HTML from our new snippet look
nice. In package_item.html above we added a {% resource %} tag to
include a custom CSS file. Now we need to create that file,
ckanext-example_theme/ckanext/example_theme/fanstatic/example_theme_popover.css:
.dataset-list .popover .nums {
/* We're reusing the .nums class from the dataset read page,
* but we don't want the border, margin and padding, get rid of them. */
border: none;
margin: 0;
padding: 0;
/* We want the license and numbers to appear side by side, so float the
* numbers list to the left and make it take up just over half of
* the width of the popover. */
float: left;
width: 55%;
}
.dataset-list .popover .license {
/* Prevent the words in the license from being wrapped mid-word. */
word-break: keep-all;
}
.dataset-list .popover .go-to-dataset {
/* Float the "Go to dataset" button to the right side of the popover,
* this puts some space between the two buttons. */
float: right;
}
Restart CKAN, and your dataset popovers should be looking much better.
Error handling
What if our JavaScript makes an Ajax request to CKAN, such as our
getTemplate() call above, and gets an error in
response? We can simulate this by changing the name of the requested template
file to one that doesn’t exist:
this.sandbox.client.getTemplate('foobar.html',
this.options,
this._onReceiveSnippet);
If you reload the datasets page after making this change, you’ll see that when
you click on a popover its contents remain Loading.... If you have a
development console open in your browser, you’ll see the error response from
CKAN each time you click to open a popover.
Our JavaScript module’s _onReceiveSnippet() function is only called if the
request gets a successful response from CKAN.
getTemplate() also accepts a second callback
function parameter that will be called when CKAN sends an error response.
Add this parameter to the getTemplate() call:
this.sandbox.client.getTemplate('foobar.html',
this.options,
this._onReceiveSnippet,
this._onReceiveSnippetError);
}
},
Now add the new error function to the JavaScript module:
_onReceiveSnippetError: function(error) {
this.el.popover('destroy');
var content = error.status + ' ' + error.statusText + ' :(';
this.el.popover({title: this.options.title, html: true,
content: content, placement: 'left'});
this.el.popover('show');
this._snippetReceived = true;
},
After making these changes, you should see that if CKAN responds with an
error, the contents of the popover are replaced with the error message from
CKAN.
Pubsub
You may have noticed that, with our example code so far, if you click on the
info button of one dataset on the page then click on the info button of another
dataset, both dataset’s popovers are shown. The first popover doesn’t disappear
when the second appears, and the popovers may overlap. If you click on all the
info buttons on the page, popovers for all of them will be shown at once:
To make one popover disappear when another appears, we can use CKAN’s
publish() and
subscribe() functions. These pair of functions
allow different instances of a JavaScript module (or instances of different
JavaScript modules) on the same page to talk to each other.
The way it works is:
Modules can subscribe to events by calling
this.sandbox.client.subscribe(), passing the ‘topic’
(a string that identifies the type of event to subscribe to) and a callback
function.
Modules can call this.sandbox.client.publish() to
publish an event for all subscribed modules to receive, passing the topic
string and one or more further parameters that will be passed on as
parameters to the receiver functions.
When a module calls publish(), any callback
functions registered by previous calls to
subscribe() with the same topic string will
be called, and passed the parameters that were passed to publish.
If a module no longer wants to receive events for a topic, it calls
unsubscribe().
All modules that subscribe to events should have a teardown() function
that unsubscribes from the event, to prevent memory leaks. CKAN calls the
teardown() functions of modules when those modules are removed from the
page. See JavaScript modules should unsubscribe from events in teardown().
Remember that because we attach our example_theme_popover.js module to a
<button> element that is rendered once for each dataset on the page, CKAN
creates one instance of our module for each dataset. The only way these objects
can communicate with each other so that one object can hide its popover when
another object shows its popover, is by using pubsub.
Here’s a modified version of our example_theme_popover.js file that uses
pubsub to make the dataset popovers disappear whenever a new popover appears:
"use strict";
ckan.module('example_theme_popover', function ($, _) {
return {
initialize: function () {
$.proxyAll(this, /_on/);
this.el.popover({title: this.options.title, html: true,
content: 'Loading...', placement: 'left'});
this.el.on('click', this._onClick);
// Subscribe to 'dataset_popover_clicked' events.
// Whenever any line of code publishes an event with this topic,
// our _onPopoverClicked function will be called.
this.sandbox.subscribe('dataset_popover_clicked',
this._onPopoverClicked);
},
teardown: function() {
this.sandbox.unsubscribe('dataset_popover_clicked',
this._onPopoverClicked);
},
_snippetReceived: false,
_onClick: function(event) {
if (!this._snippetReceived) {
this.sandbox.client.getTemplate('example_theme_popover.html',
this.options,
this._onReceiveSnippet);
this._snippetReceived = true;
}
// Publish a 'dataset_popover_clicked' event for other interested
// JavaScript modules to receive. Pass the button that was clicked as a
// parameter to the receiver functions.
this.sandbox.publish('dataset_popover_clicked', this.el);
},
// This callback function is called whenever a 'dataset_popover_clicked'
// event is published.
_onPopoverClicked: function(button) {
// Wrap this in an if, because we don't want this object to respond to
// its own 'dataset_popover_clicked' event.
if (button != this.el) {
// Hide this button's popover.
// (If the popover is not currently shown anyway, this does nothing).
this.el.popover('hide');
}
},
_onReceiveSnippet: function(html) {
this.el.popover('destroy');
this.el.popover({title: this.options.title, html: true,
content: html, placement: 'left'});
this.el.popover('show');
},
};
});
jQuery plugins
CKAN provides a number of custom jQuery plugins for JavaScript modules to use
by default, see CKAN jQuery plugins reference.
Extensions can also add their own jQuery plugins, and the plugins will then be
available to all JavaScript code via the this.$ object.
See also
- How to Create a Basic Plugin
- jQuery’s own documentation on writing jQuery plugins. Read this for all
the details on writing jQuery plugins, here we’ll only provide a simple
example and show you how to integrate it with CKAN.
It’s a good idea to implement any JavaScript functionality not directly related
to CKAN as a jQuery plugin. That way your CKAN JavaScript modules will be
smaller as they’ll contain only the CKAN-specific code, and your jQuery plugins
will also be reusable on non-CKAN sites. CKAN core uses jQuery plugins to
implement features including date formatting, warning users about unsaved
changes when leaving a page containing a form without submitting the form,
restricting the set of characters that can be typed into an input field, etc.
Let’s add a jQuery plugin to our CKAN extension that makes our info buttons
turn green when clicked.
Todo
Replace this with a more realistic example.
First we need to write the jQuery plugin itself. Create the file
ckanext-example_theme/ckanext/example_theme/fanstatic/jquery.greenify.js
with the following contents:
"use strict";
(function (jQuery) {
jQuery.fn.greenify = function() {
this.css( "color", "green" );
return this;
};
})(this.jQuery);
If this JavaScript code looks a little confusing at first, it’s probably
because it’s using the
Immediately-Invoked Function Expression (IIFE)
pattern. This is a common JavaScript code pattern in which an anonymous
function is created and then immediately called once, in a single expression.
In the example above, we create an unnamed function that takes a single
parameter, jQuery, and then we call the function passing this.jQuery
to its jQuery parameter. The code inside the body of the function is the
important part. Writing jQuery plugins in this way ensures that
any variables defined inside the plugin are private to the plugin, and don’t
pollute the global namespace.
In the body of our jQuery plugin, we add a new function called greenify()
to the jQuery object:
jQuery.fn.greenify = function() {
this.css( "color", "green" );
return this;
};
jquery.fn is the jQuery prototype object, the object that normal jQuery
objects get all their methods from. By adding a method to this object, we
enable any code that has a jQuery object to call our method on any HTML element
or set of elements. For example, to turn all <a> elements on the page green
you could do: jQuery('a').greenify().
The code inside the greenify() function just calls jQuery’s standard
css() method to set the CSS color
attribute of the element to green. This is just standard jQuery code,
except that within a custom jQuery function you use this to refer to the
jQuery object, instead of using $ or jquery (as you would normally do
when calling jQuery methods from code external to jQuery).
Our method then returns this to allow jQuery method chaining to be used
with our method. For example, a user can set an element’s CSS color
attribute to green and add the CSS class greenified to the element in
a single expression by chaining our jQuery method with another method:
$('a').greenify().addClass('greenified');
Before we can use our greenify() method in CKAN, we need to import the
jquery.greenify.js file into the CKAN page. To do this, add a
{% resource %} tag to a template file, just as you would do to include any
other JavaScript or CSS file in CKAN. Edit the package_item.html file:
{% ckan_extends %}
{% block content %}
{{ super() }}
{% resource 'example_theme/example_theme_popover.js' %}
{% resource 'example_theme/example_theme_popover.css' %}
{% resource 'example_theme/jquery.greenify.js' %}
<button data-module="example_theme_popover"
data-module-id="{{ package.id }}"
data-module-title="{{ package.title }}"
data-module-license_title="{{ package.license_title }}"
data-module-num_resources="{{ package.num_resources }}">
<i class="icon-info-sign"></i>
</button>
{% endblock %}
Now we can call the greenify() method from our example_theme_popover
JavaScript module. For example, we could add a line to the _onClick()
method in example_theme_popover.js so that when a dataset info button is
clicked it turns green:
_onClick: function(event) {
// Make all the links on the page turn green.
this.$('i').greenify();
if (!this._snippetReceived) {
this.sandbox.client.getTemplate('example_theme_popover.html',
this.options,
this._onReceiveSnippet);
this._snippetReceived = true;
}
this.sandbox.publish('dataset_popover_clicked', this.el);
},
Internationalization
Todo
Show how to Internationalize a JavaScript module.
Testing JavaScript modules
Todo
Show how to write tests for the example module.
Best practices for writing CKAN themes
Don’t use c
As much as possible, avoid accessing the Pylons template context c
(or tmpl_context). c is a thread-global variable, which
encourages spaghetti code that’s difficult to understand and to debug.
Instead, have controller methods add variables to the extra_vars
parameter of render(), or have the templates
call
template helper functions instead.
extra_vars has the advantage that it allows templates, which are
difficult to debug, to be simpler and shifts logic into the easier-to-test and
easier-to-debug Python code. On the other hand, template helper functions are
easier to reuse as they’re available to all templates and they avoid
inconsistencies between the namespaces of templates that are rendered by
different controllers (e.g. one controller method passes the package dict as an
extra var named package, another controller method passes the same thing
but calls it pkg, a third calls it pkg_dict).
You can use the ITemplateHelpers plugin
interface to add custom helper functions, see
Adding your own template helper functions.
Use url_for()
Always use url_for() (available to templates as
h.url_for()) when linking to other CKAN pages, instead of hardcoding URLs
like <a href="/dataset">. Links created with
url_for() will update themselves if the URL routing
changes in a new version of CKAN, or if a plugin changes the URL routing.
Use {% trans %}, {% pluralize %}, _() and ungettext()
All user-visible strings should be internationalized, see
String internationalization.
Helper function names should begin with the name of the extension
Namespacing helper functions in this way avoids accidentally overriding, or
being overriden by, a core helper function, or a helper function from another
extension. For example:
def get_helpers(self):
'''Register the most_popular_groups() function above as a template
helper function.
'''
# Template helper function names should begin with the name of the
# extension they belong to, to avoid clashing with functions from
# other extensions.
return {'example_theme_most_popular_groups': most_popular_groups}
Snippet filenames should begin with the name of the extension
Namespacing snippets in this way avoids accidentally overriding, or being
overridden by, a core snippet, or a snippet from another extension.
For example:
snippets/example_theme_most_popular_groups.html
JavaScript modules names should begin with the name of the extension
Namespacing JavaScript modules in this way avoids accidentally overriding, or
being overridden by, a core module, or a module from another extension. For
example: fanstatic/example_theme_popover.js:
// Enable JavaScript's strict mode. Strict mode catches some common
// programming errors and throws exceptions, prevents some unsafe actions from
// being taken, and disables some confusing and bad JavaScript features.
"use strict";
ckan.module('example_theme_popover', function ($, _) {
return {
initialize: function () {
console.log("I've been initialized for element: ", this.el);
}
};
});
JavaScript modules should have docstrings
A JavaScript module should have a docstring at the top of the file, briefly
documentating what the module does and what options it takes. For example:
"use strict";
/* example_theme_popover
*
* This JavaScript module adds a Bootstrap popover with some extra info about a
* dataset to the HTML element that the module is applied to. Users can click
* on the HTML element to show the popover.
*
* title - the title of the dataset
* license - the title of the dataset's copyright license
* num_resources - the number of resources that the dataset has.
*
*/
ckan.module('example_theme_popover', function ($, _) {
return {
initialize: function () {
// Access some options passed to this JavaScript module by the calling
// template.
var num_resources = this.options.num_resources;
var license = this.options.license;
// Format a simple string with the number of resources and the license,
// e.g. "3 resources, Open Data Commons Attribution License".
var content = 'NUM resources, LICENSE'
.replace('NUM', this.options.num_resources)
.replace('LICENSE', this.options.license)
// Add a Bootstrap popover to the HTML element (this.el) that this
// JavaScript module was initialized on.
this.el.popover({title: this.options.title,
content: content,
placement: 'left'});
}
};
});
JavaScript modules should unsubscribe from events in teardown()
Any JavaScript module that calls this.sandbox.client.subscribe()
should have a teardown() function that calls
unsubscribe(), to prevent memory leaks.
CKAN calls the teardown() functions of modules when those modules are
removed from the page.
Don’t overuse pubsub
There shouldn’t be very many cases where a JavaScript module really needs to
use Pubsub, try to only use it when you really need to.
JavaScript modules in CKAN are designed to be small and loosely-coupled,
for example modules don’t share any global variables and don’t call
each other’s functions. But pubsub offers a way to tightly couple JavaScript
modules together, by making modules depend on multiple events published by
other modules. This can make the code buggy and difficult to understand.
Use {% snippet %}, not {% include %}
Always use CKAN’s custom {% snippet %} tag instead of Jinja’s default
{% include %} tag. Snippets can only access certain global variables, and
any variables explicitly passed to them by the calling template. They don’t
have access to the full context of the calling template, as included files do.
This makes snippets more reusable, and much easier to debug.
Snippets should have docstrings
A snippet should have a docstring comment at the top of the file that briefly
documents what the snippet does and what parameters it requires. For example:
{#
Renders a list of the site's most popular groups.
groups - the list of groups to render
#}
<h3>Most popular groups</h3>
<ul>
{% for group in groups %}
<li>
<a href="{{ h.url_for('group_read', action='read', id=group.name) }}">
<h3>{{ group.display_name }}</h3>
</a>
{% if group.description %}
<p>
{{ h.markdown_extract(group.description, extract_length=80) }}
</p>
{% else %}
<p>{{ _('This group has no description') }}</p>
{% endif %}
{% if group.packages %}
<strong>{{ ungettext('{num} Dataset', '{num} Datasets', group.packages).format(num=group.packages) }}</strong>
{% else %}
<span>{{ _('0 Datasets') }}</span>
{% endif %}
</li>
{% endfor %}
</ul>