Skip to content

Commit

Permalink
Merge pull request #3239 from ckan/3229-api-blueprint
Browse files Browse the repository at this point in the history
[#3229] API Blueprint
  • Loading branch information
wardi committed Sep 19, 2017
2 parents 3d41e74 + 370b5f7 commit b8c1dba
Show file tree
Hide file tree
Showing 29 changed files with 565 additions and 328 deletions.
2 changes: 2 additions & 0 deletions ckan/config/middleware/__init__.py
Expand Up @@ -3,10 +3,12 @@
"""WSGI app initialization"""
import urllib
import urlparse
import urllib

import webob
from routes import request_config as routes_request_config

from ckan.lib.i18n import get_locales_from_config
from ckan.config.environment import load_environment
from ckan.config.middleware.flask_app import make_flask_stack
from ckan.config.middleware.pylons_app import make_pylons_stack
Expand Down
55 changes: 44 additions & 11 deletions ckan/config/middleware/flask_app.py
Expand Up @@ -18,10 +18,13 @@
from beaker.middleware import SessionMiddleware
from paste.deploy.converters import asbool
from fanstatic import Fanstatic
from repoze.who.config import WhoConfig
from repoze.who.middleware import PluggableAuthenticationMiddleware

import ckan.model as model
from ckan.lib import helpers
from ckan.lib import jinja_extensions
from ckan.common import config, g, request
from ckan.common import config, g, request, ungettext
import ckan.lib.app_globals as app_globals
from ckan.plugins import PluginImplementations
from ckan.plugins.interfaces import IBlueprint, IMiddleware
Expand Down Expand Up @@ -120,22 +123,21 @@ def save_session(self, app, session, response):
app.context_processor(helper_functions)
app.context_processor(c_object)

@app.context_processor
def ungettext_alias():
u'''
Provide `ungettext` as an alias of `ngettext` for backwards
compatibility
'''
return dict(ungettext=ungettext)

# Babel
app.config[u'BABEL_TRANSLATION_DIRECTORIES'] = os.path.join(root, u'i18n')
app.config[u'BABEL_DOMAIN'] = 'ckan'

babel = Babel(app)

@babel.localeselector
def get_locale():
u'''
Return the value of the `CKAN_LANG` key of the WSGI environ,
set by the I18nMiddleware based on the URL.
If no value is defined, it defaults to `ckan.locale_default` or `en`.
'''
return request.environ.get(
u'CKAN_LANG',
config.get(u'ckan.locale_default', u'en'))
babel.localeselector(get_locale)

@app.route('/hello', methods=['GET'])
def hello_world():
Expand Down Expand Up @@ -188,6 +190,23 @@ def hello_world_post():
'make_error_log_middleware.'
.format(plugin.__class__.__name__))

# Initialize repoze.who
who_parser = WhoConfig(conf['here'])
who_parser.parse(open(app_conf['who.config_file']))

app = PluggableAuthenticationMiddleware(
app,
who_parser.identifiers,
who_parser.authenticators,
who_parser.challengers,
who_parser.mdproviders,
who_parser.request_classifier,
who_parser.challenge_decider,
logging.getLogger('repoze.who'),
logging.WARN, # ignored
who_parser.remote_user_key
)

# Update the main CKAN config object with the Flask specific keys
# that were set here or autogenerated
flask_config_keys = set(flask_app.config.keys()) - set(config.keys())
Expand All @@ -200,6 +219,17 @@ def hello_world_post():
return app


def get_locale():
u'''
Return the value of the `CKAN_LANG` key of the WSGI environ,
set by the I18nMiddleware based on the URL.
If no value is defined, it defaults to `ckan.locale_default` or `en`.
'''
return request.environ.get(
u'CKAN_LANG',
config.get(u'ckan.locale_default', u'en'))


def ckan_before_request():
u'''Common handler executed before all Flask requests'''

Expand All @@ -214,6 +244,9 @@ def ckan_before_request():
def ckan_after_request(response):
u'''Common handler executed after all Flask requests'''

# Dispose of the SQLALchemy session
model.Session.remove()

# Check session cookie
response = check_session_cookie(response)

