diff --git a/.gitignore b/.gitignore index 3e3f0e3..51d2d72 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ syntax: glob *.pyc *.sw* *.egg-info + +.DS_Store diff --git a/ckanext/odata/actions.py b/ckanext/odata/actions.py index dd7f587..356a3c3 100644 --- a/ckanext/odata/actions.py +++ b/ckanext/odata/actions.py @@ -1,8 +1,9 @@ import re import simplejson as json +import ckan.plugins.toolkit as t -import ckan.plugins as p - +from flask import make_response +from ckan.exceptions import CkanVersionException try: from collections import OrderedDict # from python 2.7 @@ -21,11 +22,20 @@ 'text': 'Edm.String', } +name_pattern = r'[^:A-Z_a-z.0-9\u00B7\u00C0-\u00D6\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u0300-\u036F\u203F-\u2040]' -t = p.toolkit _base_url = None +def get_request_param(): + try: + requires_ckan_version("2.9") + except: + return t.request.params + else: + return t.request.args + + def name_2_xml_tag(name): ''' Convert a name into a xml safe name. @@ -42,13 +52,10 @@ def name_2_xml_tag(name): ''' # leave well-formed XML element characters only - name = re.sub(ur'[^A-Z_a-z\u00C0-\u00D6\u0370-\u037D\u037F-\u1FFF' - ur'\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF' - ur'\uF900-\uFDCF\uFDF0-\uFFFD-0-9\u00B7\u0300-\u036F' - ur'\u203F-\u2040]', '', name) + name = re.sub(name_pattern, '', name) # add '_' in front of non-NameStart characters - name = re.sub(re.compile(ur'(?P^[-.0-9\u00B7#\u0300-\u036F\u203F-\u2040])', re.MULTILINE), + name = re.sub(re.compile(r'(?P^[-.0-9\u00B7#\u0300-\u036F\u203F-\u2040])', re.MULTILINE), '_\g', name) # No valid XML element at all @@ -59,8 +66,9 @@ def name_2_xml_tag(name): def get_qs_int(param, default): - ''' Get a query sting param as an int ''' - value = t.request.GET.get(param, default) + ''' Get a query string param as an int ''' + request_params = get_request_param() + value = request_params.get(param, default) try: value = int(value) except ValueError: @@ -90,15 +98,16 @@ def odata(context, data_dict): resource_id = uri filters = {} - output_json = t.request.GET.get('$format') == 'json' + request_params = get_request_param() + output_json = request_params.get('$format') == 'json' # Ignore $limit & $top paramters if $sqlfilter is specified # as they should be specified by the sql query - if t.request.GET.get('$sqlfilter'): + if request_params.get('$sqlfilter'): action = t.get_action('datastore_search_sql') - query = t.request.GET.get('$sqlfilter') - sql = "SELECT * FROM \"%s\" %s"%(resource_id, query) + query = request_params.get('$sqlfilter', '') + sql = "SELECT * FROM \"{}\" {}".format(resource_id, query) data_dict = { 'sql': sql @@ -125,7 +134,7 @@ def odata(context, data_dict): except t.ValidationError as e: return json.dumps(e.error_dict) - if not t.request.GET.get('$sqlfilter'): + if not request_params.get('$sqlfilter'): num_results = result['total'] if num_results > offset + limit: next_query_string = '$skip=%s&$top=%s' % (offset + limit, limit) @@ -162,8 +171,15 @@ def odata(context, data_dict): 'entries': result['records'], 'next_query_string': next_query_string, } - t.response.headers['Content-Type'] = 'application/atom+xml;type=feed;charset=utf-8' - return t.render('ckanext-odata/collection.xml', data) + try: + t.requires_ckan_version("2.9") + except CkanVersionException: + t.response.headers['Content-Type'] = 'application/atom+xml;type=feed;charset=utf-8' + return t.render('ckanext-odata/collection.xml', data) + else: + response = make_response(t.render('ckanext-odata/collection.xml', data)) + response.headers['Content-Type'] = 'application/atom+xml;type=feed;charset=utf-8' + return response def odata_metadata(context, data_dict): @@ -211,5 +227,12 @@ def odata_metadata(context, data_dict): data = { 'collections' : collections } - t.response.headers['Content-Type'] = 'application/xml;charset=utf-8' - return t.render('ckanext-odata/metadata.xml', data) + try: + t.requires_ckan_version("2.9") + except CkanVersionException: + t.response.headers['Content-Type'] = 'application/xml;charset=utf-8' + return t.render('ckanext-odata/metadata.xml', data) + else: + response = make_response(t.render('ckanext-odata/metadata.xml', data)) + response.headers['Content-Type'] = 'application/xml;charset=utf-8' + return response diff --git a/ckanext/odata/controller.py b/ckanext/odata/controller.py index 6c3d931..3582810 100644 --- a/ckanext/odata/controller.py +++ b/ckanext/odata/controller.py @@ -1,15 +1,10 @@ -import ckan.plugins as p +from ckan.plugins.toolkit import BaseController +from ckanext.odata import utils - -class ODataController(p.toolkit.BaseController): +class ODataController(BaseController): def odata(self, uri): - data_dict = {'uri': uri} - action = p.toolkit.get_action('ckanext-odata_odata') - result = action({}, data_dict) - return result + return utils.odata(uri) def odata_metadata(self): - action = p.toolkit.get_action('ckanext-odata_metadata') - result = action({},{}) - return result + return utils.odata_metadata() diff --git a/ckanext/odata/plugin.py b/ckanext/odata/plugin.py deleted file mode 100644 index 4f4eb3f..0000000 --- a/ckanext/odata/plugin.py +++ /dev/null @@ -1,40 +0,0 @@ -import ckan.plugins as p - -import ckanext.odata.actions as action - - -def link(resource_id): - return '%s%s' % (action.base_url(), resource_id) - - -class ODataPlugin(p.SingletonPlugin): - - p.implements(p.IConfigurer) - p.implements(p.IRoutes, inherit=True) - p.implements(p.IActions) - p.implements(p.ITemplateHelpers, inherit=True) - - def update_config(self, config): - p.toolkit.add_template_directory(config, 'templates') - p.toolkit.add_resource('resources', 'odata') - - def before_map(self, m): - m.connect('/datastore/odata3.0/$metadata', - controller='ckanext.odata.controller:ODataController', - action='odata_metadata') - m.connect('/datastore/odata3.0/{uri:.*?}', - controller='ckanext.odata.controller:ODataController', - action='odata') - return m - - def get_actions(self): - actions = { - 'ckanext-odata_metadata': action.odata_metadata, - 'ckanext-odata_odata': action.odata, - } - return actions - - def get_helpers(self): - return { - 'ckanext_odata_link': link, - } diff --git a/ckanext/odata/plugin/__init__.py b/ckanext/odata/plugin/__init__.py new file mode 100644 index 0000000..6ffacfd --- /dev/null +++ b/ckanext/odata/plugin/__init__.py @@ -0,0 +1,33 @@ +import ckan.plugins as p +import ckanext.odata.actions as action + +if p.toolkit.check_ckan_version('2.9'): + from ckanext.odata.plugin.flask_plugin import MixinPlugin +else: + from ckanext.odata.plugin.pylons_plugin import MixinPlugin + + +def link(resource_id): + return '{0}{1}'.format(action.base_url(), resource_id) + + +class ODataPlugin(MixinPlugin, p.SingletonPlugin): + p.implements(p.IConfigurer) + p.implements(p.IActions) + p.implements(p.ITemplateHelpers, inherit=True) + + def update_config(self, config): + p.toolkit.add_template_directory(config, '../templates') + p.toolkit.add_resource('../resources', 'odata') + + def get_actions(self): + actions = { + 'ckanext-odata_metadata': action.odata_metadata, + 'ckanext-odata_odata': action.odata, + } + return actions + + def get_helpers(self): + return { + 'ckanext_odata_link': link, + } diff --git a/ckanext/odata/plugin/flask_plugin.py b/ckanext/odata/plugin/flask_plugin.py new file mode 100644 index 0000000..28ad468 --- /dev/null +++ b/ckanext/odata/plugin/flask_plugin.py @@ -0,0 +1,9 @@ +import ckan.plugins as p +import ckanext.odata.views as views + + +class MixinPlugin(p.SingletonPlugin): + p.implements(p.IBlueprint) + + def get_blueprint(self): + return views.get_blueprints() diff --git a/ckanext/odata/plugin/pylons_plugin.py b/ckanext/odata/plugin/pylons_plugin.py new file mode 100644 index 0000000..56e18b6 --- /dev/null +++ b/ckanext/odata/plugin/pylons_plugin.py @@ -0,0 +1,14 @@ +import ckan.plugins as p + + +class MixinPlugin(p.SingletonPlugin): + p.implements(p.IRoutes, inherit=True) + + def before_map(self, m): + m.connect('/datastore/odata3.0/$metadata', + controller='ckanext.odata.controller:ODataController', + action='odata_metadata') + m.connect('/datastore/odata3.0/{uri:.*?}', + controller='ckanext.odata.controller:ODataController', + action='odata') + return m diff --git a/ckanext/odata/templates/ajax_snippets/api_info.html b/ckanext/odata/templates/ajax_snippets/api_info.html index 33b1eb1..bef8bb9 100644 --- a/ckanext/odata/templates/ajax_snippets/api_info.html +++ b/ckanext/odata/templates/ajax_snippets/api_info.html @@ -13,143 +13,148 @@ {% set resource_id = h.sanitize_id(resource_id) %} {% set sql_example_url = h.url_for(controller='api', action='action', ver=3, logic_function='datastore_search_sql', qualified=True) + '?sql=SELECT * from "' + resource_id + '" WHERE \"title\" LIKE \'jones\'' %} {# not urlencoding the sql because its clearer #} - - - -

