2011-04-11 18:23:27 +02:00
|
|
|
import os
|
2012-03-07 16:07:37 +01:00
|
|
|
import re
|
2011-04-11 18:23:27 +02:00
|
|
|
from logging import getLogger
|
2013-01-15 20:57:31 +01:00
|
|
|
|
2012-02-21 12:05:13 +01:00
|
|
|
from pylons import config
|
2011-04-11 18:23:27 +02:00
|
|
|
from genshi.input import HTML
|
|
|
|
from genshi.filters import Transformer
|
|
|
|
|
2013-01-15 20:57:31 +01:00
|
|
|
from ckan import plugins as p
|
2011-04-11 18:23:27 +02:00
|
|
|
|
2012-12-06 19:17:45 +01:00
|
|
|
from ckan.lib.search import SearchError, PackageSearchQuery
|
2011-09-29 16:40:49 +02:00
|
|
|
from ckan.lib.helpers import json
|
|
|
|
|
2011-04-11 18:23:27 +02:00
|
|
|
import html
|
|
|
|
|
2012-12-06 19:17:45 +01:00
|
|
|
from ckanext.spatial.lib import save_package_extent,validate_bbox, bbox_query, bbox_query_ordered
|
2012-10-19 12:19:01 +02:00
|
|
|
from ckanext.spatial.model.package_extent import setup as setup_model
|
2012-02-07 17:50:46 +01:00
|
|
|
|
2011-04-11 18:23:27 +02:00
|
|
|
log = getLogger(__name__)
|
|
|
|
|
2012-03-07 16:07:37 +01:00
|
|
|
def package_error_summary(error_dict):
|
|
|
|
''' Do some i18n stuff on the error_dict keys '''
|
|
|
|
|
|
|
|
def prettify(field_name):
|
|
|
|
field_name = re.sub('(?<!\w)[Uu]rl(?!\w)', 'URL',
|
|
|
|
field_name.replace('_', ' ').capitalize())
|
2013-01-15 20:57:31 +01:00
|
|
|
return p.toolkit._(field_name.replace('_', ' '))
|
2012-03-07 16:07:37 +01:00
|
|
|
|
|
|
|
summary = {}
|
|
|
|
for key, error in error_dict.iteritems():
|
|
|
|
if key == 'resources':
|
2013-01-15 20:57:31 +01:00
|
|
|
summary[p.toolkit._('Resources')] = p.toolkit._('Package resource(s) invalid')
|
2012-03-07 16:07:37 +01:00
|
|
|
elif key == 'extras':
|
2013-01-15 20:57:31 +01:00
|
|
|
summary[p.toolkit._('Extras')] = p.toolkit._('Missing Value')
|
2012-03-07 16:07:37 +01:00
|
|
|
elif key == 'extras_validation':
|
2013-01-15 20:57:31 +01:00
|
|
|
summary[p.toolkit._('Extras')] = error[0]
|
2012-03-07 16:07:37 +01:00
|
|
|
else:
|
2013-01-15 20:57:31 +01:00
|
|
|
summary[p.toolkit._(prettify(key))] = error[0]
|
2012-03-07 16:07:37 +01:00
|
|
|
return summary
|
|
|
|
|
2013-01-15 20:57:31 +01:00
|
|
|
class SpatialMetadata(p.SingletonPlugin):
|
2011-04-11 19:04:28 +02:00
|
|
|
|
2013-01-15 20:57:31 +01:00
|
|
|
p.implements(p.IPackageController, inherit=True)
|
|
|
|
p.implements(p.IConfigurable, inherit=True)
|
2013-01-15 21:00:46 +01:00
|
|
|
p.implements(p.IConfigurer, inherit=True)
|
2012-02-07 17:50:46 +01:00
|
|
|
|
|
|
|
|
2013-01-15 20:57:31 +01:00
|
|
|
def configure(self, config):
|
2013-01-15 21:00:46 +01:00
|
|
|
|
|
|
|
if not p.toolkit.asbool(config.get('ckan.spatial.testing', 'False')):
|
2012-02-07 17:50:46 +01:00
|
|
|
setup_model()
|
2011-04-11 19:04:28 +02:00
|
|
|
|
2013-01-15 21:00:46 +01:00
|
|
|
def update_config(self, config):
|
|
|
|
''' Set up the resource library, public directory and
|
|
|
|
template directory for all the spatial extensions
|
|
|
|
'''
|
|
|
|
p.toolkit.add_public_directory(config, 'public')
|
|
|
|
p.toolkit.add_template_directory(config, 'templates')
|
|
|
|
p.toolkit.add_resource('public', 'ckanext-spatial')
|
|
|
|
|
2011-09-29 16:40:49 +02:00
|
|
|
def create(self, package):
|
|
|
|
self.check_spatial_extra(package)
|
|
|
|
|
|
|
|
def edit(self, package):
|
|
|
|
self.check_spatial_extra(package)
|
|
|
|
|
|
|
|
def check_spatial_extra(self,package):
|
2012-12-05 16:01:16 +01:00
|
|
|
'''
|
|
|
|
For a given package, looks at the spatial extent (as given in the
|
|
|
|
extra "spatial" in GeoJSON format) and records it in PostGIS.
|
|
|
|
'''
|
2011-10-17 00:40:19 +02:00
|
|
|
if not package.id:
|
|
|
|
log.warning('Couldn\'t store spatial extent because no id was provided for the package')
|
|
|
|
return
|
|
|
|
|
|
|
|
# TODO: deleted extra
|
2011-09-29 16:40:49 +02:00
|
|
|
for extra in package.extras_list:
|
2013-03-14 13:36:39 +01:00
|
|
|
if extra.key == 'spatial':
|
2011-09-29 16:40:49 +02:00
|
|
|
if extra.state == 'active':
|
|
|
|
try:
|
|
|
|
log.debug('Received: %r' % extra.value)
|
|
|
|
geometry = json.loads(extra.value)
|
|
|
|
except ValueError,e:
|
|
|
|
error_dict = {'spatial':[u'Error decoding JSON object: %s' % str(e)]}
|
2013-01-15 20:57:31 +01:00
|
|
|
raise p.toolkit.ValidationError(error_dict, error_summary=package_error_summary(error_dict))
|
2011-09-29 16:40:49 +02:00
|
|
|
except TypeError,e:
|
|
|
|
error_dict = {'spatial':[u'Error decoding JSON object: %s' % str(e)]}
|
2013-01-15 20:57:31 +01:00
|
|
|
raise p.toolkit.ValidationError(error_dict, error_summary=package_error_summary(error_dict))
|
2011-09-29 16:40:49 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
save_package_extent(package.id,geometry)
|
|
|
|
|
|
|
|
except ValueError,e:
|
|
|
|
error_dict = {'spatial':[u'Error creating geometry: %s' % str(e)]}
|
2013-01-15 20:57:31 +01:00
|
|
|
raise p.toolkit.ValidationError(error_dict, error_summary=package_error_summary(error_dict))
|
2011-09-29 16:40:49 +02:00
|
|
|
except Exception, e:
|
2012-10-19 12:19:01 +02:00
|
|
|
if bool(os.getenv('DEBUG')):
|
|
|
|
raise
|
2011-09-29 16:40:49 +02:00
|
|
|
error_dict = {'spatial':[u'Error: %s' % str(e)]}
|
2013-01-15 20:57:31 +01:00
|
|
|
raise p.toolkit.ValidationError(error_dict, error_summary=package_error_summary(error_dict))
|
2011-09-29 16:40:49 +02:00
|
|
|
|
|
|
|
elif extra.state == 'deleted':
|
|
|
|
# Delete extent from table
|
|
|
|
save_package_extent(package.id,None)
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
def delete(self, package):
|
|
|
|
save_package_extent(package.id,None)
|
2011-04-11 19:04:28 +02:00
|
|
|
|
2013-01-15 20:57:31 +01:00
|
|
|
class SpatialQuery(p.SingletonPlugin):
|
2012-02-21 13:00:47 +01:00
|
|
|
|
2013-01-15 20:57:31 +01:00
|
|
|
p.implements(p.IRoutes, inherit=True)
|
|
|
|
p.implements(p.IPackageController, inherit=True)
|
2012-02-21 13:00:47 +01:00
|
|
|
|
|
|
|
def before_map(self, map):
|
|
|
|
|
|
|
|
map.connect('api_spatial_query', '/api/2/search/{register:dataset|package}/geo',
|
|
|
|
controller='ckanext.spatial.controllers.api:ApiController',
|
|
|
|
action='spatial_query')
|
|
|
|
return map
|
|
|
|
|
2012-01-17 18:20:16 +01:00
|
|
|
def before_search(self,search_params):
|
2011-12-13 18:40:47 +01:00
|
|
|
if 'extras' in search_params and 'ext_bbox' in search_params['extras'] \
|
|
|
|
and search_params['extras']['ext_bbox']:
|
|
|
|
|
|
|
|
bbox = validate_bbox(search_params['extras']['ext_bbox'])
|
|
|
|
if not bbox:
|
|
|
|
raise SearchError('Wrong bounding box provided')
|
|
|
|
|
2012-12-06 19:17:45 +01:00
|
|
|
# Note: This will be deprecated at some point in favour of the
|
|
|
|
# Solr 4 spatial sorting capabilities
|
|
|
|
if search_params['sort'] == 'spatial desc' and \
|
|
|
|
p.toolkit.asbool(config.get('ckanext.spatial.use_postgis_sorting', 'False')):
|
|
|
|
if search_params['q'] or search_params['fq']:
|
|
|
|
raise SearchError('Spatial ranking cannot be mixed with other search parameters')
|
|
|
|
# ...because it is too inefficient to use SOLR to filter
|
|
|
|
# results and return the entire set to this class and
|
|
|
|
# after_search do the sorting and paging.
|
|
|
|
extents = bbox_query_ordered(bbox)
|
|
|
|
are_no_results = not extents
|
|
|
|
search_params['extras']['ext_rows'] = search_params['rows']
|
|
|
|
search_params['extras']['ext_start'] = search_params['start']
|
|
|
|
# this SOLR query needs to return no actual results since
|
|
|
|
# they are in the wrong order anyway. We just need this SOLR
|
|
|
|
# query to get the count and facet counts.
|
|
|
|
rows = 0
|
|
|
|
search_params['sort'] = None # SOLR should not sort.
|
|
|
|
# Store the rankings of the results for this page, so for
|
|
|
|
# after_search to construct the correctly sorted results
|
|
|
|
rows = search_params['extras']['ext_rows'] = search_params['rows']
|
|
|
|
start = search_params['extras']['ext_start'] = search_params['start']
|
|
|
|
search_params['extras']['ext_spatial'] = [
|
|
|
|
(extent.package_id, extent.spatial_ranking) \
|
|
|
|
for extent in extents[start:start+rows]]
|
|
|
|
else:
|
|
|
|
extents = bbox_query(bbox)
|
|
|
|
are_no_results = extents.count() == 0
|
2011-12-13 18:40:47 +01:00
|
|
|
|
2012-12-06 19:17:45 +01:00
|
|
|
if are_no_results:
|
2012-01-17 18:20:16 +01:00
|
|
|
# We don't need to perform the search
|
|
|
|
search_params['abort_search'] = True
|
|
|
|
else:
|
|
|
|
# We'll perform the existing search but also filtering by the ids
|
|
|
|
# of datasets within the bbox
|
|
|
|
bbox_query_ids = [extent.package_id for extent in extents]
|
2011-12-13 18:40:47 +01:00
|
|
|
|
2012-12-06 19:17:45 +01:00
|
|
|
q = search_params.get('q','').strip() or '""'
|
2012-01-17 18:20:16 +01:00
|
|
|
new_q = '%s AND ' % q if q else ''
|
|
|
|
new_q += '(%s)' % ' OR '.join(['id:%s' % id for id in bbox_query_ids])
|
2011-12-13 18:40:47 +01:00
|
|
|
|
2012-01-17 18:20:16 +01:00
|
|
|
search_params['q'] = new_q
|
2011-12-13 18:40:47 +01:00
|
|
|
|
2012-01-17 18:20:16 +01:00
|
|
|
return search_params
|
2011-12-13 18:40:47 +01:00
|
|
|
|
2012-12-06 19:17:45 +01:00
|
|
|
def after_search(self, search_results, search_params):
|
|
|
|
|
|
|
|
# Note: This will be deprecated at some point in favour of the
|
|
|
|
# Solr 4 spatial sorting capabilities
|
|
|
|
|
|
|
|
if search_params.get('extras', {}).get('ext_spatial') and \
|
|
|
|
p.toolkit.asbool(config.get('ckanext.spatial.use_postgis_sorting', 'False')):
|
|
|
|
# Apply the spatial sort
|
|
|
|
querier = PackageSearchQuery()
|
|
|
|
pkgs = []
|
|
|
|
for package_id, spatial_ranking in search_params['extras']['ext_spatial']:
|
|
|
|
# get package from SOLR
|
|
|
|
pkg = querier.get_index(package_id)['data_dict']
|
|
|
|
pkgs.append(json.loads(pkg))
|
|
|
|
search_results['results'] = pkgs
|
|
|
|
return search_results
|
|
|
|
|
|
|
|
|
2013-01-15 20:57:31 +01:00
|
|
|
class SpatialQueryWidget(p.SingletonPlugin):
|
2012-02-21 13:00:47 +01:00
|
|
|
|
2013-01-15 20:57:31 +01:00
|
|
|
p.implements(p.IGenshiStreamFilter)
|
2012-02-21 13:00:47 +01:00
|
|
|
|
2011-12-13 18:40:47 +01:00
|
|
|
def filter(self, stream):
|
|
|
|
from pylons import request, tmpl_context as c
|
|
|
|
routes = request.environ.get('pylons.routes_dict')
|
|
|
|
if routes.get('controller') == 'package' and \
|
|
|
|
routes.get('action') == 'search':
|
|
|
|
|
2012-02-21 12:05:13 +01:00
|
|
|
data = {
|
|
|
|
'bbox': request.params.get('ext_bbox',''),
|
|
|
|
'default_extent': config.get('ckan.spatial.default_map_extent','')
|
|
|
|
}
|
2011-12-13 18:40:47 +01:00
|
|
|
stream = stream | Transformer('body//div[@id="dataset-search-ext"]')\
|
|
|
|
.append(HTML(html.SPATIAL_SEARCH_FORM % data))
|
2012-01-18 12:03:21 +01:00
|
|
|
stream = stream | Transformer('head')\
|
|
|
|
.append(HTML(html.SPATIAL_SEARCH_FORM_EXTRA_HEADER % data))
|
|
|
|
stream = stream | Transformer('body')\
|
|
|
|
.append(HTML(html.SPATIAL_SEARCH_FORM_EXTRA_FOOTER % data))
|
2011-12-13 18:40:47 +01:00
|
|
|
|
|
|
|
return stream
|
|
|
|
|
2011-04-11 19:04:28 +02:00
|
|
|
|
2013-01-15 20:57:31 +01:00
|
|
|
class CatalogueServiceWeb(p.SingletonPlugin):
|
|
|
|
p.implements(p.IConfigurable)
|
|
|
|
p.implements(p.IRoutes)
|
2012-10-17 17:59:02 +02:00
|
|
|
|
|
|
|
def configure(self, config):
|
|
|
|
config.setdefault("cswservice.title", "Untitled Service - set cswservice.title in config")
|
|
|
|
config.setdefault("cswservice.abstract", "Unspecified service description - set cswservice.abstract in config")
|
|
|
|
config.setdefault("cswservice.keywords", "")
|
|
|
|
config.setdefault("cswservice.keyword_type", "theme")
|
|
|
|
config.setdefault("cswservice.provider_name", "Unnamed provider - set cswservice.provider_name in config")
|
|
|
|
config.setdefault("cswservice.contact_name", "No contact - set cswservice.contact_name in config")
|
|
|
|
config.setdefault("cswservice.contact_position", "")
|
|
|
|
config.setdefault("cswservice.contact_voice", "")
|
|
|
|
config.setdefault("cswservice.contact_fax", "")
|
|
|
|
config.setdefault("cswservice.contact_address", "")
|
|
|
|
config.setdefault("cswservice.contact_city", "")
|
|
|
|
config.setdefault("cswservice.contact_region", "")
|
|
|
|
config.setdefault("cswservice.contact_pcode", "")
|
|
|
|
config.setdefault("cswservice.contact_country", "")
|
|
|
|
config.setdefault("cswservice.contact_email", "")
|
|
|
|
config.setdefault("cswservice.contact_hours", "")
|
|
|
|
config.setdefault("cswservice.contact_instructions", "")
|
|
|
|
config.setdefault("cswservice.contact_role", "")
|
|
|
|
|
|
|
|
config["cswservice.rndlog_threshold"] = float(config.get("cswservice.rndlog_threshold", "0.01"))
|
|
|
|
|
|
|
|
def before_map(self, route_map):
|
|
|
|
c = "ckanext.spatial.controllers.csw:CatalogueServiceWebController"
|
|
|
|
route_map.connect("/csw", controller=c, action="dispatch_get",
|
|
|
|
conditions={"method": ["GET"]})
|
|
|
|
route_map.connect("/csw", controller=c, action="dispatch_post",
|
|
|
|
conditions={"method": ["POST"]})
|
|
|
|
|
|
|
|
return route_map
|
|
|
|
|
|
|
|
def after_map(self, route_map):
|
|
|
|
return route_map
|
2012-10-19 12:19:01 +02:00
|
|
|
|
2013-01-15 20:57:31 +01:00
|
|
|
class HarvestMetadataApi(p.SingletonPlugin):
|
2012-10-19 12:19:01 +02:00
|
|
|
'''
|
|
|
|
Harvest Metadata API
|
|
|
|
(previously called "InspireApi")
|
|
|
|
|
|
|
|
A way for a user to view the harvested metadata XML, either as a raw file or
|
|
|
|
styled to view in a web browser.
|
|
|
|
'''
|
2013-01-15 20:57:31 +01:00
|
|
|
p.implements(p.IRoutes)
|
2012-10-19 12:19:01 +02:00
|
|
|
|
|
|
|
def before_map(self, route_map):
|
|
|
|
controller = "ckanext.spatial.controllers.api:HarvestMetadataApiController"
|
|
|
|
|
2013-02-19 18:02:28 +01:00
|
|
|
# Showing the harvest object content is an action of the default
|
|
|
|
# harvest plugin, so just redirect there
|
|
|
|
route_map.redirect('/api/2/rest/harvestobject/{id:.*}/xml',
|
|
|
|
'/harvest/object/{id}',
|
|
|
|
_redirect_code='301 Moved Permanently')
|
|
|
|
|
|
|
|
route_map.connect('/harvest/object/{id}/original', controller=controller,
|
|
|
|
action='display_xml_original')
|
|
|
|
|
|
|
|
route_map.connect('/harvest/object/{id}/html', controller=controller,
|
|
|
|
action='display_html')
|
|
|
|
route_map.connect('/harvest/object/{id}/html/original', controller=controller,
|
|
|
|
action='display_html_original')
|
|
|
|
|
|
|
|
# Redirect old URL to a nicer and unversioned one
|
|
|
|
route_map.redirect('/api/2/rest/harvestobject/:id/html',
|
|
|
|
'/harvest/object/{id}/html',
|
|
|
|
_redirect_code='301 Moved Permanently')
|
2012-10-19 12:19:01 +02:00
|
|
|
|
|
|
|
return route_map
|
|
|
|
|
|
|
|
def after_map(self, route_map):
|
|
|
|
return route_map
|