Expand Down
30 changes: 1 addition & 29 deletions ckan/config/routing.py
Expand Up @@ -111,11 +111,8 @@ def make_map():
if not hasattr(route, '_ckan_core'):
route._ckan_core = False

map.connect('invite', '/__invite__/', controller='partyline', action='join_party')

map.connect('home', '/', controller='home', action='index')
map.connect('about', '/about', controller='home', action='about')

# CKAN API versioned.
register_list = [
'package',
Expand All @@ -131,12 +128,6 @@ def make_map():
]
register_list_str = '|'.join(register_list)

# /api ver 3 or none
with SubMapper(map, controller='api', path_prefix='/api{ver:/3|}',
ver='/3') as m:
m.connect('/action/{logic_function}', action='action',
conditions=GET_POST)

# /api ver 1, 2, 3 or none
with SubMapper(map, controller='api', path_prefix='/api{ver:/1|/2|/3|}',
ver='/1') as m:
Expand All @@ -145,9 +136,7 @@ def make_map():
# /api ver 1, 2 or none
with SubMapper(map, controller='api', path_prefix='/api{ver:/1|/2|}',
ver='/1') as m:
m.connect('/tag_counts', action='tag_counts')
m.connect('/rest', action='index')
m.connect('/qos/throughput/', action='throughput', conditions=GET)

