-
Notifications
You must be signed in to change notification settings - Fork 2k
/
toolkit.py
374 lines (302 loc) · 14.6 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
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 = [
## Imported functions/objects ##
'_', # i18n translation
'c', # template context
'request', # http request object
'render', # template render function
'render_text', # Genshi NewTextTemplate render function
'render_snippet', # snippet render function
'asbool', # converts an object to a boolean
'asint', # converts an object to an integer
'aslist', # converts an object to a list
'literal', # stop tags in a string being escaped
'get_action', # get logic action function
'get_converter', # get navl schema converter
'get_validator', # get navl schema validator
'check_access', # check logic function authorisation
'navl_validate', # implements validate method with navl schema
'ObjectNotFound', # action not found exception
# (ckan.logic.NotFound)
'NotAuthorized', # action not authorized exception
'UnknownConverter', # convertor not found exception
'UnknownValidator', # validator not found exception
'ValidationError', # model update validation error
'Invalid', # validation invalid exception
'CkanCommand', # class for providing cli interfaces
'DefaultDatasetForm', # base class for IDatasetForm plugins
'response', # response object for cookies etc
'BaseController', # Allow controllers to be created
'abort', # abort actions
'redirect_to', # allow redirections
'url_for', # create urls
'get_or_bust', # helpful for actions
'side_effect_free', # actions can be accessed via api
'auth_sysadmins_check', # allow auth functions to be checked for sysadmins
'auth_allow_anonymous_access', # allow anonymous access to an auth function
'auth_disallow_anonymous_access', # disallow anonymous access to an auth function
'get_new_resources', # gets all new resources in current commit
## Fully defined in this file ##
'add_template_directory',
'add_resource',
'add_public_directory',
'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
import ckan.lib.datapreview as datapreview
from ckan.exceptions import CkanVersionException
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['_'] = common._
self.docstring_overrides['_'] = '''The Pylons ``_()`` function.
The Pylons ``_()`` 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")
'''
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['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['render_text'] = base.render_text
t['asbool'] = converters.asbool
self.docstring_overrides['asbool'] = '''Convert a string 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 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['get_converter'] = logic.get_converter
t['get_validator'] = logic.get_validator
t['check_access'] = logic.check_access
t['navl_validate'] = dictization_functions.validate
t['ObjectNotFound'] = logic.NotFound # Name change intentional
t['NotAuthorized'] = logic.NotAuthorized
t['ValidationError'] = logic.ValidationError
t['UnknownConverter'] = logic.UnknownConverter
t['UnknownValidator'] = logic.UnknownValidator
t['Invalid'] = logic_validators.Invalid
t['CkanCommand'] = cli.CkanCommand
t['DefaultDatasetForm'] = lib_plugins.DefaultDatasetForm
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
t['get_new_resources'] = datapreview.get_new_resources
# 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['requires_ckan_version'] = self._requires_ckan_version
t['check_ckan_version'] = self._check_ckan_version
t['CkanVersionException'] = CkanVersionException
# 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
frame, filename, line_number, function_name, lines, index =\
inspect.getouterframes(inspect.currentframe())[2]
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 Fanstatic resource library to CKAN.
Fanstatic libraries are directories containing static resource files
(e.g. CSS, JavaScript or image files) that can be accessed from CKAN.
See :doc:`/theming/index` for more details.
'''
import inspect
import os
# we want the filename that of the function caller but they will
# have used one of the available helper functions
frame, filename, line_number, function_name, lines, index =\
inspect.getouterframes(inspect.currentframe())[1]
this_dir = os.path.dirname(filename)
absolute_path = os.path.join(this_dir, path)
import ckan.lib.fanstatic_resources
ckan.lib.fanstatic_resources.create_library(name, absolute_path)
@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 %s and %s' % \
(min_version, max_version)
raise CkanVersionException(error)
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 Exception('`%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()