import os | import os |
from logging import getLogger | from logging import getLogger |
from pylons import request | |
from genshi.input import HTML | |
from genshi.filters.transform import Transformer | from genshi.filters.transform import Transformer |
from ckan.plugins import implements, SingletonPlugin | from ckan.plugins import implements, SingletonPlugin |
from ckan.plugins import IConfigurer | from ckan.plugins import IConfigurer |
from ckan.plugins import IGenshiStreamFilter | from ckan.plugins import IGenshiStreamFilter |
from ckan.plugins import IRoutes | from ckan.plugins import IRoutes |
log = getLogger(__name__) | log = getLogger(__name__) |
class ExamplePlugin(SingletonPlugin): | class ExamplePlugin(SingletonPlugin): |
"""This plugin demonstrates how a theme packaged as a CKAN | """This plugin demonstrates how a theme packaged as a CKAN |
extension might extend CKAN behaviour. | extension might extend CKAN behaviour. |
In this case, we implement three extension interfaces: | In this case, we implement three extension interfaces: |
- ``IConfigurer`` allows us to override configuration normally | - ``IConfigurer`` allows us to override configuration normally |
found in the ``ini``-file. Here we use it to specify the site | found in the ``ini``-file. Here we use it to specify the site |
title, and to tell CKAN to look in this package for templates | title, and to tell CKAN to look in this package for templates |
and resources that customise the core look and feel. | and resources that customise the core look and feel. |
- ``IGenshiStreamFilter`` allows us to filter and transform the | - ``IGenshiStreamFilter`` allows us to filter and transform the |
HTML stream just before it is rendered. In this case we use | HTML stream just before it is rendered. In this case we use |
it to rename "frob" to "foobar" | it to rename "frob" to "foobar" |
- ``IRoutes`` allows us to add new URLs, or override existing | - ``IRoutes`` allows us to add new URLs, or override existing |
URLs. In this example we use it to override the default | URLs. In this example we use it to override the default |
``/register`` behaviour with a custom controller | ``/register`` behaviour with a custom controller |
""" | """ |
implements(IConfigurer, inherit=True) | implements(IConfigurer, inherit=True) |
implements(IGenshiStreamFilter, inherit=True) | implements(IGenshiStreamFilter, inherit=True) |
implements(IRoutes, inherit=True) | implements(IRoutes, inherit=True) |
def update_config(self, config): | def update_config(self, config): |
"""This IConfigurer implementation causes CKAN to look in the | """This IConfigurer implementation causes CKAN to look in the |
```public``` and ```templates``` directories present in this | ```public``` and ```templates``` directories present in this |
package for any customisations. | package for any customisations. |
It also shows how to set the site title here (rather than in | It also shows how to set the site title here (rather than in |
the main site .ini file), and causes CKAN to use the | the main site .ini file), and causes CKAN to use the |
customised package form defined in ``package_form.py`` in this | customised package form defined in ``package_form.py`` in this |
directory. | directory. |
""" | """ |
here = os.path.dirname(__file__) | here = os.path.dirname(__file__) |
rootdir = os.path.dirname(os.path.dirname(here)) | rootdir = os.path.dirname(os.path.dirname(here)) |
our_public_dir = os.path.join(rootdir, 'ckanext', | our_public_dir = os.path.join(rootdir, 'ckanext', |
'example', 'theme', 'public') | 'example', 'theme', 'public') |
template_dir = os.path.join(rootdir, 'ckanext', | template_dir = os.path.join(rootdir, 'ckanext', |
'example', 'theme', | 'example', 'theme', |
'templates') | 'templates') |
# set our local template and resource overrides | # set our local template and resource overrides |
config['extra_public_paths'] = ','.join([our_public_dir, | config['extra_public_paths'] = ','.join([our_public_dir, |
config.get('extra_public_paths', '')]) | config.get('extra_public_paths', '')]) |
config['extra_template_paths'] = ','.join([template_dir, | config['extra_template_paths'] = ','.join([template_dir, |
config.get('extra_template_paths', '')]) | config.get('extra_template_paths', '')]) |
# add in the extra.css | # add in the extra.css |
config['ckan.template_head_end'] = config.get('ckan.template_head_end', '') +\ | config['ckan.template_head_end'] = config.get('ckan.template_head_end', '') +\ |
'<link rel="stylesheet" href="/css/extra.css" type="text/css"> ' | '<link rel="stylesheet" href="/css/extra.css" type="text/css"> ' |
# set the title | # set the title |
config['ckan.site_title'] = "Example CKAN theme" | config['ckan.site_title'] = "Example CKAN theme" |
# set the customised package form (see ``setup.py`` for entry point) | # set the customised package form (see ``setup.py`` for entry point) |
config['package_form'] = "example_form" | config['package_form'] = "example_form" |
def filter(self, stream): | def filter(self, stream): |
"""Conform to IGenshiStreamFilter interface. | """Conform to IGenshiStreamFilter interface. |
This example filter renames 'frob' to 'foobar' (this string is | This example filter renames 'frob' to 'foobar' (this string is |
found in the custom ``home/index.html`` template provided as | found in the custom ``home/index.html`` template provided as |
part of the package). | part of the package). |
It also adds the chosen JQuery plugin to the page if viewing the | |
dataset edit page (provides a better UX for working with tags with vocabularies) | |
""" | """ |
stream = stream | Transformer('//p[@id="examplething"]/text()')\ | stream = stream | Transformer('//p[@id="examplething"]/text()')\ |
.substitute(r'frob', r'foobar') | .substitute(r'frob', r'foobar') |
routes = request.environ.get('pylons.routes_dict') | |
if routes.get('controller') == 'package' \ | |
and routes.get('action') == 'edit': | |
stream = stream | Transformer('head').append(HTML( | |
'<link rel="stylesheet" href="/css/chosen.css" />' | |
)) | |
stream = stream | Transformer('body').append(HTML( | |
''' | |
<script src="/scripts/chosen.jquery.min.js" type="text/javascript"></script>' | |
<script type="text/javascript">$(".chzn-select").chosen();</script> | |
''' | |
)) | |
return stream | return stream |
def before_map(self, map): | def before_map(self, map): |
"""This IRoutes implementation overrides the standard | """This IRoutes implementation overrides the standard |
``/user/register`` behaviour with a custom controller. You | ``/user/register`` behaviour with a custom controller. You |
might instead use it to provide a completely new page, for | might instead use it to provide a completely new page, for |
example. | example. |
Note that we have also provided a custom register form | Note that we have also provided a custom register form |
template at ``theme/templates/user/register.html``. | template at ``theme/templates/user/register.html``. |
""" | """ |
# Hook in our custom user controller at the points of creation | # Hook in our custom user controller at the points of creation |
# and edition. | # and edition. |
map.connect('/user/register', | map.connect('/user/register', |
controller='ckanext.example.controller:CustomUserController', | controller='ckanext.example.controller:CustomUserController', |
action='register') | action='register') |
map.connect('/user/edit', | map.connect('/user/edit', |
controller='ckanext.example.controller:CustomUserController', | controller='ckanext.example.controller:CustomUserController', |
action='edit') | action='edit') |
map.connect('/user/edit/{id:.*}', | map.connect('/user/edit/{id:.*}', |
controller='ckanext.example.controller:CustomUserController', | controller='ckanext.example.controller:CustomUserController', |
action='edit') | action='edit') |
map.connect('/package/new', controller='package_formalchemy', action='new') | map.connect('/package/new', controller='package_formalchemy', action='new') |
map.connect('/package/edit/{id}', controller='package_formalchemy', action='edit') | map.connect('/package/edit/{id}', controller='package_formalchemy', action='edit') |
return map | return map |
Binary files /dev/null and b/ckanext/example/theme/public/css/chosen-sprite.png differ
/* @group Base */ | |
.chzn-container { | |
font-size: 13px; | |
position: relative; | |
display: inline-block; | |
zoom: 1; | |
*display: inline; | |
} | |
.chzn-container .chzn-drop { | |
background: #fff; | |
border: 1px solid #aaa; | |
border-top: 0; | |
position: absolute; | |
top: 29px; | |
left: 0; | |
-webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15); | |
-moz-box-shadow : 0 4px 5px rgba(0,0,0,.15); | |
-o-box-shadow : 0 4px 5px rgba(0,0,0,.15); | |
box-shadow : 0 4px 5px rgba(0,0,0,.15); | |
z-index: 999; | |
} | |
/* @end */ | |
/* @group Single Chosen */ | |
.chzn-container-single .chzn-single { | |
background-color: #ffffff; | |
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0 ); | |
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4)); | |
background-image: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); | |
background-image: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); | |
background-image: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); | |
background-image: -ms-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); | |
background-image: linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); | |
-webkit-border-radius: 5px; | |
-moz-border-radius : 5px; | |
border-radius : 5px; | |
-moz-background-clip : padding; | |
-webkit-background-clip: padding-box; | |
background-clip : padding-box; | |
border: 1px solid #aaaaaa; | |
-webkit-box-shadow: 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); | |
-moz-box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); | |
box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); | |
display: block; | |
overflow: hidden; | |
white-space: nowrap; | |
position: relative; | |
height: 23px; | |
line-height: 24px; | |
padding: 0 0 0 8px; | |
color: #444444; | |
text-decoration: none; | |
} | |
.chzn-container-single .chzn-single span { | |
margin-right: 26px; | |
display: block; | |
overflow: hidden; | |
white-space: nowrap; | |
-o-text-overflow: ellipsis; | |
-ms-text-overflow: ellipsis; | |
text-overflow: ellipsis; | |
} | |
.chzn-container-single .chzn-single abbr { | |
display: block; | |
position: absolute; | |
right: 26px; | |
top: 6px; | |
width: 12px; | |
height: 13px; | |
font-size: 1px; | |
background: url(chosen-sprite.png) right top no-repeat; | |
} | |
.chzn-container-single .chzn-single abbr:hover { | |
background-position: right -11px; | |
} | |
.chzn-container-single .chzn-single div { | |
position: absolute; | |
right: 0; | |
top: 0; | |
display: block; | |
height: 100%; | |
width: 18px; | |
} | |
.chzn-container-single .chzn-single div b { | |
background: url('chosen-sprite.png') no-repeat 0 0; | |
display: block; | |
width: 100%; | |
height: 100%; | |
} | |
.chzn-container-single .chzn-search { | |
padding: 3px 4px; | |
position: relative; | |
margin: 0; | |
white-space: nowrap; | |
z-index: 1010; | |
} | |
.chzn-container-single .chzn-search input { | |
background: #fff url('chosen-sprite.png') no-repeat 100% -22px; | |
background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); | |
background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); | |
background: url('chosen-sprite.png') no-repeat 100% -22px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); | |
background: url('chosen-sprite.png') no-repeat 100% -22px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); | |
background: url('chosen-sprite.png') no-repeat 100% -22px, -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%); | |
background: url('chosen-sprite.png') no-repeat 100% -22px, linear-gradient(top, #eeeeee 1%, #ffffff 15%); | |
margin: 1px 0; | |
padding: 4px 20px 4px 5px; | |
outline: 0; | |
border: 1px solid #aaa; | |
font-family: sans-serif; | |
font-size: 1em; | |
} | |
.chzn-container-single .chzn-drop { | |
-webkit-border-radius: 0 0 4px 4px; | |
-moz-border-radius : 0 0 4px 4px; | |
border-radius : 0 0 4px 4px; | |
-moz-background-clip : padding; | |
-webkit-background-clip: padding-box; | |
background-clip : padding-box; | |
} | |
/* @end */ | |
.chzn-container-single-nosearch .chzn-search input { | |
position: absolute; | |
left: -9000px; | |
} | |
/* @group Multi Chosen */ | |
.chzn-container-multi .chzn-choices { | |
background-color: #fff; | |
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); | |
background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); | |
background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); | |
background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); | |
background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%); | |
background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%); | |
border: 1px solid #aaa; | |
margin: 0; | |
padding: 0; | |
cursor: text; | |
overflow: hidden; | |
height: auto !important; | |
height: 1%; | |
position: relative; | |
} | |
.chzn-container-multi .chzn-choices li { | |
float: left; | |
list-style: none; | |
} | |
.chzn-container-multi .chzn-choices .search-field { | |
white-space: nowrap; | |
margin: 0; | |
padding: 0; | |
} | |
.chzn-container-multi .chzn-choices .search-field input { | |
color: #666; | |
background: transparent !important; | |
border: 0 !important; | |
font-family: sans-serif; | |
font-size: 100%; | |
height: 15px; | |
padding: 5px; | |
margin: 1px 0; | |
outline: 0; | |
-webkit-box-shadow: none; | |
-moz-box-shadow : none; | |
-o-box-shadow : none; | |
box-shadow : none; | |
} | |
.chzn-container-multi .chzn-choices .search-field .default { | |
color: #999; | |
} | |
.chzn-container-multi .chzn-choices .search-choice { | |
-webkit-border-radius: 3px; | |
-moz-border-radius : 3px; | |
border-radius : 3px; | |
-moz-background-clip : padding; | |
-webkit-background-clip: padding-box; | |
background-clip : padding-box; | |
background-color: #e4e4e4; | |
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f4f4', endColorstr='#eeeeee', GradientType=0 ); | |
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee)); | |
background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); | |
background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); | |
background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); | |
background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); | |
background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); | |
-webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); | |
-moz-box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); | |
box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); | |
color: #333; | |
border: 1px solid #aaaaaa; | |
line-height: 13px; | |
padding: 3px 20px 3px 5px; | |
margin: 3px 0 3px 5px; | |
position: relative; | |
cursor: default; | |
} | |
.chzn-container-multi .chzn-choices .search-choice-focus { | |
background: #d4d4d4; | |
} | |
.chzn-container-multi .chzn-choices .search-choice .search-choice-close { | |
display: block; | |
position: absolute; | |
right: 3px; | |
top: 4px; | |
width: 12px; | |
height: 13px; | |
font-size: 1px; | |
background: url(chosen-sprite.png) right top no-repeat; | |
} | |
.chzn-container-multi .chzn-choices .search-choice .search-choice-close:hover { | |
background-position: right -11px; | |
} | |
.chzn-container-multi .chzn-choices .search-choice-focus .search-choice-close { | |
background-position: right -11px; | |
} | |
/* @end */ | |
/* @group Results */ | |
.chzn-container .chzn-results { | |
margin: 0 4px 4px 0; | |
max-height: 240px; | |
padding: 0 0 0 4px; | |
position: relative; | |
overflow-x: hidden; | |
overflow-y: auto; | |
} | |
.chzn-container-multi .chzn-results { | |
margin: -1px 0 0; | |
padding: 0; | |
} | |
.chzn-container .chzn-results li { | |
display: none; | |
line-height: 15px; | |
padding: 5px 6px; | |
margin: 0; | |
list-style: none; | |
} | |
.chzn-container .chzn-results .active-result { | |
cursor: pointer; | |
display: list-item; | |
} | |
.chzn-container .chzn-results .highlighted { | |
background-color: #3875d7; | |
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3875d7', endColorstr='#2a62bc', GradientType=0 ); | |
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc)); | |
background-image: -webkit-linear-gradient(top, #3875d7 20%, #2a62bc 90%); | |
background-image: -moz-linear-gradient(top, #3875d7 20%, #2a62bc 90%); | |
background-image: -o-linear-gradient(top, #3875d7 20%, #2a62bc 90%); | |
background-image: -ms-linear-gradient(top, #3875d7 20%, #2a62bc 90%); | |
background-image: linear-gradient(top, #3875d7 20%, #2a62bc 90%); | |
color: #fff; | |
} |