# /api/rest ver 1, 2 or none
with SubMapper(map, controller='api', path_prefix='/api{ver:/1|/2|}',
Expand All @@ -173,31 +162,14 @@ def make_map():
m.connect('/rest/{register}/{id}/:subregister/{id2}', action='delete',
conditions=DELETE)


# /api/util ver 1, 2 or none
with SubMapper(map, controller='api', path_prefix='/api{ver:/1|/2|}',
ver='/1') as m:
m.connect('/util/user/autocomplete', action='user_autocomplete')
m.connect('/util/is_slug_valid', action='is_slug_valid',
conditions=GET)
m.connect('/util/dataset/autocomplete', action='dataset_autocomplete',
conditions=GET)
m.connect('/util/tag/autocomplete', action='tag_autocomplete',
conditions=GET)
m.connect('/util/resource/format_autocomplete',
action='format_autocomplete', conditions=GET)
m.connect('/util/resource/format_icon',
action='format_icon', conditions=GET)
m.connect('/util/group/autocomplete', action='group_autocomplete')
m.connect('/util/organization/autocomplete', action='organization_autocomplete',
conditions=GET)
m.connect('/util/markdown', action='markdown')
m.connect('/util/dataset/munge_name', action='munge_package_name')
m.connect('/util/dataset/munge_title_to_name',
action='munge_title_to_package_name')
m.connect('/util/tag/munge', action='munge_tag')
m.connect('/util/status', action='status')
m.connect('/util/snippet/{snippet_path:.*}', action='snippet')
m.connect('/i18n/{lang}', action='i18n_js_translations')

###########
## /END API
Expand Down
90 changes: 0 additions & 90 deletions ckan/controllers/api.py
Expand Up @@ -155,12 +155,6 @@ def get_api(self, ver=None):
response_data['version'] = ver
return self._finish_ok(response_data)

def snippet(self, snippet_path, ver=None):
''' Renders and returns a snippet used by ajax calls '''
# we only allow snippets in templates/ajax_snippets and it's subdirs
snippet_path = u'ajax_snippets/' + snippet_path
return base.render(snippet_path, extra_vars=dict(request.params))

def action(self, logic_function, ver=None):
try:
function = get_action(logic_function)
Expand Down Expand Up @@ -334,10 +328,6 @@ def show(self, ver=None, register=None, subregister=None,
except NotAuthorized, e:
return self._finish_not_authz(unicode(e))

def _represent_package(self, package):
return package.as_dict(ref_package_by=self.ref_package_by,
ref_group_by=self.ref_group_by)

def create(self, ver=None, register=None, subregister=None,
id=None, id2=None):

Expand Down Expand Up @@ -611,44 +601,6 @@ def _get_search_params(cls, request_params):
raise ValueError(msg)
return params

def markdown(self, ver=None):
raw_markdown = request.params.get('q', '')
results = h.render_markdown(raw_markdown)

return self._finish_ok(results)

def tag_counts(self, ver=None):
c.q = request.params.get('q', '')

context = {'model': model, 'session': model.Session,
'user': c.user, 'auth_user_obj': c.userobj}

tag_names = get_action('tag_list')(context, {})
results = []
for tag_name in tag_names:
tag_count = len(context['model'].Tag.get(tag_name).packages)
results.append((tag_name, tag_count))
return self._finish_ok(results)

def throughput(self, ver=None):
qos = self._calc_throughput()
qos = str(qos)
return self._finish_ok(qos)

def _calc_throughput(self, ver=None):
period = 10 # Seconds.
timing_cache_path = self._get_timing_cache_path()
call_count = 0
for t in range(0, period):
expr = '%s/%s*' % (
timing_cache_path,
(datetime.datetime.now() -
datetime.timedelta(0, t)).isoformat()[0:19],
)
call_count += len(glob.glob(expr))
# Todo: Clear old records.
return float(call_count) / period

@jsonp.jsonpify
def user_autocomplete(self):
q = request.params.get('q', '')
Expand Down Expand Up @@ -699,34 +651,6 @@ def organization_autocomplete(self):
get_action('organization_autocomplete')(context, data_dict)
return organization_list

def is_slug_valid(self):

def package_exists(val):
if model.Session.query(model.Package) \
.autoflush(False).filter_by(name=val).count():
return True
return False

def group_exists(val):
if model.Session.query(model.Group) \
.autoflush(False).filter_by(name=val).count():
return True
return False

slug = request.params.get('slug') or ''
slugtype = request.params.get('type') or ''
# TODO: We need plugins to be able to register new disallowed names
disallowed = ['new', 'edit', 'search']
if slugtype == u'package':
response_data = dict(valid=not (package_exists(slug)
or slug in disallowed))
return self._finish_ok(response_data)
if slugtype == u'group':
response_data = dict(valid=not (group_exists(slug) or
slug in disallowed))
return self._finish_ok(response_data)
return self._finish_bad_request('Bad slug type: %s' % slugtype)

def dataset_autocomplete(self):
q = request.params.get('incomplete', '')
limit = request.params.get('limit', 10)
Expand Down Expand Up @@ -795,20 +719,6 @@ def munge_tag(self):
munged_tag = munge.munge_tag(tag)
return self._finish_ok(munged_tag)

def format_icon(self):
f = request.params.get('format')
out = {
'format': f,
'icon': h.icon_url(h.format_icon(f))
}
return self._finish_ok(out)

def status(self):
context = {'model': model, 'session': model.Session}
data_dict = {}
status = get_action('status_show')(context, data_dict)
return self._finish_ok(status)

def i18n_js_translations(self, lang):
''' translation strings for front end '''
ckan_path = os.path.join(os.path.dirname(__file__), '..')
Expand Down
12 changes: 9 additions & 3 deletions ckan/lib/activity_streams.py
Expand Up @@ -8,7 +8,7 @@
import ckan.lib.base as base
import ckan.logic as logic

from ckan.common import _
from ckan.common import _, is_flask_request

# get_snippet_*() functions replace placeholders like {user}, {dataset}, etc.
# in activity strings with HTML representations of particular users, datasets,
Expand Down Expand Up @@ -252,5 +252,11 @@ def activity_list_to_html(context, activity_stream, extra_vars):
'timestamp': activity['timestamp'],
'is_new': activity.get('is_new', False)})
extra_vars['activities'] = activity_list
return literal(base.render('activity_streams/activity_stream_items.html',
extra_vars=extra_vars))

# TODO: Do this properly without having to check if it's Flask or not
if is_flask_request():
return base.render('activity_streams/activity_stream_items.html',
extra_vars=extra_vars)
else:
return literal(base.render('activity_streams/activity_stream_items.html',
extra_vars=extra_vars))
2 changes: 1 addition & 1 deletion ckan/lib/alphabet_paginate.py
Expand Up @@ -21,7 +21,7 @@
from sqlalchemy import __version__ as sqav
from sqlalchemy.orm.query import Query
from webhelpers.html.builder import HTML
from routes import url_for
from ckan.lib.helpers import url_for


class AlphaPage(object):
Expand Down

0 comments on commit b8c1dba

Please sign in to comment.