Creating a custom theme¶
The project as been developed from start with the goal to make it generic and reusable. The theme engine is based on Flask-Themes2 so reading its documentation might help.
To create your custom theme, the easiest way is to start from the dedicated cookiecutter template:
pip install cookiecutter
cookiecutter gh:opendatateam/cookiecutter-udata-theme
This will create a new theme skeleton on which you can start hacking.
As udata is Bootstrap-based (only the style part), the difficulty will depends on what you cant to do:
- customize some style and colors ⇨ very easy
- customize the layouts or how some part of udata is rendered ⇨ easy
- switch to another style framework ⇨ hard
Project layout¶
A theme has the following layout.
├── my_theme
│ ├── static
│ │ ├── theme.css
│ │ ├── admin.css
│ │ └── img
│ │ ├── flags
│ │ └── placeholders
│ ├── templates
│ │ └── *.html
│ ├── translations
│ │ ├── xx/LC_MESSAGES
│ │ │ └── my-theme.po
│ │ └── my-theme.pot
│ ├── info.json
│ └── __init__.py
├── babel.cfg
├── CHANGELOG.md
├── MANIFEST.in
├── README.md
├── setup.cfg
└── setup.py
At the root level, you will have some basic python project files:
- a
README.md
presenting the theme and how to use it - a
CHANGELOG.md
to let people know the last changes in your theme - a
MANIFEST.in
which specify which files should be included in your theme package - a
setup.py
exposing the package metadata (including the theme presence) - a
setup.cfg
configuring translations commands - a
babel.cfg
configuring translations extractor
In the package directory (my_theme
in this example), you need to have two files:
info.json
exposing metadata required by the theme loader__init__.py
which is required by a Python package. It can be empty or contains hooks.
There can also be three directories:
static
containing static assets (images, styles, extra scripts…)templates
containing the overriden templates (optionnal).translations
containing the overriden translated strings (optionnal).
Note
This is a proposal layout for a standalone theme.
As long as the theme package has the proper layout (info.json
, __init__.py
…),
it can be wherever you want if you properly expose it in your setup.py
file.
setup.py
¶
The setup.py
is a classic python setup.py
file.
The only requirement is that you properly expose the udata theme packaging
as udata.themes
entrypoint:
setup(
'...'
entry_points={
'udata.themes': [
'any-identifier = canonical.theme.package',
]
},
'...'
)
info.json
¶
The info.json
looks like this:
{
"application": "udata",
"identifier": "my-theme",
"name": "My awesome theme",
"author": "Me",
"description": "An awesome theme for udata",
"website": "http://awesome.opendata.tem",
"license": "AGPL",
"version": "0.1.0",
"doctype": "html5"
}
The application
and the doctype
attributes needs to have specific values, respectively udata
and html5
.
The identifier
attribute is important: this is the value you will be using in udata.cfg
to use your theme
(the THEME
parameter).
Any other attribute can have any value, this is only metadata.
Static assets¶
The static
should contain at least 2 files:
theme.css
which is loaded automatically on every pageadmin.css
which is loaded automatically on on the admin page
Some images are required and used everywhere in the application:
img/flags
contains the language switcher flags in the form{language code}.png
(ex.en.png
).img/placeholders
contains default images for:- users without avatar (
user.png
) - organizations without logo (
organization.png
) - reuses without image (
reuse.png
) - territory without logo or flag (
territory.png
)
- users without avatar (
Then you are free to add any static assets required by your theme.
Overriding templates¶
You can override or extend any default Jinja template.
You only need to put the templates with the same name into the templates
folder.
To extend a template and change some details, just extend the base template and override the block you want to change:
{% extends "raw.html" %}
{% block raw_head %}
{{ super() }}
<link rel="shortcut icon" href="{{ theme_static('img/favicon.png') }}">
{% endblock %}
You can reference static assets from your theme with the theme_static
global function.
Note
Don’t forget to call {{ super() }}
to includes the original block.
See the Jinja documentation for advanced usage.
You can also rewrite entirely the template, but don’t forget you need to have the proper blocks for template inheritance and use the proper context variables.
Hooks¶
Your theme can also customize some behavior by using hooks in your __init__.py
.
Currently there are 2 available hooks:
theme.menu()
to register a custom main menutheme.context()
to add extra context variable to some views
You can also expose extras menus using the udata.app.nav
extension.
They will be available in the template context under the nav
object.
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import
from udata import theme
from udata.app import nav
from udata.i18n import lazy_gettext as _
# Expose a menu available globaly as `nav.my_menu`
my_menu = nav.Bar('my_menu', [
nav.Item(_('Data'), 'datasets.list', items=[
nav.Item(_('Datasets'), 'datasets.list'),
nav.Item(_('Reuses'), 'reuses.list'),
nav.Item(_('Organizations'), 'organizations.list'),
]),
nav.Item(_('Dashboard'), 'site.dashboard'),
])
# Register it as default main menu
theme.menu(my_menu)
# Expose another menu available globaly as 'nav.my_network'
nav.Bar('my_network', [
nav.Item(label, label, url=url) for label, url in [
('awesome.fr', 'http://www.awesome.fr'),
('somewhere.net', 'https://somewhere.net'),
]
])
# Add some context to the home view
@theme.context('home')
def home_context(context):
context['something'] = 'some value'
return context
Note
You can see an example of advanced hooks usage in the gouvfr
theme.
Translations¶
You can also (and optionnaly) add or override some translations in you theme.
If the translations
directory is present and contains some gettext-based translations(po/mo
files),
they will be loaded after all others and so they will override existing ones.
The cookiecutter template makes use of Babel to extract string from your template or compile them.
You can extract translations from your own templates using:
python setup.py extract_messages # Extract messages in your pot file
Then you can either add new supported locale:
python setup.py init_catalog -l xx # where XX is the locale you want to add. ex: fr
or update the existing ones:
python setup.py update_catalog
You can then translate the po file using the editor of your choice (take a look at Poedit).
When translation is done, you can compile translations catalogs using:
python setup.py compile_catalog # Compile .mo files for each language
Warning
Don’t forget to compile and include translations in your theme distribution when you publish it.
Publish and use¶
Once your theme is ready, you can publish it on PyPI to share it to the world (and notify us so we can be glad of your work).
To do so, simply execute the following command at the root of your theme project:
python setup.py bdist_wheel upload
Then it will be available on PyPI and you can use it on your platform by installing it
and setting properly the THEME
parameter in your udata.cfg
.
Known themes¶
Here a list of known themes for udata:
default
: default theme packaged with udata (does nothing)gouvfr
as part of thegouvfr
plugin.
Note
Don’t hesitate to submit a pull-request to add your theme to this list.
Help¶
You can ask for help on the udata gitter channel. Please report any difficulty you encounter with a dedicated Github issue.