Skip to content

Commit

Permalink
Merge branch 'master' into 3229-api-blueprint
Browse files Browse the repository at this point in the history
  • Loading branch information
amercader committed Apr 4, 2017
2 parents 7a5ed72 + 1c40b38 commit 16f745b
Show file tree
Hide file tree
Showing 18 changed files with 343 additions and 80 deletions.
2 changes: 1 addition & 1 deletion ckan/config/middleware/flask_app.py
Expand Up @@ -13,7 +13,6 @@
from werkzeug.routing import Rule

from flask_babel import Babel
from flask_debugtoolbar import DebugToolbarExtension

from beaker.middleware import SessionMiddleware
from paste.deploy.converters import asbool
Expand Down Expand Up @@ -71,6 +70,7 @@ def make_flask_stack(conf, **app_conf):
' with the SECRET_KEY config option')

if debug:
from flask_debugtoolbar import DebugToolbarExtension
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
DebugToolbarExtension(app)

Expand Down
13 changes: 8 additions & 5 deletions ckan/controllers/package.py
Expand Up @@ -390,11 +390,14 @@ def read(self, id):
try:
return render(template,
extra_vars={'dataset_type': package_type})
except ckan.lib.render.TemplateNotFound:
msg = _("Viewing {package_type} datasets in {format} format is "
"not supported (template file {file} not found).".format(
package_type=package_type, format=format,
file=template))
except ckan.lib.render.TemplateNotFound as e:
msg = _(
"Viewing datasets of type \"{package_type}\" is "
"not supported ({file_!r}).".format(
package_type=package_type,
file_=e.message
)
)
abort(404, msg)

