-
Notifications
You must be signed in to change notification settings - Fork 2k
/
toolkit.py
509 lines (427 loc) · 18.2 KB
/
toolkit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# encoding: utf-8
import sys
class _Toolkit(object):
'''This class is intended to make functions/objects consistently
available to plugins, whilst giving core CKAN developers the ability move
code around or change underlying frameworks etc. This object allows
us to avoid circular imports while making functions/objects
available to plugins.
It should not be used internally within ckan - only by extensions.
Functions/objects should only be removed after reasonable
deprecation notice has been given.'''
# contents should describe the available functions/objects. We check
# that this list matches the actual availables in the initialisation
contents = [
# Global CKAN configuration object
'config',
# i18n translation
'_',
# i18n translation (plural form)
'ungettext',
# template context
'c',
# template helpers
'h',
# http request object
'request',
# template render function
'render',
# snippet render function
'render_snippet',
# converts a string to a boolean
'asbool',
# converts a string to an integer
'asint',
# converts a string to a list
'aslist',
# stop tags in a string being escaped
'literal',
# get logic action function
'get_action',
# get flask/pylons endpoint fragments
'get_endpoint',
# decorator for chained action
'chained_action',
# get navl schema converter
'get_converter',
# get navl schema validator
'get_validator',
# check logic function authorisation
'check_access',
# decorator for chained authentication functions
'chained_auth_function',
# implements validate method with navl schema
'navl_validate',
# placeholder for missing values for validation
'missing',
# action not found exception (ckan.logic.NotFound)
'ObjectNotFound',
# action not authorized exception
'NotAuthorized',
# validator not found exception
'UnknownValidator',
# model update validation error
'ValidationError',
# validation exception to stop further validators from being called
'StopOnError',
# validation invalid exception
'Invalid',
# old class for providing CLI interfaces
'CkanCommand',
# function for initializing CLI interfaces
'load_config',
# base class for IDatasetForm plugins
'DefaultDatasetForm',
# base class for IGroupForm plugins
'DefaultGroupForm',
# base class for IGroupForm plugins for orgs
'DefaultOrganizationForm',
# response object for cookies etc
'response',
# Allow controllers to be created
'BaseController',
# abort actions
'abort',
# allow redirections
'redirect_to',
# create urls
'url_for',
# helpful for actions
'get_or_bust',
# actions can be accessed via api
'side_effect_free',
# allow auth functions to be checked for sysadmins
'auth_sysadmins_check',
# allow anonymous access to an auth function
'auth_allow_anonymous_access',
# disallow anonymous access to an auth function
'auth_disallow_anonymous_access',
# Helper not found error.
'HelperError',
# Enqueue background job
'enqueue_job',
# Fully defined in this file ##
'add_template_directory',
'add_resource',
'add_public_directory',
'add_ckan_admin_tab',
'requires_ckan_version',
'check_ckan_version',
'CkanVersionException',
]
def __init__(self):
self._toolkit = {}
# For some members in the the toolkit (e.g. that are exported from
# third-party libraries) we override their docstrings by putting our
# own docstrings into this dict. The Sphinx plugin that documents this
# plugins toolkit will use these docstring overrides instead of the
# object's actual docstring, when present.
self.docstring_overrides = {}
def _initialize(self):
''' get the required functions/objects, store them for later
access and check that they match the contents dict. '''
import ckan
import ckan.lib.base as base
import ckan.logic as logic
import ckan.logic.validators as logic_validators
import ckan.lib.navl.dictization_functions as dictization_functions
import ckan.lib.helpers as h
import ckan.lib.cli as cli
import ckan.lib.plugins as lib_plugins
import ckan.common as common
from ckan.exceptions import (
CkanVersionException,
HelperError
)
from ckan.lib.jobs import enqueue as enqueue_job
from paste.deploy import converters
import pylons
import webhelpers.html.tags
# Allow class access to these modules
self.__class__.ckan = ckan
self.__class__.base = base
t = self._toolkit
# imported functions
t['config'] = common.config
self.docstring_overrides['config'] = '''The CKAN configuration object.
It stores the configuration values defined in the :ref:`config_file`, eg::
title = toolkit.config.get("ckan.site_title")
'''
t['_'] = common._
self.docstring_overrides['_'] = '''Translates a string to the
current locale.
The ``_()`` function is a reference to the ``ugettext()`` function.
Everywhere in your code where you want strings to be internationalized
(made available for translation into different languages), wrap them in the
``_()`` function, eg.::
msg = toolkit._("Hello")
Returns the localized unicode string.
'''
t['ungettext'] = common.ungettext
self.docstring_overrides['ungettext'] = '''Translates a string with
plural forms to the current locale.
Mark a string for translation that has pural forms in the format
``ungettext(singular, plural, n)``. Returns the localized unicode string of
the pluralized value.
Mark a string to be localized as follows::
msg = toolkit.ungettext("Mouse", "Mice", len(mouses))
'''
t['c'] = common.c
self.docstring_overrides['c'] = '''The Pylons template context object.
This object is used to pass request-specific information to different parts of
the code in a thread-safe way (so that variables from different requests being
executed at the same time don't get confused with each other).
Any attributes assigned to :py:attr:`~ckan.plugins.toolkit.c` are
available throughout the template and application code, and are local to the
current request.
'''
t['h'] = h.helper_functions
t['request'] = common.request
self.docstring_overrides['request'] = '''The Pylons request object.
A new request object is created for each HTTP request. It has methods and
attributes for getting things like the request headers, query-string variables,
request body variables, cookies, the request URL, etc.
'''
t['render'] = base.render
t['asbool'] = converters.asbool
self.docstring_overrides['asbool'] = '''Convert a string (e.g. 1,
true, True) from the config file into a boolean.
For example: ``if toolkit.asbool(config.get('ckan.legacy_templates', False)):``
'''
t['asint'] = converters.asint
self.docstring_overrides['asint'] = '''Convert a string from the config
file into an int.
For example: ``bar = toolkit.asint(config.get('ckan.foo.bar', 0))``
'''
t['aslist'] = converters.aslist
self.docstring_overrides['aslist'] = '''Convert a space-separated
string from the config file into a list.
For example: ``bar = toolkit.aslist(config.get('ckan.foo.bar', []))``
'''
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
t['chained_auth_function'] = logic.chained_auth_function
t['navl_validate'] = dictization_functions.validate
t['missing'] = dictization_functions.missing
t['ObjectNotFound'] = logic.NotFound # Name change intentional
t['NotAuthorized'] = logic.NotAuthorized
t['ValidationError'] = logic.ValidationError
t['StopOnError'] = dictization_functions.StopOnError
t['UnknownValidator'] = logic.UnknownValidator
t['Invalid'] = logic_validators.Invalid
t['CkanCommand'] = cli.CkanCommand
t['load_config'] = cli.load_config
t['DefaultDatasetForm'] = lib_plugins.DefaultDatasetForm
t['DefaultGroupForm'] = lib_plugins.DefaultGroupForm
t['DefaultOrganizationForm'] = lib_plugins.DefaultOrganizationForm
t['response'] = pylons.response
self.docstring_overrides['response'] = '''The Pylons response object.
Pylons uses this object to generate the HTTP response it returns to the web
browser. It has attributes like the HTTP status code, the response headers,
content type, cookies, etc.
'''
t['BaseController'] = base.BaseController
t['abort'] = base.abort
t['redirect_to'] = h.redirect_to
t['url_for'] = h.url_for
t['get_or_bust'] = logic.get_or_bust
t['side_effect_free'] = logic.side_effect_free
t['auth_sysadmins_check'] = logic.auth_sysadmins_check
t['auth_allow_anonymous_access'] = logic.auth_allow_anonymous_access
t['auth_disallow_anonymous_access'] = (
logic.auth_disallow_anonymous_access
)
# class functions
t['render_snippet'] = self._render_snippet
t['add_template_directory'] = self._add_template_directory
t['add_public_directory'] = self._add_public_directory
t['add_resource'] = self._add_resource
t['add_ckan_admin_tab'] = self._add_ckan_admin_tabs
t['requires_ckan_version'] = self._requires_ckan_version
t['check_ckan_version'] = self._check_ckan_version
t['get_endpoint'] = self._get_endpoint
t['CkanVersionException'] = CkanVersionException
t['HelperError'] = HelperError
t['enqueue_job'] = enqueue_job
# check contents list correct
errors = set(t).symmetric_difference(set(self.contents))
if errors:
raise Exception('Plugin toolkit error %s not matching' % errors)
# wrappers
# Wrapper for the render_snippet function as it uses keywords rather than
# dict to pass data.
@classmethod
def _render_snippet(cls, template, data=None):
'''Render a template snippet and return the output.
See :doc:`/theming/index`.
'''
data = data or {}
return cls.base.render_snippet(template, **data)
# new functions
@classmethod
def _add_template_directory(cls, config, relative_path):
'''Add a path to the :ref:`extra_template_paths` config setting.
The path is relative to the file calling this function.
'''
cls._add_served_directory(config, relative_path,
'extra_template_paths')
@classmethod
def _add_public_directory(cls, config, relative_path):
'''Add a path to the :ref:`extra_public_paths` config setting.
The path is relative to the file calling this function.
'''
cls._add_served_directory(config, relative_path, 'extra_public_paths')
@classmethod
def _add_served_directory(cls, config, relative_path, config_var):
''' Add extra public/template directories to config. '''
import inspect
import os
assert config_var in ('extra_template_paths', 'extra_public_paths')
# we want the filename that of the function caller but they will
# have used one of the available helper functions
# TODO: starting from python 3.5, `inspect.stack` returns list
# of named tuples `FrameInfo`. Don't forget to remove
# `getframeinfo` wrapper after migration.
filename = inspect.getframeinfo(inspect.stack()[2][0]).filename
this_dir = os.path.dirname(filename)
absolute_path = os.path.join(this_dir, relative_path)
if absolute_path not in config.get(config_var, ''):
if config.get(config_var):
config[config_var] += ',' + absolute_path
else:
config[config_var] = absolute_path
@classmethod
def _add_resource(cls, path, name):
'''Add a WebAssets library to CKAN.
WebAssets libraries are directories containing static resource
files (e.g. CSS, JavaScript or image files) that can be
compiled into WebAsset Bundles.
See :doc:`/theming/index` for more details.
'''
import inspect
import os
from ckan.lib.webassets_tools import create_library
# we want the filename that of the function caller but they
# will have used one of the available helper functions
# TODO: starting from python 3.5, `inspect.stack` returns list
# of named tuples `FrameInfo`. Don't forget to remove
# `getframeinfo` wrapper after migration.
filename = inspect.getframeinfo(inspect.stack()[1][0]).filename
this_dir = os.path.dirname(filename)
absolute_path = os.path.join(this_dir, path)
create_library(name, absolute_path)
# TODO: remove next two lines after dropping Fanstatic support
import ckan.lib.fanstatic_resources
ckan.lib.fanstatic_resources.create_library(name, absolute_path)
@classmethod
def _add_ckan_admin_tabs(cls, config, route_name, tab_label,
config_var='ckan.admin_tabs'):
'''
Update 'ckan.admin_tabs' dict the passed config dict.
'''
# get the admin_tabs dict from the config, or an empty dict.
admin_tabs_dict = config.get(config_var, {})
# update the admin_tabs dict with the new values
admin_tabs_dict.update({route_name: tab_label})
# update the config with the updated admin_tabs dict
config.update({config_var: admin_tabs_dict})
@classmethod
def _version_str_2_list(cls, v_str):
''' convert a version string into a list of ints
eg 1.6.1b --> [1, 6, 1] '''
import re
v_str = re.sub(r'[^0-9.]', '', v_str)
return [int(part) for part in v_str.split('.')]
@classmethod
def _check_ckan_version(cls, min_version=None, max_version=None):
'''Return ``True`` if the CKAN version is greater than or equal to
``min_version`` and less than or equal to ``max_version``,
return ``False`` otherwise.
If no ``min_version`` is given, just check whether the CKAN version is
less than or equal to ``max_version``.
If no ``max_version`` is given, just check whether the CKAN version is
greater than or equal to ``min_version``.
:param min_version: the minimum acceptable CKAN version,
eg. ``'2.1'``
:type min_version: string
:param max_version: the maximum acceptable CKAN version,
eg. ``'2.3'``
:type max_version: string
'''
current = cls._version_str_2_list(cls.ckan.__version__)
if min_version:
min_required = cls._version_str_2_list(min_version)
if current < min_required:
return False
if max_version:
max_required = cls._version_str_2_list(max_version)
if current > max_required:
return False
return True
@classmethod
def _requires_ckan_version(cls, min_version, max_version=None):
'''Raise :py:exc:`~ckan.plugins.toolkit.CkanVersionException` if the
CKAN version is not greater than or equal to ``min_version`` and
less then or equal to ``max_version``.
If no ``max_version`` is given, just check whether the CKAN version is
greater than or equal to ``min_version``.
Plugins can call this function if they require a certain CKAN version,
other versions of CKAN will crash if a user tries to use the plugin
with them.
:param min_version: the minimum acceptable CKAN version,
eg. ``'2.1'``
:type min_version: string
:param max_version: the maximum acceptable CKAN version,
eg. ``'2.3'``
:type max_version: string
'''
from ckan.exceptions import CkanVersionException
if not cls._check_ckan_version(min_version=min_version,
max_version=max_version):
if not max_version:
error = 'Requires ckan version %s or higher' % min_version
else:
error = 'Requires ckan version between {0} and {1}'.format(
min_version,
max_version
)
raise CkanVersionException(error)
@classmethod
def _get_endpoint(cls):
"""Returns tuple in format: (controller|blueprint, action|view).
"""
import ckan.common as common
try:
# CKAN >= 2.8
endpoint = tuple(common.request.endpoint.split('.'))
except AttributeError:
try:
return common.c.controller, common.c.action
except AttributeError:
return (None, None)
# there are some routes('hello_world') that are not using blueprint
# For such case, let's assume that view function is a controller
# itself and action is None.
if len(endpoint) is 1:
return endpoint + (None,)
return endpoint
def __getattr__(self, name):
''' return the function/object requested '''
if not self._toolkit:
self._initialize()
if name in self._toolkit:
return self._toolkit[name]
else:
if name == '__bases__':
return self.__class__.__bases__
raise AttributeError('`%s` not found in plugins toolkit' % name)
def __dir__(self):
if not self._toolkit:
self._initialize()
return sorted(self._toolkit.keys())
# https://mail.python.org/pipermail/python-ideas/2012-May/014969.html
sys.modules[__name__] = _Toolkit()