diff --git a/ckanext/odsh/controller.py b/ckanext/odsh/controller.py index aecd986ecd97aa21b64d1994a37bfa354ffc8f16..20dbbd1b770a82310889a7adf8ce246535de73ee 100644 --- a/ckanext/odsh/controller.py +++ b/ckanext/odsh/controller.py @@ -20,6 +20,7 @@ import ckan.plugins.toolkit as toolkit from ckanext.dcat.controllers import DCATController import ckan.model as model import helpers +import json abort = base.abort @@ -143,6 +144,12 @@ class OdshGroupController(OrganizationController): class OdshApiController(ApiController): def action(self, logic_function, ver=None): + if toolkit.asbool(config.get('ckanext.odsh.log_api_requests', 'false')): + try: + request_data = self._get_request_data(try_url_params=False) + log.info('POST request body: {}'.format(json.dumps(json.loads(request_data)))) + except Exception, e: + log.error(e) if logic_function == 'resource_qv4yAI2rgotamXGk98gJ': return helpers.odsh_get_version_id() if logic_function == 'resourcelog_qv4yAI2rgotamXGk98gJ': diff --git a/ckanext/odsh/logic/action.py b/ckanext/odsh/logic/action.py index 481a72ac130bf8447c89be96dcc27f6ddde28832..8bca8dd62c81372c2059a43152b46cf29fd9414d 100644 --- a/ckanext/odsh/logic/action.py +++ b/ckanext/odsh/logic/action.py @@ -1,19 +1,21 @@ -import logging import ckan.logic as logic from ckan.logic.action.update import user_update -from ckan.logic.action.create import package_create, user_create, group_member_create +from ckan.logic.action.create import package_create, resource_create, user_create, group_member_create import ckan.model as model import ckan.lib.dictization.model_dictize as model_dictize from ckan.lib.munge import munge_title_to_name import ckan.plugins.toolkit as toolkit -from ckan.lib.search.common import ( - make_connection, SearchError, SearchQueryError -) +from ckan.lib.search.common import make_connection, SearchError import pysolr import datetime +import cgi +import urllib2 +import logging log = logging.getLogger(__name__) +from ckanext.odsh.setup_proxy import setup_proxy, clear_proxy + def odsh_package_create(context, data_dict): pkg_type = data_dict.get('type', None) @@ -52,8 +54,10 @@ def check_password(password): any(c.isupper() for c in password) and any((c.isalpha()==False) for c in password)) #Number or Special character + PASSWORD_ERROR_MESSAGE = {'security': ['Passwort muss mindestens acht Zeichen, einen Gross-, einen Kleinbuchstaben und entweder eine Zahl oder ein Sondernzeichen enthalten!']} + def odsh_user_create(context, data_dict): model = context['model'] password = data_dict.get('password') @@ -69,6 +73,7 @@ def odsh_user_create(context, data_dict): else: raise logic.ValidationError(PASSWORD_ERROR_MESSAGE) + def tpsh_user_update(context, data_dict): password = data_dict.get('password') if not password: @@ -78,8 +83,6 @@ def tpsh_user_update(context, data_dict): return user_update(context, data_dict) - - @toolkit.side_effect_free def autocomplete(context, data_dict): query = { @@ -102,3 +105,41 @@ def autocomplete(context, data_dict): filtered_suggestions.append(suggestion) final_suggestions = list(sorted(set(filtered_suggestions), key=filtered_suggestions.index))[:5] return final_suggestions + + +def odsh_resource_create(context, data_dict): + is_linked_resource = not isinstance(data_dict['upload'], cgi.FieldStorage) + if is_linked_resource: + _download_linked_resource_to_tmp(data_dict['url']) + _emulate_file_upload(data_dict) + return resource_create(context, data_dict) + +TMP_FILE_PATH = '/tmp/temp_file_upload' + +def _download_linked_resource_to_tmp(url): + log.debug('Downloading linked resource from {}.'.format(url)) + setup_proxy() + test_file = urllib2.urlopen(url).read() + clear_proxy() + with open(TMP_FILE_PATH, 'wb') as temporary_file: + temporary_file.write(test_file) + +def _emulate_file_upload(data_dict): + ''' + This function updates the data_dict in order to emulate + the behaviour of resource creation with uploaded file for + resource creation with link + ''' + temporary_file = open(TMP_FILE_PATH, 'rb') + upload_file = temporary_file + filename = data_dict['name'] + upload = cgi.FieldStorage() + upload.disposition = 'form-data' + upload.disposition_options = {'filename': filename, 'name': 'upload'} + upload.fp = upload_file + upload.file = upload_file + upload.type = 'application/pdf' + upload.headers['content-type'] = upload.type + upload.name = 'upload' + upload.filename = filename + data_dict['upload'] = upload \ No newline at end of file diff --git a/ckanext/odsh/plugin.py b/ckanext/odsh/plugin.py index 3dfc22ab246746e60cb7c1f9cc232f779e5e7e66..4509a5982fff9a9edde8c99a9fa325839d144fe3 100644 --- a/ckanext/odsh/plugin.py +++ b/ckanext/odsh/plugin.py @@ -51,7 +51,9 @@ class OdshPlugin(plugins.SingletonPlugin, DefaultTranslation, DefaultDatasetForm def get_actions(self): return {'package_create': action.odsh_package_create, 'user_update':action.tpsh_user_update, - 'user_create': action.odsh_user_create} + 'user_create': action.odsh_user_create, + 'resource_create': action.odsh_resource_create, + } # IConfigurer diff --git a/ckanext/odsh/setup_proxy.py b/ckanext/odsh/setup_proxy.py new file mode 100644 index 0000000000000000000000000000000000000000..13338f65a60156bf8958107746035d50d8e2c9ab --- /dev/null +++ b/ckanext/odsh/setup_proxy.py @@ -0,0 +1,24 @@ +import urllib2 +from ckan.common import config + + +def setup_proxy(): + ''' + This function declares that a proxy server shall be used to access the web via + urllib2. It takes the proxy address from ckan's config file + ( + field "ckanext.odsh.download_proxy", + example: ckanext.odsh.download_proxy = http://1.2.3.4:4123 + ) + ''' + + proxy_url = config.get('ckanext.odsh.download_proxy', None) + if proxy_url: + proxy = urllib2.ProxyHandler({'http': proxy_url, 'https': proxy_url}) + opener = urllib2.build_opener(proxy) + urllib2.install_opener(opener) + +def clear_proxy(): + proxy = urllib2.ProxyHandler({}) + opener = urllib2.build_opener(proxy) + urllib2.install_opener(opener) \ No newline at end of file diff --git a/ckanext/odsh/tests_tpsh/resources/licenses.json b/ckanext/odsh/tests_tpsh/resources/licenses.json new file mode 100644 index 0000000000000000000000000000000000000000..0380142b9ed86cdac02c6def8c5557a716b5ab7f --- /dev/null +++ b/ckanext/odsh/tests_tpsh/resources/licenses.json @@ -0,0 +1,187 @@ +[ + { + "id": "http://dcat-ap.de/def/licenses/dl-zero-de/2.0", + "is_okd_compliant": true, + "is_osi_compliant": true, + "status": "active", + "title": "Datenlizenz Deutschland – Zero – Version 2.0", + "url": "https://www.govdata.de/dl-de/zero-2-0" + }, + { + "id": "http://dcat-ap.de/def/licenses/dl-by-de/2.0", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Datenlizenz Deutschland Namensnennung 2.0", + "url": "https://www.govdata.de/dl-de/by-2-0" + }, + { + "id": "http://dcat-ap.de/def/licenses/officialWork", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Amtliches Werk, lizenzfrei nach §5 Abs. 1 UrhG", + "url": "http://www.gesetze-im-internet.de/urhg/__5.html" + }, + { + "id": "http://dcat-ap.de/def/licenses/cc-zero", + "title": "Creative Commons CC Zero License (cc-zero)", + "url": "http://www.opendefinition.org/licenses/cc-zero", + "status": "active", + "od_conformance": "not reviewed", + "osd_conformance": "not reviewed" + }, + { + "id": "http://dcat-ap.de/def/licenses/cc-by/4.0", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Creative Commons Namensnennung – 4.0 International (CC BY 4.0)", + "url": "http://creativecommons.org/licenses/by/4.0/" + }, + { + "id": "http://dcat-ap.de/def/licenses/cc-by-nd/4.0", + "is_okd_compliant": false, + "is_osi_compliant": false, + "status": "active", + "title": "Creative Commons Namensnennung - - Keine Bearbeitung 4.0 International (CC BY-ND 4.0)", + "url": "https://creativecommons.org/licenses/by-nd/4.0/" + }, + { + "id": "http://dcat-ap.de/def/licenses/cc-by-sa/4.0", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Creative Commons Namensnennung - Weitergabe unter gleichen Bedingungen 4.0 International (CC-BY-SA 4.0)", + "url": "http://creativecommons.org/licenses/by-sa/4.0/" + }, + { + "id": "http://dcat-ap.de/def/licenses/cc-by-nc/4.0", + "is_okd_compliant": false, + "is_osi_compliant": false, + "status": "active", + "title": "Creative Commons Namensnennung - Nicht kommerziell 4.0 International (CC BY-NC 4.0)", + "url": "http://creativecommons.org/licenses/by-nc/4.0/" + }, + { + "id": "http://dcat-ap.de/def/licenses/odbl", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Open Data Commons Open Database License (ODbL)", + "url": "http://www.opendefinition.org/licenses/odc-odbl" + }, + { + "id": "http://dcat-ap.de/def/licenses/odby", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Open Data Commons Attribution License (ODC-BY 1.0)", + "url": "http://www.opendefinition.org/licenses/odc-by" + }, + { + "id": "http://dcat-ap.de/def/licenses/odcpddl", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Open Data Commons Public Domain Dedication and Licence (ODC PDDL)", + "url": "http://www.opendefinition.org/licenses/odc-pddl" + }, + { + "id": "http://dcat-ap.de/def/licenses/apache", + "is_okd_compliant": false, + "is_osi_compliant": true, + "status": "active", + "title": "Freie Softwarelizenz der Apache Software Foundation", + "url": "http://www.apache.org/licenses" + }, + { + "id": "http://dcat-ap.de/def/licenses/bsd", + "is_okd_compliant": false, + "is_osi_compliant": true, + "status": "active", + "title": "BSD Lizenz", + "url": "http://www.opensource.org/licenses/bsd-license.php" + }, + { + "id": "http://dcat-ap.de/def/licenses/gpl/3.0", + "is_okd_compliant": false, + "is_osi_compliant": true, + "status": "active", + "title": "GNU General Public License version 3.0 (GPLv3)", + "url": "http://www.opensource.org/licenses/gpl-3.0.html" + }, + { + "id": "http://dcat-ap.de/def/licenses/mozilla", + "is_okd_compliant": false, + "is_osi_compliant": true, + "status": "active", + "tags": [], + "title": "Mozilla Public License 2.0 (MPL)", + "url": "http://www.mozilla.org/MPL" + }, + { + "id": "http://dcat-ap.de/def/licenses/gfdl", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "GNU Free Documentation License (GFDL)", + "url": "http://www.opendefinition.org/licenses/gfdl" + }, + { + "id": "http://dcat-ap.de/def/licenses/cc-by-de/3.0", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Creative Commons Namensnennung 3.0 Deutschland (CC BY 3.0 DE)", + "url": "https://creativecommons.org/licenses/by/3.0/de/" + }, + { + "id": "http://dcat-ap.de/def/licenses/cc-by-nd/3.0", + "is_okd_compliant": false, + "is_osi_compliant": false, + "status": "active", + "title": "Creative Commons Namensnennung -- Keine Bearbeitung 3.0 Unported (CC BY-ND 3.0)", + "url": "http://creativecommons.org/licenses/by-nd/3.0/" + }, + { + "id": "http://dcat-ap.de/def/licenses/cc-by-nc-de/3.0", + "is_okd_compliant": false, + "is_osi_compliant": false, + "status": "active", + "title": "Creative Commons Namensnennung-Nicht kommerziell 3.0 Deutschland (CC BY-NC 3.0 DE)", + "url": "https://creativecommons.org/licenses/by-nc/3.0/de/" + }, + { + "id": "http://dcat-ap.de/def/licenses/ccpdm/1.0", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Public Domain Mark 1.0 (PDM)", + "url": "http://creativecommons.org/publicdomain/mark/1.0/" + }, + { + "id": "http://dcat-ap.de/def/licenses/geonutz/20130319", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Nutzungsbestimmungen für die Bereitstellung von Geodaten des Bundes", + "url": "http://www.geodatenzentrum.de/docpdf/geonutzv.pdf" + }, + { + "id": "http://dcat-ap.de/def/licenses/dl-by-de/1.0", + "is_okd_compliant": true, + "is_osi_compliant": false, + "status": "active", + "title": "Datenlizenz Deutschland Namensnennung 1.0", + "url": "https://www.govdata.de/dl-de/by-1-0" + }, + { + "id": "http://dcat-ap.de/def/licenses/dl-by-nc-de/1.0", + "is_okd_compliant": false, + "is_osi_compliant": false, + "status": "active", + "title": "Datenlizenz Deutschland Namensnennung nicht-kommerziell 1.0", + "url": "https://www.govdata.de/dl-de/by-nc-1-0" + } +] diff --git a/ckanext/odsh/tests_tpsh/resources/test_2.pdf b/ckanext/odsh/tests_tpsh/resources/test_2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1a04e6fbc818da347686b8010415eda159fef24b Binary files /dev/null and b/ckanext/odsh/tests_tpsh/resources/test_2.pdf differ diff --git a/ckanext/odsh/tests_tpsh/test_checksum.py b/ckanext/odsh/tests_tpsh/test_checksum.py index 6bff5d4fe469025e396c8b7e291e0d99106aad36..2fc411b51bf841ed7205b7024418d4e7d7dd78bd 100644 --- a/ckanext/odsh/tests_tpsh/test_checksum.py +++ b/ckanext/odsh/tests_tpsh/test_checksum.py @@ -10,50 +10,56 @@ path = os.path.abspath(__file__) dir_path = os.path.dirname(path) class testHashException(object): - text = 'Transparenz SH' - # hash produced by following command in bash: - # echo -n "Transparenz SH" | md5sum - hash_for_text = '791759e98a3ec4cc9c03141a3293292b' + text = 'Transparenz SH' + # hash produced by following command in bash: + # echo -n "Transparenz SH" | md5sum + hash_for_text = '791759e98a3ec4cc9c03141a3293292b' - def test_without_hash(self): - resource = {'package_id':'Test_id',} - with patch("__builtin__.open", mock_open(read_data=self.text)) as mock_file: - with open('some/file') as f: - assert _raise_validation_error_if_hash_values_differ(f, resource) == None + def test_without_hash(self): + resource = {'package_id':'Test_id',} + with patch("__builtin__.open", mock_open(read_data=self.text)) as mock_file: + with open('some/file') as f: + assert _raise_validation_error_if_hash_values_differ(f, resource) == None - def test_with_correct_hash(self): - resource = {'package_id':'Test_id', 'hash':self.hash_for_text} - with patch("__builtin__.open", mock_open(read_data=self.text)) as mock_file: - with open('some/file') as f: - assert _raise_validation_error_if_hash_values_differ(f, resource) == None + def test_with_correct_hash(self): + resource = {'package_id':'Test_id', 'hash':self.hash_for_text} + with patch("__builtin__.open", mock_open(read_data=self.text)) as mock_file: + with open('some/file') as f: + assert _raise_validation_error_if_hash_values_differ(f, resource) == None - def test_with_wrong_hash(self): - resource = {'package_id':'Test_id', 'hash':'incorrect_hash'} - with patch("__builtin__.open", mock_open(read_data=self.text)) as mock_file: - with open('some/file') as f: - with nt.assert_raises(logic.ValidationError) as e: - _raise_validation_error_if_hash_values_differ(f, resource) - exception_upload = e.exception.error_dict.get('upload') - assert exception_upload[0] == 'Berechneter Hash und mitgelieferter Hash sind unterschiedlich' - - def test_mock_file(self): - with patch("__builtin__.open", mock_open(read_data=self.text)) as mock_file: - with open('some/file') as f: - file_content = f.read() - nt.assert_equal(file_content, self.text) - - def test_hash_of_empty_string(self): - hash_empty_string = 'd41d8cd98f00b204e9800998ecf8427e' - nt.assert_equal(hash_empty_string, hashlib.md5('').hexdigest()) - - def test_pdf(self): - # expected_hash_pdf produced by following command in bash: - # md5sum test.pdf - expected_hash_pdf = '66123edf64fabf1c073fc45478bf4a57' - with open(dir_path + '/resources/test.pdf') as f: - hash = calculate_hash(f) - nt.assert_equal(hash, expected_hash_pdf) + def test_with_wrong_hash(self): + resource = {'package_id':'Test_id', 'hash':'incorrect_hash'} + with patch("__builtin__.open", mock_open(read_data=self.text)) as mock_file: + with open('some/file') as f: + with nt.assert_raises(logic.ValidationError) as e: + _raise_validation_error_if_hash_values_differ(f, resource) + exception_upload = e.exception.error_dict.get('upload') + assert exception_upload[0] == 'Berechneter Hash und mitgelieferter Hash sind unterschiedlich' + + def test_mock_file(self): + with patch("__builtin__.open", mock_open(read_data=self.text)) as mock_file: + with open('some/file') as f: + file_content = f.read() + nt.assert_equal(file_content, self.text) + + def test_hash_of_empty_string(self): + hash_empty_string = 'd41d8cd98f00b204e9800998ecf8427e' + nt.assert_equal(hash_empty_string, hashlib.md5('').hexdigest()) + + def test_pdf(self): + # expected_hash_pdf produced by following command in bash: + # md5sum test.pdf + expected_hash_pdf = '66123edf64fabf1c073fc45478bf4a57' + with open(dir_path + '/resources/test.pdf') as f: + hash = calculate_hash(f) + nt.assert_equal(hash, expected_hash_pdf) - + def test_pdf_2(self): + # expected_hash_pdf produced by following command in bash: + # md5sum test.pdf + expected_hash_pdf = 'c897e2791f8a396dc73285ca48d824a4' + with open(dir_path + '/resources/test_2.pdf') as f: + hash = calculate_hash(f) + nt.assert_equal(hash, expected_hash_pdf) \ No newline at end of file diff --git a/ckanext/odsh/tests_tpsh/test_validation.py b/ckanext/odsh/tests_tpsh/test_validation.py new file mode 100644 index 0000000000000000000000000000000000000000..3c4d145405acb554fe8e4d233654d768f9fbe266 --- /dev/null +++ b/ckanext/odsh/tests_tpsh/test_validation.py @@ -0,0 +1,198 @@ +import nose.tools as nt +from ckan.common import config +from .test_validation_mocks import WithFrontendValidationMocks, WithAPIValidationMocks +from ckanext.odsh.validation import validate_licenseAttributionByText, validate_extra_groups +import ckan.plugins.toolkit as toolkit + + +class Test_validate_licenseAttributionByText(WithFrontendValidationMocks): + def test_it_passes(self): + validate_licenseAttributionByText( + self.key_mock, + self.data_mock_without_groups, + self.error_mock, + self.context_mock + ) + + def test_fails_if_licenseAttributionByText_missing(self): + data_mock = self.data_mock_without_groups.copy() + data_mock.update({ + ('extras', 4, 'key'): u'', + }) + with nt.assert_raises(toolkit.Invalid) as err: + validate_licenseAttributionByText( + self.key_mock, + data_mock, + self.error_mock, + self.context_mock + ) + nt.assert_equal(err.exception.error, + 'licenseAttributionByText: empty not allowed') + + def test_passes_if_licenseAttributionByText_missing_and_flag_set_to_false(self): + config.update({ + 'ckanext.odsh.require_license_attribution': 'False', + }) + data_mock = self.data_mock_without_groups.copy() + data_mock.update({ + ('extras', 4, 'key'): u'', + }) + validate_licenseAttributionByText( + self.key_mock, + data_mock, + self.error_mock, + self.context_mock + ) + + def test_fails_if_licenseAttributionByText_given_for_license_wo_name(self): + data_mock = self.data_mock_without_groups.copy() + data_mock.update({ + ('license_id',): u'http://dcat-ap.de/def/licenses/dl-zero-de/2.0', + }) + with nt.assert_raises(toolkit.Invalid) as err: + validate_licenseAttributionByText( + self.key_mock, + data_mock, + self.error_mock, + self.context_mock + ) + nt.assert_equal( + err.exception.error, 'licenseAttributionByText: text not allowed for this license') + + +class Test_validate_extra_groupsFrontend(WithFrontendValidationMocks): + def test_fails_if_required_and_empty_string_in_extras(self): + data = self.data_mock_without_groups.copy() + errors = self.error_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=True, + errors=errors + ) + nt.assert_equal(errors.get('groups'), 'at least one group needed') + + def test_passes_if_required_and_groups_in_extras(self): + data = self.data_mock_with_groups.copy() + errors = self.error_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=True, + errors=errors + ) + nt.assert_equal(errors.get('groups'), None) + + def test_copies_groups_from_extras_one_level_up(self): + data = self.data_mock_with_groups.copy() + errors = self.error_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=True, + errors=errors + ) + nt.assert_equal( + data.get(('groups', 0, 'id')), + u'soci' + ) + nt.assert_equal( + data.get(('groups', 1, 'id')), + u'ener' + ) + nt.assert_equal( + data.get(('groups', 2, 'id')), + u'intr' + ) + + def test_adds_empty_group_to_data_if_groups_in_extra_empty(self): + data = self.data_mock_without_groups.copy() + errors = self.error_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=False, + errors=errors + ) + nt.assert_equal( + data.get(('groups', 0, 'id')), + u'' + ) + + +class Test_validate_extra_groupsAPI(WithAPIValidationMocks): + def test_passes_if_required_and_outside_extras(self): + data = self.data_mock_with_groups.copy() + errors = self.errors_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=True, + errors=errors + ) + nt.assert_equal(len(errors), 0) + + def test_passes_if_required_and_in_extras(self): + data = self.data_mock_with_groups_in_extras.copy() + errors = self.errors_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=True, + errors=errors + ) + nt.assert_equal(len(errors), 0) + + def test_fails_if_required_and_not_given(self): + data = self.data_mock_without_groups.copy() + errors = self.errors_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=True, + errors=errors + ) + nt.assert_equal(len(errors), 1) + + def test_fails_if_required_and_empty(self): + data = self.data_mock_with_empty_groups.copy() + errors = self.errors_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=True, + errors=errors + ) + nt.assert_equal(len(errors), 1) + + def test_fails_if_required_and_empty_in_extras(self): + data = self.data_mock_with_empty_groups_in_extras.copy() + errors = self.errors_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=True, + errors=errors + ) + nt.assert_equal(len(errors), 1) + + def test_passes_if_not_required_and_outside_extras(self): + data = self.data_mock_with_groups.copy() + errors = self.errors_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=False, + errors=errors + ) + nt.assert_equal(len(errors), 0) + + def test_passes_if_not_required_and_not_given(self): + data = self.data_mock_without_groups.copy() + errors = self.errors_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=False, + errors=errors + ) + nt.assert_equal(len(errors), 0) + + def test_passes_if_required_and_empty(self): + data = self.data_mock_with_empty_groups + errors = self.errors_mock.copy() + validate_extra_groups( + data=data, + requireAtLeastOne=False, + errors=errors + ) + nt.assert_equal(len(errors), 0) diff --git a/ckanext/odsh/tests_tpsh/test_validation_mocks.py b/ckanext/odsh/tests_tpsh/test_validation_mocks.py new file mode 100644 index 0000000000000000000000000000000000000000..6d61d04e4125ac7dd352fd048bb8d9d7a216d4de --- /dev/null +++ b/ckanext/odsh/tests_tpsh/test_validation_mocks.py @@ -0,0 +1,316 @@ +import os +from ckan.common import config +from ckan.lib.navl.dictization_functions import Missing + + +def resource_dir(): + this_module_dir = os.path.dirname(os.path.abspath(__file__)) + result = os.path.abspath(os.path.join(this_module_dir, 'resources/')) + print(result) + return result + + +def license_file_url(): + result = 'file://' + \ + os.path.abspath(os.path.join(resource_dir(), 'licenses.json')) + return result + + +class WithConfig(object): + def setUp(self): + config.update({ + 'ckanext.odsh.require_license_attribution': 'True', + 'licenses_group_url': license_file_url(), + }) + + +class WithFrontendValidationMocks(WithConfig): + def setUp(self): + super(WithFrontendValidationMocks, self).setUp() + + self.key_mock = ('extras', 0, 'key') + + self.data_mock_without_groups = { + ('__extras',): { + 'pkg_name': u'458ccd2c-5e1b-474f-943f-3bab9439a0e5', + 'spatial_uri_temp': u'', + 'tags': [] + }, + ('extras', 0, 'key'): u'groups', + ('extras', 0, 'revision_timestamp'): Missing(), + ('extras', 0, 'state'): Missing(), + ('extras', 0, 'value'): u'', + ('extras', 1, 'deleted'): Missing(), + ('extras', 1, 'id'): Missing(), + ('extras', 1, 'key'): u'issued', + ('extras', 1, 'revision_timestamp'): Missing(), + ('extras', 1, 'state'): Missing(), + ('extras', 1, 'value'): u'2019-12-17', + ('extras', 2, 'deleted'): Missing(), + ('extras', 2, 'id'): Missing(), + ('extras', 2, 'key'): u'temporal_end', + ('extras', 2, 'revision_timestamp'): Missing(), + ('extras', 2, 'state'): Missing(), + ('extras', 2, 'value'): u'', + ('extras', 3, 'deleted'): Missing(), + ('extras', 3, 'id'): Missing(), + ('extras', 3, 'key'): u'temporal_start', + ('extras', 3, 'revision_timestamp'): Missing(), + ('extras', 3, 'state'): Missing(), + ('extras', 3, 'value'): u'', + ('extras', 4, 'deleted'): Missing(), + ('extras', 4, 'id'): Missing(), + ('extras', 4, 'key'): u'licenseAttributionByText', + ('extras', 4, 'revision_timestamp'): Missing(), + ('extras', 4, 'state'): Missing(), + ('extras', 4, 'value'): u'Test-Organisation', + ('extras', 5, 'deleted'): Missing(), + ('extras', 5, 'id'): Missing(), + ('extras', 5, 'key'): 'thumbnail', + ('extras', 5, 'revision_timestamp'): Missing(), + ('extras', 5, 'state'): Missing(), + ('extras', 5, 'value'): u'thumbnail_picture_4519ffc6a956a638b7b6f7a6a82418.jpg', + ('extras', 6, 'key'): 'language', + ('extras', 6, 'value'): u'http://publications.europa.eu/resource/authority/language/DEU', + ('extras', 7, 'key'): 'subject_text', + ('extras', 7, 'value'): u'Verwaltungsvorschrift', + ('extras', 8, 'key'): 'subject', + ('extras', 8, 'value'): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Verwaltungsvorschrift', + ('extras', 9, 'key'): 'spatial_uri', + ('extras', 9, 'value'): u'', + ('id',): u'458ccd2c-5e1b-474f-943f-3bab9439a0e5', + ('language',): u'http://publications.europa.eu/resource/authority/language/DEU', + ('license_id',): u'http://dcat-ap.de/def/licenses/dl-by-de/2.0', + ('notes',): u'', + ('owner_org',): u'63c87e74-60a9-4a4a-a980-d7983c47f92b', + ('private',): False, + ('subject',): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Verwaltungsvorschrift', + ('tag_string',): u'', + ('title',): u'Link again', + ('type',): u'dataset' + } + + self.data_mock_with_groups = { + ('__extras',): {'pkg_name': u'458ccd2c-5e1b-474f-943f-3bab9439a0e5', + 'spatial_uri_temp': u'', + 'tags': []}, + ('extras', 0, 'key'): u'groups', + ('extras', 0, 'value'): u'soci,ener,intr', + ('extras', 1, 'key'): u'issued', + ('extras', 1, 'value'): u'2019-12-17', + ('extras', 2, 'key'): u'temporal_end', + ('extras', 2, 'value'): u'', + ('extras', 3, 'key'): u'temporal_start', + ('extras', 3, 'value'): u'', + ('extras', 5, 'key'): 'thumbnail', + ('extras', 5, 'value'): u'thumbnail_picture_4519ffc6a956a638b7b6f7a6a82418.jpg', + ('extras', 6, 'key'): 'language', + ('extras', 6, 'value'): u'http://publications.europa.eu/resource/authority/language/DEU', + ('extras', 7, 'key'): 'subject_text', + ('extras', 7, 'value'): u'Verwaltungsvorschrift', + ('extras', 8, 'key'): 'subject', + ('extras', 8, 'value'): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Verwaltungsvorschrift', + ('extras', 9, 'key'): 'spatial_uri', + ('extras', 9, 'value'): u'', + ('extras', 10, 'key'): 'licenseAttributionByText', + ('extras', 10, 'value'): '', + ('extras', 11, 'key'): 'licenseAttributionByText', + ('extras', 11, 'value'): '', + ('extras', 12, 'key'): 'licenseAttributionByText', + ('extras', 12, 'value'): '', + ('extras', 13, 'key'): 'licenseAttributionByText', + ('extras', 13, 'value'): '', + ('extras', 14, 'key'): 'licenseAttributionByText', + ('extras', 14, 'value'): '', + ('extras', 15, 'key'): 'licenseAttributionByText', + ('extras', 15, 'value'): '', + ('id',): u'458ccd2c-5e1b-474f-943f-3bab9439a0e5', + ('language',): u'http://publications.europa.eu/resource/authority/language/DEU', + ('license_id',): u'http://dcat-ap.de/def/licenses/dl-zero-de/2.0', + ('notes',): u'', + ('owner_org',): u'63c87e74-60a9-4a4a-a980-d7983c47f92b', + ('private',): False, + ('subject',): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Verwaltungsvorschrift', + ('tag_string',): u'', + ('title',): u'Link again', + ('type',): u'dataset' + } + + self.error_mock = { + ('__before',): [], + ('__extras',): [], + ('__junk',): [], + ('author',): [], + ('author_email',): [], + ('extras', 0, '__extras'): [], + ('extras', 0, 'deleted'): [], + ('extras', 0, 'id'): [], + ('extras', 0, 'key'): [], + ('extras', 0, 'revision_timestamp'): [], + ('extras', 0, 'state'): [], + ('extras', 0, 'value'): [], + ('extras', 1, '__extras'): [], + ('extras', 1, 'deleted'): [], + ('extras', 1, 'id'): [], + ('extras', 1, 'key'): [], + ('extras', 1, 'revision_timestamp'): [], + ('extras', 1, 'state'): [], + ('extras', 1, 'value'): [], + ('extras', 2, '__extras'): [], + ('extras', 2, 'deleted'): [], + ('extras', 2, 'id'): [], + ('extras', 2, 'key'): [], + ('extras', 2, 'revision_timestamp'): [], + ('extras', 2, 'state'): [], + ('extras', 2, 'value'): [], + ('extras', 3, '__extras'): [], + ('extras', 3, 'deleted'): [], + ('extras', 3, 'id'): [], + ('extras', 3, 'key'): [], + ('extras', 3, 'revision_timestamp'): [], + ('extras', 3, 'state'): [], + ('extras', 3, 'value'): [], + ('extras', 4, '__extras'): [], + ('extras', 4, 'deleted'): [], + ('extras', 4, 'id'): [], + ('extras', 4, 'key'): [], + ('extras', 4, 'revision_timestamp'): [], + ('extras', 4, 'state'): [], + ('extras', 4, 'value'): [], + ('extras', 5, '__extras'): [], + ('extras', 5, 'deleted'): [], + ('extras', 5, 'id'): [], + ('extras', 5, 'key'): [], + ('extras', 5, 'revision_timestamp'): [], + ('extras', 5, 'state'): [], + ('extras', 5, 'value'): [], + ('id',): [], + ('language',): [], + ('license_id',): [], + ('log_message',): [], + ('maintainer',): [], + ('maintainer_email',): [], + ('name',): [], + ('notes',): [], + ('owner_org',): [], + ('private',): [], + ('return_to',): [], + ('revision_id',): [], + ('save',): [], + ('state',): [], + ('subject',): [], + ('tag_string',): [], + ('thumbnail',): [], + ('title',): [], + ('type',): [], + ('url',): [], + ('version',): [] + } + + self.context_mock = None + + +class WithAPIValidationMocks(WithConfig): + def setUp(self): + super(WithAPIValidationMocks, self).setUp() + + self.data_mock_with_groups = { + ('__extras',): {'tags': []}, + ('extras', 0, 'key'): u'issued', + ('extras', 0, 'value'): u'2020-02-28T00:00:00.000', + ('extras', 1, 'key'): 'subject_text', + ('extras', 1, 'value'): u'Gutachten', + ('extras', 2, 'key'): 'subject', + ('extras', 2, 'value'): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Gutachten', + ('extras', 3, 'key'): 'licenseAttributionByText', + ('extras', 3, 'value'): '', + ('groups', 0, 'name'): u'heal', + ('license_id',): u'http://dcat-ap.de/def/licenses/dl-zero-de/2.0', + ('name',): u'aaa', + ('owner_org',): u'ce91bacd-043d-4546-8239-c891d4a221f8', + ('subject',): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Gutachten', + ('title',): u'aaa', + ('type',): u'dataset' + } + + self.data_mock_with_empty_groups = { + ('__extras',): {'groups': [], 'tags': []}, + ('extras', 0, 'key'): u'issued', + ('extras', 0, 'value'): u'2020-02-28T00:00:00.000', + ('extras', 1, 'key'): 'subject_text', + ('extras', 1, 'value'): u'Gutachten', + ('extras', 2, 'key'): 'subject', + ('extras', 2, 'value'): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Gutachten', + ('extras', 3, 'key'): 'licenseAttributionByText', + ('extras', 3, 'value'): '', + ('license_id',): u'http://dcat-ap.de/def/licenses/dl-zero-de/2.0', + ('name',): u'aaa', + ('owner_org',): u'ce91bacd-043d-4546-8239-c891d4a221f8', + ('subject',): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Gutachten', + ('title',): u'aaa', + ('type',): u'dataset' + } + + self.data_mock_with_groups_in_extras = { + ('__extras',): {'tags': []}, + ('extras', 0, 'key'): u'issued', + ('extras', 0, 'value'): u'2020-02-28T00:00:00.000', + ('extras', 1, 'key'): u'groups', + ('extras', 1, 'value'): u'soci,ener,intr', + ('extras', 2, 'key'): 'subject_text', + ('extras', 2, 'value'): u'Gutachten', + ('extras', 3, 'key'): 'subject', + ('extras', 3, 'value'): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Gutachten', + ('extras', 4, 'key'): 'licenseAttributionByText', + ('extras', 4, 'value'): '', + ('extras', 5, 'key'): 'licenseAttributionByText', + ('extras', 5, 'value'): '', + ('license_id',): u'http://dcat-ap.de/def/licenses/dl-zero-de/2.0', + ('name',): u'aaa', + ('owner_org',): u'ce91bacd-043d-4546-8239-c891d4a221f8', + ('subject',): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Gutachten', + ('title',): u'aaa', + ('type',): u'dataset' + } + + self.data_mock_with_empty_groups_in_extras = { + ('__extras',): {'tags': []}, + ('extras', 0, 'key'): u'issued', + ('extras', 0, 'value'): u'2020-02-28T00:00:00.000', + ('extras', 1, 'key'): u'groups', + ('extras', 1, 'value'): u'', + ('extras', 2, 'key'): 'subject_text', + ('extras', 2, 'value'): u'Gutachten', + ('extras', 3, 'key'): 'subject', + ('extras', 3, 'value'): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Gutachten', + ('extras', 4, 'key'): 'licenseAttributionByText', + ('extras', 4, 'value'): '', + ('extras', 5, 'key'): 'licenseAttributionByText', + ('extras', 5, 'value'): '', + ('license_id',): u'http://dcat-ap.de/def/licenses/dl-zero-de/2.0', + ('name',): u'aaa', + ('owner_org',): u'ce91bacd-043d-4546-8239-c891d4a221f8', + ('subject',): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Gutachten', + ('title',): u'aaa', + ('type',): u'dataset' + } + + self.data_mock_without_groups = { + ('__extras',): {'tags': []}, + ('extras', 0, 'key'): u'issued', + ('extras', 0, 'value'): u'2020-02-28T00:00:00.000', + ('extras', 1, 'key'): 'subject_text', + ('extras', 1, 'value'): u'Gutachten', + ('extras', 2, 'key'): 'subject', + ('extras', 2, 'value'): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Gutachten', + ('extras', 3, 'key'): 'licenseAttributionByText', + ('extras', 3, 'value'): '', + ('license_id',): u'http://dcat-ap.de/def/licenses/dl-zero-de/2.0', + ('name',): u'aaa', + ('owner_org',): u'ce91bacd-043d-4546-8239-c891d4a221f8', + ('subject',): u'http://transparenz.schleswig-holstein.de/informationsgegenstand#Gutachten', + ('title',): u'aaa', + ('type',): u'dataset' + } + + self.errors_mock = {} diff --git a/ckanext/odsh/validation.py b/ckanext/odsh/validation.py index 7fa35f8bfdfcdc215a6e92bb543acabc52b55121..8c2f955ebdd6664beb4a22387ee68d4f191d56f9 100644 --- a/ckanext/odsh/validation.py +++ b/ckanext/odsh/validation.py @@ -31,35 +31,46 @@ def _extract_value(data, field): return data[(key[0], key[1], 'value')] +ERROR_MSG_NO_GROUP = 'at least one group needed' + def validate_extra_groups(data, requireAtLeastOne, errors): - value = _extract_value(data, 'groups') - error_message_no_group = 'at least one group needed' - if value != None: - # 'value != None' means the extra key 'groups' was found, - # so the dataset came from manual editing via the web-frontend. - if not value: - if requireAtLeastOne: - errors['groups'] = error_message_no_group - data[('groups', 0, 'id')] = '' - return + groups = _groups_from_data(data) + if groups is not None: + # groups are in extras + if len(groups) == 0 and requireAtLeastOne: + errors['groups'] = ERROR_MSG_NO_GROUP + _clear_groups(data) + _copy_groups_one_level_up(data, groups) + else: + # no extra-field 'groups' + if (not _at_least_one_group_outside_extras(data)) and requireAtLeastOne: + errors['groups'] = ERROR_MSG_NO_GROUP + +def _groups_from_data(data): + groups_csv = _extract_value(data, 'groups') + try: + groups = list(csv.reader([groups_csv]))[0] + except TypeError: + groups = None + return groups - groups = [g.strip() for g in value.split(',') if value.strip()] - for k in data.keys(): - if len(k) == 3 and k[0] == 'groups': - data[k] = '' - # del data[k] - if len(groups) == 0: - if requireAtLeastOne: - errors['groups'] = error_message_no_group - return +def _clear_groups(data): + for k in data.keys(): + if len(k) == 3 and k[0] == 'groups': + data[k] = '' # dead code [?] +def _copy_groups_one_level_up(data, groups): + if len(groups) == 0: + data[('groups', 0, 'id')] = '' + else: for num, group in zip(range(len(groups)), groups): data[('groups', num, 'id')] = group - else: # no extra-field 'groups' - # dataset might come from a harvest process - if not data.get(('groups', 0, 'id'), False) and \ - not data.get(('groups', 0, 'name'), False): - errors['groups'] = error_message_no_group + +def _at_least_one_group_outside_extras(data): + return ( + ('groups', 0, 'id') in data or + ('groups', 0, 'name') in data + ) def validate_extras(key, data, errors, context): @@ -143,6 +154,24 @@ def validate_extra_date_new(key, field, data, optional, errors): def validate_licenseAttributionByText(key, data, errors, context): + require_license_attribution = toolkit.asbool( + config.get('ckanext.odsh.require_license_attribution', True) + ) + isByLicense = _isByLicense(data) + hasAttribution = _hasAttribution(data) + if not hasAttribution: + _add_empty_attribution(data) + + if isByLicense and (not hasAttribution) and require_license_attribution: + raise toolkit.Invalid( + 'licenseAttributionByText: empty not allowed') + + if (not isByLicense) and hasAttribution: + raise toolkit.Invalid( + 'licenseAttributionByText: text not allowed for this license') + + +def _isByLicense(data): register = model.Package.get_license_register() isByLicense = False for k in data: @@ -150,6 +179,10 @@ def validate_licenseAttributionByText(key, data, errors, context): 'Namensnennung' in register[data[k]].title: isByLicense = True break + return isByLicense + + +def _hasAttribution(data): hasAttribution = False for k in data: if data[k] == 'licenseAttributionByText': @@ -161,26 +194,24 @@ def validate_licenseAttributionByText(key, data, errors, context): value = data[(k[0], k[1], 'value')] hasAttribution = value != '' break - if not hasAttribution: - current_indexes = [k[1] for k in data.keys() - if len(k) > 1 and k[0] == 'extras'] + return hasAttribution - new_index = max(current_indexes) + 1 if current_indexes else 0 - data[('extras', new_index, 'key')] = 'licenseAttributionByText' - data[('extras', new_index, 'value')] = '' - if isByLicense and not hasAttribution: - raise toolkit.Invalid( - 'licenseAttributionByText: empty not allowed') - - if not isByLicense and hasAttribution: - raise toolkit.Invalid( - 'licenseAttributionByText: text not allowed for this license') +def _add_empty_attribution(data): + current_indexes = [ + k[1] for k in data.keys() + if len(k) > 1 and k[0] == 'extras' + ] + new_index = max(current_indexes) + 1 if current_indexes else 0 + data[('extras', new_index, 'key')] = 'licenseAttributionByText' + data[('extras', new_index, 'value')] = '' def known_spatial_uri(key, data, errors, context): if data.get(('__extras',)) and 'spatial_uri_temp' in data.get(('__extras',)): _copy_spatial_uri_temp_to_extras(data) + if data.get(('__extras',)) and 'spatial_url_temp' in data.get(('__extras',)): + _copy_spatial_uri_temp_to_extras(data) value = _extract_value(data, 'spatial_uri') require_spatial_uri = toolkit.asbool( config.get('ckanext.odsh.require_spatial_uri', False) @@ -243,10 +274,13 @@ def known_spatial_uri(key, data, errors, context): def _copy_spatial_uri_temp_to_extras(data): ''' - copy the field spatial_uri_temp originating + copy the fields spatial_uri_temp or + spatial_url_temp originating from the user interface to extras ''' spatial_uri = data.get(('__extras',)).get('spatial_uri_temp') + if spatial_uri is None: + spatial_uri = data.get(('__extras',)).get('spatial_url_temp') is_spatial_uri_in_extras = _extract_value(data, 'spatial_uri') is not None if not is_spatial_uri_in_extras: next_index = next_extra_index(data)