From bb452421266866e826169cd6706f4c15447cae5f Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 27 Oct 2014 17:36:12 +0000 Subject: [PATCH 01/11] Allows use of CKAN_INI env-var to locate config Implements #1597 If you don't specify a config file using the -c option, CKAN defaults to looking for development.ini in the current directory. This patch will first check the -c parameter, and if it doesn't find it will use the content of the CKAN_INI environment variable. If it cannot find that, it will then default to development.ini in the current directory. --- ckan/lib/cli.py | 19 +++++++++++++------ doc/maintaining/paster.rst | 11 +++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/ckan/lib/cli.py b/ckan/lib/cli.py index 83424b71e16..f1594627e51 100644 --- a/ckan/lib/cli.py +++ b/ckan/lib/cli.py @@ -106,7 +106,7 @@ class CkanCommand(paste.script.command.Command): ''' 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', @@ -116,12 +116,19 @@ class CkanCommand(paste.script.command.Command): def _get_config(self): from paste.deploy import appconfig - if not self.options.config: - msg = 'No config file supplied' - raise self.BadCommand(msg) - self.filename = os.path.abspath(self.options.config) + + self.filename = os.environ.get('CKAN_INI') + + if self.options.config: + self.filename = os.path.abspath(self.options.config) + + if not self.filename: + self.filename = 'development.ini' + if not os.path.exists(self.filename): - raise AssertionError('Config filename %r does not exist.' % self.filename) + msg = 'No config file supplied or found in $CKAN_INI' + raise self.BadCommand(msg) + fileConfig(self.filename) return appconfig('config:' + self.filename) diff --git a/doc/maintaining/paster.rst b/doc/maintaining/paster.rst index e10688c156e..653565f839b 100644 --- a/doc/maintaining/paster.rst +++ b/doc/maintaining/paster.rst @@ -49,6 +49,17 @@ examples below, this option can be given as ``-c`` for short. to execute. Most commands have their own subcommands and options. For example, to print out a list of all of your CKAN site's users do: +.. note:: + + You may also specify the location of your config file using the CKAN_INI + environment variable. You will no longer need to use --config= or -c= to + tell paster where the config file is: + + .. parsed-literal:: + + export CKAN_INI=\ |development.ini| + + .. parsed-literal:: paster user list -c |development.ini| From ba51a431071751e0deb9942a46116492a62a7f83 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 28 Oct 2014 12:02:36 +0000 Subject: [PATCH 02/11] CHange error message, hope travis retests --- ckan/lib/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/lib/cli.py b/ckan/lib/cli.py index f1594627e51..9fb0c705fd7 100644 --- a/ckan/lib/cli.py +++ b/ckan/lib/cli.py @@ -126,7 +126,7 @@ def _get_config(self): self.filename = 'development.ini' if not os.path.exists(self.filename): - msg = 'No config file supplied or found in $CKAN_INI' + msg = 'No config file found and not specified in $CKAN_INI' raise self.BadCommand(msg) fileConfig(self.filename) From 2b6c1edb36c54114110c0490d0b1ed5287499565 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 6 Nov 2014 16:50:43 +0000 Subject: [PATCH 03/11] [#2035] Refactor new_authz.check_config_permission to not cache things Otherwise the values can not be overriden from tests --- ckan/new_authz.py | 54 +++++++++++++++++------------ ckan/new_tests/test_authz.py | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 ckan/new_tests/test_authz.py diff --git a/ckan/new_authz.py b/ckan/new_authz.py index 7d4cdc3716d..365f50371ee 100644 --- a/ckan/new_authz.py +++ b/ckan/new_authz.py @@ -94,7 +94,6 @@ def _build(self): def clear_auth_functions_cache(): _AuthFunctions.clear() - CONFIG_PERMISSIONS.clear() def auth_functions_list(): @@ -374,28 +373,41 @@ def get_user_id_for_username(user_name, allow_none=False): 'roles_that_cascade_to_sub_groups': 'admin', } -CONFIG_PERMISSIONS = {} - def check_config_permission(permission): - ''' Returns the permission configuration, usually True/False ''' - # set up perms if not already done - if not CONFIG_PERMISSIONS: - for perm in CONFIG_PERMISSIONS_DEFAULTS: - key = 'ckan.auth.' + perm - default = CONFIG_PERMISSIONS_DEFAULTS[perm] - CONFIG_PERMISSIONS[perm] = config.get(key, default) - if perm == 'roles_that_cascade_to_sub_groups': - # this permission is a list of strings (space separated) - CONFIG_PERMISSIONS[perm] = \ - CONFIG_PERMISSIONS[perm].split(' ') \ - if CONFIG_PERMISSIONS[perm] else [] - else: - # most permissions are boolean - CONFIG_PERMISSIONS[perm] = asbool(CONFIG_PERMISSIONS[perm]) - if permission in CONFIG_PERMISSIONS: - return CONFIG_PERMISSIONS[permission] - return False + '''Returns the configuration value for the provided permission + + Permission is a string indentifying the auth permission (eg + `anon_create_dataset`), optionally prefixed with `ckan.auth.`. + + The possible values for `permission` are the keys of + CONFIG_PERMISSIONS_DEFAULTS. These can be overriden in the config file + by prefixing them with `ckan.auth.`. + + Returns the permission value, generally True or False, except on + `roles_that_cascade_to_sub_groups` which is a list of strings. + + ''' + + key = permission.replace('ckan.auth.', '') + + if key not in CONFIG_PERMISSIONS_DEFAULTS: + return False + + default_value = CONFIG_PERMISSIONS_DEFAULTS.get(key) + + config_key = 'ckan.auth.' + key + + value = config.get(config_key, default_value) + + if key == 'roles_that_cascade_to_sub_groups': + # This permission is set as a list of strings (space separated) + value = value.split(' ') if value else [] + else: + value = asbool(value) + + return value + @maintain.deprecated('Use auth_is_loggedin_user instead') def auth_is_registered_user(): diff --git a/ckan/new_tests/test_authz.py b/ckan/new_tests/test_authz.py new file mode 100644 index 00000000000..e963821afbc --- /dev/null +++ b/ckan/new_tests/test_authz.py @@ -0,0 +1,66 @@ +import nose + +from ckan import new_authz as auth + +from ckan.new_tests import helpers + + +assert_equals = nose.tools.assert_equals + + +class TestCheckConfigPermission(object): + + @helpers.change_config('ckan.auth.anon_create_dataset', None) + def test_get_default_value_if_not_set_in_config(self): + + assert_equals(auth.check_config_permission( + 'anon_create_dataset'), + auth.CONFIG_PERMISSIONS_DEFAULTS['anon_create_dataset']) + + @helpers.change_config('ckan.auth.anon_create_dataset', None) + def test_get_default_value_also_works_with_prefix(self): + + assert_equals(auth.check_config_permission( + 'ckan.auth.anon_create_dataset'), + auth.CONFIG_PERMISSIONS_DEFAULTS['anon_create_dataset']) + + @helpers.change_config('ckan.auth.anon_create_dataset', True) + def test_config_overrides_default(self): + + assert_equals(auth.check_config_permission( + 'anon_create_dataset'), + True) + + @helpers.change_config('ckan.auth.anon_create_dataset', True) + def test_config_override_also_works_with_prefix(self): + + assert_equals(auth.check_config_permission( + 'ckan.auth.anon_create_dataset'), + True) + + @helpers.change_config('ckan.auth.unknown_permission', True) + def test_unknown_permission_returns_false(self): + + assert_equals(auth.check_config_permission( + 'unknown_permission'), + False) + + def test_unknown_permission_not_in_config_returns_false(self): + + assert_equals(auth.check_config_permission( + 'unknown_permission'), + False) + + def test_default_roles_that_cascade_to_sub_groups_is_a_list(self): + + assert isinstance(auth.check_config_permission( + 'roles_that_cascade_to_sub_groups'), + list) + + @helpers.change_config('ckan.auth.roles_that_cascade_to_sub_groups', + 'admin editor') + def test_roles_that_cascade_to_sub_groups_is_a_list(self): + + assert_equals(sorted(auth.check_config_permission( + 'roles_that_cascade_to_sub_groups')), + sorted(['admin', 'editor'])) From 07f2d0d2bb455eed82d16cb13b189fef659663a9 Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 7 Nov 2014 16:19:00 +0000 Subject: [PATCH 04/11] [#2035] Remove uneeded calls to clear_auth_functions_cache() --- ckan/new_tests/helpers.py | 2 -- ckan/tests/functional/api/test_user.py | 8 -------- 2 files changed, 10 deletions(-) diff --git a/ckan/new_tests/helpers.py b/ckan/new_tests/helpers.py index 2623ad9df7d..7cb632aa120 100644 --- a/ckan/new_tests/helpers.py +++ b/ckan/new_tests/helpers.py @@ -295,13 +295,11 @@ def decorator(func): def wrapper(*args, **kwargs): _original_config = config.copy() config[key] = value - new_authz.clear_auth_functions_cache() return_value = func(*args, **kwargs) config.clear() config.update(_original_config) - new_authz.clear_auth_functions_cache() return return_value return nose.tools.make_decorator(func)(wrapper) diff --git a/ckan/tests/functional/api/test_user.py b/ckan/tests/functional/api/test_user.py index 875e8a668ab..0b0fabd7e34 100644 --- a/ckan/tests/functional/api/test_user.py +++ b/ckan/tests/functional/api/test_user.py @@ -68,7 +68,6 @@ class TestCreateUserApiDisabled(PylonsTestCase): def setup_class(cls): CreateTestData.create() cls._original_config = config.copy() - new_authz.clear_auth_functions_cache() wsgiapp = ckan.config.middleware.make_app( config['global_conf'], **config) cls.app = paste.fixture.TestApp(wsgiapp) @@ -79,7 +78,6 @@ def setup_class(cls): def teardown_class(cls): config.clear() config.update(cls._original_config) - new_authz.clear_auth_functions_cache() PylonsTestCase.teardown_class() model.repo.rebuild_db() @@ -120,7 +118,6 @@ def setup_class(cls): CreateTestData.create() cls._original_config = config.copy() config['ckan.auth.create_user_via_api'] = True - new_authz.clear_auth_functions_cache() wsgiapp = ckan.config.middleware.make_app( config['global_conf'], **config) cls.app = paste.fixture.TestApp(wsgiapp) @@ -131,7 +128,6 @@ def setup_class(cls): def teardown_class(cls): config.clear() config.update(cls._original_config) - new_authz.clear_auth_functions_cache() PylonsTestCase.teardown_class() model.repo.rebuild_db() @@ -170,7 +166,6 @@ def setup_class(cls): CreateTestData.create() cls._original_config = config.copy() config['ckan.auth.create_user_via_web'] = False - new_authz.clear_auth_functions_cache() wsgiapp = ckan.config.middleware.make_app( config['global_conf'], **config) cls.app = paste.fixture.TestApp(wsgiapp) @@ -181,7 +176,6 @@ def setup_class(cls): def teardown_class(cls): config.clear() config.update(cls._original_config) - new_authz.clear_auth_functions_cache() PylonsTestCase.teardown_class() model.repo.rebuild_db() @@ -208,7 +202,6 @@ def setup_class(cls): CreateTestData.create() cls._original_config = config.copy() config['ckan.auth.create_user_via_web'] = True - new_authz.clear_auth_functions_cache() wsgiapp = ckan.config.middleware.make_app( config['global_conf'], **config) cls.app = paste.fixture.TestApp(wsgiapp) @@ -219,7 +212,6 @@ def setup_class(cls): def teardown_class(cls): config.clear() config.update(cls._original_config) - new_authz.clear_auth_functions_cache() PylonsTestCase.teardown_class() model.repo.rebuild_db() From 3665e3851d125554dd79c57e6f680fdc5dc54d04 Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 7 Nov 2014 22:36:03 +0000 Subject: [PATCH 05/11] [#2035] Fix old auth tests --- ckan/tests/logic/test_auth.py | 58 ++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/ckan/tests/logic/test_auth.py b/ckan/tests/logic/test_auth.py index 2be34db1634..81e53b18223 100644 --- a/ckan/tests/logic/test_auth.py +++ b/ckan/tests/logic/test_auth.py @@ -1,5 +1,6 @@ -import mock - +import paste +from pylons import config +import ckan.config.middleware import ckan.tests as tests from ckan.logic import get_action import ckan.model as model @@ -7,19 +8,6 @@ from ckan.lib.create_test_data import CreateTestData import json -INITIAL_TEST_CONFIG_PERMISSIONS = { - 'anon_create_dataset': False, - 'create_dataset_if_not_in_organization': False, - 'user_create_groups': False, - 'user_create_organizations': False, - 'user_delete_groups': False, - 'user_delete_organizations': False, - 'create_unowned_dataset': False, - 'create_user_via_api': False, - 'create_user_via_web': True, - 'roles_that_cascade_to_sub_groups': ['admin'], -} - class TestAuth(tests.WsgiAppCase): @classmethod @@ -30,12 +18,28 @@ def setup_class(cls): ## add apikeys as they go along cls.apikeys = {'sysadmin': admin_api, 'random_key': 'moo'} - cls.old_perm = new_authz.CONFIG_PERMISSIONS.copy() - new_authz.CONFIG_PERMISSIONS.update(INITIAL_TEST_CONFIG_PERMISSIONS) + cls._original_config = config.copy() + + config['ckan.auth.anon_create_dataset'] = False + config['ckan.auth.create_dataset_if_not_in_organization'] = False + config['ckan.auth.user_create_groups'] = False + config['ckan.auth.user_create_organizations'] = False + config['ckan.auth.user_delete_groups'] = False + config['ckan.auth.user_delete_organizations'] = False + config['ckan.auth.create_unowned_dataset'] = False + config['ckan.auth.create_user_via_api'] = False + config['ckan.auth.create_user_via_web'] = True + config['ckan.auth.roles_that_cascade_to_sub_groups'] = 'admin' + + wsgiapp = ckan.config.middleware.make_app( + config['global_conf'], **config) + cls.app = paste.fixture.TestApp(wsgiapp) @classmethod def teardown_class(cls): - new_authz.CONFIG_PERMISSIONS.update(cls.old_perm) + config.clear() + config.update(cls._original_config) + model.repo.rebuild_db() @classmethod @@ -227,9 +231,6 @@ def test_11_delete_org(self): self._call_api('organization_delete', org, 'org_editor', 403) self._call_api('organization_delete', org, 'org_admin', 403) -ORG_HIERARCHY_PERMISSIONS = { - 'roles_that_cascade_to_sub_groups': ['admin'], - } class TestAuthOrgHierarchy(TestAuth): # Tests are in the same vein as TestAuthOrgs, testing the cases where the @@ -241,12 +242,25 @@ def setup_class(cls): CreateTestData.create_group_hierarchy_test_data() for user in model.Session.query(model.User): cls.apikeys[user.name] = str(user.apikey) - new_authz.CONFIG_PERMISSIONS.update(ORG_HIERARCHY_PERMISSIONS) + + cls._original_config = config.copy() + + config['ckan.auth.roles_that_cascade_to_sub_groups'] = 'admin' + + wsgiapp = ckan.config.middleware.make_app( + config['global_conf'], **config) + cls.app = paste.fixture.TestApp(wsgiapp) + CreateTestData.create_arbitrary( package_dicts= [{'name': 'adataset', 'groups': ['national-health-service']}], extra_user_names=['john']) + @classmethod + def teardown_class(cls): + config.clear() + config.update(cls._original_config) + def _reset_a_datasets_owner_org(self): rev = model.repo.new_revision() get_action('package_owner_org_update')( From 00b192d489f1f680e8bb8372a23b3e3caf7f689c Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 10 Nov 2014 16:47:40 +0000 Subject: [PATCH 06/11] [#2035] Further fixes to auth tests The old auth tests are terrible in that they depend on each other to run. Also the auth cache thing masked some bad assumptions. The whole file should be rewritten as new tests. --- ckan/tests/logic/test_auth.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/ckan/tests/logic/test_auth.py b/ckan/tests/logic/test_auth.py index 81e53b18223..9eafe6415f2 100644 --- a/ckan/tests/logic/test_auth.py +++ b/ckan/tests/logic/test_auth.py @@ -16,7 +16,7 @@ def setup_class(cls): {'model': model, 'ignore_auth': True}, {})['apikey'] ## This is a mutable dict on the class level so tests can ## add apikeys as they go along - cls.apikeys = {'sysadmin': admin_api, 'random_key': 'moo'} + cls.apikeys = {'sysadmin': str(admin_api), 'random_key': 'moo'} cls._original_config = config.copy() @@ -89,13 +89,19 @@ def test_auth_deleted_users_are_always_unauthorized(self): class TestAuthOrgs(TestAuth): - def test_01_create_users(self): + + @classmethod + def setup_class(cls): + + super(TestAuthOrgs, cls).setup_class() + # actual roles assigned later - self.create_user('org_admin') - self.create_user('no_org') - self.create_user('org_editor') - self.create_user('editor_wannabe') + cls.create_user('org_admin') + cls.create_user('no_org') + cls.create_user('org_editor') + cls.create_user('editor_wannabe') + def test_01_create_users(self): user = {'name': 'user_no_auth', 'password': 'pass', 'email': 'moo@moo.com'} @@ -153,7 +159,6 @@ def test_05_add_users_to_org(self): 'id': 'org_with_user'} self._call_api('organization_member_create', member, 'org_admin') - ## admin user should be able to add users now ## editor should not be able to approve others as editors member = {'username': 'editor_wannabe', 'role': 'editor', @@ -161,7 +166,6 @@ def test_05_add_users_to_org(self): self._call_api('organization_member_create', member, 'org_editor', 403) def _add_datasets(self, user): - #org admin/editor should be able to add dataset to org. dataset = {'name': user + '_dataset', 'owner_org': 'org_with_user'} self._call_api('package_create', dataset, user, 200) @@ -238,8 +242,10 @@ class TestAuthOrgHierarchy(TestAuth): @classmethod def setup_class(cls): - TestAuth.setup_class() +# TestAuth.setup_class() CreateTestData.create_group_hierarchy_test_data() + + cls.apikeys = {} for user in model.Session.query(model.User): cls.apikeys[user.name] = str(user.apikey) @@ -409,10 +415,7 @@ def test_10_edit_org_1(self): def test_10_edit_org_2(self): org = {'id': 'national-health-service', 'title': 'test'} self._flesh_out_organization(org) - import pprint; pprint.pprint(org) - print model.Session.query(model.Member).filter_by(state='deleted').all() self._call_api('organization_update', org, 'nhsadmin', 200) - print model.Session.query(model.Member).filter_by(state='deleted').all() def test_10_edit_org_3(self): org = {'id': 'nhs-wirral-ccg', 'title': 'test'} @@ -441,7 +444,7 @@ def test_11_delete_org_1(self): def test_11_delete_org_2(self): org = {'id': 'national-health-service'} - self._call_api('organization_delete', org, 'nhsadmin', 403) + self._call_api('organization_delete', org, 'nhsadmin', 200) self._call_api('organization_delete', org, 'nhseditor', 403) def test_11_delete_org_3(self): From 7d30f7cc0b8c2879dfa1e1481269f6a55958287b Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 10 Nov 2014 17:09:12 +0000 Subject: [PATCH 07/11] [#2035] Don't use http://foo.bar as a test url Because `.bar` is being recognized as a gTLD, it points to `127.0.53.53`, which if you have a web server running will return localhost (and a 200 status). More details: https://stackoverflow.com/questions/25662590/domain-foo-bar-points-127-0-53-53-why --- ckanext/resourceproxy/tests/test_proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ckanext/resourceproxy/tests/test_proxy.py b/ckanext/resourceproxy/tests/test_proxy.py index dac7c747606..b09cbf6a6e3 100644 --- a/ckanext/resourceproxy/tests/test_proxy.py +++ b/ckanext/resourceproxy/tests/test_proxy.py @@ -139,11 +139,12 @@ def test_invalid_url(self): assert 'Invalid URL' in result.body, result.body def test_non_existent_url(self): - self.data_dict = set_resource_url('http://foo.bar') + self.data_dict = set_resource_url('http://does_not_exist') def f1(): url = self.data_dict['resource']['url'] requests.get(url) + self.assertRaises(requests.ConnectionError, f1) proxied_url = proxy.get_proxified_resource_url(self.data_dict) From 44c8c7cf62d0289ed1ea87c4e069982e98c53bdc Mon Sep 17 00:00:00 2001 From: David Read Date: Thu, 13 Nov 2014 16:52:14 +0000 Subject: [PATCH 08/11] [#1597] Refactored for code clarity and improved error message. --- ckan/lib/cli.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ckan/lib/cli.py b/ckan/lib/cli.py index 9fb0c705fd7..70709acf3dd 100644 --- a/ckan/lib/cli.py +++ b/ckan/lib/cli.py @@ -117,16 +117,19 @@ class CkanCommand(paste.script.command.Command): def _get_config(self): from paste.deploy import appconfig - self.filename = os.environ.get('CKAN_INI') - if self.options.config: self.filename = os.path.abspath(self.options.config) - - if not self.filename: + config_source = '-c parameter' + elif os.environ.get('CKAN_INI'): + self.filename = os.environ.get('CKAN_INI') + config_source = '$CKAN_INI' + else: self.filename = 'development.ini' + config_source = 'default value' if not os.path.exists(self.filename): - msg = 'No config file found and not specified in $CKAN_INI' + msg = 'Config file not found: %s' % self.filename + msg += '\n(Given by: %s)' % config_source raise self.BadCommand(msg) fileConfig(self.filename) From 370c811c5ebe2cbca086fa0181bf5787e8c6e254 Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 14 Nov 2014 11:11:01 +0000 Subject: [PATCH 09/11] [#2035] Improve non-existent URL added on 7d30f7c --- ckanext/resourceproxy/tests/test_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/resourceproxy/tests/test_proxy.py b/ckanext/resourceproxy/tests/test_proxy.py index b09cbf6a6e3..db071a10d8e 100644 --- a/ckanext/resourceproxy/tests/test_proxy.py +++ b/ckanext/resourceproxy/tests/test_proxy.py @@ -139,7 +139,7 @@ def test_invalid_url(self): assert 'Invalid URL' in result.body, result.body def test_non_existent_url(self): - self.data_dict = set_resource_url('http://does_not_exist') + self.data_dict = set_resource_url('http://nonexistent.example.com') def f1(): url = self.data_dict['resource']['url'] From 99e5ee0d6c8499a262038aa7a724590298b5d81f Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 14 Nov 2014 11:12:21 +0000 Subject: [PATCH 10/11] [#2035] Split all whitespace when parsing roles_that_cascade_to_sub_groups Use split() rather than split(' ') --- ckan/new_authz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/new_authz.py b/ckan/new_authz.py index 365f50371ee..643e2987631 100644 --- a/ckan/new_authz.py +++ b/ckan/new_authz.py @@ -402,7 +402,7 @@ def check_config_permission(permission): if key == 'roles_that_cascade_to_sub_groups': # This permission is set as a list of strings (space separated) - value = value.split(' ') if value else [] + value = value.split() if value else [] else: value = asbool(value) From 5a3cd397408719c6a2a80409366cb9eac806f3c6 Mon Sep 17 00:00:00 2001 From: jqnatividad Date: Mon, 17 Nov 2014 16:19:33 -0500 Subject: [PATCH 11/11] 2054-fix meta property og:description fixes #2054 by trimming whitespace --- ckan/templates/package/read_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/package/read_base.html b/ckan/templates/package/read_base.html index 559ca0b6972..b6f93106a83 100644 --- a/ckan/templates/package/read_base.html +++ b/ckan/templates/package/read_base.html @@ -11,7 +11,7 @@ {{ super() }} {% set description = h.markdown_extract(pkg.notes, extract_length=200)|forceescape %} - + {% endblock -%} {% block content_action %}