commit
8df820e974
|
@ -0,0 +1,2 @@
|
|||
Mikael Karlsson <i8myshoes@gmail.com>
|
||||
Sean Hammond <seanhammond@lavabit.com>
|
306
README.md
306
README.md
|
@ -1,301 +1,11 @@
|
|||
How to implement a temporal search widget for CKAN
|
||||
==================================================
|
||||
# "Filter by year" extension for CKAN
|
||||
|
||||
CKAN supports date-range searches out of the box. For example, to search for
|
||||
datasets that were modified in August 2013, you can simply type this into the
|
||||
search bar:
|
||||
## License
|
||||
GNU Affero General Public License version 3 (AGPLv3)
|
||||
|
||||
metadata_modified:[2013-08-01T00:00:00Z TO 2013-08-31T00:00:00Z]
|
||||
## Contains external libraries
|
||||
- [bootstrap-datepicker.js](https://github.com/eternicode/bootstrap-datepicker/), Apache License 2.0
|
||||
- [Moment.js](http://momentjs.com/), MIT License
|
||||
|
||||
Here's the result of that search on datahub.io: <http://datahub.io/dataset?q=metadata_modified%3A[2013-08-01T00%3A00%3A00Z+TO+2013-08-31T00%3A00%3A00Z]>.
|
||||
|
||||
You can also search for `metadata_created` instead of `metadata_modified`, or
|
||||
you can search on any custom date fields you've added to datasets. You can do
|
||||
other kinds of date math besides ranges too, e.g. less than, greater than
|
||||
(it's just Solr's date query syntax).
|
||||
|
||||
So the challenge of integrating date-range searches into your CKAN site is
|
||||
mostly a user interface problem: how to present a user-friendly way to select
|
||||
a start date and an end date, and integrate the date-range facet with the rest
|
||||
of CKAN's search tools such as the query box, sorting, and other facets.
|
||||
|
||||
It's not enough just to do a date-range search, the user should be able to
|
||||
search for datasets in the *climate* group, with an open license, matching the
|
||||
term "carbon", *and* within a given date-range, and then also sort those
|
||||
results, and they should be able to do all this in a simple, user-friendly way.
|
||||
|
||||
|
||||
Create extension and plugin
|
||||
---------------------------
|
||||
|
||||
First create a CKAN extension `ckanext-datesearch` containing a plugin
|
||||
`datesearch`. The plugin doesn't need to do anything yet.
|
||||
See <http://docs.ckan.org/en/943-writing-extensions-tutorial/writing-extensions.html>.
|
||||
|
||||
|
||||
Add template directory and Fanstatic library
|
||||
--------------------------------------------
|
||||
|
||||
The plugin needs to implement `IConfigurable` and register a template
|
||||
directory and a Fanstatic library directory with CKAN.
|
||||
[ckanext/datesearch/plugin.py](ckanext/datesearch/plugin.py):
|
||||
|
||||
import ckan.plugins as plugins
|
||||
import ckan.plugins.toolkit as toolkit
|
||||
|
||||
|
||||
class DateSearchPlugin(plugins.SingletonPlugin):
|
||||
plugins.implements(plugins.IConfigurer)
|
||||
|
||||
def update_config(self, config):
|
||||
toolkit.add_template_directory(config, 'templates')
|
||||
toolkit.add_resource('fanstatic', 'ckanext-datesearch')
|
||||
|
||||
Also create the directories
|
||||
[ckanext/datesearch/templates/](ckanext/datesearch/templates/) and
|
||||
[ckanext/datesearch/fanstatic/](ckanext/datesearch/fanstatic/).
|
||||
|
||||
`templates` is a directory where we can put custom templates that will override
|
||||
the CKAN core ones.
|
||||
|
||||
`fanstatic` is a [Fanstatic](ckanext/datesearch/fanstatic/) resource directory.
|
||||
CKAN uses Fanstatic to serve static files like CSS, JavaScript or image files,
|
||||
because Fanstatic provides features like bundling, minifying, caching, etc.
|
||||
|
||||
|
||||
Add static files for the date-range picker widget
|
||||
-------------------------------------------------
|
||||
|
||||
[fanstatic/daterangepicker-bs3.css](ckanext/datesearch/fanstatic/daterangepicker-bs3.css),
|
||||
[fanstatic/daterangepicker.js](ckanext/datesearch/fanstatic/daterangepicker.js),
|
||||
and [fanstatic/moment.js](ckanext/datesearch/fanstatic/moment.js)
|
||||
are some static CSS and JavaScript files that we're
|
||||
using to get a GUI date-range picker widget. We're using Dan Grossman's
|
||||
[bootstrap-daterangepicker](https://github.com/dangrossman/bootstrap-daterangepicker).
|
||||
|
||||
Adding the files into the `fanstatic` directory in our extension just means
|
||||
that those files will be available when we need them in our templates later.
|
||||
|
||||
|
||||
Customize the dataset search page template
|
||||
------------------------------------------
|
||||
|
||||
Create the directory [ckanext/datesearch/templates/package/](ckanext/datesearch/templates/package/),
|
||||
and open the file [ckanext/datesearch/templates/package/search.html](ckanext/datesearch/templates/package/search.html):
|
||||
|
||||
{% ckan_extends %}
|
||||
|
||||
{% block secondary_content %}
|
||||
{% resource 'ckanext-datesearch/moment.js' %}
|
||||
{% resource 'ckanext-datesearch/daterangepicker.js' %}
|
||||
{% resource 'ckanext-datesearch/daterangepicker-bs3.css' %}
|
||||
{% resource 'ckanext-datesearch/daterangepicker-module.js' %}
|
||||
|
||||
{# This <section> is the date-range picker widget in the sidebar. #}
|
||||
<section class="module module-narrow module-shallow">
|
||||
<h2 class="module-heading">
|
||||
<i class="icon-medium icon-calendar"></i> {{ _('Filter by date') }}
|
||||
<a href="{{ h.remove_url_param(['ext_startdate', 'ext_enddate']) }}"
|
||||
class="action">{{ _('Clear') }}</a>
|
||||
</h2>
|
||||
<div class="module-content input-prepend" data-module="daterange-query">
|
||||
<span class="add-on"><i class="icon-calendar"></i></span>
|
||||
<input type="text" style="width: 150px" id="daterange"
|
||||
data-module="daterangepicker-module" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
This is a Jinja template that overrides the [templates/package/search.html](https://github.com/okfn/ckan/blob/release-v2.0.2/ckan/templates/package/search.html)
|
||||
template in CKAN core:
|
||||
|
||||
* `{% ckan_extends %}` means this template inherits from the corresponding
|
||||
core template
|
||||
|
||||
* Look in the core template file to see what Jinja blocks are available, in
|
||||
this case we're overriding the `{% block secondary_content %}`.
|
||||
|
||||
* `{% resource 'ckanext-datesearch/moment.js' %}` tells Fanstatic to load the
|
||||
`moment.js` file from our `fanstatic` directory into this page. We load a
|
||||
number of custom resource files in our template:
|
||||
|
||||
{% resource 'ckanext-datesearch/moment.js' %}
|
||||
{% resource 'ckanext-datesearch/daterangepicker.js' %}
|
||||
{% resource 'ckanext-datesearch/daterangepicker-bs3.css' %}
|
||||
{% resource 'ckanext-datesearch/daterangepicker-module.js' %}
|
||||
|
||||
* `{{ super() }}` means to insert the contents of the block from the core
|
||||
template.
|
||||
|
||||
* The rest of the stuff inside `{% block secondary_content %}` is the custom
|
||||
stuff that we're adding to the top of the sidebar. Most of the code is just
|
||||
to make it fit into the CKAN theme. The important bit is:
|
||||
|
||||
<input type="text" style="width: 150px" id="daterange"
|
||||
data-module="daterangepicker-module" />
|
||||
|
||||
Adding a `data-module="daterangepicker-module"` attribute to an HTML element
|
||||
tells CKAN to find the `daterangepicker-module` JavaScript module and load
|
||||
it into the page after this element is rendered.
|
||||
|
||||
|
||||
Implement the `daterangepicker-module` JavaScript module
|
||||
--------------------------------------------------------
|
||||
|
||||
CKAN uses _JavaScript modules_ to load snippets of JavaScript into the page.
|
||||
When we added the `data-module="daterangepicker-module"` attribute to our
|
||||
`<input>` tag in our template earlier, we told CKAN to include the
|
||||
`daterangepicker-module` JavaScript module into our page. We now need to
|
||||
provide that module. [fanstatic/daterangepicker-module.js](ckanext/datesearch/fanstatic/daterangepicker-module.js):
|
||||
|
||||
this.ckan.module('daterangepicker-module', function($, _) {
|
||||
return {
|
||||
initialize: function() {
|
||||
|
||||
// Add hidden <input> tags #ext_startdate and #ext_enddate,
|
||||
// if they don't already exist.
|
||||
var form = $("#dataset-search");
|
||||
if ($("#ext_startdate").length === 0) {
|
||||
$('<input type="hidden" id="ext_startdate" name="ext_startdate" />').appendTo(form);
|
||||
}
|
||||
if ($("#ext_enddate").length === 0) {
|
||||
$('<input type="hidden" id="ext_enddate" name="ext_enddate" />').appendTo(form);
|
||||
}
|
||||
|
||||
// Add a date-range picker widget to the <input> with id #daterange
|
||||
$('input[id="daterange"]').daterangepicker({
|
||||
showDropdowns: true,
|
||||
timePicker: true
|
||||
},
|
||||
function(start, end) {
|
||||
|
||||
// Bootstrap-daterangepicker calls this function after the user
|
||||
// picks a start and end date.
|
||||
|
||||
// Format the start and end dates into strings in a date format
|
||||
// that Solr understands.
|
||||
start = start.format('YYYY-MM-DDTHH:mm:ss') + 'Z';
|
||||
end = end.format('YYYY-MM-DDTHH:mm:ss') + 'Z';
|
||||
|
||||
// Set the value of the hidden <input id="ext_startdate"> to
|
||||
// the chosen start date.
|
||||
$('#ext_startdate').val(start);
|
||||
|
||||
// Set the value of the hidden <input id="ext_enddate"> to
|
||||
// the chosen end date.
|
||||
$('#ext_enddate').val(end);
|
||||
|
||||
// Submit the <form id="dataset-search">.
|
||||
$("#dataset-search").submit();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CKAN will call the `initialize` function when the page loads. This function
|
||||
uses jQuery and bootstrap-daterangepicker to add a JavaScript date-range picker
|
||||
widget to the `<input>` tag with `id="daterange"`:
|
||||
|
||||
$('input[id="daterange"]').daterangepicker({
|
||||
showDropdowns: true,
|
||||
timePicker: true
|
||||
},
|
||||
|
||||
Finally, bootstrap-daterangepicker supports a *callback function* that will
|
||||
be called after the user selects two dates. We use the callback function to add
|
||||
the selected start and end dates to the hidden `<input>` tags, and
|
||||
submit the form:
|
||||
|
||||
function(start, end) {
|
||||
|
||||
// Bootstrap-daterangepicker calls this function after the user
|
||||
// picks a start and end date.
|
||||
|
||||
// Format the start and end dates into strings in a date format
|
||||
// that Solr understands.
|
||||
start = start.format('YYYY-MM-DDTHH:mm:ss') + 'Z';
|
||||
end = end.format('YYYY-MM-DDTHH:mm:ss') + 'Z';
|
||||
|
||||
// Set the value of the hidden <input id="ext_startdate"> to
|
||||
// the chosen start date.
|
||||
$('#ext_startdate').val(start);
|
||||
|
||||
// Set the value of the hidden <input id="ext_enddate"> to
|
||||
// the chosen end date.
|
||||
$('#ext_enddate').val(end);
|
||||
|
||||
// Submit the <form id="dataset-search">.
|
||||
$("#dataset-search").submit();
|
||||
});
|
||||
|
||||
|
||||
Implement `IPackageController` to put the dates into the Solr query
|
||||
-------------------------------------------------------------------
|
||||
|
||||
Finally, when the dataset search form is submitted along with our extra start
|
||||
and end dates (via the hidden `<input>` tags), we need to catch these start
|
||||
and end dates and insert them into the Solr query. We can do this by
|
||||
implementing CKAN's `IPackageController` plugin interface and using its
|
||||
`before_search()` method to modify the Solr query parameters before the search
|
||||
is done. In `plugin.py`:
|
||||
|
||||
class DateSearchPlugin(plugins.SingletonPlugin):
|
||||
plugins.implements(plugins.IConfigurer)
|
||||
plugins.implements(plugins.IPackageController, inherit=True)
|
||||
|
||||
def update_config(self, config):
|
||||
toolkit.add_template_directory(config, 'templates')
|
||||
toolkit.add_resource('fanstatic', 'ckanext-datesearch')
|
||||
|
||||
def before_search(self, search_params):
|
||||
extras = search_params.get('extras')
|
||||
if not extras:
|
||||
# There are no extras in the search params, so do nothing.
|
||||
return search_params
|
||||
start_date = extras.get('ext_startdate')
|
||||
end_date = extras.get('ext_enddate')
|
||||
if not start_date or not end_date:
|
||||
# The user didn't select a start and end date, so do nothing.
|
||||
return search_params
|
||||
|
||||
# Add a date-range query with the selected start and end dates into the
|
||||
# Solr facet queries.
|
||||
fq = search_params['fq']
|
||||
fq = '{fq} +metadata_modified:[{start_date} TO {end_date}]'.format(
|
||||
fq=fq, start_date=start_date, end_date=end_date)
|
||||
search_params['fq'] = fq
|
||||
return search_params
|
||||
|
||||
Our `before_search()` method receives the values from our `#ext_startdate` and
|
||||
`#ext_enddate` input tags in the `extras` dict of the `search_params` dict.
|
||||
|
||||
It's important that the tag IDs begin with `ext_`, this tells CKAN to pass the
|
||||
values to our plugin.
|
||||
|
||||
We simply take these start and end date values and add them to the
|
||||
[Solr filter query](http://wiki.apache.org/solr/CommonQueryParameters#fq)
|
||||
(`fq`) in a date-range format that Solr understands:
|
||||
|
||||
# Add a date-range query with the selected start and end dates into the
|
||||
# Solr facet queries.
|
||||
fq = search_params['fq']
|
||||
fq = '{fq} +metadata_modified:[{start_date} TO {end_date}]'.format(
|
||||
fq=fq, start_date=start_date, end_date=end_date)
|
||||
search_params['fq'] = fq
|
||||
return search_params
|
||||
|
||||
|
||||
Todo
|
||||
----
|
||||
|
||||
There's still some work on the details of the user interface to be done here,
|
||||
including:
|
||||
|
||||
* After the user selects a date-range and the page reloads with the filtered
|
||||
search results, the date-range should remain filled-in in the date-range
|
||||
picker widget.
|
||||
|
||||
* If the user types something into the search box and hits return while they
|
||||
have a date-range filter on, the date-range filter should not be lost.
|
||||
## Acknowledgement
|
||||
Originally started by [Sean Hammond](https://github.com/seanh).
|
||||
|
|
|
@ -14,11 +14,11 @@ this.ckan.module('daterangepicker-module', function ($, _) {
|
|||
|
||||
// Populate the datepicker and hidden fields
|
||||
if (param_start) {
|
||||
$('#datepicker #start').val(moment.utc(param_start).years());
|
||||
$('#datepicker #start').val(moment.utc(param_start).year());
|
||||
$('#ext_startdate').val(param_start);
|
||||
}
|
||||
if (param_end) {
|
||||
$('#datepicker #end').val(moment.utc(param_end).years());
|
||||
$('#datepicker #end').val(moment.utc(param_end).year());
|
||||
$('#ext_enddate').val(param_end);
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ this.ckan.module('daterangepicker-module', function ($, _) {
|
|||
|
||||
// Format the start and end dates into strings in a date format that Solr understands.
|
||||
var v = moment(ev.date);
|
||||
var fs = 'YYYY-MM-DDTHH:mm:ss'
|
||||
var fs = 'YYYY-MM-DDTHH:mm:ss';
|
||||
|
||||
switch (ev.target.name) {
|
||||
case 'start':
|
||||
|
|
|
@ -16,16 +16,13 @@ class DateSearchPlugin(plugins.SingletonPlugin):
|
|||
|
||||
def before_search(self, search_params):
|
||||
extras = search_params.get('extras')
|
||||
log.debug("extras: {0}".format(extras))
|
||||
if not extras:
|
||||
# There are no extras in the search params, so do nothing.
|
||||
return search_params
|
||||
|
||||
start_date = extras.get('ext_startdate')
|
||||
log.debug("start_date: {0}".format(start_date))
|
||||
|
||||
end_date = extras.get('ext_enddate')
|
||||
log.debug("end_date: {0}".format(end_date))
|
||||
|
||||
if not start_date and not end_date:
|
||||
# The user didn't select either a start and/or end date, so do nothing.
|
||||
|
@ -36,12 +33,9 @@ class DateSearchPlugin(plugins.SingletonPlugin):
|
|||
end_date = '*'
|
||||
|
||||
# Add a date-range query with the selected start and/or end dates into the Solr facet queries.
|
||||
fq = search_params['fq']
|
||||
log.debug("fq: {0}".format(fq))
|
||||
fq = search_params.get('fq', '')
|
||||
fq = '{fq} +extras_PublicationTimestamp:[{sd} TO {ed}]'.format(fq=fq, sd=start_date, ed=end_date)
|
||||
|
||||
log.debug("fq: {0}".format(fq))
|
||||
search_params['fq'] = fq
|
||||
log.debug("search_params: {0}".format(search_params))
|
||||
|
||||
return search_params
|
||||
|
|
14
setup.py
14
setup.py
|
@ -1,14 +1,12 @@
|
|||
from setuptools import setup, find_packages
|
||||
|
||||
version = '0.2'
|
||||
|
||||
setup(
|
||||
name='ckanext-datesearch',
|
||||
version=version,
|
||||
description="CKAN extension for publication year facet",
|
||||
version='0.3.0',
|
||||
description='CKAN extension for publication year facet',
|
||||
long_description=
|
||||
"""
|
||||
""",
|
||||
'''
|
||||
''',
|
||||
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
keywords='ckan ckanext datesearch facet',
|
||||
author='Mikael Karlsson',
|
||||
|
@ -23,8 +21,8 @@ setup(
|
|||
# -*- Extra requirements: -*-
|
||||
],
|
||||
entry_points=
|
||||
"""
|
||||
'''
|
||||
[ckan.plugins]
|
||||
datesearch=ckanext.datesearch.plugin:DateSearchPlugin
|
||||
""",
|
||||
''',
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue