From fca35e5f6929e3008640be1d1c2460dbc2a2ba32 Mon Sep 17 00:00:00 2001 From: Toby Date: Thu, 28 Jun 2012 13:48:56 +0100 Subject: [PATCH 001/103] [#1387] fix resource urls with no protocols --- ckan/lib/dictization/model_dictize.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index 74e15ed8baa..d3a3620fc6b 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -102,6 +102,10 @@ def resource_dictize(res, context): model = context['model'] tracking = model.TrackingSummary.get_for_resource(res.url) resource['tracking_summary'] = tracking + # some urls do not have the protocol this adds http:// to these + url = resource['url'] + if not (url.startswith('http://') or url.startswith('https://')): + resource['url'] = u'http://' + url return resource def related_dictize(rel, context): From 85d88059e921deb93025d9d8ef55dc551f1f788d Mon Sep 17 00:00:00 2001 From: Toby Date: Thu, 28 Jun 2012 14:31:46 +0100 Subject: [PATCH 002/103] [#1387] fix tests with bad urls --- ckan/tests/lib/test_dictization.py | 16 ++++++++-------- ckan/tests/logic/test_action.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ckan/tests/lib/test_dictization.py b/ckan/tests/lib/test_dictization.py index 7b986d6c120..aadfcbebe18 100644 --- a/ckan/tests/lib/test_dictization.py +++ b/ckan/tests/lib/test_dictization.py @@ -364,7 +364,7 @@ def test_09_package_alter(self): anna_dictized = package_dictize(anna1, context) anna_dictized["name"] = u'annakarenina_changed' - anna_dictized["resources"][0]["url"] = u'new_url' + anna_dictized["resources"][0]["url"] = u'http://new_url' model.repo.new_revision() package_dict_save(anna_dictized, context) @@ -402,7 +402,7 @@ def test_10_package_alter_pending(self): anna_dictized = package_dictize(anna1, context) anna_dictized['name'] = u'annakarenina_changed2' - anna_dictized['resources'][0]['url'] = u'new_url2' + anna_dictized['resources'][0]['url'] = u'http://new_url2' anna_dictized['tags'][0]['name'] = u'new_tag' anna_dictized['tags'][0].pop('id') #test if anna_dictized['extras'][0]['value'] = u'"new_value"' @@ -496,7 +496,7 @@ def test_11_add_pending(self): anna_dictized['notes'] = 'wee' anna_dictized['resources'].append({ 'format': u'plain text', - 'url': u'newurl'} + 'url': u'http://newurl'} ) anna_dictized['tags'].append({'name': u'newnew_tag'}) anna_dictized['extras'].append({'key': 'david', @@ -676,7 +676,7 @@ def test_13_get_package_in_past(self): second_dictized = self.remove_changable_columns(package_dictize(anna1, context)) first_dictized["name"] = u'annakarenina_changed' - first_dictized["resources"][0]["url"] = u'new_url' + first_dictized["resources"][0]["url"] = u'http://new_url' assert second_dictized == first_dictized @@ -684,7 +684,7 @@ def test_13_get_package_in_past(self): third_dictized = self.remove_changable_columns(package_dictize(anna1, context)) second_dictized['name'] = u'annakarenina_changed2' - second_dictized['resources'][0]['url'] = u'new_url2' + second_dictized['resources'][0]['url'] = u'http://new_url2' second_dictized['tags'][0]['name'] = u'new_tag' second_dictized['tags'][0]['display_name'] = u'new_tag' second_dictized['extras'][0]['value'] = u'"new_value"' @@ -711,7 +711,7 @@ def test_13_get_package_in_past(self): u'size': None, u'state': u'active', u'tracking_summary': {'total': 0, 'recent': 0}, - u'url': u'newurl', + u'url': u'http://newurl', u'webstore_last_updated': None, u'webstore_url': None}) @@ -741,7 +741,7 @@ def test_14_resource_no_id(self): 'description': u'Full text. Needs escaping: " Umlaut: \xfc', 'format': u'plain text', 'tracking_summary': {'recent': 0, 'total': 0}, - 'url': u'test_new', + 'url': u'http://test_new', 'cache_url': None, 'webstore_url': None, 'cache_last_updated': None, @@ -760,7 +760,7 @@ def test_14_resource_no_id(self): model.Session.commit() model.Session.remove() - res = model.Session.query(model.Resource).filter_by(url=u'test_new').one() + res = model.Session.query(model.Resource).filter_by(url=u'http://test_new').one() res_dictized = self.remove_changable_columns(resource_dictize(res, context)) diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index d104c5d3e94..83c1097ad3e 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -192,7 +192,7 @@ def test_18_create_package_not_authorized(self): def test_41_create_resource(self): anna_id = model.Package.by_name(u'annakarenina').id - resource = {'package_id': anna_id, 'url': 'new_url'} + resource = {'package_id': anna_id, 'url': 'http://new_url'} postparams = '%s=1' % json.dumps(resource) res = self.app.post('/api/action/resource_create', params=postparams, @@ -200,7 +200,7 @@ def test_41_create_resource(self): resource = json.loads(res.body)['result'] - assert resource['url'] == 'new_url' + assert resource['url'] == 'http://new_url' def test_42_create_resource_with_error(self): From 528d27bf699b8df85edf8c55f7cb91b8fcf64c7c Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 2 Jul 2012 13:05:58 +0100 Subject: [PATCH 003/103] [#1678] Make synchronous search the default behaviour Unless already loaded or explicitly disabled via the `ckan.search.automatic_index` configuration option, the synchronous search plugin will be loaded during startup time. --- ckan/config/deployment.ini_tmpl | 7 ++++++- ckan/config/environment.py | 8 ++++++++ doc/configuration.rst | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ckan/config/deployment.ini_tmpl b/ckan/config/deployment.ini_tmpl index d3c0199687f..d66d6543248 100644 --- a/ckan/config/deployment.ini_tmpl +++ b/ckan/config/deployment.ini_tmpl @@ -24,7 +24,7 @@ app_instance_uuid = ${app_instance_uuid} # List the names of CKAN extensions to activate. # Note: This line is required to be here for packaging, even if it is empty. -ckan.plugins = stats synchronous_search +ckan.plugins = stats # If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings @@ -112,6 +112,11 @@ ckan.gravatar_default = identicon ## Solr support #solr_url = http://127.0.0.1:8983/solr +## Automatic indexing. Make all changes immediately available via the search +## after editing or creating a dataset. Default is true. If for some reason +## you need the indexing to occur asynchronously, set this option to 0. +# ckan.search.automatic_indexing = 1 + ## An 'id' for the site (using, for example, when creating entries in a common search index) ## If not specified derived from the site_url # ckan.site_id = ckan.net diff --git a/ckan/config/environment.py b/ckan/config/environment.py index bb46ea2db8a..8fca75b7548 100644 --- a/ckan/config/environment.py +++ b/ckan/config/environment.py @@ -19,6 +19,7 @@ import ckan.lib.search as search import ckan.lib.app_globals as app_globals +log = logging.getLogger(__name__) # Suppress benign warning 'Unbuilt egg for setuptools' warnings.simplefilter('ignore', UserWarning) @@ -122,6 +123,13 @@ def find_controller(self, controller): # load all CKAN plugins p.load_all(config) + # Load the synchronous search plugin, unless already loaded or + # explicitly disabled + if not 'synchronous_search' in config.get('ckan.plugins') and \ + asbool(config.get('ckan.search.automatic_indexing',True)): + log.debug('Loading the synchronous search plugin') + p.load('synchronous_search') + for plugin in p.PluginImplementations(p.IConfigurer): # must do update in place as this does not work: # config = plugin.update_config(config) diff --git a/doc/configuration.rst b/doc/configuration.rst index 9b753e480d9..ebb09845bdc 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -496,6 +496,23 @@ Optionally, ``solr_user`` and ``solr_password`` can also be configured to specif Note, if you change this value, you need to rebuild the search index. +.. index:: + single: ckan.search.automatic_indexing + +ckan.search.automatic_indexing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Example:: + + ckan.search.automatic_indexing = 1 + +Make all changes immediately available via the search after editing or +creating a dataset. Default is true. If for some reason you need the indexing +to occur asynchronously, set this option to 0. + +Note, this is equivalent to explicitly load the `synchronous_search` plugin. + + simple_search ^^^^^^^^^^^^^ From 67d7135ed8c2494279dbe00b67cfea20ed2bfc6c Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 2 Jul 2012 13:44:37 +0100 Subject: [PATCH 004/103] PEP8 environment.py --- ckan/config/environment.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/ckan/config/environment.py b/ckan/config/environment.py index 8fca75b7548..d388937ff33 100644 --- a/ckan/config/environment.py +++ b/ckan/config/environment.py @@ -24,6 +24,7 @@ # Suppress benign warning 'Unbuilt egg for setuptools' warnings.simplefilter('ignore', UserWarning) + class _Helpers(object): ''' Helper object giving access to template helpers stopping missing functions from causing template exceptions. Useful if @@ -94,13 +95,16 @@ def load_environment(global_conf, app_conf): from pylons.wsgiapp import PylonsApp import pkg_resources find_controller_generic = PylonsApp.find_controller + # This is from pylons 1.0 source, will monkey-patch into 0.9.7 def find_controller(self, controller): if controller in self.controller_classes: return self.controller_classes[controller] # Check to see if its a dotted name if '.' in controller or ':' in controller: - mycontroller = pkg_resources.EntryPoint.parse('x=%s' % controller).load(False) + mycontroller = pkg_resources \ + .EntryPoint \ + .parse('x=%s' % controller).load(False) self.controller_classes[controller] = mycontroller return mycontroller return find_controller_generic(self, controller) @@ -126,7 +130,7 @@ def find_controller(self, controller): # Load the synchronous search plugin, unless already loaded or # explicitly disabled if not 'synchronous_search' in config.get('ckan.plugins') and \ - asbool(config.get('ckan.search.automatic_indexing',True)): + asbool(config.get('ckan.search.automatic_indexing', True)): log.debug('Loading the synchronous search plugin') p.load('synchronous_search') @@ -160,11 +164,13 @@ def find_controller(self, controller): config['pylons.app_globals'] = app_globals.Globals() # add helper functions - restrict_helpers = asbool(config.get('ckan.restrict_template_vars', 'true')) + restrict_helpers = asbool( + config.get('ckan.restrict_template_vars', 'true')) helpers = _Helpers(h, restrict_helpers) config['pylons.h'] = helpers - ## redo template setup to use genshi.search_path (so remove std template setup) + # Redo template setup to use genshi.search_path + # (so remove std template setup) template_paths = [paths['templates'][0]] extra_template_paths = config.get('extra_template_paths', '') if extra_template_paths: @@ -173,6 +179,7 @@ def find_controller(self, controller): # Translator (i18n) translator = Translator(pylons.translator) + def template_loaded(template): translator.setup(template) @@ -203,8 +210,6 @@ def template_loaded(template): # # ################################################################# - - ''' This code is based on Genshi code @@ -273,11 +278,14 @@ def genshi_lookup_attr(cls, obj, key): # Setup the SQLAlchemy database engine # Suppress a couple of sqlalchemy warnings - warnings.filterwarnings('ignore', '^Unicode type received non-unicode bind param value', sqlalchemy.exc.SAWarning) - warnings.filterwarnings('ignore', "^Did not recognize type 'BIGINT' of column 'size'", sqlalchemy.exc.SAWarning) - warnings.filterwarnings('ignore', "^Did not recognize type 'tsvector' of column 'search_vector'", sqlalchemy.exc.SAWarning) + msgs = ['^Unicode type received non-unicode bind param value', + "^Did not recognize type 'BIGINT' of column 'size'", + "^Did not recognize type 'tsvector' of column 'search_vector'" + ] + for msg in msgs: + warnings.filterwarnings('ignore', msg, sqlalchemy.exc.SAWarning) - ckan_db = os.environ.get('CKAN_DB') + ckan_db = os.environ.get('CKAN_DB') if ckan_db: config['sqlalchemy.url'] = ckan_db @@ -296,4 +304,3 @@ def genshi_lookup_attr(cls, obj, key): for plugin in p.PluginImplementations(p.IConfigurable): plugin.configure(config) - From b094c4eda6e61cacd3da3e638573803457f014d4 Mon Sep 17 00:00:00 2001 From: Toby Date: Tue, 3 Jul 2012 11:18:48 +0100 Subject: [PATCH 005/103] [#2362] toolkit docs --- doc/index.rst | 1 + doc/toolkit.rst | 148 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 doc/toolkit.rst diff --git a/doc/index.rst b/doc/index.rst index e8973a28272..ccd7a8d6dae 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -36,6 +36,7 @@ Customizing and Extending commenting extensions writing-extensions + toolkit forms tag-vocabularies form-integration diff --git a/doc/toolkit.rst b/doc/toolkit.rst new file mode 100644 index 00000000000..29ba543ab00 --- /dev/null +++ b/doc/toolkit.rst @@ -0,0 +1,148 @@ + +Plugins Toolkit +=============== + +To allow a safe way for extensions to interact with ckan a toolkit is +provided. We aim to keep this toolkit stable across ckan versions so +that extensions will work across diferent versions of ckan. + +.. Note:: + + It is advised that when writing extensions that all interaction with + ckan is done via the toolkit so that they do not break when new + versions of ckan are released. + +Over time we will be expanding the functionality available via +this toolkit. + +Example extension that registers a new helper function available to +templates via h.example_helper() :: + + import ckan.plugins as p + + + class ExampleExtension(p.SingletonPlugin): + + p.implements(p.IConfigurer) + p.implements(p.ITemplateHelpers) + + def update_config(self, config): + # add template directory that contains our snippet + p.toolkit.add_template_directory(config, 'templates') + + @classmethod + def example_helper(cls, data=None): + # render our custom snippet + return p.toolkit.render_snippet('custom_snippet.html', data) + + + def get_helpers(self): + # register our helper function + return {'example_helper': self.example_helper} + +The following functions, classes and exceptions are provided by the toolkit. + +*class* **CkanCommand** + Base class for building paster functions. + + +*exception* **CkanVersionException** + Exception raised if required ckan version is not available. + + +*exception* **NotAuthorized** + Exception raised when an action is not permitted by a user. + + +*exception* **ObjectNotFound** + Exception raised when an object cannot be found. + + +*exception* **ValidationError** + Exception raised when supplied data is invalid. + it contains details of the error that occurred. + + +**_** (*value*) + Mark a string for translation. Returns the localized unicode + string of value. + + Mark a string to be localized as follows:: + + _('This should be in lots of languages') + + + + +**add_public_directory** (*config, relative_path*) + Function to aid adding extra public paths to the config. + The path is relative to the file calling this function. + + +**add_template_directory** (*config, relative_path*) + Function to aid adding extra template paths to the config. + The path is relative to the file calling this function. + + +**asbool** (*obj*) + part of paste.deploy.converters: convert strings like yes, no, true, false, 0, 1 to boolean + + +**asint** (*obj*) + part of paste.deploy.converters: convert stings to integers + + +**aslist** (*obj, sep=None, strip=True*) + part of paste.deploy.converters: convert string objects to a list + + +**check_access** (*action, context, data_dict=None*) + check that the named action with the included context and + optional data dict is allowed raises NotAuthorized if the action is + not permitted or True. + + +**check_ckan_version** (*min_version=None, max_version=None*) + Check that the ckan version is correct for the plugin. + + +**get_action** (*action*) + Get the requested action function. + + +*class* **literal** + Represents an HTML literal. + + This subclass of unicode has a ``.__html__()`` method that is + detected by the ``escape()`` function. + + Also, if you add another string to this string, the other string + will be quoted and you will get back another literal object. Also + ``literal(...) % obj`` will quote any value(s) from ``obj``. If + you do something like ``literal(...) + literal(...)``, neither + string will be changed because ``escape(literal(...))`` doesn't + change the original literal. + + Changed in WebHelpers 1.2: the implementation is now now a subclass of + ``markupsafe.Markup``. This brings some new methods: ``.escape`` (class + method), ``.unescape``, and ``.striptags``. + + + + +**render** (*template_name, data=None*) + Main template render function. + + +**render_snippet** (*template_name, data=None*) + helper for the render_snippet function + similar to the render function. + + +**render_text** (*template_name, data=None*) + Render genshi text template. + + +**requires_ckan_version** (*min_version, max_version=None*) + Check that the ckan version is correct for the plugin. + From 9b72e88919fcdd25c0156a430f00968c9ec76ec3 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 6 Jul 2012 14:02:30 +0200 Subject: [PATCH 006/103] [#2627] Refactor form_to_db_schema_options Make it call self.form_to_db_schema so that the form_to_db_schema() methods of IDatasetForm extensions get called. Also make it call new form_to_db_schema_api_create() and form_to_db_schema_api_update() methods which could potentially be overridden by extensions also. --- ckan/lib/plugins.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py index 258e1449681..4ce1f4236ca 100644 --- a/ckan/lib/plugins.py +++ b/ckan/lib/plugins.py @@ -221,11 +221,21 @@ def form_to_db_schema_options(self, options): if options.get('api'): if options.get('type') == 'create': - return logic.schema.default_create_package_schema() + return self.form_to_db_schema_api_create() else: - return logic.schema.default_update_package_schema() + assert options.get('type') == 'update' + return self.form_to_db_schema_api_update() else: - return logic.schema.package_form_schema() + return self.form_to_db_schema() + + def form_to_db_schema(self, options): + return logic.schema.package_form_schema() + + def form_to_db_schema_api_create(self): + return logic.schema.default_create_package_schema() + + def form_to_db_schema_api_update(self): + return logic.schema.default_update_package_schema() def db_to_form_schema(self): '''This is an interface to manipulate data from the database From d93cb58049bf79a1983f3b96759361c0627684a1 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 6 Jul 2012 14:32:00 +0200 Subject: [PATCH 007/103] Fix a crasher in package/read.html template Sometimes c.pkg_dict has no member named groups (e.g. when there is an active IDatasetForm plugin with a db_to_form_schema() method). --- ckan/templates/package/read.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/package/read.html b/ckan/templates/package/read.html index 29a03720bbd..7c76ef39192 100644 --- a/ckan/templates/package/read.html +++ b/ckan/templates/package/read.html @@ -45,7 +45,7 @@

Tags

${tag_list(c.pkg_dict.get('tags', ''))} -