assert False, "We should never get here"
Expand Down
14 changes: 11 additions & 3 deletions ckan/lib/cli.py
Expand Up @@ -137,7 +137,7 @@ class CkanCommand(paste.script.command.Command):
'''Base class for classes that implement CKAN paster commands to inherit.'''
parser = paste.script.command.Command.standard_parser(verbose=True)
parser.add_option('-c', '--config', dest='config',
default='development.ini', help='Config file to use.')
help='Config file to use.')
parser.add_option('-f', '--file',
action='store',
dest='file_path',
Expand All @@ -155,8 +155,16 @@ def _get_config(self):
self.filename = os.environ.get('CKAN_INI')
config_source = '$CKAN_INI'
else:
self.filename = os.path.join(os.getcwd(), 'development.ini')
config_source = 'default value'
default_filename = 'development.ini'
self.filename = os.path.join(os.getcwd(), default_filename)
if not os.path.exists(self.filename):
# give really clear error message for this common situation
msg = 'ERROR: You need to specify the CKAN config (.ini) '\
'file path.'\
'\nUse the --config parameter or set environment ' \
'variable CKAN_INI or have {}\nin the current directory.' \
.format(default_filename)
raise self.BadCommand(msg)

if not os.path.exists(self.filename):
msg = 'Config file not found: %s' % self.filename
Expand Down
34 changes: 28 additions & 6 deletions ckan/logic/__init__.py
Expand Up @@ -4,6 +4,7 @@
import logging
import re
import sys
from collections import defaultdict

import formencode.validators

Expand Down Expand Up @@ -312,6 +313,15 @@ def clear_actions_cache():
_actions.clear()


def chained_action(func):
func.chained_action = True
return func


def _is_chained_action(func):
return getattr(func, 'chained_action', False)


def get_action(action):
'''Return the named :py:mod:`ckan.logic.action` function.
Expand Down Expand Up @@ -394,20 +404,32 @@ def get_action(action):
# Then overwrite them with any specific ones in the plugins:
resolved_action_plugins = {}
fetched_actions = {}
chained_actions = defaultdict(list)
for plugin in p.PluginImplementations(p.IActions):
for name, auth_function in plugin.get_actions().items():
if name in resolved_action_plugins:
if _is_chained_action(auth_function):
chained_actions[name].append(auth_function)
elif name in resolved_action_plugins:
raise NameConflict(
'The action %r is already implemented in %r' % (
name,
resolved_action_plugins[name]
)
)
resolved_action_plugins[name] = plugin.name
# Extensions are exempted from the auth audit for now
# This needs to be resolved later
auth_function.auth_audit_exempt = True
fetched_actions[name] = auth_function
else:
resolved_action_plugins[name] = plugin.name
# Extensions are exempted from the auth audit for now
# This needs to be resolved later
auth_function.auth_audit_exempt = True
fetched_actions[name] = auth_function
for name, func_list in chained_actions.iteritems():
if name not in fetched_actions:
raise NotFound('The action %r is not found for chained action' % (
name))
for func in reversed(func_list):
prev_func = fetched_actions[name]
fetched_actions[name] = functools.partial(func, prev_func)

# Use the updated ones in preference to the originals.
_actions.update(fetched_actions)

Expand Down
78 changes: 55 additions & 23 deletions ckan/plugins/interfaces.py
Expand Up @@ -4,6 +4,9 @@
extend CKAN.
'''
from inspect import isclass
from pyutilib.component.core import Interface as _pca_Interface

__all__ = [
u'Interface',
u'IRoutes',
Expand Down Expand Up @@ -37,18 +40,26 @@
u'IPermissionLabels',
]

from inspect import isclass
from pyutilib.component.core import Interface as _pca_Interface


class Interface(_pca_Interface):
u'''Base class for custom interfaces.
Marker base class for extension point interfaces. This class
is not intended to be instantiated. Instead, the declaration
of subclasses of Interface are recorded, and these
classes are used to define extension points.
'''

@classmethod
def provided_by(cls, instance):
u'''Check that object is an instance of class that implements interface.
'''
return cls.implemented_by(instance.__class__)

@classmethod
def implemented_by(cls, other):
u'''Check wheter class implements current interface.
'''
if not isclass(other):
raise TypeError(u'Class expected', other)
try:
Expand Down Expand Up @@ -125,7 +136,7 @@ def after_map(self, map):
class IMapper(Interface):
u'''
A subset of the SQLAlchemy mapper extension hooks.
See http://docs.sqlalchemy.org/en/rel_0_9/orm/deprecated.html#sqlalchemy.orm.interfaces.MapperExtension
See `sqlalchemy MapperExtension`_
Example::
Expand All @@ -135,6 +146,9 @@ class IMapper(Interface):
...
... def after_update(self, mapper, connection, instance):
... log(u'Updated: %r', instance)
.. _sqlalchemy MapperExtension:\
http://docs.sqlalchemy.org/en/rel_0_9/orm/deprecated.html#sqlalchemy.orm.interfaces.MapperExtension
'''

def before_insert(self, mapper, connection, instance):
Expand Down Expand Up @@ -506,21 +520,23 @@ class IGroupController(Interface):
'''

def read(self, entity):
u'''Called after IGroupController.before_view inside group_read.
'''
pass

def create(self, entity):
u'''Called after group had been created inside group_create.
'''
pass

def edit(self, entity):
pass

def authz_add_role(self, object_role):
pass

def authz_remove_role(self, object_role):
u'''Called after group had been updated inside group_update.
'''
pass

def delete(self, entity):
u'''Called before commit inside group_delete.
'''
pass

def before_view(self, pkg_dict):
Expand All @@ -541,21 +557,24 @@ class IOrganizationController(Interface):
'''

def read(self, entity):
u'''Called after IOrganizationController.before_view inside
organization_read.
'''
pass

def create(self, entity):
u'''Called after organization had been created inside organization_create.
'''
pass

def edit(self, entity):
pass

def authz_add_role(self, object_role):
pass

def authz_remove_role(self, object_role):
u'''Called after organization had been updated inside organization_update.
'''
pass

def delete(self, entity):
u'''Called before commit inside organization_delete.
'''
pass

def before_view(self, pkg_dict):
Expand All @@ -574,21 +593,23 @@ class IPackageController(Interface):
'''

def read(self, entity):
u'''Called after IGroupController.before_view inside package_read.
'''
pass

def create(self, entity):
u'''Called after group had been created inside package_create.
'''
pass

def edit(self, entity):
pass

def authz_add_role(self, object_role):
pass

def authz_remove_role(self, object_role):
u'''Called after group had been updated inside package_update.
'''
pass

def delete(self, entity):
u'''Called before commit inside package_delete.
'''
pass

def after_create(self, context, pkg_dict):
Expand Down Expand Up @@ -892,6 +913,17 @@ def get_actions(self):
By decorating a function with the `ckan.logic.side_effect_free`
decorator, the associated action will be made available by a GET
request (as well as the usual POST request) through the action API.
By decrorating a function with the 'ckan.plugins.toolkit.chained_action,
the action will be chained to another function defined in plugins with a
"first plugin wins" pattern, which means the first plugin declaring a
chained action should be called first. Chained actions must be
defined as action_function(original_action, context, data_dict)
where the first parameter will be set to the action function in
the next plugin or in core ckan. The chained action may call the
original_action function, optionally passing different values,
handling exceptions, returning different values and/or raising
different exceptions to the caller.
'''


Expand Down Expand Up @@ -1649,7 +1681,7 @@ def get_resource_uploader(self):
Optionally, this method can set the following two attributes
on the class instance so they are set in the resource object:
filesize (int): Uploaded file filesize.
mimetype (str): Uploaded file mimetype.
Expand Down
3 changes: 3 additions & 0 deletions ckan/plugins/toolkit.py
Expand Up @@ -44,6 +44,8 @@ class _Toolkit(object):
'literal',
# get logic action function
'get_action',
# decorator for chained action
'chained_action',
# get navl schema converter
'get_converter',
# get navl schema validator
Expand Down Expand Up @@ -229,6 +231,7 @@ def _initialize(self):
t['literal'] = webhelpers.html.tags.literal

t['get_action'] = logic.get_action
t['chained_action'] = logic.chained_action
t['get_converter'] = logic.get_validator # For backwards compatibility
t['get_validator'] = logic.get_validator
t['check_access'] = logic.check_access
Expand Down
8 changes: 6 additions & 2 deletions ckan/templates/package/resource_read.html
Expand Up @@ -151,8 +151,12 @@ <h2>{{ _('Additional Information') }}</h2>
</thead>
<tbody>
<tr>
<th scope="row">{{ _('Last updated') }}</th>
<td>{{ h.render_datetime(res.last_modified) or h.render_datetime(res.revision_timestamp) or h.render_datetime(res.created) or _('unknown') }}</td>
<th scope="row">{{ _('Data last updated') }}</th>
<td>{{ h.render_datetime(res.last_modified) or h.render_datetime(res.created) or _('unknown') }}</td>
</tr>
<tr>
<th scope="row">{{ _('Metadata last updated') }}</th>
<td>{{ h.render_datetime(res.revision_timestamp) or h.render_datetime(res.created) or _('unknown') }}</td>
</tr>
<tr>
<th scope="row">{{ _('Created') }}</th>
Expand Down
12 changes: 0 additions & 12 deletions ckan/tests/legacy/ckantestplugins.py
Expand Up @@ -87,12 +87,6 @@ def create(self, entity):
def edit(self, entity):
self.calls['edit'] += 1

def authz_add_role(self, object_role):
self.calls['authz_add_role'] += 1

def authz_remove_role(self, object_role):
self.calls['authz_remove_role'] += 1

def delete(self, entity):
self.calls['delete'] += 1

Expand All @@ -116,12 +110,6 @@ def create(self, entity):
def edit(self, entity):
self.calls['edit'] += 1

def authz_add_role(self, object_role):
self.calls['authz_add_role'] += 1

def authz_remove_role(self, object_role):
self.calls['authz_remove_role'] += 1

def delete(self, entity):
self.calls['delete'] += 1

Expand Down
5 changes: 1 addition & 4 deletions ckanext/datastore/controller.py
@@ -1,9 +1,6 @@
# encoding: utf-8

import StringIO
import md5

import pylons
import json

from ckan.plugins.toolkit import (
Invalid,
Expand Down

0 comments on commit 16f745b

Please sign in to comment.