{{ _('Access resource data via a web API with powerful query support') }}. - {% trans %} - Further information in the main - CKAN Data API and DataStore documentation.

- {% endtrans %} -
-
-
- - {{ _('Endpoints') }} » + + - -
diff --git a/ckanext/odata/utils.py b/ckanext/odata/utils.py new file mode 100644 index 0000000..4fda840 --- /dev/null +++ b/ckanext/odata/utils.py @@ -0,0 +1,10 @@ +import ckan.plugins.toolkit as toolkit + +def odata(uri): + data_dict = {'uri': uri} + result = toolkit.get_action('ckanext-odata_odata')({}, data_dict) + return result + +def odata_metadata(): + result = toolkit.get_action('ckanext-odata_metadata')({}, {}) + return result diff --git a/ckanext/odata/views.py b/ckanext/odata/views.py new file mode 100644 index 0000000..10119d9 --- /dev/null +++ b/ckanext/odata/views.py @@ -0,0 +1,20 @@ +from flask import Blueprint +from ckanext.odata import utils + + +odata_blueprint = Blueprint(u'odata_blueprint', __name__) + + +def odata(uri): + return utils.odata(uri) + +def odata_metadata(): + return utils.odata_metadata() + + +odata_blueprint.add_url_rule('/datastore/odata3.0/', view_func=odata) +odata_blueprint.add_url_rule('/datastore/odata3.0/$metadata', view_func=odata_metadata) + + +def get_blueprints(): + return [odata_blueprint]