+ {{ _("Custom Text") }} |
+ {{ pkg_dict.custom_text }} |
+
+ {% endif %}
+{% endblock %}
diff --git a/ckanext/example_idatasetform/templates/package/snippets/resource_form.html b/ckanext/example_idatasetform/templates/package/snippets/resource_form.html
new file mode 100644
index 00000000000..5e36126ac62
--- /dev/null
+++ b/ckanext/example_idatasetform/templates/package/snippets/resource_form.html
@@ -0,0 +1,7 @@
+{% ckan_extends %}
+
+{% block basic_fields_url %}
+{{ super() }}
+
+ {{ form.input('custom_resource_text', label=_('Custom Text'), id='field-custom_resource_text', placeholder=_('custom resource text'), value=data.custom_resource_text, error=errors.custom_resource_text, classes=['control-medium']) }}
+{% endblock %}
diff --git a/ckanext/example_idatasetform/tests/__init__.py b/ckanext/example_idatasetform/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/ckanext/example_idatasetform/tests/test_example_idatasetform.py b/ckanext/example_idatasetform/tests/test_example_idatasetform.py
new file mode 100644
index 00000000000..cbe34059e92
--- /dev/null
+++ b/ckanext/example_idatasetform/tests/test_example_idatasetform.py
@@ -0,0 +1,248 @@
+import nose.tools as nt
+
+import pylons.config as config
+
+import ckan.model as model
+import ckan.plugins as plugins
+import ckan.new_tests.helpers as helpers
+import ckanext.example_idatasetform as idf
+import ckan.lib.search
+
+
+class ExampleIDatasetFormPluginBase(object):
+ '''Version 1, 2 and 3 of the plugin are basically the same, so this class
+ provides the tests that all three versions of the plugins will run'''
+ @classmethod
+ def setup_class(cls):
+ cls.original_config = config.copy()
+
+ def teardown(self):
+ model.repo.rebuild_db()
+ ckan.lib.search.clear()
+
+ @classmethod
+ def teardown_class(cls):
+ helpers.reset_db()
+ model.repo.rebuild_db()
+ ckan.lib.search.clear()
+
+ config.clear()
+ config.update(cls.original_config)
+
+ def test_package_create(self):
+ result = helpers.call_action('package_create', name='test_package',
+ custom_text='this is my custom text')
+ nt.assert_equals('this is my custom text', result['custom_text'])
+
+ def test_package_update(self):
+ helpers.call_action('package_create', name='test_package',
+ custom_text='this is my custom text')
+ result = helpers.call_action('package_update', name='test_package',
+ custom_text='this is my updated text')
+ nt.assert_equals('this is my updated text', result['custom_text'])
+
+ def test_package_show(self):
+ helpers.call_action('package_create', name='test_package',
+ custom_text='this is my custom text')
+ result = helpers.call_action('package_show', name_or_id='test_package')
+ nt.assert_equals('this is my custom text', result['custom_text'])
+
+
+class TestVersion1(ExampleIDatasetFormPluginBase):
+ @classmethod
+ def setup_class(cls):
+ super(TestVersion1, cls).setup_class()
+ plugins.load('example_idatasetform_v1')
+
+ @classmethod
+ def teardown_class(cls):
+ plugins.unload('example_idatasetform_v1')
+ super(TestVersion1, cls).teardown_class()
+
+
+class TestVersion2(ExampleIDatasetFormPluginBase):
+ @classmethod
+ def setup_class(cls):
+ super(TestVersion2, cls).setup_class()
+ plugins.load('example_idatasetform_v2')
+
+ @classmethod
+ def teardown_class(cls):
+ plugins.unload('example_idatasetform_v2')
+ super(TestVersion2, cls).teardown_class()
+
+
+class TestVersion3(ExampleIDatasetFormPluginBase):
+ @classmethod
+ def setup_class(cls):
+ super(TestVersion3, cls).setup_class()
+ plugins.load('example_idatasetform_v3')
+
+ @classmethod
+ def teardown_class(cls):
+ plugins.unload('example_idatasetform_v3')
+ super(TestVersion3, cls).teardown_class()
+
+
+class TestIDatasetFormPluginVersion4(object):
+ @classmethod
+ def setup_class(cls):
+ cls.original_config = config.copy()
+ plugins.load('example_idatasetform_v4')
+
+ def teardown(self):
+ model.repo.rebuild_db()
+
+ @classmethod
+ def teardown_class(cls):
+ plugins.unload('example_idatasetform_v4')
+ helpers.reset_db()
+ ckan.lib.search.clear()
+
+ config.clear()
+ config.update(cls.original_config)
+
+ def test_package_create(self):
+ idf.plugin_v4.create_country_codes()
+ result = helpers.call_action('package_create', name='test_package',
+ custom_text='this is my custom text',
+ country_code='uk')
+ nt.assert_equals('this is my custom text', result['custom_text'])
+ nt.assert_equals([u'uk'], result['country_code'])
+
+ def test_package_create_wrong_country_code(self):
+ idf.plugin_v4.create_country_codes()
+ nt.assert_raises(plugins.toolkit.ValidationError,
+ helpers.call_action,
+ 'package_create',
+ name='test_package',
+ custom_text='this is my custom text',
+ country_code='notcode')
+
+ def test_package_update(self):
+ idf.plugin_v4.create_country_codes()
+ helpers.call_action('package_create', name='test_package',
+ custom_text='this is my custom text',
+ country_code='uk')
+ result = helpers.call_action('package_update', name='test_package',
+ custom_text='this is my updated text',
+ country_code='ie')
+ nt.assert_equals('this is my updated text', result['custom_text'])
+ nt.assert_equals([u'ie'], result['country_code'])
+
+
+class TestIDatasetFormPlugin(object):
+ @classmethod
+ def setup_class(cls):
+ cls.original_config = config.copy()
+ plugins.load('example_idatasetform')
+
+ def teardown(self):
+ model.repo.rebuild_db()
+ ckan.lib.search.clear()
+
+ @classmethod
+ def teardown_class(cls):
+ plugins.unload('example_idatasetform')
+ helpers.reset_db()
+ ckan.lib.search.clear()
+
+ config.clear()
+ config.update(cls.original_config)
+
+ def test_package_create(self):
+ idf.plugin.create_country_codes()
+ result = helpers.call_action(
+ 'package_create', name='test_package',
+ custom_text='this is my custom text', country_code='uk',
+ resources=[{
+ 'url': 'http://test.com/',
+ 'custom_resource_text': 'my custom resource',
+ }])
+ nt.assert_equals('my custom resource',
+ result['resources'][0]['custom_resource_text'])
+
+ def test_package_update(self):
+ idf.plugin.create_country_codes()
+ helpers.call_action(
+ 'package_create', name='test_package',
+ custom_text='this is my custom text', country_code='uk',
+ resources=[{
+ 'url': 'http://test.com/',
+ 'custom_resource_text': 'my custom resource',
+ }])
+ result = helpers.call_action(
+ 'package_update',
+ name='test_package',
+ custom_text='this is my updated text',
+ country_code='ie',
+ resources=[{
+ 'url': 'http://test.com/',
+ 'custom_resource_text': 'updated custom resource',
+ }]
+ )
+ nt.assert_equals('this is my updated text', result['custom_text'])
+ nt.assert_equals([u'ie'], result['country_code'])
+ nt.assert_equals('updated custom resource',
+ result['resources'][0]['custom_resource_text'])
+
+ def test_package_show(self):
+ idf.plugin.create_country_codes()
+ helpers.call_action(
+ 'package_create', name='test_package',
+ custom_text='this is my custom text', country_code='uk',
+ resources=[{
+ 'url': 'http://test.com/',
+ 'custom_resource_text': 'my custom resource',
+ }]
+ )
+ result = helpers.call_action('package_show', name_or_id='test_package')
+ nt.assert_equals('my custom resource',
+ result['resources'][0]['custom_resource_text'])
+ nt.assert_equals('my custom resource',
+ result['resources'][0]['custom_resource_text'])
+
+
+class TestCustomSearch(object):
+ @classmethod
+ def setup_class(cls):
+ cls.original_config = config.copy()
+ cls.app = helpers._get_test_app()
+ plugins.load('example_idatasetform')
+
+ def teardown(self):
+ model.repo.rebuild_db()
+ ckan.lib.search.clear()
+
+ @classmethod
+ def teardown_class(cls):
+ plugins.unload('example_idatasetform')
+ helpers.reset_db()
+ ckan.lib.search.clear()
+
+ config.clear()
+ config.update(cls.original_config)
+
+ def test_custom_search(self):
+ helpers.call_action('package_create', name='test_package_a',
+ custom_text='z')
+ helpers.call_action('package_create', name='test_package_b',
+ custom_text='y')
+
+ response = self.app.get('/dataset')
+
+ # change the sort by form to our custom_text ascending
+ response.forms[1].fields['sort'][0].value = 'custom_text asc'
+ response = response.forms[1].submit()
+ # check that package_b appears before package a (y < z)
+ a = response.body.index('test_package_a')
+ b = response.body.index('test_package_b')
+ nt.assert_true(b < a)
+
+ response.forms[1].fields['sort'][0].value = 'custom_text desc'
+ # check that package_a appears before package b (z is first in
+ # descending order)
+ response = response.forms[1].submit()
+ a = response.body.index('test_package_a')
+ b = response.body.index('test_package_b')
+ nt.assert_true(a < b)
diff --git a/ckanext/resourceproxy/controller.py b/ckanext/resourceproxy/controller.py
index 0e8fd9bfd11..c0bd4db47a0 100644
--- a/ckanext/resourceproxy/controller.py
+++ b/ckanext/resourceproxy/controller.py
@@ -5,6 +5,7 @@
import ckan.logic as logic
import ckan.lib.base as base
+from ckan.common import _
log = getLogger(__name__)
@@ -20,7 +21,11 @@ def proxy_resource(context, data_dict):
than the maximum file size. '''
resource_id = data_dict['resource_id']
log.info('Proxify resource {id}'.format(id=resource_id))
- resource = logic.get_action('resource_show')(context, {'id': resource_id})
+ try:
+ resource = logic.get_action('resource_show')(context, {'id':
+ resource_id})
+ except logic.NotFound:
+ base.abort(404, _('Resource not found'))
url = resource['url']
parts = urlparse.urlsplit(url)
diff --git a/ckanext/resourceproxy/tests/test_proxy.py b/ckanext/resourceproxy/tests/test_proxy.py
index dac7c747606..7de6e965436 100644
--- a/ckanext/resourceproxy/tests/test_proxy.py
+++ b/ckanext/resourceproxy/tests/test_proxy.py
@@ -170,3 +170,12 @@ def test_resource_url_doesnt_proxy_non_http_or_https_urls_by_default(self):
proxied_url = proxy.get_proxified_resource_url(data_dict, scheme)
assert non_proxied_url == url, non_proxied_url
assert proxied_url != url, proxied_url
+
+ def test_non_existent_resource(self):
+ self.data_dict = {'package': {'name': 'doesnotexist'},
+ 'resource': {'id': 'doesnotexist'}}
+
+ proxied_url = proxy.get_proxified_resource_url(self.data_dict)
+ result = self.app.get(proxied_url, status='*')
+ assert result.status == 404, result.status
+ assert 'Resource not found' in result.body, result.body
diff --git a/ckanext/stats/__init__.py b/ckanext/stats/__init__.py
index b646540d6be..de40ea7ca05 100644
--- a/ckanext/stats/__init__.py
+++ b/ckanext/stats/__init__.py
@@ -1 +1 @@
-# empty file needed for pylons to find templates in this directory
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/doc/contributing/architecture.rst b/doc/contributing/architecture.rst
index 50114ff53d4..be571c13b3a 100644
--- a/doc/contributing/architecture.rst
+++ b/doc/contributing/architecture.rst
@@ -160,21 +160,32 @@ the helper functions found in ``ckan.lib.helpers.__allowed_functions__``.
Deprecation
-----------
-- Anything that may be used by extensions (see :doc:`/extensions/index`) needs
- to maintain backward compatibility at call-site. ie - template helper
- functions and functions defined in the plugins toolkit.
+- Anything that may be used by :doc:`extensions `,
+ :doc:`themes ` or :doc:`API clients ` needs to
+ maintain backward compatibility at call-site. For example: action functions,
+ template helper functions and functions defined in the plugins toolkit.
- The length of time of deprecation is evaluated on a function-by-function
- basis. At minimum, a function should be marked as deprecated during a point
+ basis. At minimum, a function should be marked as deprecated during a point
release.
-- To mark a helper function, use the ``deprecated`` decorator found in
- ``ckan.lib.maintain`` eg: ::
-
- @deprecated()
- def facet_items(*args, **kwargs):
- """
- DEPRECATED: Use the new facet data structure, and `unselected_facet_items()`
- """
- # rest of function definition.
-
+- To deprecate a function use the :py:func:`ckan.lib.maintain.deprecated`
+ decorator and add "deprecated" to the function's docstring::
+
+ @maintain.deprecated("helpers.get_action() is deprecated and will be removed "
+ "in a future version of CKAN. Instead, please use the "
+ "extra_vars param to render() in your controller to pass "
+ "results from action functions to your templates.")
+ def get_action(action_name, data_dict=None):
+ '''Calls an action function from a template. Deprecated in CKAN 2.3.'''
+ if data_dict is None:
+ data_dict = {}
+ return logic.get_action(action_name)({}, data_dict)
+
+- Any deprecated functions should be added to an *API changes and deprecations*
+ section in the :doc:`/changelog` entry for the next release (do this before
+ merging the deprecation into master)
+
+- Keep the deprecation messages passed to the decorator short, they appear in
+ logs. Put longer explanations of why something was deprecated in the
+ changelog.
diff --git a/doc/contributing/pull-requests.rst b/doc/contributing/pull-requests.rst
index 6909657b5c3..8452e0a5c53 100644
--- a/doc/contributing/pull-requests.rst
+++ b/doc/contributing/pull-requests.rst
@@ -64,7 +64,11 @@ This section will walk you through the steps for making a pull request.
- Your branch should contain new or changed tests for any new or changed
code, and all the CKAN tests should pass on your branch, see
- :doc:`test`.
+ `Testing CKAN