Customizing a CLLD app¶
Extending or customizing the default behaviour of a CLLD app is basically what pyramid
calls configuration.
So, since the clld_app
scaffold is somewhat tuned towards imperative configuration,
this means calling methods on the config object returned by the call to
clld.web.app.get_configurator()
in the apps main
function.
Since the config object is an instance of the pyramid
Configurator
this includes all the standard ways to configure pyramid apps, in particular adding
routes and views to provide additional pages and functionality with an app.
Wording¶
Most text displayed on the HTML pages of the default app can be customized using a technique commonly called localization. I.e. the default is set up in an “internationalized” way, which can be “localized” by providing alternative “translations”.
These translations are provided in form of a PO file which can be edited by hand or with tools such as Poedit.
The workflow to create alternative translations for core terms of a CLLD app is as follows:
Extract terms from your code to create the app specific translations file
myapp/locale/en/LC_MESSAGES/clld.po
:python setup.py extract_messages
Look up the terms available for translation in
clld/locale/en/LC_MESSAGES/clld.po
. If the term you want to translate is found, go on. Otherwise file an issue at https://github.com/clld/clld/issuesInitialize a localized catalog for your app running:
python setup.py init_catalog -l en
When installing
clld
tools have been installed to extract terms from python code files. To make the term available for extraction, include code like below inmyapp
.
# _ is a recognized name for a function to mark translatable strings
_ = lambda s: s
_('term you wish to translate')
Extract terms from your code and update the local
myapp/locale/en/LC_MESSAGES/clld.po
:python setup.py extract_messages python setup.py update_catalog
Add a translation by editing
myapp/locale/en/LC_MESSAGES/clld.po
.Compile the catalog:
python setup.py compile_catalog
If you restart your app you should see your translation at places where previously the core term appeared. Whenever you want to add translations, you have to go through steps 3–6 above.
Static Pages¶
TODO: reserved route names, …
Templates¶
The default CLLD app comes with a set of Mako templates
(in clld/web/templates
) which control the rendering of HTML pages. Each of these can be
overridden locally by providing a template file with the same path (relative to the templates
directory); i.e. to override clld/web/templates/language/detail_html.mako
– the template
rendered for the details page of languages (see Templates) – you’d have to provide a file
myapp/templates/language/detail_html.mako
.
Static assets¶
CLLD Apps may provide custom css and js code. If this code is placed in the default
locations myapp/static/project.[css|js]
, it will automatically be packaged for
production. Note that in this case the code should not contain any URLs relative to
the file, because these may break in production.
Additionally, you may provide the logo of the publisher of the dataser as a PNG image.
If this file is located at myapp/static/publisher_logo.png
it will be picked up
automatically by the default application footer template.
Other static content can still be placed in the myapp/static
directory but must be
explicitly included on pages making use of it, e.g. with template code like:
<link href="${request.static_url('myapp:static/css/introjs.min.css')}" rel="stylesheet">
<script src="${request.static_url('myapp:static/js/intro.min.js')}"></script>
Datatables¶
A main building block of CLLD apps are dynamic data tables. Although there are default implementations which may be good enough in many cases, each data table can be fully customized as follows.
1. Define a customized datatable class in myapp/datables.py
inheriting from either
clld.web.datatables.base.DataTable
or one
of its subclasses in clld.web.datatables
.
2. Register this datatable for the page you want to display it on by
adding a line like the following to the function myapp.datatables.includeme
:
config.register_datatable('routename', DataTableClassName)
The register_datatable
method of the config object has the following signature:
-
register_datatable
(route_name, cls)¶ Parameters: - route_name (str) – Name of the route which maps to the view serving the data (see Routes).
- cld (class) – Python class inheriting from
clld.web.datatables.base.DataTable
.
Datatables are always registered for the routes serving the data. Often they are
displayed on the corresponding resource’s index page, but sometimes you will want to
display a datatable on some other page, e.g. a list of parameter values on the
parameter detail’s page. This can be done be inserting a call to
clld.web.app.ClldRequest.get_datatable()
to create a datatable instance which can
then be rendered calling its render
method.
As an example, the code to render a values datatable restricted to the values for a
particular parameter instance param
would look like
request.get_datatable('values', h.models.Value, parameter=param).render()
Customize column definitions¶
Customize query¶
Data model¶
The core clld
data model can be extended for CLLD apps by defining additional
mappings
in myapp.models
in two ways:
1. Additional mappings (thus additional database tables) deriving from clld.db.meta.Base
can be defined.
Note
While deriving from clld.db.meta.Base
may add some columns to your table which
you don’t actually need (e.g. created
, …), it is still important to do so, to
ensure custom objects behave the same as core ones.
2. Customizations of core models can be defined using joined table inheritance:
from sqlalchemy import Column, Integer, ForeignKey
from zope.interface import implementer
from clld.interfaces import IContribution
from clld.db.meta import CustomModelMixin
from clld.db.models.common import Contribution
@implementer(IContribution)
class Chapter(Contribution, CustomModelMixin):
"""Contributions in WALS are chapters chapters. These comprise a set of features with
corresponding values and a descriptive text.
"""
pk = Column(Integer, ForeignKey('contribution.pk'), primary_key=True)
# add more Columns and relationships here
Note
Inheriting from clld.db.meta.CustomModelMixin
takes care of half of the
boilerplate code necessary to make inheritance work. The primary key still has to be
defined “by hand”.
To give an example, here’s how one could model the many-to-many relation between words and meanings often encountered in lexical databases:
from clld import interfaces
from clld.db.models import common
from clld.db.meta import CustomModelMixin
@implementer(interfaces.IParameter)
class Meaning(CustomModelMixin, common.Parameter):
pk = Column(Integer, ForeignKey('parameter.pk'), primary_key=True)
@implementer(interfaces.IValueSet)
class SynSet(CustomModelMixin, common.ValueSet):
pk = Column(Integer, ForeignKey('valueset.pk'), primary_key=True)
@implementer(interfaces.IUnit)
class Word(CustomModelMixin, common.Unit):
pk = Column(Integer, ForeignKey('unit.pk'), primary_key=True)
@implementer(interfaces.IValue)
class Counterpart(CustomModelMixin, common.Value):
"""a counterpart relates a meaning with a word
"""
pk = Column(Integer, ForeignKey('value.pk'), primary_key=True)
word_pk = Column(Integer, ForeignKey('unit.pk'))
word = relationship(Word, backref='counterparts')
The definitions of Meaning
, Synset
and Word
above are not strictly necessary
(because they do not add any relations or columns to the base classes) and are only
added to make the semantics of the model clear.
Now if we have an instance of Word
, we can iterate over its meanings like this
for counterpart in word.counterparts:
print counterpart.valueset.parameter.name
A more involved example for the case of tree-structured data is given in Handling Trees.
Adding a resource¶
You may also want to add new resources in your app, i.e. objects that behave like builtin resources in that routes get automatically registered and view and template lookup works as explained in Requesting a resource. An example for this technique are the families in e.g. WALS.
The steps required to add a custom resource are:
- Define an interface for the resource in
myapp/interfaces.py
:
from zope.interface import Interface
class IFamily(Interface):
"""marker"""
- Define a model in
myapp/models.py
.
@implementer(myapp.interfaces.IFamily)
class Family(Base, common.IdNameDescriptionMixin):
pass
- Register the resource in
myapp.main
:
config.register_resource('family', Family, IFamily)
- Create templates for HTML views, e.g.
myapp/templates/family/detail_html.mako
, - and register these:
from clld.web.adapters.base import adapter_factory
...
config.register_adapter(adapter_factory('family/detail_html.mako'), IFamily)
Custom maps¶
The appearance of Maps in clld
apps depends on various factors which can be
tweaked for customization:
- the Python code that renders the HTML for the map,
- the GeoJSON data which is passed as map layers,
- the JavaScript code implementing the map.
GeoJSON adapters¶
GeoJSON in clld
is just another type of representation of a resource, thus it is
created by a suitable adapter, usually derived from
clld.web.adapters.geojson.GeoJSON
.
Map classes¶
Maps in clld
are implemented as subclasses of clld.web.maps.Map
. These
classes tie together behavior implemented in javascript (based on leaflet) with Python
code used to assemble the map data, options and legends.
The following clld.web.maps.Map.options
are recognized:
name | type | default | description |
---|---|---|---|
sidebar | bool |
False |
whether the map is rendered in the sidebar |
show_labels | bool |
False |
whether labels are shown by default |
no_showlabels | bool |
False |
whether the control to show labels should be hidden |
no_popup | bool |
False |
whether clicking on markers opens an info window |
no_link | bool |
False |
whether clicking on markers links to the language page |
info_route | str |
'language_alt' |
name of the route to query for info window contents |
info_query | dict |
{} |
query parameters to pass when requesting info window content |
hash | bool |
False |
whether map state should be tracked via URL fragment |
max_zoom | int |
6 |
maximal zoom level allowed for the map |
zoom | int |
5 |
zoom level of the map |
center | (lat, lon) |
None |
center of the map |
icon_size | int |
20 if sidebar else 30 |
size of marker icons in pixels |
icons | str |
'base' |
name of a javascript marker factory function |
on_init | str |
None |
name of a javascript function to call when initialization is done |
base_layer | str |
None |
name of a base layer which should be selected upon map load |
Custom URLs¶
When an established database is ported to CLLD it may be necessary to support legacy URLs
for its resources (as was the case for WALS). This can be achieved by passing a route_patterns
dict, mapping route names to custom patterns, in the settings to clld.web.app.get_configurator()
like in the following example from WALS:
def main(global_config, **settings):
settings['route_patterns'] = {
'languages': '/languoid',
'language': '/languoid/lect/wals_code_{id:[^/\.]+}',
}
config = get_configurator('wals3', **dict(settings=settings))
Misc Utilities¶
http://www.muthukadan.net/docs/zca.html#utility
- IMapMarker
- ILinkAttrs
- ICtxFactoryQuery