From 3f19f960357b2bda85baf4a834e07247f4a7298d Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 9 Sep 2021 13:07:51 +0200 Subject: [PATCH 01/32] Remove Travis conf files --- bin/travis-build.bash | 70 ------------------------------------------- bin/travis-run.sh | 14 --------- 2 files changed, 84 deletions(-) delete mode 100644 bin/travis-build.bash delete mode 100644 bin/travis-run.sh diff --git a/bin/travis-build.bash b/bin/travis-build.bash deleted file mode 100644 index e14c720f..00000000 --- a/bin/travis-build.bash +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash -set -e - -echo "This is travis-build.bash..." - -echo "Postgres version" -psql --version - -echo "Installing the packages that CKAN requires..." -sudo apt-get update -qq -sudo apt-get install solr-jetty - -echo "Installing CKAN and its Python dependencies..." -git clone https://github.com/ckan/ckan -cd ckan -if [ $CKAN_VERSION != 'master' ] -then - git checkout $CKAN_VERSION -fi -# Unpin CKAN's psycopg2 dependency get an important bugfix -# https://stackoverflow.com/questions/47044854/error-installing-psycopg2-2-6-2 -sed -i '/psycopg2/c\psycopg2' requirements.txt -python setup.py develop -pip install -r requirements.txt -pip install -r dev-requirements.txt -cd - - -echo "Creating the PostgreSQL user and database..." -sudo -u postgres psql -c "CREATE USER ckan_default WITH PASSWORD 'pass';" -sudo -u postgres psql -c 'CREATE DATABASE ckan_test WITH OWNER ckan_default;' - -echo "SOLR config..." -# Solr is multicore for tests on ckan master, but it's easier to run tests on -# Travis single-core. See https://github.com/ckan/ckan/issues/2972 -sed -i -e 's/solr_url.*/solr_url = http:\/\/127.0.0.1:8983\/solr/' ckan/test-core.ini - -echo "Initialising the database..." -cd ckan -paster db init -c test-core.ini -cd - - -echo "Installing other extensions requirements..." -git clone https://github.com/ckan/ckanext-scheming -cd ckanext-scheming -pip install -r requirements.txt -python setup.py develop -cd - -if [ $CKAN_VERSION == 'dev-v2.6' ] || [ $CKAN_VERSION == 'release-v2.5-latest' ] || [ $CKAN_VERSION == 'release-v2.4-latest' ] -then - git clone https://github.com/ckan/ckanext-rq - cd ckanext-rq - pip install -r requirements.txt - pip install -r dev-requirements.txt - python setup.py develop - cd - - # Enable the rq plugin - sed -i -e 's/ckan.plugins = /ckan.plugins = rq /' test.ini -fi - -echo "Installing ckanext-validation and its requirements..." -python setup.py develop -pip install -r requirements.txt -pip install -r dev-requirements.txt -paster --plugin=ckanext-validation validation init-db -c ckan/test-core.ini - -echo "Moving test.ini into a subdir..." -mkdir subdir -mv test.ini subdir - -echo "travis-build.bash is done." diff --git a/bin/travis-run.sh b/bin/travis-run.sh deleted file mode 100644 index f5aa2131..00000000 --- a/bin/travis-run.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -e - -echo "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8983\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty -sudo cp ckan/ckan/config/solr/schema.xml /etc/solr/conf/schema.xml -sudo service jetty restart - -nosetests --ckan \ - --nologcapture \ - --with-pylons=subdir/test.ini \ - --with-coverage \ - --cover-package=ckanext.validation \ - --cover-inclusive \ - --cover-erase \ - --cover-tests From 3d5d93519aec07a87604d2e382068065219f5724 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 9 Sep 2021 13:10:08 +0200 Subject: [PATCH 02/32] Enable github actions for tests --- .github/workflows/test.yml | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..430aa436 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,69 @@ +name: Tests +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: '3.6' + - name: Install requirements + run: pip install flake8 pycodestyle + - name: Check syntax + run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics --exclude ckan + + test: + needs: lint + strategy: + matrix: + ckan-version: [2.9, 2.9-py2, 2.8, 2.7] + fail-fast: false + + name: CKAN ${{ matrix.ckan-version }} + runs-on: ubuntu-latest + container: + image: openknowledge/ckan-dev:${{ matrix.ckan-version }} + services: + solr: + image: ckan/ckan-solr-dev:${{ matrix.ckan-version }} + postgres: + image: ckan/ckan-postgres-dev:${{ matrix.ckan-version }} + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + redis: + image: redis:3 + env: + CKAN_SQLALCHEMY_URL: postgresql://ckan_default:pass@postgres/ckan_test + CKAN_DATASTORE_WRITE_URL: postgresql://datastore_write:pass@postgres/datastore_test + CKAN_DATASTORE_READ_URL: postgresql://datastore_read:pass@postgres/datastore_test + CKAN_SOLR_URL: http://solr:8983/solr/ckan + CKAN_REDIS_URL: redis://redis:6379/1 + + steps: + - uses: actions/checkout@v2 + - name: Install requirements + run: | + pip install -r requirements.txt + pip install -r dev-requirements.txt + pip install -e . + # Replace default path to CKAN core config file with the one on the container + sed -i -e 's/use = config:.*/use = config:\/srv\/app\/src\/ckan\/test-core.ini/' test.ini + - name: Setup extension (CKAN >= 2.9) + if: ${{ matrix.ckan-version != '2.7' && matrix.ckan-version != '2.8' }} + run: | + ckan -c test.ini db init + - name: Setup extension (CKAN < 2.9) + if: ${{ matrix.ckan-version == '2.7' || matrix.ckan-version == '2.8' }} + run: | + paster --plugin=ckan db init -c test.ini + - name: Run tests + run: pytest --ckan-ini=test.ini --cov=ckanext.validation --cov-report=xml --cov-append --disable-warnings ckanext/validation/tests + + - name: Upload coverage report to codecov + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml From a9190d9cc67e962c54acce1250f3d6ed36d2b395 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 9 Sep 2021 13:16:40 +0200 Subject: [PATCH 03/32] Fix flake8 tests --- ckanext/validation/jobs.py | 5 +++-- ckanext/validation/logic.py | 9 +++++---- ckanext/validation/model.py | 3 ++- ckanext/validation/plugin.py | 4 +++- ckanext/validation/validators.py | 4 +++- requirements.txt | 1 + 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ckanext/validation/jobs.py b/ckanext/validation/jobs.py index 8f7a608b..a02c3236 100644 --- a/ckanext/validation/jobs.py +++ b/ckanext/validation/jobs.py @@ -5,6 +5,7 @@ import json import re +import six import requests from sqlalchemy.orm.exc import NoResultFound from goodtables import validate @@ -45,7 +46,7 @@ def run_validation_job(resource): options = {} resource_options = resource.get(u'validation_options') - if resource_options and isinstance(resource_options, basestring): + if resource_options and isinstance(resource_options, six.string_types): resource_options = json.loads(resource_options) if resource_options: options.update(resource_options) @@ -77,7 +78,7 @@ def run_validation_job(resource): source = resource[u'url'] schema = resource.get(u'schema') - if schema and isinstance(schema, basestring): + if schema and isinstance(schema, six.string_types): if schema.startswith('http'): r = requests.get(schema) schema = r.json() diff --git a/ckanext/validation/logic.py b/ckanext/validation/logic.py index 0da6b684..277bab8a 100644 --- a/ckanext/validation/logic.py +++ b/ckanext/validation/logic.py @@ -4,6 +4,7 @@ import logging import json +import six from sqlalchemy.orm.exc import NoResultFound import ckan.plugins as plugins @@ -262,14 +263,14 @@ def resource_validation_run_batch(context, data_dict): count_resources = 0 dataset_ids = data_dict.get('dataset_ids') - if isinstance(dataset_ids, basestring): + if isinstance(dataset_ids, six.string_types): try: dataset_ids = json.loads(dataset_ids) except ValueError as e: dataset_ids = [dataset_ids] search_params = data_dict.get('query') - if isinstance(search_params, basestring): + if isinstance(search_params, six.string_types): try: search_params = json.loads(search_params) except ValueError as e: @@ -467,7 +468,7 @@ def resource_create(context, data_dict): context['use_cache'] = False t.get_action('package_update')(context, pkg_dict) context.pop('defer_commit') - except t.ValidationError, e: + except t.ValidationError as e: try: raise t.ValidationError(e.error_dict['resources'][-1]) except (KeyError, IndexError): @@ -588,7 +589,7 @@ def resource_update(context, data_dict): context['use_cache'] = False updated_pkg_dict = t.get_action('package_update')(context, pkg_dict) context.pop('defer_commit') - except t.ValidationError, e: + except t.ValidationError as e: try: raise t.ValidationError(e.error_dict['resources'][-1]) except (KeyError, IndexError): diff --git a/ckanext/validation/model.py b/ckanext/validation/model.py index 35eebc46..a317e82c 100644 --- a/ckanext/validation/model.py +++ b/ckanext/validation/model.py @@ -4,6 +4,7 @@ import uuid import logging +import six from sqlalchemy import Column, Unicode, DateTime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.dialects.postgresql import JSON @@ -14,7 +15,7 @@ def make_uuid(): - return unicode(uuid.uuid4()) + return six.ensure_text(uuid.uuid4()) Base = declarative_base(metadata=metadata) diff --git a/ckanext/validation/plugin.py b/ckanext/validation/plugin.py index a2f6643a..6582adb3 100644 --- a/ckanext/validation/plugin.py +++ b/ckanext/validation/plugin.py @@ -4,6 +4,7 @@ import cgi import json +import six import ckan.plugins as p import ckantoolkit as t @@ -136,7 +137,8 @@ def _process_schema_fields(self, data_dict): if isinstance(schema_upload, cgi.FieldStorage): data_dict[u'schema'] = schema_upload.file.read() elif schema_url: - if (not isinstance(schema_url, basestring) or + + if (not isinstance(schema_url, six.string_types) or not schema_url.lower()[:4] == u'http'): raise t.ValidationError({u'schema_url': 'Must be a valid URL'}) data_dict[u'schema'] = schema_url diff --git a/ckanext/validation/validators.py b/ckanext/validation/validators.py index 62743784..ad466749 100644 --- a/ckanext/validation/validators.py +++ b/ckanext/validation/validators.py @@ -1,6 +1,8 @@ # encoding: utf-8 import json +import six + import tableschema from ckantoolkit import Invalid, config @@ -15,7 +17,7 @@ def resource_schema_validator(value, context): msg = None - if isinstance(value, basestring): + if isinstance(value, six.string_types): if value.lower().startswith('http'): return value diff --git a/requirements.txt b/requirements.txt index 9feccf79..4ffed1cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +six ckantoolkit>=0.0.3 goodtables==1.5.1 -e git+https://github.com/ckan/ckanext-scheming.git#egg=ckanext-scheming From 30e90a7b3e10a341de958d548910dacfbe9fcb18 Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 10 Sep 2021 09:07:03 +0200 Subject: [PATCH 04/32] Some pytest changes --- ckanext/validation/tests/test_helpers.py | 103 +++++++++++++---------- dev-requirements.txt | 4 +- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/ckanext/validation/tests/test_helpers.py b/ckanext/validation/tests/test_helpers.py index 43e7aaa4..cc3318e1 100644 --- a/ckanext/validation/tests/test_helpers.py +++ b/ckanext/validation/tests/test_helpers.py @@ -1,7 +1,5 @@ import datetime -from nose.tools import assert_equals, assert_in - from ckan.tests.helpers import reset_db from ckan.tests import factories @@ -15,11 +13,10 @@ class TestBadges(object): - @classmethod def setup_class(cls): cls._original_config = dict(config) - config['ckanext.validation.run_on_create_sync'] = False + config["ckanext.validation.run_on_create_sync"] = False reset_db() if not tables_exist(): @@ -36,108 +33,122 @@ def teardown_class(cls): def test_get_validation_badge_no_validation(self): resource = factories.Resource( - format='CSV', + format="CSV", ) - assert_equals(get_validation_badge(resource), '') + assert get_validation_badge(resource) == "" def test_get_validation_badge_success(self): resource = factories.Resource( - format='CSV', - validation_status='success', - validation_timestamp=datetime.datetime.utcnow() + format="CSV", + validation_status="success", + validation_timestamp=datetime.datetime.utcnow(), ) out = get_validation_badge(resource) - assert 'href="/dataset/{}/resource/{}/validation"'.format( - resource['package_id'], resource['id']) in out + assert ( + 'href="/dataset/{}/resource/{}/validation"'.format( + resource["package_id"], resource["id"] + ) + in out + ) assert 'src="/images/badges/data-success-flat.svg"' in out assert 'alt="Valid data"' in out - assert 'title="{}"'.format(resource['validation_timestamp']) in out + assert 'title="{}"'.format(resource["validation_timestamp"]) in out def test_get_validation_badge_failure(self): resource = factories.Resource( - format='CSV', - validation_status='failure', - validation_timestamp=datetime.datetime.utcnow() + format="CSV", + validation_status="failure", + validation_timestamp=datetime.datetime.utcnow(), ) out = get_validation_badge(resource) - assert 'href="/dataset/{}/resource/{}/validation"'.format( - resource['package_id'], resource['id']) in out + assert ( + 'href="/dataset/{}/resource/{}/validation"'.format( + resource["package_id"], resource["id"] + ) + in out + ) assert 'src="/images/badges/data-failure-flat.svg"' in out assert 'alt="Invalid data"' in out - assert 'title="{}"'.format(resource['validation_timestamp']) in out + assert 'title="{}"'.format(resource["validation_timestamp"]) in out def test_get_validation_badge_error(self): resource = factories.Resource( - format='CSV', - validation_status='error', - validation_timestamp=datetime.datetime.utcnow() + format="CSV", + validation_status="error", + validation_timestamp=datetime.datetime.utcnow(), ) out = get_validation_badge(resource) - assert 'href="/dataset/{}/resource/{}/validation"'.format( - resource['package_id'], resource['id']) in out + assert ( + 'href="/dataset/{}/resource/{}/validation"'.format( + resource["package_id"], resource["id"] + ) + in out + ) assert 'src="/images/badges/data-error-flat.svg"' in out assert 'alt="Error during validation"' in out - assert 'title="{}"'.format(resource['validation_timestamp']) in out + assert 'title="{}"'.format(resource["validation_timestamp"]) in out def test_get_validation_badge_other(self): resource = factories.Resource( - format='CSV', - validation_status='not-sure', + format="CSV", + validation_status="not-sure", ) out = get_validation_badge(resource) - assert 'href="/dataset/{}/resource/{}/validation"'.format( - resource['package_id'], resource['id']) in out + assert ( + 'href="/dataset/{}/resource/{}/validation"'.format( + resource["package_id"], resource["id"] + ) + in out + ) assert 'src="/images/badges/data-unknown-flat.svg"' in out assert 'alt="Data validation unknown"' in out assert 'title=""' in out class TestExtractReportFromErrors(object): - def test_report_extracted(self): - report = { - 'tables': [{'source': '/some/path'}], - 'error-count': 8 - } + report = {"tables": [{"source": "/some/path"}], "error-count": 8} errors = { - 'some_field': ['Some error'], - 'validation': [report], + "some_field": ["Some error"], + "validation": [report], } - extracted_report, errors = validation_extract_report_from_errors( - errors) + extracted_report, errors = validation_extract_report_from_errors(errors) - assert_equals(extracted_report, report) - assert_equals(errors['some_field'], ['Some error']) - assert str(errors['validation'][0]).strip().startswith( - 'There are validation issues with this file') + assert extracted_report == report + assert errors["some_field"] == ["Some error"] + assert ( + str(errors["validation"][0]) + .strip() + .startswith("There are validation issues with this file") + ) - assert_in('data-module="modal-dialog"', str(errors['validation'][0])) + assert 'data-module="modal-dialog"' in str(errors["validation"][0]) def test_report_not_extracted(self): errors = { - 'some_field': ['Some error'], - 'some_other_field': ['Some other error'] + "some_field": ["Some error"], + "some_other_field": ["Some other error"], } report, errors = validation_extract_report_from_errors(errors) assert report is None - assert_equals(errors['some_field'], ['Some error']) - assert_equals(errors['some_other_field'], ['Some other error']) + assert errors["some_field"] == ["Some error"] + assert errors["some_other_field"] == ["Some other error"] diff --git a/dev-requirements.txt b/dev-requirements.txt index 54bcef7a..216bcedb 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1 +1,3 @@ -pyfakefs==2.7 +pyfakefs==4.5.1 +pytest-ckan +pytest-cov From 99899288b3d2629d87dd874d712231f2395569f8 Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 14 Sep 2021 12:36:36 +0200 Subject: [PATCH 05/32] First py3 fixes, blueprint --- ckanext/validation/blueprints.py | 45 ++++++++++++++++++ ckanext/validation/model.py | 2 +- .../{plugin.py => plugin/__init__.py} | 21 +++------ ckanext/validation/plugin/flask_plugin.py | 14 ++++++ ckanext/validation/plugin/pylons_plugin.py | 18 +++++++ ckanext/validation/views.py | 47 +++++++++++++++++++ 6 files changed, 131 insertions(+), 16 deletions(-) create mode 100644 ckanext/validation/blueprints.py rename ckanext/validation/{plugin.py => plugin/__init__.py} (96%) create mode 100644 ckanext/validation/plugin/flask_plugin.py create mode 100644 ckanext/validation/plugin/pylons_plugin.py create mode 100644 ckanext/validation/views.py diff --git a/ckanext/validation/blueprints.py b/ckanext/validation/blueprints.py new file mode 100644 index 00000000..7a4d29bf --- /dev/null +++ b/ckanext/validation/blueprints.py @@ -0,0 +1,45 @@ +# encoding: utf-8 + +from flask import Blueprint + +from ckantoolkit import c, NotAuthorized, ObjectNotFound, abort, _, render, get_action + +validation = Blueprint("validation", __name__) + + +def read(self, resource_id): + + try: + validation = get_action(u"resource_validation_show")( + {u"user": c.user}, {u"resource_id": resource_id} + ) + + resource = get_action(u"resource_show")({u"user": c.user}, {u"id": resource_id}) + + dataset = get_action(u"package_show")( + {u"user": c.user}, {u"id": resource[u"package_id"]} + ) + + # Needed for core resource templates + c.package = c.pkg_dict = dataset + c.resource = resource + + return render( + u"validation/validation_read.html", + extra_vars={ + u"validation": validation, + u"resource": resource, + u"dataset": dataset, + }, + ) + + except NotAuthorized: + abort(403, _(u"Unauthorized to read this validation report")) + except ObjectNotFound: + + abort(404, _(u"No validation report exists for this resource")) + + +validation.add_url_rule( + "/dataset//resource//validation", view_func=read +) diff --git a/ckanext/validation/model.py b/ckanext/validation/model.py index a317e82c..7f1448b4 100644 --- a/ckanext/validation/model.py +++ b/ckanext/validation/model.py @@ -15,7 +15,7 @@ def make_uuid(): - return six.ensure_text(uuid.uuid4()) + return six.text_type(uuid.uuid4()) Base = declarative_base(metadata=metadata) diff --git a/ckanext/validation/plugin.py b/ckanext/validation/plugin/__init__.py similarity index 96% rename from ckanext/validation/plugin.py rename to ckanext/validation/plugin/__init__.py index 6582adb3..a365fcee 100644 --- a/ckanext/validation/plugin.py +++ b/ckanext/validation/plugin/__init__.py @@ -34,14 +34,18 @@ ) from ckanext.validation.interfaces import IDataValidation +if t.check_ckan_version(min_version="2.9"): + from ckanext.validation.plugin.flask_plugin import ValidationMixin +else: + from ckanext.validation.plugin.pylons_plugin import ValidationMixin + log = logging.getLogger(__name__) -class ValidationPlugin(p.SingletonPlugin): +class ValidationPlugin(ValidationMixin): p.implements(p.IConfigurer) p.implements(p.IActions) - p.implements(p.IRoutes, inherit=True) p.implements(p.IAuthFunctions) p.implements(p.IResourceController, inherit=True) p.implements(p.IPackageController, inherit=True) @@ -64,19 +68,6 @@ def update_config(self, config_): t.add_public_directory(config_, u'public') t.add_resource(u'fanstatic', 'ckanext-validation') - # IRoutes - - def before_map(self, map_): - - controller = u'ckanext.validation.controller:ValidationController' - - map_.connect( - u'validation_read', - u'/dataset/{id}/resource/{resource_id}/validation', - controller=controller, action=u'validation') - - return map_ - # IActions def get_actions(self): diff --git a/ckanext/validation/plugin/flask_plugin.py b/ckanext/validation/plugin/flask_plugin.py new file mode 100644 index 00000000..be4ce2df --- /dev/null +++ b/ckanext/validation/plugin/flask_plugin.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from ckan import plugins as p + +from ckanext.validation import blueprints + + +class ValidationMixin(p.SingletonPlugin): + p.implements(p.IBlueprint) + + # IBlueprint + + def get_blueprint(self): + return [blueprints.validation] diff --git a/ckanext/validation/plugin/pylons_plugin.py b/ckanext/validation/plugin/pylons_plugin.py new file mode 100644 index 00000000..2f538d76 --- /dev/null +++ b/ckanext/validation/plugin/pylons_plugin.py @@ -0,0 +1,18 @@ +from ckan import plugins as p + + +class ValidationMixin(p.SingletonPlugin): + p.implements(p.IRoutes, inherit=True) + + # IRoutes + + def before_map(self, map_): + + controller = u'ckanext.validation.controller:ValidationController' + + map_.connect( + u'validation_read', + u'/dataset/{id}/resource/{resource_id}/validation', + controller=controller, action=u'validation') + + return map_ diff --git a/ckanext/validation/views.py b/ckanext/validation/views.py new file mode 100644 index 00000000..c17f44ee --- /dev/null +++ b/ckanext/validation/views.py @@ -0,0 +1,47 @@ +# encoding: utf-8 + +from flask import Blueprint + +from ckantoolkit import ( + c, NotAuthorized, ObjectNotFound, + abort, _, render, get_action) + +validation = Blueprint("service_proxy", __name__) + + +def validation_read(self, id, resource_id): + + try: + validation = get_action(u'resource_validation_show')( + {u'user': c.user}, + {u'resource_id': resource_id}) + + resource = get_action(u'resource_show')( + {u'user': c.user}, + {u'id': resource_id}) + + dataset = get_action(u'package_show')( + {u'user': c.user}, + {u'id': resource[u'package_id']}) + + # Needed for core resource templates + c.package = c.pkg_dict = dataset + c.resource = resource + + return render(u'validation/validation_read.html', extra_vars={ + u'validation': validation, + u'resource': resource, + u'dataset': dataset, + }) + + except NotAuthorized: + abort(403, _(u'Unauthorized to read this validation report')) + except ObjectNotFound: + + abort(404, _(u'No validation report exists for this resource')) + + +validation.add_url_rule( + '/dataset/{id}/resource/{resource_id}/validation', + view_func=validation_read +) From 0c9d039b9d0a5c27d200bacfc2c35bae208fecea Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 14 Sep 2021 14:27:58 +0200 Subject: [PATCH 06/32] Fix logic tests --- ckanext/validation/tests/fixtures.py | 9 + ckanext/validation/tests/helpers.py | 18 +- ckanext/validation/tests/test_logic.py | 847 ++++++++++++------------- ckanext/validation/validators.py | 2 +- conftest.py | 5 + 5 files changed, 452 insertions(+), 429 deletions(-) create mode 100644 ckanext/validation/tests/fixtures.py create mode 100644 conftest.py diff --git a/ckanext/validation/tests/fixtures.py b/ckanext/validation/tests/fixtures.py new file mode 100644 index 00000000..965056e0 --- /dev/null +++ b/ckanext/validation/tests/fixtures.py @@ -0,0 +1,9 @@ +import pytest + +from ckanext.validation.model import create_tables, tables_exist + + +@pytest.fixture +def validation_setup(): + if not tables_exist(): + create_tables() diff --git a/ckanext/validation/tests/helpers.py b/ckanext/validation/tests/helpers.py index 4ea04d6f..9bba0aa2 100644 --- a/ckanext/validation/tests/helpers.py +++ b/ckanext/validation/tests/helpers.py @@ -1,7 +1,9 @@ -import __builtin__ as builtins +from six.moves import builtins import cgi import functools import mock +import six +from six import StringIO, BytesIO from pyfakefs import fake_filesystem @@ -148,3 +150,17 @@ def __init__(self, fp, filename): self.filename = filename self.name = 'upload' self.list = None + + def __bool__(self): + return self.file is not None + + +def get_mock_file(contents): + if six.PY3: + mock_file = BytesIO() + mock_file.write(contents.encode('utf8')) + else: + mock_file = StringIO() + mock_file.write(contents) + + return mock_file diff --git a/ckanext/validation/tests/test_logic.py b/ckanext/validation/tests/test_logic.py index 4815338a..3bf990cb 100644 --- a/ckanext/validation/tests/test_logic.py +++ b/ckanext/validation/tests/test_logic.py @@ -1,820 +1,813 @@ import datetime -import StringIO +import six +from six import StringIO, BytesIO import io import json -from nose.tools import assert_raises, assert_equals +import pytest import mock from ckan import model -from ckan.tests.helpers import ( - call_action, call_auth, change_config, reset_db, FunctionalTestBase -) +from ckan.tests.helpers import call_action, call_auth from ckan.tests import factories +from ckan.tests.helpers import _get_test_app import ckantoolkit as t -from ckanext.validation.model import create_tables, tables_exist, Validation +from ckanext.validation.model import Validation from ckanext.validation.tests.helpers import ( - VALID_CSV, INVALID_CSV, VALID_REPORT, - mock_uploads, MockFieldStorage + VALID_CSV, + INVALID_CSV, + VALID_REPORT, + mock_uploads, + MockFieldStorage, + get_mock_file, ) Session = model.Session +@pytest.mark.usefixtures("clean_db", "validation_setup") class TestResourceValidationRun(object): - - def setup(self): - - # We don't use FunctionalTestBase here as we need to change the config - # in individual tests - - reset_db() - - if not tables_exist(): - create_tables() - def test_resource_validation_run_param_missing(self): - assert_raises( - t.ValidationError, - call_action, 'resource_validation_run') + pytest.raises(t.ValidationError, call_action, "resource_validation_run") def test_resource_validation_run_not_exists(self): - assert_raises( + pytest.raises( t.ObjectNotFound, - call_action, 'resource_validation_run', resource_id='not_exists') + call_action, + "resource_validation_run", + resource_id="not_exists", + ) def test_resource_validation_wrong_format(self): - resource = factories.Resource(format='pdf') + resource = factories.Resource(format="pdf") - with assert_raises(t.ValidationError) as e: + with pytest.raises(t.ValidationError) as e: - call_action('resource_validation_run', resource_id=resource['id']) + call_action("resource_validation_run", resource_id=resource["id"]) - assert 'Unsupported resource format' in str(e.exception) + assert "Unsupported resource format" in str(e) def test_resource_validation_no_url_or_upload(self): - resource = factories.Resource(url='', format='csv') + resource = factories.Resource(url="", format="csv") - with assert_raises(t.ValidationError) as e: + with pytest.raises(t.ValidationError) as e: - call_action('resource_validation_run', resource_id=resource['id']) + call_action("resource_validation_run", resource_id=resource["id"]) - assert 'Resource must have a valid URL' in str(e.exception) + assert "Resource must have a valid URL" in str(e) - @mock.patch('ckanext.validation.logic.enqueue_job') + @mock.patch("ckanext.validation.logic.enqueue_job") def test_resource_validation_with_url(self, mock_enqueue_job): - resource = factories.Resource(url='http://example.com', format='csv') + resource = factories.Resource(url="http://example.com", format="csv") - call_action('resource_validation_run', resource_id=resource['id']) + call_action("resource_validation_run", resource_id=resource["id"]) - @mock.patch('ckanext.validation.logic.enqueue_job') + @mock.patch("ckanext.validation.logic.enqueue_job") def test_resource_validation_with_upload(self, mock_enqueue_job): - resource = factories.Resource(url='', url_type='upload', format='csv') + resource = factories.Resource(url="", url_type="upload", format="csv") - call_action('resource_validation_run', resource_id=resource['id']) + call_action("resource_validation_run", resource_id=resource["id"]) def test_resource_validation_run_starts_job(self): - resource = factories.Resource(format='csv') + resource = factories.Resource(format="csv") - jobs = call_action('job_list') + jobs = call_action("job_list") - call_action('resource_validation_run', resource_id=resource['id']) + call_action("resource_validation_run", resource_id=resource["id"]) - jobs_after = call_action('job_list') + jobs_after = call_action("job_list") assert len(jobs_after) == len(jobs) + 1 - @mock.patch('ckanext.validation.logic.enqueue_job') - def test_resource_validation_creates_validation_object( - self, mock_enqueue_job): + @mock.patch("ckanext.validation.logic.enqueue_job") + def test_resource_validation_creates_validation_object(self, mock_enqueue_job): - resource = factories.Resource(format='csv') + resource = factories.Resource(format="csv") - call_action('resource_validation_run', resource_id=resource['id']) + call_action("resource_validation_run", resource_id=resource["id"]) - validation = Session.query(Validation).filter( - Validation.resource_id == resource['id']).one() + validation = ( + Session.query(Validation) + .filter(Validation.resource_id == resource["id"]) + .one() + ) - assert_equals(validation.resource_id, resource['id']) - assert_equals(validation.status, 'created') + assert validation.resource_id == resource["id"] + assert validation.status == "created" assert validation.created - assert_equals(validation.finished, None) - assert_equals(validation.report, None) - assert_equals(validation.error, None) + assert validation.finished is None + assert validation.report is None + assert validation.error is None - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_resource_validation_resets_existing_validation_object( - self, mock_enqueue_job): + self, mock_enqueue_job + ): - resource = {'format': 'CSV', 'url': 'https://some.url'} + resource = {"format": "CSV", "url": "https://some.url"} dataset = factories.Dataset(resources=[resource]) timestamp = datetime.datetime.utcnow() old_validation = Validation( - resource_id=dataset['resources'][0]['id'], + resource_id=dataset["resources"][0]["id"], created=timestamp, finished=timestamp, - status='valid', - report={'some': 'report'}, - error={'some': 'error'}) + status="valid", + report={"some": "report"}, + error={"some": "error"}, + ) Session.add(old_validation) Session.commit() call_action( - 'resource_validation_run', - resource_id=dataset['resources'][0]['id'] + "resource_validation_run", resource_id=dataset["resources"][0]["id"] ) - validation = Session.query(Validation).filter( - Validation.resource_id == dataset['resources'][0]['id']).one() + validation = ( + Session.query(Validation) + .filter(Validation.resource_id == dataset["resources"][0]["id"]) + .one() + ) - assert_equals(validation.resource_id, dataset['resources'][0]['id']) - assert_equals(validation.status, 'created') + assert validation.resource_id == dataset["resources"][0]["id"] + assert validation.status == "created" assert validation.created is not timestamp - assert_equals(validation.finished, None) - assert_equals(validation.report, None) - assert_equals(validation.error, None) - + assert validation.finished is None + assert validation.report is None + assert validation.error is None -class TestResourceValidationShow(FunctionalTestBase): - - def setup(self): - - super(TestResourceValidationShow, self).setup() - - if not tables_exist(): - create_tables() +@pytest.mark.usefixtures("clean_db", "validation_setup") +class TestResourceValidationShow(object): def test_resource_validation_show_param_missing(self): - assert_raises( - t.ValidationError, - call_action, 'resource_validation_show') + pytest.raises(t.ValidationError, call_action, "resource_validation_show") def test_resource_validation_show_not_exists(self): - assert_raises( + pytest.raises( t.ObjectNotFound, - call_action, 'resource_validation_show', resource_id='not_exists') + call_action, + "resource_validation_show", + resource_id="not_exists", + ) - @change_config('ckanext.validation.run_on_create_async', False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) def test_resource_validation_show_validation_does_not_exists(self): - resource = {'format': 'CSV', 'url': 'https://some.url'} + resource = {"format": "CSV", "url": "https://some.url"} dataset = factories.Dataset(resources=[resource]) - assert_raises( + pytest.raises( t.ObjectNotFound, - call_action, 'resource_validation_show', - resource_id=dataset['resources'][0]['id']) + call_action, + "resource_validation_show", + resource_id=dataset["resources"][0]["id"], + ) - @change_config('ckanext.validation.run_on_create_async', False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) def test_resource_validation_show_returns_all_fields(self): - resource = {'format': 'CSV', 'url': 'https://some.url'} + resource = {"format": "CSV", "url": "https://some.url"} dataset = factories.Dataset(resources=[resource]) timestamp = datetime.datetime.utcnow() validation = Validation( - resource_id=dataset['resources'][0]['id'], + resource_id=dataset["resources"][0]["id"], created=timestamp, finished=timestamp, - status='valid', - report={'some': 'report'}, - error={'some': 'error'}) + status="valid", + report={"some": "report"}, + error={"some": "error"}, + ) Session.add(validation) Session.commit() validation_show = call_action( - 'resource_validation_show', - resource_id=dataset['resources'][0]['id']) - - assert_equals(validation_show['id'], validation.id) - assert_equals(validation_show['resource_id'], validation.resource_id) - assert_equals(validation_show['status'], validation.status) - assert_equals(validation_show['report'], validation.report) - assert_equals(validation_show['error'], validation.error) - assert_equals( - validation_show['created'], validation.created.isoformat()) - assert_equals( - validation_show['finished'], validation.finished.isoformat()) - - -class TestResourceValidationDelete(FunctionalTestBase): - - def setup(self): + "resource_validation_show", resource_id=dataset["resources"][0]["id"] + ) - super(TestResourceValidationDelete, self).setup() + assert validation_show["id"] == validation.id + assert validation_show["resource_id"] == validation.resource_id + assert validation_show["status"] == validation.status + assert validation_show["report"] == validation.report + assert validation_show["error"] == validation.error + assert validation_show["created"] == validation.created.isoformat() + assert validation_show["finished"] == validation.finished.isoformat() - if not tables_exist(): - create_tables() +@pytest.mark.usefixtures("clean_db", "validation_setup") +class TestResourceValidationDelete(object): def test_resource_validation_delete_param_missing(self): - assert_raises( - t.ValidationError, - call_action, 'resource_validation_delete') + pytest.raises(t.ValidationError, call_action, "resource_validation_delete") def test_resource_validation_delete_not_exists(self): - assert_raises( + pytest.raises( t.ObjectNotFound, - call_action, 'resource_validation_delete', - resource_id='not_exists') + call_action, + "resource_validation_delete", + resource_id="not_exists", + ) - @change_config('ckanext.validation.run_on_create_async', False) - @change_config('ckanext.validation.run_on_update_async', False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_async", False) def test_resource_validation_delete_removes_object(self): - resource = factories.Resource(format='csv') + resource = factories.Resource(format="csv") timestamp = datetime.datetime.utcnow() validation = Validation( - resource_id=resource['id'], + resource_id=resource["id"], created=timestamp, finished=timestamp, - status='valid', - report={'some': 'report'}, - error={'some': 'error'}) + status="valid", + report={"some": "report"}, + error={"some": "error"}, + ) Session.add(validation) Session.commit() - count_before = Session.query(Validation).filter( - Validation.resource_id == resource['id']).count() - - assert_equals(count_before, 1) - - call_action('resource_validation_delete', resource_id=resource['id']) - - count_after = Session.query(Validation).filter( - Validation.resource_id == resource['id']).count() - - assert_equals(count_after, 0) + count_before = ( + Session.query(Validation) + .filter(Validation.resource_id == resource["id"]) + .count() + ) + assert count_before == 1 -class TestAuth(FunctionalTestBase): + call_action("resource_validation_delete", resource_id=resource["id"]) - def setup(self): + count_after = ( + Session.query(Validation) + .filter(Validation.resource_id == resource["id"]) + .count() + ) - super(TestAuth, self).setup() + assert count_after == 0 - if not tables_exist(): - create_tables() +@pytest.mark.usefixtures("clean_db", "validation_setup") +class TestAuth(object): def test_run_anon(self): resource = factories.Resource() - context = { - 'user': None, - 'model': model - } + context = {"user": None, "model": model} - assert_raises(t.NotAuthorized, - call_auth, 'resource_validation_run', context=context, - resource_id=resource['id']) + pytest.raises( + t.NotAuthorized, + call_auth, + "resource_validation_run", + context=context, + resource_id=resource["id"], + ) def test_run_sysadmin(self): resource = factories.Resource() sysadmin = factories.Sysadmin() - context = { - 'user': sysadmin['name'], - 'model': model - } + context = {"user": sysadmin["name"], "model": model} - assert_equals(call_auth('resource_validation_run', context=context, - resource_id=resource['id']), - True) + assert ( + call_auth( + "resource_validation_run", context=context, resource_id=resource["id"] + ) + is True + ) def test_run_non_auth_user(self): user = factories.User() org = factories.Organization() dataset = factories.Dataset( - owner_org=org['id'], resources=[factories.Resource()]) + owner_org=org["id"], resources=[factories.Resource()] + ) - context = { - 'user': user['name'], - 'model': model - } + context = {"user": user["name"], "model": model} - assert_raises(t.NotAuthorized, - call_auth, 'resource_validation_run', context=context, - resource_id=dataset['resources'][0]['id']) + pytest.raises( + t.NotAuthorized, + call_auth, + "resource_validation_run", + context=context, + resource_id=dataset["resources"][0]["id"], + ) def test_run_auth_user(self): user = factories.User() org = factories.Organization( - users=[{'name': user['name'], 'capacity': 'editor'}]) + users=[{"name": user["name"], "capacity": "editor"}] + ) dataset = factories.Dataset( - owner_org=org['id'], resources=[factories.Resource()]) + owner_org=org["id"], resources=[factories.Resource()] + ) - context = { - 'user': user['name'], - 'model': model - } + context = {"user": user["name"], "model": model} - assert_equals(call_auth('resource_validation_run', context=context, - resource_id=dataset['resources'][0]['id']), - True) + assert ( + call_auth( + "resource_validation_run", + context=context, + resource_id=dataset["resources"][0]["id"], + ) + is True + ) def test_delete_anon(self): resource = factories.Resource() - context = { - 'user': None, - 'model': model - } + context = {"user": None, "model": model} - assert_raises(t.NotAuthorized, - call_auth, 'resource_validation_delete', context=context, - resource_id=resource['id']) + pytest.raises( + t.NotAuthorized, + call_auth, + "resource_validation_delete", + context=context, + resource_id=resource["id"], + ) def test_delete_sysadmin(self): resource = factories.Resource() sysadmin = factories.Sysadmin() - context = { - 'user': sysadmin['name'], - 'model': model - } + context = {"user": sysadmin["name"], "model": model} - assert_equals(call_auth('resource_validation_delete', context=context, - resource_id=resource['id']), - True) + assert ( + call_auth( + "resource_validation_delete", + context=context, + resource_id=resource["id"], + ) + is True + ) def test_delete_non_auth_user(self): user = factories.User() org = factories.Organization() dataset = factories.Dataset( - owner_org=org['id'], resources=[factories.Resource()]) + owner_org=org["id"], resources=[factories.Resource()] + ) - context = { - 'user': user['name'], - 'model': model - } + context = {"user": user["name"], "model": model} - assert_raises(t.NotAuthorized, - call_auth, 'resource_validation_delete', context=context, - resource_id=dataset['resources'][0]['id']) + pytest.raises( + t.NotAuthorized, + call_auth, + "resource_validation_delete", + context=context, + resource_id=dataset["resources"][0]["id"], + ) def test_delete_auth_user(self): user = factories.User() org = factories.Organization( - users=[{'name': user['name'], 'capacity': 'editor'}]) + users=[{"name": user["name"], "capacity": "editor"}] + ) dataset = factories.Dataset( - owner_org=org['id'], resources=[factories.Resource()]) + owner_org=org["id"], resources=[factories.Resource()] + ) - context = { - 'user': user['name'], - 'model': model - } + context = {"user": user["name"], "model": model} - assert_equals(call_auth('resource_validation_delete', context=context, - resource_id=dataset['resources'][0]['id']), - True) + assert ( + call_auth( + "resource_validation_delete", + context=context, + resource_id=dataset["resources"][0]["id"], + ) + is True + ) def test_show_anon(self): resource = factories.Resource() - context = { - 'user': None, - 'model': model - } + context = {"user": None, "model": model} - assert_equals(call_auth('resource_validation_show', context=context, - resource_id=resource['id']), - True) + assert ( + call_auth( + "resource_validation_show", context=context, resource_id=resource["id"] + ) + is True + ) def test_show_anon_public_dataset(self): user = factories.User() org = factories.Organization() dataset = factories.Dataset( - owner_org=org['id'], resources=[factories.Resource()], - private=False) + owner_org=org["id"], resources=[factories.Resource()], private=False + ) - context = { - 'user': user['name'], - 'model': model - } + context = {"user": user["name"], "model": model} - assert_equals(call_auth('resource_validation_show', context=context, - resource_id=dataset['resources'][0]['id']), - True) + assert ( + call_auth( + "resource_validation_show", + context=context, + resource_id=dataset["resources"][0]["id"], + ) + is True + ) def test_show_anon_private_dataset(self): user = factories.User() org = factories.Organization() dataset = factories.Dataset( - owner_org=org['id'], resources=[factories.Resource()], - private=True) - - context = { - 'user': user['name'], - 'model': model - } + owner_org=org["id"], resources=[factories.Resource()], private=True + ) - assert_raises(t.NotAuthorized, - call_auth, 'resource_validation_run', context=context, - resource_id=dataset['resources'][0]['id']) + context = {"user": user["name"], "model": model} + pytest.raises( + t.NotAuthorized, + call_auth, + "resource_validation_run", + context=context, + resource_id=dataset["resources"][0]["id"], + ) -class TestResourceValidationOnCreate(FunctionalTestBase): +@pytest.mark.usefixtures("clean_db", "validation_setup") +class TestResourceValidationOnCreate(object): @classmethod - def _apply_config_changes(cls, cfg): - cfg['ckanext.validation.run_on_create_sync'] = True - - def setup(self): + def setup_class(cls): + # Needed to apply the config changes at the right time so they can be picked up + # during startup + cls._original_config = dict(t.config) + t.config["ckanext.validation.run_on_create_sync"] = True - super(TestResourceValidationOnCreate, self).setup() + @classmethod + def teardown_class(cls): - if not tables_exist(): - create_tables() + t.config.clear() + t.config.update(cls._original_config) @mock_uploads def test_validation_fails_on_upload(self, mock_open): - invalid_file = StringIO.StringIO() - invalid_file.write(INVALID_CSV) + # This is needed to trigger a new environment reload that will reload the + # actions + _get_test_app() + + invalid_file = get_mock_file(INVALID_CSV) - mock_upload = MockFieldStorage(invalid_file, 'invalid.csv') + mock_upload = MockFieldStorage(invalid_file, "invalid.csv") dataset = factories.Dataset() - invalid_stream = io.BufferedReader(io.BytesIO(INVALID_CSV)) + invalid_stream = io.BufferedReader(io.BytesIO(INVALID_CSV.encode('utf8'))) - with mock.patch('io.open', return_value=invalid_stream): + with mock.patch("io.open", return_value=invalid_stream): - with assert_raises(t.ValidationError) as e: + with pytest.raises(t.ValidationError) as e: - call_action( - 'resource_create', - package_id=dataset['id'], - format='CSV', - upload=mock_upload - ) + call_action( + "resource_create", + package_id=dataset["id"], + format="CSV", + upload=mock_upload, + ) - assert 'validation' in e.exception.error_dict - assert 'missing-value' in str(e.exception) - assert 'Row 2 has a missing value in column 4' in str(e.exception) + assert "validation" in e.value.error_dict + assert "missing-value" in str(e) + assert "Row 2 has a missing value in column 4" in str(e) @mock_uploads def test_validation_fails_no_validation_object_stored(self, mock_open): - invalid_file = StringIO.StringIO() - invalid_file.write(INVALID_CSV) + # This is needed to trigger a new environment reload that will reload the + # actions + _get_test_app() + + invalid_file = get_mock_file(INVALID_CSV) - mock_upload = MockFieldStorage(invalid_file, 'invalid.csv') + mock_upload = MockFieldStorage(invalid_file, "invalid.csv") dataset = factories.Dataset() - invalid_stream = io.BufferedReader(io.BytesIO(INVALID_CSV)) + invalid_stream = io.BufferedReader(io.BytesIO(INVALID_CSV.encode('utf8'))) validation_count_before = model.Session.query(Validation).count() - with mock.patch('io.open', return_value=invalid_stream): + with mock.patch("io.open", return_value=invalid_stream): - with assert_raises(t.ValidationError): + with pytest.raises(t.ValidationError): call_action( - 'resource_create', - package_id=dataset['id'], - format='CSV', - upload=mock_upload + "resource_create", + package_id=dataset["id"], + format="CSV", + upload=mock_upload, ) validation_count_after = model.Session.query(Validation).count() - assert_equals(validation_count_after, validation_count_before) + assert validation_count_after == validation_count_before @mock_uploads def test_validation_passes_on_upload(self, mock_open): - invalid_file = StringIO.StringIO() - invalid_file.write(VALID_CSV) + # This is needed to trigger a new environment reload that will reload the + # actions + _get_test_app() + + valid_file = get_mock_file(VALID_CSV) - mock_upload = MockFieldStorage(invalid_file, 'invalid.csv') + mock_upload = MockFieldStorage(valid_file, "invalid.csv") dataset = factories.Dataset() - valid_stream = io.BufferedReader(io.BytesIO(VALID_CSV)) + valid_stream = io.BufferedReader(io.BytesIO(VALID_CSV.encode('utf8'))) - with mock.patch('io.open', return_value=valid_stream): + with mock.patch("io.open", return_value=valid_stream): resource = call_action( - 'resource_create', - package_id=dataset['id'], - format='CSV', - upload=mock_upload + "resource_create", + package_id=dataset["id"], + format="CSV", + upload=mock_upload, ) - assert_equals(resource['validation_status'], 'success') - assert 'validation_timestamp' in resource + assert resource["validation_status"] == "success" + assert "validation_timestamp" in resource - @mock.patch('ckanext.validation.jobs.validate', - return_value=VALID_REPORT) + @mock.patch("ckanext.validation.jobs.validate", return_value=VALID_REPORT) def test_validation_passes_with_url(self, mock_validate): - url = 'https://example.com/valid.csv' + url = "https://example.com/valid.csv" dataset = factories.Dataset() resource = call_action( - 'resource_create', - package_id=dataset['id'], - format='csv', + "resource_create", + package_id=dataset["id"], + format="csv", url=url, ) - assert_equals(resource['validation_status'], 'success') - assert 'validation_timestamp' in resource + assert resource["validation_status"] == "success" + assert "validation_timestamp" in resource -class TestResourceValidationOnUpdate(FunctionalTestBase): - +@pytest.mark.usefixtures("clean_db", "validation_setup") +class TestResourceValidationOnUpdate(object): @classmethod - def _apply_config_changes(cls, cfg): - cfg['ckanext.validation.run_on_update_sync'] = True - - def setup(self): + def setup_class(cls): + # Needed to apply the config changes at the right time so they can be picked up + # during startup + cls._original_config = dict(t.config) + t.config["ckanext.validation.run_on_update_sync"] = True - super(TestResourceValidationOnUpdate, self).setup() + @classmethod + def teardown_class(cls): - if not tables_exist(): - create_tables() + t.config.clear() + t.config.update(cls._original_config) @mock_uploads def test_validation_fails_on_upload(self, mock_open): - dataset = factories.Dataset(resources=[ - { - 'url': 'https://example.com/data.csv' - } - ]) + _get_test_app() - invalid_file = StringIO.StringIO() - invalid_file.write(INVALID_CSV) + dataset = factories.Dataset(resources=[{"url": "https://example.com/data.csv"}]) - mock_upload = MockFieldStorage(invalid_file, 'invalid.csv') + invalid_file = get_mock_file(INVALID_CSV) - invalid_stream = io.BufferedReader(io.BytesIO(INVALID_CSV)) + mock_upload = MockFieldStorage(invalid_file, "invalid.csv") - with mock.patch('io.open', return_value=invalid_stream): + invalid_stream = io.BufferedReader(io.BytesIO(INVALID_CSV.encode('utf8'))) - with assert_raises(t.ValidationError) as e: + with mock.patch("io.open", return_value=invalid_stream): + + with pytest.raises(t.ValidationError) as e: call_action( - 'resource_update', - id=dataset['resources'][0]['id'], - format='CSV', - upload=mock_upload + "resource_update", + id=dataset["resources"][0]["id"], + format="CSV", + upload=mock_upload, ) - assert 'validation' in e.exception.error_dict - assert 'missing-value' in str(e.exception) - assert 'Row 2 has a missing value in column 4' in str(e.exception) + assert "validation" in e.value.error_dict + assert "missing-value" in str(e) + assert "Row 2 has a missing value in column 4" in str(e) @mock_uploads def test_validation_fails_no_validation_object_stored(self, mock_open): - dataset = factories.Dataset(resources=[ - { - 'url': 'https://example.com/data.csv' - } - ]) + _get_test_app() + + dataset = factories.Dataset(resources=[{"url": "https://example.com/data.csv"}]) - invalid_file = StringIO.StringIO() - invalid_file.write(INVALID_CSV) + invalid_file = get_mock_file(INVALID_CSV) - mock_upload = MockFieldStorage(invalid_file, 'invalid.csv') + mock_upload = MockFieldStorage(invalid_file, "invalid.csv") - invalid_stream = io.BufferedReader(io.BytesIO(INVALID_CSV)) + invalid_stream = io.BufferedReader(io.BytesIO(INVALID_CSV.encode('utf8'))) - with mock.patch('io.open', return_value=invalid_stream): + with mock.patch("io.open", return_value=invalid_stream): - with assert_raises(t.ValidationError): + with pytest.raises(t.ValidationError): call_action( - 'resource_update', - id=dataset['resources'][0]['id'], - format='CSV', - upload=mock_upload + "resource_update", + id=dataset["resources"][0]["id"], + format="CSV", + upload=mock_upload, ) validation_count_after = model.Session.query(Validation).count() - assert_equals(validation_count_after, 0) + assert validation_count_after == 0 @mock_uploads def test_validation_passes_on_upload(self, mock_open): - dataset = factories.Dataset(resources=[ - { - 'url': 'https://example.com/data.csv' - } - ]) + _get_test_app() + + dataset = factories.Dataset(resources=[{"url": "https://example.com/data.csv"}]) - valid_file = StringIO.StringIO() - valid_file.write(INVALID_CSV) + valid_file = get_mock_file(VALID_CSV) - mock_upload = MockFieldStorage(valid_file, 'valid.csv') + mock_upload = MockFieldStorage(valid_file, "valid.csv") - valid_stream = io.BufferedReader(io.BytesIO(VALID_CSV)) + valid_stream = io.BufferedReader(io.BytesIO(VALID_CSV.encode('utf8'))) - with mock.patch('io.open', return_value=valid_stream): + with mock.patch("io.open", return_value=valid_stream): resource = call_action( - 'resource_update', - id=dataset['resources'][0]['id'], - format='CSV', - upload=mock_upload + "resource_update", + id=dataset["resources"][0]["id"], + format="CSV", + upload=mock_upload, ) - assert_equals(resource['validation_status'], 'success') - assert 'validation_timestamp' in resource + assert resource["validation_status"] == "success" + assert "validation_timestamp" in resource - @mock.patch('ckanext.validation.jobs.validate', - return_value=VALID_REPORT) + @mock.patch("ckanext.validation.jobs.validate", return_value=VALID_REPORT) def test_validation_passes_with_url(self, mock_validate): - dataset = factories.Dataset(resources=[ - { - 'url': 'https://example.com/data.csv' - } - ]) + dataset = factories.Dataset(resources=[{"url": "https://example.com/data.csv"}]) resource = call_action( - 'resource_update', - id=dataset['resources'][0]['id'], - format='CSV', - url='https://example.com/some.other.csv', + "resource_update", + id=dataset["resources"][0]["id"], + format="CSV", + url="https://example.com/some.other.csv", ) - assert_equals(resource['validation_status'], 'success') - assert 'validation_timestamp' in resource + assert resource["validation_status"] == "success" + assert "validation_timestamp" in resource -class TestSchemaFields(FunctionalTestBase): - - def setup(self): - - super(TestSchemaFields, self).setup() - - if not tables_exist(): - create_tables() - +@pytest.mark.usefixtures("clean_db", "validation_setup") +class TestSchemaFields(object): def test_schema_field(self): dataset = factories.Dataset() resource = call_action( - 'resource_create', - package_id=dataset['id'], - url='http://example.com/file.csv', - schema='{"fields":[{"name":"id"}]}' + "resource_create", + package_id=dataset["id"], + url="http://example.com/file.csv", + schema='{"fields":[{"name":"id"}]}', ) - assert_equals(resource['schema'], {'fields': [{'name': 'id'}]}) + assert resource["schema"] == {"fields": [{"name": "id"}]} - assert 'schema_upload' not in resource - assert 'schema_url' not in resource + assert "schema_upload" not in resource + assert "schema_url" not in resource def test_schema_field_url(self): - url = 'https://example.com/schema.json' + url = "https://example.com/schema.json" dataset = factories.Dataset() resource = call_action( - 'resource_create', - package_id=dataset['id'], - url='http://example.com/file.csv', - schema=url + "resource_create", + package_id=dataset["id"], + url="http://example.com/file.csv", + schema=url, ) - assert_equals(resource['schema'], url) + assert resource["schema"] == url - assert 'schema_upload' not in resource - assert 'schema_url' not in resource + assert "schema_upload" not in resource + assert "schema_url" not in resource def test_schema_url_field(self): - url = 'https://example.com/schema.json' + url = "https://example.com/schema.json" dataset = factories.Dataset() resource = call_action( - 'resource_create', - package_id=dataset['id'], - url='http://example.com/file.csv', - schema_url=url + "resource_create", + package_id=dataset["id"], + url="http://example.com/file.csv", + schema_url=url, ) - assert_equals(resource['schema'], url) + assert resource["schema"] == url - assert 'schema_upload' not in resource - assert 'schema_url' not in resource + assert "schema_upload" not in resource + assert "schema_url" not in resource def test_schema_url_field_wrong_url(self): - url = 'not-a-url' + url = "not-a-url" - assert_raises( - t.ValidationError, call_action, 'resource_create', - url='http://example.com/file.csv', - schema_url=url + pytest.raises( + t.ValidationError, + call_action, + "resource_create", + url="http://example.com/file.csv", + schema_url=url, ) @mock_uploads def test_schema_upload_field(self, mock_open): - schema_file = StringIO.StringIO('{"fields":[{"name":"category"}]}') + schema_file = StringIO('{"fields":[{"name":"category"}]}') - mock_upload = MockFieldStorage(schema_file, 'schema.json') + mock_upload = MockFieldStorage(schema_file, "schema.json") dataset = factories.Dataset() resource = call_action( - 'resource_create', - package_id=dataset['id'], - url='http://example.com/file.csv', - schema_upload=mock_upload + "resource_create", + package_id=dataset["id"], + url="http://example.com/file.csv", + schema_upload=mock_upload, ) - assert_equals(resource['schema'], {'fields': [{'name': 'category'}]}) - - assert 'schema_upload' not in resource - assert 'schema_url' not in resource + assert resource["schema"] == {"fields": [{"name": "category"}]} + assert "schema_upload" not in resource + assert "schema_url" not in resource -class TestValidationOptionsField(FunctionalTestBase): - - def setup(self): - - super(TestValidationOptionsField, self).setup() - - if not tables_exist(): - create_tables() +@pytest.mark.usefixtures("clean_db", "validation_setup") +class TestValidationOptionsField(object): def test_validation_options_field(self): dataset = factories.Dataset() validation_options = { - 'delimiter': ';', - 'headers': 2, - 'skip_rows': ['#'], + "delimiter": ";", + "headers": 2, + "skip_rows": ["#"], } resource = call_action( - 'resource_create', - package_id=dataset['id'], - url='http://example.com/file.csv', + "resource_create", + package_id=dataset["id"], + url="http://example.com/file.csv", validation_options=validation_options, ) - assert_equals(resource['validation_options'], validation_options) + assert resource["validation_options"] == validation_options def test_validation_options_field_string(self): dataset = factories.Dataset() - validation_options = '''{ + validation_options = """{ "delimiter": ";", "headers": 2, "skip_rows": ["#"] - }''' + }""" resource = call_action( - 'resource_create', - package_id=dataset['id'], - url='http://example.com/file.csv', + "resource_create", + package_id=dataset["id"], + url="http://example.com/file.csv", validation_options=validation_options, ) - assert_equals(resource['validation_options'], - json.loads(validation_options)) + assert resource["validation_options"] == json.loads(validation_options) diff --git a/ckanext/validation/validators.py b/ckanext/validation/validators.py index ad466749..7aa889c9 100644 --- a/ckanext/validation/validators.py +++ b/ckanext/validation/validators.py @@ -39,7 +39,7 @@ def resource_schema_validator(value, context): except tableschema.exceptions.ValidationError as e: errors = [] for error in e.errors: - errors.append(error.message) + errors.append(str(error)) msg = u'Invalid Table Schema: {}'.format(u', '.join(errors)) if msg: diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..ea78b539 --- /dev/null +++ b/conftest.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +pytest_plugins = [ + u'ckanext.validation.tests.fixtures', +] From 874352e69c8e786b235cb86e40e17dd2dfae00d6 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 16 Sep 2021 11:50:02 +0200 Subject: [PATCH 07/32] Fanstatic to webassets --- ckanext/validation/helpers.py | 6 ++- ckanext/validation/plugin/__init__.py | 8 ++-- .../templates/package/resource_read.html | 7 +++- .../package/snippets/resource_item.html | 6 ++- .../form_snippets/resource_schema.html | 7 +++- .../validation/snippets/validation_asset.html | 1 + .../snippets/validation_report_dialog.html | 8 +++- .../validation_report_dialog_bs2.html | 9 +++-- .../snippets/validation_resource.html | 1 + .../templates/validation/validation_read.html | 8 +++- .../{fanstatic => webassets}/.gitignore | 0 .../css/validation-report-form.css | 0 .../css/validation-report.css | 0 .../css/validation.css | 0 .../js/module-modal-dialog.js | 0 .../js/module-resource-schema.js | 0 .../js/module-validation-report.js | 0 .../{fanstatic => webassets}/resource.config | 0 .../vendor/goodtables-ui/goodtables-ui.css | 0 .../vendor/goodtables-ui/goodtables-ui.js | 0 .../goodtables-ui/goodtables-ui.min.css | 0 .../vendor/goodtables-ui/goodtables-ui.min.js | 0 ckanext/validation/webassets/webassets.yml | 39 +++++++++++++++++++ 23 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 ckanext/validation/templates/validation/snippets/validation_asset.html create mode 100644 ckanext/validation/templates/validation/snippets/validation_resource.html rename ckanext/validation/{fanstatic => webassets}/.gitignore (100%) rename ckanext/validation/{fanstatic => webassets}/css/validation-report-form.css (100%) rename ckanext/validation/{fanstatic => webassets}/css/validation-report.css (100%) rename ckanext/validation/{fanstatic => webassets}/css/validation.css (100%) rename ckanext/validation/{fanstatic => webassets}/js/module-modal-dialog.js (100%) rename ckanext/validation/{fanstatic => webassets}/js/module-resource-schema.js (100%) rename ckanext/validation/{fanstatic => webassets}/js/module-validation-report.js (100%) rename ckanext/validation/{fanstatic => webassets}/resource.config (100%) rename ckanext/validation/{fanstatic => webassets}/vendor/goodtables-ui/goodtables-ui.css (100%) rename ckanext/validation/{fanstatic => webassets}/vendor/goodtables-ui/goodtables-ui.js (100%) rename ckanext/validation/{fanstatic => webassets}/vendor/goodtables-ui/goodtables-ui.min.css (100%) rename ckanext/validation/{fanstatic => webassets}/vendor/goodtables-ui/goodtables-ui.min.js (100%) create mode 100644 ckanext/validation/webassets/webassets.yml diff --git a/ckanext/validation/helpers.py b/ckanext/validation/helpers.py index ad5c57d8..fbedb352 100644 --- a/ckanext/validation/helpers.py +++ b/ckanext/validation/helpers.py @@ -2,7 +2,7 @@ import json from ckan.lib.helpers import url_for_static -from ckantoolkit import url_for, _, config, asbool, literal +from ckantoolkit import url_for, _, config, asbool, literal, h def get_validation_badge(resource, in_listing=False): @@ -89,3 +89,7 @@ def bootstrap_version(): return '3' else: return '2' + + +def use_webassets(): + return int(h.ckan_version().split('.')[1]) >= 9 diff --git a/ckanext/validation/plugin/__init__.py b/ckanext/validation/plugin/__init__.py index a365fcee..4f0fa42b 100644 --- a/ckanext/validation/plugin/__init__.py +++ b/ckanext/validation/plugin/__init__.py @@ -23,6 +23,7 @@ validation_extract_report_from_errors, dump_json_value, bootstrap_version, + use_webassets, ) from ckanext.validation.validators import ( resource_schema_validator, @@ -64,9 +65,9 @@ def update_config(self, config_): else: log.debug(u'Validation tables exist') - t.add_template_directory(config_, u'templates') - t.add_public_directory(config_, u'public') - t.add_resource(u'fanstatic', 'ckanext-validation') + t.add_template_directory(config_, u'../templates') + t.add_public_directory(config_, u'../public') + t.add_resource(u'../webassets', 'ckanext-validation') # IActions @@ -103,6 +104,7 @@ def get_helpers(self): u'validation_extract_report_from_errors': validation_extract_report_from_errors, u'dump_json_value': dump_json_value, u'bootstrap_version': bootstrap_version, + u'use_webassets': use_webassets, } # IResourceController diff --git a/ckanext/validation/templates/package/resource_read.html b/ckanext/validation/templates/package/resource_read.html index 7d11e120..1aebef1a 100644 --- a/ckanext/validation/templates/package/resource_read.html +++ b/ckanext/validation/templates/package/resource_read.html @@ -6,6 +6,11 @@

{{ h.resource_display_name(res) | truncate(50) }} {{ h.get_validation_badge(res)|safe }}

- {% resource 'ckanext-validation/main' %} + {% if h.use_webassets() %} + {% snippet 'validation/snippets/validation_asset.html', name='ckanext-validation/validation-css' %} + {% else %} + {% snippet 'validation/snippets/validation_resource.html', name='ckanext-validation/main' %} + {% endif %} + {% endblock %} diff --git a/ckanext/validation/templates/package/snippets/resource_item.html b/ckanext/validation/templates/package/snippets/resource_item.html index 4d31fd38..f0913960 100644 --- a/ckanext/validation/templates/package/snippets/resource_item.html +++ b/ckanext/validation/templates/package/snippets/resource_item.html @@ -4,8 +4,12 @@ {{ super() }} {{ h.get_validation_badge(res, in_listing=True)|safe }} + {% if h.use_webassets() %} + {% snippet 'validation/snippets/validation_asset.html', name='ckanext-validation/validation-css' %} + {% else %} + {% snippet 'validation/snippets/validation_resource.html', name='ckanext-validation/main' %} + {% endif %} -{% resource 'ckanext-validation/main' %} {% endblock %} diff --git a/ckanext/validation/templates/scheming/form_snippets/resource_schema.html b/ckanext/validation/templates/scheming/form_snippets/resource_schema.html index 08815301..55a92146 100644 --- a/ckanext/validation/templates/scheming/form_snippets/resource_schema.html +++ b/ckanext/validation/templates/scheming/form_snippets/resource_schema.html @@ -60,7 +60,10 @@ {% set existing_value = h.scheming_display_json_value(value, indent=None) if is_json else value %} - {% resource 'ckanext-validation/resource-schema-form' %} + {% if h.use_webassets() %} + {% snippet 'validation/snippets/validation_asset.html', name='ckanext-validation/resource-schema-form' %} + {% else %} + {% snippet 'validation/snippets/validation_resource.html', name='ckanext-validation/resource-schema-form' %} + {% endif %} - diff --git a/ckanext/validation/templates/validation/snippets/validation_asset.html b/ckanext/validation/templates/validation/snippets/validation_asset.html new file mode 100644 index 00000000..211c76b4 --- /dev/null +++ b/ckanext/validation/templates/validation/snippets/validation_asset.html @@ -0,0 +1 @@ +{% asset name %} diff --git a/ckanext/validation/templates/validation/snippets/validation_report_dialog.html b/ckanext/validation/templates/validation/snippets/validation_report_dialog.html index 095973e7..812b04ab 100644 --- a/ckanext/validation/templates/validation/snippets/validation_report_dialog.html +++ b/ckanext/validation/templates/validation/snippets/validation_report_dialog.html @@ -14,5 +14,9 @@

-{% resource 'ckanext-validation/report-form' %} - +{% if h.use_webassets() %} + {% snippet 'validation/snippets/validation_asset.html', name='ckanext-validation/report-form-css' %} + {% snippet 'validation/snippets/validation_asset.html', name='ckanext-validation/report-form-js' %} +{% else %} + {% snippet 'validation/snippets/validation_resource.html', name='ckanext-validation/report-form' %} +{% endif %} diff --git a/ckanext/validation/templates/validation/snippets/validation_report_dialog_bs2.html b/ckanext/validation/templates/validation/snippets/validation_report_dialog_bs2.html index b5c2de4f..1d1ac758 100644 --- a/ckanext/validation/templates/validation/snippets/validation_report_dialog_bs2.html +++ b/ckanext/validation/templates/validation/snippets/validation_report_dialog_bs2.html @@ -7,6 +7,9 @@

{{ _('Data Validation Report') }}

- -{% resource 'ckanext-validation/report-form' %} - +{% if h.use_webassets() %} + {% snippet 'validation/snippets/validation_asset.html', name='ckanext-validation/report-form-css' %} + {% snippet 'validation/snippets/validation_asset.html', name='ckanext-validation/report-form-js' %} +{% else %} + {% snippet 'validation/snippets/validation_resource.html', name='ckanext-validation/report-form' %} +{% endif %} diff --git a/ckanext/validation/templates/validation/snippets/validation_resource.html b/ckanext/validation/templates/validation/snippets/validation_resource.html new file mode 100644 index 00000000..252e6ca8 --- /dev/null +++ b/ckanext/validation/templates/validation/snippets/validation_resource.html @@ -0,0 +1 @@ +{% resource name %} diff --git a/ckanext/validation/templates/validation/validation_read.html b/ckanext/validation/templates/validation/validation_read.html index d845c64c..dc5436b8 100644 --- a/ckanext/validation/templates/validation/validation_read.html +++ b/ckanext/validation/templates/validation/validation_read.html @@ -37,8 +37,12 @@

{{ h.resource_display_name(resource) | truncate(50) }} - - {% resource 'ckanext-validation/report' %} +{% if h.use_webassets() %} + {% snippet 'validation/snippets/validation_asset.html', name='ckanext-validation/report-css' %} + {% snippet 'validation/snippets/validation_asset.html', name='ckanext-validation/report-js' %} +{% else %} + {% snippet 'validation/snippets/validation_resource.html', name='ckanext-validation/report' %} +{% endif %} {% endblock %} diff --git a/ckanext/validation/fanstatic/.gitignore b/ckanext/validation/webassets/.gitignore similarity index 100% rename from ckanext/validation/fanstatic/.gitignore rename to ckanext/validation/webassets/.gitignore diff --git a/ckanext/validation/fanstatic/css/validation-report-form.css b/ckanext/validation/webassets/css/validation-report-form.css similarity index 100% rename from ckanext/validation/fanstatic/css/validation-report-form.css rename to ckanext/validation/webassets/css/validation-report-form.css diff --git a/ckanext/validation/fanstatic/css/validation-report.css b/ckanext/validation/webassets/css/validation-report.css similarity index 100% rename from ckanext/validation/fanstatic/css/validation-report.css rename to ckanext/validation/webassets/css/validation-report.css diff --git a/ckanext/validation/fanstatic/css/validation.css b/ckanext/validation/webassets/css/validation.css similarity index 100% rename from ckanext/validation/fanstatic/css/validation.css rename to ckanext/validation/webassets/css/validation.css diff --git a/ckanext/validation/fanstatic/js/module-modal-dialog.js b/ckanext/validation/webassets/js/module-modal-dialog.js similarity index 100% rename from ckanext/validation/fanstatic/js/module-modal-dialog.js rename to ckanext/validation/webassets/js/module-modal-dialog.js diff --git a/ckanext/validation/fanstatic/js/module-resource-schema.js b/ckanext/validation/webassets/js/module-resource-schema.js similarity index 100% rename from ckanext/validation/fanstatic/js/module-resource-schema.js rename to ckanext/validation/webassets/js/module-resource-schema.js diff --git a/ckanext/validation/fanstatic/js/module-validation-report.js b/ckanext/validation/webassets/js/module-validation-report.js similarity index 100% rename from ckanext/validation/fanstatic/js/module-validation-report.js rename to ckanext/validation/webassets/js/module-validation-report.js diff --git a/ckanext/validation/fanstatic/resource.config b/ckanext/validation/webassets/resource.config similarity index 100% rename from ckanext/validation/fanstatic/resource.config rename to ckanext/validation/webassets/resource.config diff --git a/ckanext/validation/fanstatic/vendor/goodtables-ui/goodtables-ui.css b/ckanext/validation/webassets/vendor/goodtables-ui/goodtables-ui.css similarity index 100% rename from ckanext/validation/fanstatic/vendor/goodtables-ui/goodtables-ui.css rename to ckanext/validation/webassets/vendor/goodtables-ui/goodtables-ui.css diff --git a/ckanext/validation/fanstatic/vendor/goodtables-ui/goodtables-ui.js b/ckanext/validation/webassets/vendor/goodtables-ui/goodtables-ui.js similarity index 100% rename from ckanext/validation/fanstatic/vendor/goodtables-ui/goodtables-ui.js rename to ckanext/validation/webassets/vendor/goodtables-ui/goodtables-ui.js diff --git a/ckanext/validation/fanstatic/vendor/goodtables-ui/goodtables-ui.min.css b/ckanext/validation/webassets/vendor/goodtables-ui/goodtables-ui.min.css similarity index 100% rename from ckanext/validation/fanstatic/vendor/goodtables-ui/goodtables-ui.min.css rename to ckanext/validation/webassets/vendor/goodtables-ui/goodtables-ui.min.css diff --git a/ckanext/validation/fanstatic/vendor/goodtables-ui/goodtables-ui.min.js b/ckanext/validation/webassets/vendor/goodtables-ui/goodtables-ui.min.js similarity index 100% rename from ckanext/validation/fanstatic/vendor/goodtables-ui/goodtables-ui.min.js rename to ckanext/validation/webassets/vendor/goodtables-ui/goodtables-ui.min.js diff --git a/ckanext/validation/webassets/webassets.yml b/ckanext/validation/webassets/webassets.yml new file mode 100644 index 00000000..1e277569 --- /dev/null +++ b/ckanext/validation/webassets/webassets.yml @@ -0,0 +1,39 @@ +validation-css: + output: ckanext-validation/%(version)s_validation.css + contents: + - css/validation.css + +resource-schema-form: + filter: rjsmin + output: ckanext-validation/%(version)s_resource_schema_form.js + contents: + - js/module-resource-schema.js + +report-form-js: + filter: rjsmin + output: ckanext-validation/%(version)s_report_form.js + contents: + - vendor/goodtables-ui/goodtables-ui.js + - js/module-validation-report.js + - js/module-modal-dialog.js + +report-form-css: + output: ckanext-validation/%(version)s_validation_report_form.css + contents: + - vendor/goodtables-ui/goodtables-ui.css + - css/validation-report.css + - css/validation-report-form.css + +report-js: + filter: rjsmin + output: ckanext-validation/%(version)s_report.js + contents: + - vendor/goodtables-ui/goodtables-ui.js + - js/module-validation-report.js + + +report-css: + output: ckanext-validation/%(version)s_validation_report.css + contents: + - vendor/goodtables-ui/goodtables-ui.css + - css/validation-report.css From 1a0fb2777a486ebcd53cf287f1c63fc85bf52ddb Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 16 Sep 2021 12:05:04 +0200 Subject: [PATCH 08/32] Multiple fixes and updates for py3 --- ckanext/validation/logic.py | 1 - ckanext/validation/plugin/__init__.py | 15 +- ckanext/validation/tests/test_form.py | 677 ++++++++++++-------------- ckanext/validation/tests/test_jobs.py | 314 ++++++------ ckanext/validation/validators.py | 7 +- 5 files changed, 492 insertions(+), 522 deletions(-) diff --git a/ckanext/validation/logic.py b/ckanext/validation/logic.py index 277bab8a..bc6db3ee 100644 --- a/ckanext/validation/logic.py +++ b/ckanext/validation/logic.py @@ -481,7 +481,6 @@ def resource_create(context, data_dict): uploader.get_max_resource_size()) # Custom code starts - if get_create_mode_from_config() == u'sync': run_validation = True diff --git a/ckanext/validation/plugin/__init__.py b/ckanext/validation/plugin/__init__.py index 4f0fa42b..e914198e 100644 --- a/ckanext/validation/plugin/__init__.py +++ b/ckanext/validation/plugin/__init__.py @@ -5,6 +5,7 @@ import json import six +from werkzeug.datastructures import FileStorage as FlaskFileStorage import ckan.plugins as p import ckantoolkit as t @@ -41,6 +42,7 @@ from ckanext.validation.plugin.pylons_plugin import ValidationMixin +ALLOWED_UPLOAD_TYPES = (cgi.FieldStorage, FlaskFileStorage) log = logging.getLogger(__name__) @@ -126,9 +128,10 @@ def _process_schema_fields(self, data_dict): schema_upload = data_dict.pop(u'schema_upload', None) schema_url = data_dict.pop(u'schema_url', None) schema_json = data_dict.pop(u'schema_json', None) - - if isinstance(schema_upload, cgi.FieldStorage): - data_dict[u'schema'] = schema_upload.file.read() + if bool(schema_upload) and \ + isinstance(schema_upload, ALLOWED_UPLOAD_TYPES): + uploaded_file = _get_underlying_file(schema_upload) + data_dict[u'schema'] = uploaded_file.read() elif schema_url: if (not isinstance(schema_url, six.string_types) or @@ -299,3 +302,9 @@ def _run_async_validation(resource_id): log.warning( u'Could not run validation for resource {}: {}'.format( resource_id, str(e))) + +def _get_underlying_file(wrapper): + if isinstance(wrapper, FlaskFileStorage): + return wrapper.stream + return wrapper.file + diff --git a/ckanext/validation/tests/test_form.py b/ckanext/validation/tests/test_form.py index 29fc7b19..701b20cb 100644 --- a/ckanext/validation/tests/test_form.py +++ b/ckanext/validation/tests/test_form.py @@ -3,25 +3,22 @@ import mock import datetime -from nose.tools import assert_in, assert_equals +import pytest +import ckantoolkit as t from ckantoolkit.tests.factories import Sysadmin, Dataset from ckantoolkit.tests.helpers import ( - FunctionalTestBase, submit_and_follow, webtest_submit, call_action, - reset_db + call_action, ) -from ckanext.validation.model import create_tables, tables_exist -from ckanext.validation.tests.helpers import ( - VALID_CSV, INVALID_CSV, mock_uploads -) +from ckanext.validation.tests.helpers import VALID_CSV, INVALID_CSV, mock_uploads def _get_resource_new_page_as_sysadmin(app, id): user = Sysadmin() - env = {'REMOTE_USER': user['name'].encode('ascii')} + env = {"REMOTE_USER": user["name"].encode("ascii")} response = app.get( - url='/dataset/new_resource/{}'.format(id), + url="/dataset/new_resource/{}".format(id), extra_environ=env, ) return env, response @@ -29,516 +26,482 @@ def _get_resource_new_page_as_sysadmin(app, id): def _get_resource_update_page_as_sysadmin(app, id, resource_id): user = Sysadmin() - env = {'REMOTE_USER': user['name'].encode('ascii')} + env = {"REMOTE_USER": user["name"].encode("ascii")} response = app.get( - url='/dataset/{}/resource_edit/{}'.format(id, resource_id), + url="/dataset/{}/resource_edit/{}".format(id, resource_id), extra_environ=env, ) return env, response -class TestResourceSchemaForm(FunctionalTestBase): - - def setup(self): - reset_db() - if not tables_exist(): - create_tables() - - def test_resource_form_includes_json_fields(self): +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") +class TestResourceSchemaForm(object): + def test_resource_form_includes_json_fields(self, app): dataset = Dataset() + env, response = _get_resource_new_page_as_sysadmin(app, dataset["id"]) + assert ' Date: Thu, 16 Sep 2021 12:12:39 +0200 Subject: [PATCH 09/32] Update interface tests --- ckanext/validation/tests/test_interfaces.py | 87 ++++++++++----------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/ckanext/validation/tests/test_interfaces.py b/ckanext/validation/tests/test_interfaces.py index d538f958..b05795ab 100644 --- a/ckanext/validation/tests/test_interfaces.py +++ b/ckanext/validation/tests/test_interfaces.py @@ -1,9 +1,11 @@ import mock -from nose.tools import assert_equals +import pytest from ckan import plugins as p from ckan.tests import helpers, factories +import ckantoolkit as t + from ckanext.validation.interfaces import IDataValidation from ckanext.validation.tests.helpers import VALID_REPORT @@ -31,41 +33,33 @@ def _get_plugin_calls(): return plugin.calls -class BaseTestInterfaces(helpers.FunctionalTestBase): - - @classmethod - def setup_class(cls): - - super(BaseTestInterfaces, cls).setup_class() - - if not p.plugin_loaded('test_validation_plugin'): - p.load('test_validation_plugin') - - @classmethod - def teardown_class(cls): - - super(BaseTestInterfaces, cls).teardown_class() - - if p.plugin_loaded('test_validation_plugin'): - p.unload('test_validation_plugin') +class BaseTestInterfaces(object): def setup(self): - super(BaseTestInterfaces, self).setup() - for plugin in p.PluginImplementations(IDataValidation): return plugin.reset_counter() +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") +@pytest.mark.ckan_config("ckan.plugins", "validation test_validation_plugin scheming_datasets") class TestInterfaceSync(BaseTestInterfaces): + @classmethod + def setup_class(cls): + # Needed to apply the config changes at the right time so they can be picked up + # during startup + cls._original_config = dict(t.config) + t.config["ckanext.validation.run_on_create_sync"] = True + t.config["ckanext.validation.run_on_update_sync"] = True @classmethod - def _apply_config_changes(cls, cfg): - cfg['ckanext.validation.run_on_create_sync'] = True - cfg['ckanext.validation.run_on_update_sync'] = True + def teardown_class(cls): - @helpers.change_config('ckanext.validation.run_on_create_async', False) - @helpers.change_config('ckanext.validation.run_on_update_async', False) + t.config.clear() + t.config.update(cls._original_config) + + @pytest.mark.ckan_config('ckanext.validation.run_on_create_async', False) + @pytest.mark.ckan_config('ckanext.validation.run_on_update_async', False) @mock.patch('ckanext.validation.jobs.validate', return_value=VALID_REPORT) def test_can_validate_called_on_create_sync(self, mock_validation): @@ -77,12 +71,12 @@ def test_can_validate_called_on_create_sync(self, mock_validation): format='CSV', package_id=dataset['id'] ) - assert_equals(_get_plugin_calls(), 1) + assert _get_plugin_calls() == 1 assert mock_validation.called - @helpers.change_config('ckanext.validation.run_on_create_async', False) - @helpers.change_config('ckanext.validation.run_on_update_async', False) + @pytest.mark.ckan_config('ckanext.validation.run_on_create_async', False) + @pytest.mark.ckan_config('ckanext.validation.run_on_update_async', False) @mock.patch('ckanext.validation.jobs.validate') def test_can_validate_called_on_create_sync_no_validation(self, mock_validation): @@ -94,12 +88,12 @@ def test_can_validate_called_on_create_sync_no_validation(self, mock_validation) package_id=dataset['id'], my_custom_field='xx', ) - assert_equals(_get_plugin_calls(), 1) + assert _get_plugin_calls() == 1 assert not mock_validation.called - @helpers.change_config('ckanext.validation.run_on_create_async', False) - @helpers.change_config('ckanext.validation.run_on_update_async', False) + @pytest.mark.ckan_config('ckanext.validation.run_on_create_async', False) + @pytest.mark.ckan_config('ckanext.validation.run_on_update_async', False) @mock.patch('ckanext.validation.jobs.validate', return_value=VALID_REPORT) def test_can_validate_called_on_update_sync(self, mock_validation): @@ -113,12 +107,12 @@ def test_can_validate_called_on_update_sync(self, mock_validation): format='CSV', package_id=dataset['id'] ) - assert_equals(_get_plugin_calls(), 2) # One for create and one for update + assert _get_plugin_calls() == 2 # One for create and one for update assert mock_validation.called - @helpers.change_config('ckanext.validation.run_on_create_async', False) - @helpers.change_config('ckanext.validation.run_on_update_async', False) + @pytest.mark.ckan_config('ckanext.validation.run_on_create_async', False) + @pytest.mark.ckan_config('ckanext.validation.run_on_update_async', False) @mock.patch('ckanext.validation.jobs.validate') def test_can_validate_called_on_update_sync_no_validation(self, mock_validation): @@ -132,11 +126,12 @@ def test_can_validate_called_on_update_sync_no_validation(self, mock_validation) package_id=dataset['id'], my_custom_field='xx', ) - assert_equals(_get_plugin_calls(), 2) # One for create and one for update + assert _get_plugin_calls() == 2 # One for create and one for update assert not mock_validation.called - +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") +@pytest.mark.ckan_config("ckan.plugins", "validation test_validation_plugin scheming_datasets") class TestInterfaceAsync(BaseTestInterfaces): @classmethod @@ -144,7 +139,7 @@ def _apply_config_changes(cls, cfg): cfg['ckanext.validation.run_on_create_sync'] = False cfg['ckanext.validation.run_on_update_sync'] = False - @helpers.change_config('ckanext.validation.run_on_create_async', True) + @pytest.mark.ckan_config('ckanext.validation.run_on_create_async', True) @mock.patch('ckanext.validation.logic.enqueue_job') def test_can_validate_called_on_create_async(self, mock_validation): @@ -155,11 +150,11 @@ def test_can_validate_called_on_create_async(self, mock_validation): format='CSV', package_id=dataset['id'] ) - assert_equals(_get_plugin_calls(), 1) + assert _get_plugin_calls() == 1 assert mock_validation.called - @helpers.change_config('ckanext.validation.run_on_create_async', True) + @pytest.mark.ckan_config('ckanext.validation.run_on_create_async', True) @mock.patch('ckanext.validation.logic.enqueue_job') def test_can_validate_called_on_create_async_no_validation(self, mock_validation): @@ -171,12 +166,12 @@ def test_can_validate_called_on_create_async_no_validation(self, mock_validation package_id=dataset['id'], my_custom_field='xx', ) - assert_equals(_get_plugin_calls(), 1) + assert _get_plugin_calls() == 1 assert not mock_validation.called - @helpers.change_config('ckanext.validation.run_on_create_async', False) - @helpers.change_config('ckanext.validation.run_on_update_async', True) + @pytest.mark.ckan_config('ckanext.validation.run_on_create_async', False) + @pytest.mark.ckan_config('ckanext.validation.run_on_update_async', True) @mock.patch('ckanext.validation.logic.enqueue_job') def test_can_validate_called_on_update_async(self, mock_validation): @@ -189,12 +184,12 @@ def test_can_validate_called_on_update_async(self, mock_validation): format='CSV', package_id=dataset['id'] ) - assert_equals(_get_plugin_calls(), 1) + assert _get_plugin_calls() == 1 assert mock_validation.called - @helpers.change_config('ckanext.validation.run_on_create_async', False) - @helpers.change_config('ckanext.validation.run_on_update_async', True) + @pytest.mark.ckan_config('ckanext.validation.run_on_create_async', False) + @pytest.mark.ckan_config('ckanext.validation.run_on_update_async', True) @mock.patch('ckanext.validation.logic.enqueue_job') def test_can_validate_called_on_update_async_no_validation(self, mock_validation): @@ -209,6 +204,6 @@ def test_can_validate_called_on_update_async_no_validation(self, mock_validation my_custom_field='xx', ) - assert_equals(_get_plugin_calls(), 1) + assert _get_plugin_calls() == 1 assert not mock_validation.called From 24c09c8d6d556aa5fcdf97f018fcd9cd68703caa Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 16 Sep 2021 12:15:05 +0200 Subject: [PATCH 10/32] lint --- ckanext/validation/tests/test_form.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/ckanext/validation/tests/test_form.py b/ckanext/validation/tests/test_form.py index 701b20cb..cc548e97 100644 --- a/ckanext/validation/tests/test_form.py +++ b/ckanext/validation/tests/test_form.py @@ -378,8 +378,9 @@ def test_resource_form_create_valid(self, mock_open, app): valid_stream = io.BufferedReader(io.BytesIO(VALID_CSV)) with mock.patch("io.open", return_value=valid_stream): + pass - submit_and_follow(app, form, env, "save", upload_files=[upload]) + #submit_and_follow(app, form, env, "save", upload_files=[upload]) dataset = call_action("package_show", id=dataset["id"]) @@ -398,10 +399,11 @@ def test_resource_form_create_invalid(self, mock_open, app): invalid_stream = io.BufferedReader(io.BytesIO(INVALID_CSV)) with mock.patch("io.open", return_value=invalid_stream): + pass - response = webtest_submit( - form, "save", upload_files=[upload], extra_environ=env - ) + #response = webtest_submit( + # form, "save", upload_files=[upload], extra_environ=env + #) assert "validation" in response.body assert "missing-value" in response.body @@ -439,8 +441,8 @@ def test_resource_form_update_valid(self, mock_open, app): valid_stream = io.BufferedReader(io.BytesIO(VALID_CSV)) with mock.patch("io.open", return_value=valid_stream): - - submit_and_follow(app, form, env, "save", upload_files=[upload]) + pass + #submit_and_follow(app, form, env, "save", upload_files=[upload]) dataset = call_action("package_show", id=dataset["id"]) @@ -462,10 +464,10 @@ def test_resource_form_update_invalid(self, mock_open, app): invalid_stream = io.BufferedReader(io.BytesIO(INVALID_CSV)) with mock.patch("io.open", return_value=invalid_stream): - - response = webtest_submit( - form, "save", upload_files=[upload], extra_environ=env - ) + pass + #response = webtest_submit( + # form, "save", upload_files=[upload], extra_environ=env + #) assert "validation" in response.body assert "missing-value" in response.body From 171ae27ea4cd9eab3b56a80ca32a03f21a50d97b Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 16 Sep 2021 12:31:13 +0200 Subject: [PATCH 11/32] Load plugins in logic tests --- ckanext/validation/tests/test_logic.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ckanext/validation/tests/test_logic.py b/ckanext/validation/tests/test_logic.py index 3bf990cb..f3434733 100644 --- a/ckanext/validation/tests/test_logic.py +++ b/ckanext/validation/tests/test_logic.py @@ -28,7 +28,7 @@ Session = model.Session -@pytest.mark.usefixtures("clean_db", "validation_setup") +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestResourceValidationRun(object): def test_resource_validation_run_param_missing(self): @@ -150,7 +150,7 @@ def test_resource_validation_resets_existing_validation_object( assert validation.error is None -@pytest.mark.usefixtures("clean_db", "validation_setup") +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestResourceValidationShow(object): def test_resource_validation_show_param_missing(self): @@ -211,7 +211,7 @@ def test_resource_validation_show_returns_all_fields(self): assert validation_show["finished"] == validation.finished.isoformat() -@pytest.mark.usefixtures("clean_db", "validation_setup") +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestResourceValidationDelete(object): def test_resource_validation_delete_param_missing(self): @@ -262,7 +262,7 @@ def test_resource_validation_delete_removes_object(self): assert count_after == 0 -@pytest.mark.usefixtures("clean_db", "validation_setup") +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestAuth(object): def test_run_anon(self): @@ -451,7 +451,7 @@ def test_show_anon_private_dataset(self): ) -@pytest.mark.usefixtures("clean_db", "validation_setup") +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestResourceValidationOnCreate(object): @classmethod def setup_class(cls): @@ -572,7 +572,7 @@ def test_validation_passes_with_url(self, mock_validate): assert "validation_timestamp" in resource -@pytest.mark.usefixtures("clean_db", "validation_setup") +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestResourceValidationOnUpdate(object): @classmethod def setup_class(cls): @@ -684,7 +684,7 @@ def test_validation_passes_with_url(self, mock_validate): assert "validation_timestamp" in resource -@pytest.mark.usefixtures("clean_db", "validation_setup") +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestSchemaFields(object): def test_schema_field(self): @@ -772,7 +772,7 @@ def test_schema_upload_field(self, mock_open): assert "schema_url" not in resource -@pytest.mark.usefixtures("clean_db", "validation_setup") +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestValidationOptionsField(object): def test_validation_options_field(self): From 1f09a821317bffb91364e202d508b7075b782292 Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 17 Sep 2021 10:59:55 +0200 Subject: [PATCH 12/32] Remove asserts --- ckanext/validation/tests/test_plugin.py | 315 +++++++++----------- ckanext/validation/tests/test_utils.py | 87 +++--- ckanext/validation/tests/test_validators.py | 82 ++--- 3 files changed, 227 insertions(+), 257 deletions(-) diff --git a/ckanext/validation/tests/test_plugin.py b/ckanext/validation/tests/test_plugin.py index a2deebbf..a983ae35 100644 --- a/ckanext/validation/tests/test_plugin.py +++ b/ckanext/validation/tests/test_plugin.py @@ -1,5 +1,4 @@ import mock -from nose.tools import assert_equals from ckan.tests.helpers import call_action, reset_db from ckan.tests import factories @@ -10,378 +9,344 @@ class TestResourceControllerHooksUpdate(object): - def setup(self): reset_db() if not tables_exist(): create_tables() - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_on_other_fields(self, mock_enqueue): - resource = {'format': 'CSV'} + resource = {"format": "CSV"} dataset = factories.Dataset(resources=[resource]) - dataset['resources'][0]['description'] = 'Some resource' + dataset["resources"][0]["description"] = "Some resource" - call_action('resource_update', {}, **dataset['resources'][0]) + call_action("resource_update", {}, **dataset["resources"][0]) mock_enqueue.assert_not_called() - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_on_other_formats(self, mock_enqueue): - resource = {'format': 'PDF'} + resource = {"format": "PDF"} dataset = factories.Dataset(resources=[resource]) - call_action('resource_update', {}, **dataset['resources'][0]) + call_action("resource_update", {}, **dataset["resources"][0]) mock_enqueue.assert_not_called() - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_on_upload(self, mock_enqueue): - resource = { - 'format': 'CSV', - 'upload': 'mock_upload', - 'url_type': 'upload' - } + resource = {"format": "CSV", "upload": "mock_upload", "url_type": "upload"} dataset = factories.Dataset(resources=[resource]) - call_action('resource_update', {}, **dataset['resources'][0]) + call_action("resource_update", {}, **dataset["resources"][0]) - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals( - mock_enqueue.call_args[0][1][0]['id'], - dataset['resources'][0]['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == dataset["resources"][0]["id"] - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_on_url_change(self, mock_enqueue): - resource = {'format': 'CSV', 'url': 'https://some.url'} + resource = {"format": "CSV", "url": "https://some.url"} dataset = factories.Dataset(resources=[resource]) - dataset['resources'][0]['url'] = 'https://some.new.url' + dataset["resources"][0]["url"] = "https://some.new.url" - call_action('resource_update', {}, **dataset['resources'][0]) + call_action("resource_update", {}, **dataset["resources"][0]) - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals( - mock_enqueue.call_args[0][1][0]['id'], - dataset['resources'][0]['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == dataset["resources"][0]["id"] - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_on_schema_change(self, mock_enqueue): resource = { - 'url': 'http://some.url', - 'format': 'CSV', - 'schema': { - 'fields': [ - {'name': 'code'} - ] - } + "url": "http://some.url", + "format": "CSV", + "schema": {"fields": [{"name": "code"}]}, } dataset = factories.Dataset(resources=[resource]) - dataset['resources'][0]['schema'] = { - 'fields': [ - {'name': 'code'}, - {'name': 'date'} - ] + dataset["resources"][0]["schema"] = { + "fields": [{"name": "code"}, {"name": "date"}] } - call_action('resource_update', {}, **dataset['resources'][0]) + call_action("resource_update", {}, **dataset["resources"][0]) - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals( - mock_enqueue.call_args[0][1][0]['id'], - dataset['resources'][0]['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == dataset["resources"][0]["id"] - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_on_format_change(self, mock_enqueue): resource = factories.Resource() - resource['format'] = 'CSV' + resource["format"] = "CSV" - call_action('resource_update', {}, **resource) + call_action("resource_update", {}, **resource) - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals(mock_enqueue.call_args[0][1][0]['id'], resource['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @change_config('ckanext.validation.run_on_create_async', False) - @change_config('ckanext.validation.run_on_update_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @change_config("ckanext.validation.run_on_update_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_when_config_false(self, mock_enqueue): - resource = factories.Resource(format='CSV') + resource = factories.Resource(format="CSV") - resource['url'] = 'http://some.new.url' + resource["url"] = "http://some.new.url" - call_action('resource_update', {}, **resource) + call_action("resource_update", {}, **resource) mock_enqueue.assert_not_called() class TestResourceControllerHooksCreate(object): - def setup(self): reset_db() if not tables_exist(): create_tables() - @mock.patch('ckanext.validation.logic.enqueue_job') + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_on_other_formats(self, mock_enqueue): - factories.Resource(format='PDF') + factories.Resource(format="PDF") mock_enqueue.assert_not_called() - @mock.patch('ckanext.validation.logic.enqueue_job') - @change_config('ckanext.validation.run_on_update_async', False) + @mock.patch("ckanext.validation.logic.enqueue_job") + @change_config("ckanext.validation.run_on_update_async", False) def test_validation_run_with_upload(self, mock_enqueue): - resource = factories.Resource(format='CSV', url_type='upload') + resource = factories.Resource(format="CSV", url_type="upload") - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals(mock_enqueue.call_args[0][1][0]['id'], resource['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @mock.patch('ckanext.validation.logic.enqueue_job') - @change_config('ckanext.validation.run_on_update_async', False) + @mock.patch("ckanext.validation.logic.enqueue_job") + @change_config("ckanext.validation.run_on_update_async", False) def test_validation_run_with_url(self, mock_enqueue): - resource = factories.Resource(format='CSV', url='http://some.data') + resource = factories.Resource(format="CSV", url="http://some.data") - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals(mock_enqueue.call_args[0][1][0]['id'], resource['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @change_config('ckanext.validation.run_on_create_async', False) - @change_config('ckanext.validation.run_on_update_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @change_config("ckanext.validation.run_on_update_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_when_config_false(self, mock_enqueue): dataset = factories.Dataset() resource = { - 'format': 'CSV', - 'url': 'http://some.data', - 'package_id': dataset['id'], + "format": "CSV", + "url": "http://some.data", + "package_id": dataset["id"], } - call_action('resource_create', {}, **resource) + call_action("resource_create", {}, **resource) mock_enqueue.assert_not_called() class TestPackageControllerHooksCreate(object): - def setup(self): reset_db() if not tables_exist(): create_tables() - @mock.patch('ckanext.validation.logic.enqueue_job') + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_on_other_formats(self, mock_enqueue): - factories.Dataset(resources=[{'format': 'PDF'}]) + factories.Dataset(resources=[{"format": "PDF"}]) mock_enqueue.assert_not_called() - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_when_config_false(self, mock_enqueue): - factories.Dataset(resources=[ - {'format': 'CSV', 'url': 'http://some.data'}]) + factories.Dataset(resources=[{"format": "CSV", "url": "http://some.data"}]) mock_enqueue.assert_not_called() - @mock.patch('ckanext.validation.logic.enqueue_job') + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_with_upload(self, mock_enqueue): - resource = { - 'id': 'test-resource-id', - 'format': 'CSV', - 'url_type': 'upload' - } + resource = {"id": "test-resource-id", "format": "CSV", "url_type": "upload"} factories.Dataset(resources=[resource]) - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals(mock_enqueue.call_args[0][1][0]['id'], resource['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @mock.patch('ckanext.validation.logic.enqueue_job') + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_with_url(self, mock_enqueue): resource = { - 'id': 'test-resource-id', - 'format': 'CSV', - 'url': 'http://some.data' + "id": "test-resource-id", + "format": "CSV", + "url": "http://some.data", } factories.Dataset(resources=[resource]) - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals(mock_enqueue.call_args[0][1][0]['id'], resource['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @mock.patch('ckanext.validation.logic.enqueue_job') + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_only_supported_formats(self, mock_enqueue): resource1 = { - 'id': 'test-resource-id-1', - 'format': 'CSV', - 'url': 'http://some.data' + "id": "test-resource-id-1", + "format": "CSV", + "url": "http://some.data", } resource2 = { - 'id': 'test-resource-id-2', - 'format': 'PDF', - 'url': 'http://some.doc' + "id": "test-resource-id-2", + "format": "PDF", + "url": "http://some.doc", } factories.Dataset(resources=[resource1, resource2]) - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals(mock_enqueue.call_args[0][1][0]['id'], resource1['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == resource1["id"] class TestPackageControllerHooksUpdate(object): - def setup(self): reset_db() if not tables_exist(): create_tables() - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_runs_with_url(self, mock_enqueue): resource = { - 'id': 'test-resource-id', - 'format': 'CSV', - 'url': 'http://some.data' + "id": "test-resource-id", + "format": "CSV", + "url": "http://some.data", } - dataset = factories.Dataset(resources=[resource], id='myid') + dataset = factories.Dataset(resources=[resource], id="myid") mock_enqueue.assert_not_called() - dataset['resources'][0]['url'] = 'http://some.other.data' + dataset["resources"][0]["url"] = "http://some.other.data" - call_action('package_update', {}, **dataset) + call_action("package_update", {}, **dataset) - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals(mock_enqueue.call_args[0][1][0]['id'], resource['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_runs_with_upload(self, mock_enqueue): - resource = { - 'id': 'test-resource-id', - 'format': 'CSV', - 'url_type': 'upload' - } + resource = {"id": "test-resource-id", "format": "CSV", "url_type": "upload"} dataset = factories.Dataset(resources=[resource]) mock_enqueue.assert_not_called() - dataset['resources'][0]['url'] = 'http://some.other.data' + dataset["resources"][0]["url"] = "http://some.other.data" - call_action('package_update', {}, **dataset) + call_action("package_update", {}, **dataset) - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals(mock_enqueue.call_args[0][1][0]['id'], resource['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_on_other_formats(self, mock_enqueue): - resource = { - 'id': 'test-resource-id', - 'format': 'PDF', - 'url': 'http://some.doc' - } + resource = {"id": "test-resource-id", "format": "PDF", "url": "http://some.doc"} dataset = factories.Dataset(resources=[resource]) mock_enqueue.assert_not_called() - dataset['resources'][0]['url'] = 'http://some.other.doc' + dataset["resources"][0]["url"] = "http://some.other.doc" - call_action('package_update', {}, **dataset) + call_action("package_update", {}, **dataset) mock_enqueue.assert_not_called() - @change_config('ckanext.validation.run_on_create_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_only_supported_formats(self, mock_enqueue): resource1 = { - 'id': 'test-resource-id-1', - 'format': 'CSV', - 'url': 'http://some.data' + "id": "test-resource-id-1", + "format": "CSV", + "url": "http://some.data", } resource2 = { - 'id': 'test-resource-id-2', - 'format': 'PDF', - 'url': 'http://some.doc' + "id": "test-resource-id-2", + "format": "PDF", + "url": "http://some.doc", } dataset = factories.Dataset(resources=[resource1, resource2]) mock_enqueue.assert_not_called() - dataset['resources'][0]['url'] = 'http://some.other.data' + dataset["resources"][0]["url"] = "http://some.other.data" - call_action('package_update', {}, **dataset) + call_action("package_update", {}, **dataset) - assert_equals(mock_enqueue.call_count, 1) + assert mock_enqueue.call_count == 1 - assert_equals(mock_enqueue.call_args[0][0], run_validation_job) - assert_equals(mock_enqueue.call_args[0][1][0]['id'], resource1['id']) + assert mock_enqueue.call_args[0][0] == run_validation_job + assert mock_enqueue.call_args[0][1][0]["id"] == resource1["id"] - @change_config('ckanext.validation.run_on_create_async', False) - @change_config('ckanext.validation.run_on_update_async', False) - @mock.patch('ckanext.validation.logic.enqueue_job') + @change_config("ckanext.validation.run_on_create_async", False) + @change_config("ckanext.validation.run_on_update_async", False) + @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_when_config_false(self, mock_enqueue): resource = { - 'id': 'test-resource-id', - 'format': 'CSV', - 'url': 'http://some.data' + "id": "test-resource-id", + "format": "CSV", + "url": "http://some.data", } dataset = factories.Dataset(resources=[resource]) - call_action('package_update', {}, **dataset) + call_action("package_update", {}, **dataset) mock_enqueue.assert_not_called() diff --git a/ckanext/validation/tests/test_utils.py b/ckanext/validation/tests/test_utils.py index 7a3febe2..99714096 100644 --- a/ckanext/validation/tests/test_utils.py +++ b/ckanext/validation/tests/test_utils.py @@ -3,90 +3,89 @@ import mock from pyfakefs import fake_filesystem_unittest -from nose.tools import assert_equals from ckan.tests.helpers import change_config from ckanext.validation.tests.helpers import mock_uploads from ckanext.validation.utils import ( - get_create_mode_from_config, get_update_mode_from_config, - get_local_upload_path, delete_local_uploaded_file + get_create_mode_from_config, + get_update_mode_from_config, + get_local_upload_path, + delete_local_uploaded_file, ) class TestConfig(object): - def test_config_defaults(self): - assert_equals(get_update_mode_from_config(), 'async') - assert_equals(get_create_mode_from_config(), 'async') + assert get_update_mode_from_config() == "async" + assert get_create_mode_from_config() == "async" - @change_config('ckanext.validation.run_on_update_sync', True) + @change_config("ckanext.validation.run_on_update_sync", True) def test_config_update_true_sync(self): - assert_equals(get_update_mode_from_config(), 'sync') + assert get_update_mode_from_config() == "sync" - @change_config('ckanext.validation.run_on_update_sync', False) + @change_config("ckanext.validation.run_on_update_sync", False) def test_config_update_false_sync(self): - assert_equals(get_update_mode_from_config(), 'async') + assert get_update_mode_from_config() == "async" - @change_config('ckanext.validation.run_on_create_sync', True) + @change_config("ckanext.validation.run_on_create_sync", True) def test_config_create_true_sync(self): - assert_equals(get_create_mode_from_config(), 'sync') + assert get_create_mode_from_config() == "sync" - @change_config('ckanext.validation.run_on_create_sync', False) + @change_config("ckanext.validation.run_on_create_sync", False) def test_config_create_false_sync(self): - assert_equals(get_create_mode_from_config(), 'async') + assert get_create_mode_from_config() == "async" - @change_config('ckanext.validation.run_on_update_async', True) + @change_config("ckanext.validation.run_on_update_async", True) def test_config_update_true_async(self): - assert_equals(get_update_mode_from_config(), 'async') + assert get_update_mode_from_config() == "async" - @change_config('ckanext.validation.run_on_update_async', False) + @change_config("ckanext.validation.run_on_update_async", False) def test_config_update_false_async(self): - assert_equals(get_update_mode_from_config(), None) + assert get_update_mode_from_config() is None - @change_config('ckanext.validation.run_on_create_async', True) + @change_config("ckanext.validation.run_on_create_async", True) def test_config_create_true_async(self): - assert_equals(get_create_mode_from_config(), 'async') + assert get_create_mode_from_config() == "async" - @change_config('ckanext.validation.run_on_create_async', False) + @change_config("ckanext.validation.run_on_create_async", False) def test_config_create_false_async(self): - assert_equals(get_create_mode_from_config(), None) + assert get_create_mode_from_config() is None - @change_config('ckanext.validation.run_on_update_async', False) - @change_config('ckanext.validation.run_on_create_async', False) + @change_config("ckanext.validation.run_on_update_async", False) + @change_config("ckanext.validation.run_on_create_async", False) def test_config_both_false(self): - assert_equals(get_update_mode_from_config(), None) - assert_equals(get_create_mode_from_config(), None) + assert get_update_mode_from_config() is None + assert get_create_mode_from_config() is None class TestFiles(object): - @mock_uploads def test_local_path(self, mock_open): resource_id = str(uuid.uuid4()) - assert_equals( - get_local_upload_path(resource_id), - '/doesnt_exist/resources/{}/{}/{}'.format( - resource_id[0:3], resource_id[3:6], resource_id[6:]) + assert get_local_upload_path( + resource_id + ) == "/doesnt_exist/resources/{}/{}/{}".format( + resource_id[0:3], resource_id[3:6], resource_id[6:] ) @mock_uploads def test_delete_upload_file(self, mock_open): resource_id = str(uuid.uuid4()) - path = '/doesnt_exist/resources/{}/{}/{}'.format( + path = "/doesnt_exist/resources/{}/{}/{}".format( resource_id[0:3], resource_id[3:6], resource_id[6:] ) @@ -106,19 +105,20 @@ def test_delete_upload_file(self, mock_open): def test_delete_file_not_deleted_if_resources_first(self, mock_open): resource_id = str(uuid.uuid4()) - path = '/doesnt_exist/resources/{}'.format(resource_id) + path = "/doesnt_exist/resources/{}".format(resource_id) patcher = fake_filesystem_unittest.Patcher() patcher.setUp() patcher.fs.CreateFile(path) assert os.path.exists(path) - with mock.patch('ckanext.validation.utils.get_local_upload_path', - return_value=path): + with mock.patch( + "ckanext.validation.utils.get_local_upload_path", return_value=path + ): delete_local_uploaded_file(resource_id) assert not os.path.exists(path) - assert os.path.exists('/doesnt_exist/resources') + assert os.path.exists("/doesnt_exist/resources") patcher.tearDown() @@ -126,19 +126,20 @@ def test_delete_file_not_deleted_if_resources_first(self, mock_open): def test_delete_file_not_deleted_if_resources_second(self, mock_open): resource_id = str(uuid.uuid4()) - path = '/doesnt_exist/resources/data/{}'.format(resource_id) + path = "/doesnt_exist/resources/data/{}".format(resource_id) patcher = fake_filesystem_unittest.Patcher() patcher.setUp() patcher.fs.CreateFile(path) assert os.path.exists(path) - with mock.patch('ckanext.validation.utils.get_local_upload_path', - return_value=path): + with mock.patch( + "ckanext.validation.utils.get_local_upload_path", return_value=path + ): delete_local_uploaded_file(resource_id) assert not os.path.exists(path) - assert os.path.exists('/doesnt_exist/resources') + assert os.path.exists("/doesnt_exist/resources") patcher.tearDown() @@ -146,7 +147,7 @@ def test_delete_file_not_deleted_if_resources_second(self, mock_open): def test_delete_passes_if_os_exeception(self, mock_open): resource_id = str(uuid.uuid4()) - path = '/doesnt_exist/resources/{}/{}/{}'.format( + path = "/doesnt_exist/resources/{}/{}/{}".format( resource_id[0:3], resource_id[3:6], resource_id[6:] ) @@ -155,10 +156,8 @@ def test_delete_passes_if_os_exeception(self, mock_open): patcher.fs.CreateFile(path) assert os.path.exists(path) - with mock.patch('ckanext.validation.utils.os.remove', - side_effect=OSError): + with mock.patch("ckanext.validation.utils.os.remove", side_effect=OSError): delete_local_uploaded_file(resource_id) - patcher.tearDown() diff --git a/ckanext/validation/tests/test_validators.py b/ckanext/validation/tests/test_validators.py index e00d2c31..6e73e7b6 100644 --- a/ckanext/validation/tests/test_validators.py +++ b/ckanext/validation/tests/test_validators.py @@ -1,66 +1,68 @@ import json - -from nose.tools import assert_raises, assert_equals +import pytest from ckantoolkit import Invalid -from ckan.tests.helpers import change_config - from ckanext.validation.validators import ( - resource_schema_validator, validation_options_validator + resource_schema_validator, + validation_options_validator, ) class TestResourceSchemaValidator(object): - def test_resource_schema_none(self): - schema = '' + schema = "" assert resource_schema_validator(schema, {}) is None def test_resource_schema_invalid_json_string(self): - schema = '{a,b}' + schema = "{a,b}" - assert_raises(Invalid, resource_schema_validator, schema, {}) + with pytest.raises(Invalid): + resource_schema_validator(schema, {}) def test_resource_schema_invalid_schema_string(self): schema = '{"a": 1}' - assert_raises(Invalid, resource_schema_validator, schema, {}) + with pytest.raises(Invalid): + resource_schema_validator(schema, {}) def test_resource_schema_valid_json_not_a_dict_string(self): - schema = '[a,2]' + schema = "[a,2]" - assert_raises(Invalid, resource_schema_validator, schema, {}) + with pytest.raises(Invalid): + resource_schema_validator(schema, {}) def test_resource_schema_valid_json_empty_string(self): schema = '""' - assert_raises(Invalid, resource_schema_validator, schema, {}) + with pytest.raises(Invalid): + resource_schema_validator(schema, {}) def test_resource_schema_invalid_schema_object(self): - schema = {'a': 1} + schema = {"a": 1} - with assert_raises(Invalid) as e: + with pytest.raises(Invalid) as e: resource_schema_validator(schema, {}) - assert e.exception.error.startswith( - 'Invalid Table Schema: ' + - 'Descriptor validation error: \'fields\' is a required property') + assert e.value.error.startswith( + "Invalid Table Schema: " + + "Descriptor validation error: 'fields' is a required property" + ) def test_resource_schema_valid_schema_object(self): - schema = {'fields': [{'name': 'longitude'}]} + schema = {"fields": [{"name": "longitude"}]} value = resource_schema_validator(schema, {}) - assert_equals(value, json.dumps(schema)) + assert value == json.dumps(schema) def test_resource_schema_valid_schema_string(self): @@ -68,54 +70,58 @@ def test_resource_schema_valid_schema_string(self): value = resource_schema_validator(schema, {}) - assert_equals(value, schema) + assert value == schema def test_resource_schema_valid_schema_url(self): - schema = 'https://example.com/schema.json' + schema = "https://example.com/schema.json" value = resource_schema_validator(schema, {}) - assert_equals(value, schema) + assert value == schema def test_resource_schema_invalid_wrong_url(self): - schema = '/some/wrong/url/schema.json' + schema = "/some/wrong/url/schema.json" - assert_raises(Invalid, resource_schema_validator, schema, {}) + with pytest.raises(Invalid): + resource_schema_validator(schema, {}) class TestValidationOptionsValidator(object): - ''' + """ Note: At the point this validator is run the value should already be a valid JSON string (ie `scheming_valid_json_object` has been run) - ''' + """ def test_no_default_validation_options(self): value = '{"headers":3}' - assert_equals(validation_options_validator(value, {}), value) + assert validation_options_validator(value, {}) == value - @change_config('ckanext.validation.default_validation_options', - '{"delimiter":";"}') + @pytest.mark.ckan_config( + "ckanext.validation.default_validation_options", '{"delimiter":";"}' + ) def test_default_validation_options(self): value = '{"headers": 3}' - assert_equals( - validation_options_validator(value, {}), - '{"delimiter": ";", "headers": 3}' + assert ( + validation_options_validator(value, {}) + == '{"delimiter": ";", "headers": 3}' ) - @change_config('ckanext.validation.default_validation_options', - '{"delimiter":";", "headers":2}') + @pytest.mark.ckan_config( + "ckanext.validation.default_validation_options", + '{"delimiter":";", "headers":2}', + ) def test_default_validation_optionsi_does_not_override(self): value = '{"headers": 3}' - assert_equals( - validation_options_validator(value, {}), - '{"delimiter": ";", "headers": 3}' + assert ( + validation_options_validator(value, {}) + == '{"delimiter": ";", "headers": 3}' ) From c464350631be0e059713986dcecea8253e7d853f Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 17 Sep 2021 12:40:47 +0200 Subject: [PATCH 13/32] Fix blueprint --- ckanext/validation/blueprints.py | 3 ++- ckanext/validation/templates/validation/validation_read.html | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ckanext/validation/blueprints.py b/ckanext/validation/blueprints.py index 7a4d29bf..3ec0dc34 100644 --- a/ckanext/validation/blueprints.py +++ b/ckanext/validation/blueprints.py @@ -7,7 +7,7 @@ validation = Blueprint("validation", __name__) -def read(self, resource_id): +def read(id, resource_id): try: validation = get_action(u"resource_validation_show")( @@ -30,6 +30,7 @@ def read(self, resource_id): u"validation": validation, u"resource": resource, u"dataset": dataset, + u"pkg_dict": dataset, }, ) diff --git a/ckanext/validation/templates/validation/validation_read.html b/ckanext/validation/templates/validation/validation_read.html index dc5436b8..227ba9d3 100644 --- a/ckanext/validation/templates/validation/validation_read.html +++ b/ckanext/validation/templates/validation/validation_read.html @@ -7,7 +7,7 @@ {% block breadcrumb_content %} {{ super() }} -
  • {{ h.resource_display_name(resource)|truncate(30) }}
  • +
  • {{ h.resource_display_name(resource)|truncate(30) }}
  • Validation Report
  • {% endblock %} @@ -34,7 +34,6 @@

    {{ h.resource_display_name(resource) | truncate(50) }}
    {% endif %} - {% if h.use_webassets() %} From 79cf7d3e99960dcb903a07f212e18e9028cc7a9e Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 17 Sep 2021 12:41:03 +0200 Subject: [PATCH 14/32] db init cli --- ckanext/validation/cli.py | 22 ++++++++++++++++++++++ ckanext/validation/plugin/flask_plugin.py | 8 +++++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 ckanext/validation/cli.py diff --git a/ckanext/validation/cli.py b/ckanext/validation/cli.py new file mode 100644 index 00000000..3b8a0570 --- /dev/null +++ b/ckanext/validation/cli.py @@ -0,0 +1,22 @@ +import sys + +import click + +from ckanext.validation.model import create_tables, tables_exist + + +@click.group() +def validation(): + """Harvests remotely mastered metadata.""" + pass + + +@validation.command() +def init_db(): + """Creates the necessary tables in the database.""" + if tables_exist(): + print(u"Validation tables already exist") + sys.exit(0) + + create_tables() + print(u"Validation tables created") diff --git a/ckanext/validation/plugin/flask_plugin.py b/ckanext/validation/plugin/flask_plugin.py index be4ce2df..6e576a15 100644 --- a/ckanext/validation/plugin/flask_plugin.py +++ b/ckanext/validation/plugin/flask_plugin.py @@ -2,13 +2,19 @@ from ckan import plugins as p -from ckanext.validation import blueprints +from ckanext.validation import blueprints, cli class ValidationMixin(p.SingletonPlugin): p.implements(p.IBlueprint) + p.implements(p.IClick) # IBlueprint def get_blueprint(self): return [blueprints.validation] + + # IClick + + def get_commands(self): + return [cli.validation] From 9196a624a1e9a58aaac36f5622e260e57034c624 Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 25 Jul 2022 16:28:39 +0200 Subject: [PATCH 15/32] bump From 5bf7c55df02fbc6dac36e725da3109dd0d79ecd6 Mon Sep 17 00:00:00 2001 From: ThrawnCA Date: Wed, 8 Dec 2021 11:18:42 +1000 Subject: [PATCH 16/32] [QOL-7740] convert test dates to strings - can't automatically serialize to JSON otherwise --- ckanext/validation/tests/test_helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckanext/validation/tests/test_helpers.py b/ckanext/validation/tests/test_helpers.py index cc3318e1..46bc1b96 100644 --- a/ckanext/validation/tests/test_helpers.py +++ b/ckanext/validation/tests/test_helpers.py @@ -43,7 +43,7 @@ def test_get_validation_badge_success(self): resource = factories.Resource( format="CSV", validation_status="success", - validation_timestamp=datetime.datetime.utcnow(), + validation_timestamp=datetime.datetime.utcnow().isoformat(), ) out = get_validation_badge(resource) @@ -63,7 +63,7 @@ def test_get_validation_badge_failure(self): resource = factories.Resource( format="CSV", validation_status="failure", - validation_timestamp=datetime.datetime.utcnow(), + validation_timestamp=datetime.datetime.utcnow().isoformat(), ) out = get_validation_badge(resource) @@ -83,7 +83,7 @@ def test_get_validation_badge_error(self): resource = factories.Resource( format="CSV", validation_status="error", - validation_timestamp=datetime.datetime.utcnow(), + validation_timestamp=datetime.datetime.utcnow().isoformat(), ) out = get_validation_badge(resource) From 1d91832702f7322bca4dcc8b515e4023764a1995 Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 26 Jul 2022 15:53:54 +0200 Subject: [PATCH 17/32] Fix pytest fixtures in plugin tests --- ckanext/validation/tests/test_plugin.py | 49 +++++++++++-------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/ckanext/validation/tests/test_plugin.py b/ckanext/validation/tests/test_plugin.py index a983ae35..e2ab7e5f 100644 --- a/ckanext/validation/tests/test_plugin.py +++ b/ckanext/validation/tests/test_plugin.py @@ -1,3 +1,4 @@ +import pytest import mock from ckan.tests.helpers import call_action, reset_db @@ -8,13 +9,10 @@ from ckanext.validation.jobs import run_validation_job +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestResourceControllerHooksUpdate(object): - def setup(self): - reset_db() - if not tables_exist(): - create_tables() - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_on_other_fields(self, mock_enqueue): @@ -28,7 +26,7 @@ def test_validation_does_not_run_on_other_fields(self, mock_enqueue): mock_enqueue.assert_not_called() - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_on_other_formats(self, mock_enqueue): @@ -40,7 +38,7 @@ def test_validation_does_not_run_on_other_formats(self, mock_enqueue): mock_enqueue.assert_not_called() - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_on_upload(self, mock_enqueue): @@ -55,7 +53,7 @@ def test_validation_run_on_upload(self, mock_enqueue): assert mock_enqueue.call_args[0][0] == run_validation_job assert mock_enqueue.call_args[0][1][0]["id"] == dataset["resources"][0]["id"] - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_on_url_change(self, mock_enqueue): @@ -72,7 +70,7 @@ def test_validation_run_on_url_change(self, mock_enqueue): assert mock_enqueue.call_args[0][0] == run_validation_job assert mock_enqueue.call_args[0][1][0]["id"] == dataset["resources"][0]["id"] - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_on_schema_change(self, mock_enqueue): @@ -95,7 +93,7 @@ def test_validation_run_on_schema_change(self, mock_enqueue): assert mock_enqueue.call_args[0][0] == run_validation_job assert mock_enqueue.call_args[0][1][0]["id"] == dataset["resources"][0]["id"] - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_on_format_change(self, mock_enqueue): @@ -110,8 +108,8 @@ def test_validation_run_on_format_change(self, mock_enqueue): assert mock_enqueue.call_args[0][0] == run_validation_job assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @change_config("ckanext.validation.run_on_create_async", False) - @change_config("ckanext.validation.run_on_update_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_when_config_false(self, mock_enqueue): @@ -124,6 +122,7 @@ def test_validation_does_not_run_when_config_false(self, mock_enqueue): mock_enqueue.assert_not_called() +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestResourceControllerHooksCreate(object): def setup(self): reset_db() @@ -138,7 +137,7 @@ def test_validation_does_not_run_on_other_formats(self, mock_enqueue): mock_enqueue.assert_not_called() @mock.patch("ckanext.validation.logic.enqueue_job") - @change_config("ckanext.validation.run_on_update_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_async", False) def test_validation_run_with_upload(self, mock_enqueue): resource = factories.Resource(format="CSV", url_type="upload") @@ -149,7 +148,7 @@ def test_validation_run_with_upload(self, mock_enqueue): assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] @mock.patch("ckanext.validation.logic.enqueue_job") - @change_config("ckanext.validation.run_on_update_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_async", False) def test_validation_run_with_url(self, mock_enqueue): resource = factories.Resource(format="CSV", url="http://some.data") @@ -159,8 +158,8 @@ def test_validation_run_with_url(self, mock_enqueue): assert mock_enqueue.call_args[0][0] == run_validation_job assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @change_config("ckanext.validation.run_on_create_async", False) - @change_config("ckanext.validation.run_on_update_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_when_config_false(self, mock_enqueue): @@ -178,10 +177,6 @@ def test_validation_does_not_run_when_config_false(self, mock_enqueue): class TestPackageControllerHooksCreate(object): - def setup(self): - reset_db() - if not tables_exist(): - create_tables() @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_on_other_formats(self, mock_enqueue): @@ -190,7 +185,7 @@ def test_validation_does_not_run_on_other_formats(self, mock_enqueue): mock_enqueue.assert_not_called() - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_when_config_false(self, mock_enqueue): @@ -252,7 +247,7 @@ def setup(self): if not tables_exist(): create_tables() - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_runs_with_url(self, mock_enqueue): @@ -274,7 +269,7 @@ def test_validation_runs_with_url(self, mock_enqueue): assert mock_enqueue.call_args[0][0] == run_validation_job assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_runs_with_upload(self, mock_enqueue): @@ -292,7 +287,7 @@ def test_validation_runs_with_upload(self, mock_enqueue): assert mock_enqueue.call_args[0][0] == run_validation_job assert mock_enqueue.call_args[0][1][0]["id"] == resource["id"] - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_on_other_formats(self, mock_enqueue): @@ -307,7 +302,7 @@ def test_validation_does_not_run_on_other_formats(self, mock_enqueue): mock_enqueue.assert_not_called() - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_run_only_supported_formats(self, mock_enqueue): @@ -335,8 +330,8 @@ def test_validation_run_only_supported_formats(self, mock_enqueue): assert mock_enqueue.call_args[0][0] == run_validation_job assert mock_enqueue.call_args[0][1][0]["id"] == resource1["id"] - @change_config("ckanext.validation.run_on_create_async", False) - @change_config("ckanext.validation.run_on_update_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_when_config_false(self, mock_enqueue): From 5c5a65616a05b5498301beea8cef1a8577fb921f Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 26 Jul 2022 16:40:52 +0200 Subject: [PATCH 18/32] More pytest fixes --- ckanext/validation/tests/test_plugin.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ckanext/validation/tests/test_plugin.py b/ckanext/validation/tests/test_plugin.py index e2ab7e5f..b973d6da 100644 --- a/ckanext/validation/tests/test_plugin.py +++ b/ckanext/validation/tests/test_plugin.py @@ -124,10 +124,6 @@ def test_validation_does_not_run_when_config_false(self, mock_enqueue): @pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestResourceControllerHooksCreate(object): - def setup(self): - reset_db() - if not tables_exist(): - create_tables() @mock.patch("ckanext.validation.logic.enqueue_job") def test_validation_does_not_run_on_other_formats(self, mock_enqueue): @@ -176,6 +172,7 @@ def test_validation_does_not_run_when_config_false(self, mock_enqueue): mock_enqueue.assert_not_called() +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestPackageControllerHooksCreate(object): @mock.patch("ckanext.validation.logic.enqueue_job") @@ -241,11 +238,8 @@ def test_validation_run_only_supported_formats(self, mock_enqueue): assert mock_enqueue.call_args[0][1][0]["id"] == resource1["id"] +@pytest.mark.usefixtures("clean_db", "validation_setup", "with_plugins") class TestPackageControllerHooksUpdate(object): - def setup(self): - reset_db() - if not tables_exist(): - create_tables() @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.logic.enqueue_job") From ef74d7759712193879e8700d614676f7175d7a94 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 27 Jul 2022 12:33:29 +0200 Subject: [PATCH 19/32] More pytest fixes --- ckanext/validation/tests/test_jobs.py | 4 ++-- ckanext/validation/tests/test_plugin.py | 1 - ckanext/validation/tests/test_utils.py | 23 +++++++++++------------ 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/ckanext/validation/tests/test_jobs.py b/ckanext/validation/tests/test_jobs.py index 17da48f9..0ab4b384 100644 --- a/ckanext/validation/tests/test_jobs.py +++ b/ckanext/validation/tests/test_jobs.py @@ -6,7 +6,7 @@ import ckantoolkit from ckan.lib.uploader import ResourceUpload -from ckan.tests.helpers import call_action, reset_db, change_config +from ckan.tests.helpers import call_action from ckan.tests import factories from ckanext.validation.model import create_tables, tables_exist, Validation @@ -34,7 +34,7 @@ def mock_get_resource_uploader(data_dict): @pytest.mark.usefixtures("clean_db", "validation_setup") class TestValidationJob(object): - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) @mock.patch("ckanext.validation.jobs.validate", return_value=VALID_REPORT) @mock.patch.object(Session, "commit") @mock.patch.object(ckantoolkit, "get_action") diff --git a/ckanext/validation/tests/test_plugin.py b/ckanext/validation/tests/test_plugin.py index b973d6da..24befae6 100644 --- a/ckanext/validation/tests/test_plugin.py +++ b/ckanext/validation/tests/test_plugin.py @@ -3,7 +3,6 @@ from ckan.tests.helpers import call_action, reset_db from ckan.tests import factories -from ckan.tests.helpers import change_config from ckanext.validation.model import create_tables, tables_exist from ckanext.validation.jobs import run_validation_job diff --git a/ckanext/validation/tests/test_utils.py b/ckanext/validation/tests/test_utils.py index 99714096..606b8a2f 100644 --- a/ckanext/validation/tests/test_utils.py +++ b/ckanext/validation/tests/test_utils.py @@ -2,10 +2,9 @@ import uuid import mock +import pytest from pyfakefs import fake_filesystem_unittest -from ckan.tests.helpers import change_config - from ckanext.validation.tests.helpers import mock_uploads from ckanext.validation.utils import ( get_create_mode_from_config, @@ -21,48 +20,48 @@ def test_config_defaults(self): assert get_update_mode_from_config() == "async" assert get_create_mode_from_config() == "async" - @change_config("ckanext.validation.run_on_update_sync", True) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_sync", True) def test_config_update_true_sync(self): assert get_update_mode_from_config() == "sync" - @change_config("ckanext.validation.run_on_update_sync", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_sync", False) def test_config_update_false_sync(self): assert get_update_mode_from_config() == "async" - @change_config("ckanext.validation.run_on_create_sync", True) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_sync", True) def test_config_create_true_sync(self): assert get_create_mode_from_config() == "sync" - @change_config("ckanext.validation.run_on_create_sync", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_sync", False) def test_config_create_false_sync(self): assert get_create_mode_from_config() == "async" - @change_config("ckanext.validation.run_on_update_async", True) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_async", True) def test_config_update_true_async(self): assert get_update_mode_from_config() == "async" - @change_config("ckanext.validation.run_on_update_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_async", False) def test_config_update_false_async(self): assert get_update_mode_from_config() is None - @change_config("ckanext.validation.run_on_create_async", True) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", True) def test_config_create_true_async(self): assert get_create_mode_from_config() == "async" - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) def test_config_create_false_async(self): assert get_create_mode_from_config() is None - @change_config("ckanext.validation.run_on_update_async", False) - @change_config("ckanext.validation.run_on_create_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_update_async", False) + @pytest.mark.ckan_config("ckanext.validation.run_on_create_async", False) def test_config_both_false(self): assert get_update_mode_from_config() is None From 201ce1bafb7deb5eb8cbfaa79255b61061446def Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 27 Jul 2022 12:57:02 +0200 Subject: [PATCH 20/32] More verbosity --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 430aa436..38836542 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: run: | paster --plugin=ckan db init -c test.ini - name: Run tests - run: pytest --ckan-ini=test.ini --cov=ckanext.validation --cov-report=xml --cov-append --disable-warnings ckanext/validation/tests + run: pytest --ckan-ini=test.ini --cov=ckanext.validation --cov-report=xml --cov-append --disable-warnings ckanext/validation/tests -vv - name: Upload coverage report to codecov uses: codecov/codecov-action@v1 From 91c72e18e045784359281442f892294e042107b8 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 27 Jul 2022 13:00:47 +0200 Subject: [PATCH 21/32] py2/py3 pyfakefs requirement --- dev-requirements-py2.txt | 3 +++ dev-requirements.txt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 dev-requirements-py2.txt diff --git a/dev-requirements-py2.txt b/dev-requirements-py2.txt new file mode 100644 index 00000000..e591dfb4 --- /dev/null +++ b/dev-requirements-py2.txt @@ -0,0 +1,3 @@ +pyfakefs==3.7.2 +pytest-ckan +pytest-cov diff --git a/dev-requirements.txt b/dev-requirements.txt index 216bcedb..51697501 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,3 @@ -pyfakefs==4.5.1 +pyfakefs==4.6.* pytest-ckan pytest-cov From bf277028d0f579e62fa8918301b3d0f2c01dc393 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 27 Jul 2022 13:11:24 +0200 Subject: [PATCH 22/32] Install proper requeriments depending on py version --- .github/workflows/test.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 38836542..8c990cf0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,10 +45,17 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Install requirements + - name: Install requirements (py3) + if: ${{ matrix.ckan-version != '2.7' && matrix.ckan-version != '2.8' && matrix.ckan-version != '2.9-py2' }} run: | - pip install -r requirements.txt pip install -r dev-requirements.txt + - name: Install requirements (py2) + if: ${{ matrix.ckan-version == '2.7' || matrix.ckan-version == '2.8' || matrix.ckan-version == '2.9-py2' }} + run: | + pip install -r dev-requirements-py2.txt + - name: Install requirements (common) + run: | + pip install -r requirements.txt pip install -e . # Replace default path to CKAN core config file with the one on the container sed -i -e 's/use = config:.*/use = config:\/srv\/app\/src\/ckan\/test-core.ini/' test.ini From e0502b0a14cd788e587b242353551ff40b77162c Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 27 Jul 2022 15:23:11 +0200 Subject: [PATCH 23/32] Fix upload tests for 2.9-py2 --- ckanext/validation/tests/helpers.py | 25 ++++++++++++++++-------- ckanext/validation/tests/test_form.py | 27 +++++++++++++++----------- ckanext/validation/tests/test_jobs.py | 1 - ckanext/validation/tests/test_logic.py | 19 ------------------ 4 files changed, 33 insertions(+), 39 deletions(-) diff --git a/ckanext/validation/tests/helpers.py b/ckanext/validation/tests/helpers.py index 9bba0aa2..44d251a1 100644 --- a/ckanext/validation/tests/helpers.py +++ b/ckanext/validation/tests/helpers.py @@ -8,6 +8,7 @@ from pyfakefs import fake_filesystem import ckan.lib.uploader +from ckan.plugins import toolkit from ckan.tests.helpers import change_config @@ -142,17 +143,25 @@ def wrapper(*args, **kwargs): return wrapper -class MockFieldStorage(cgi.FieldStorage): +if toolkit.check_ckan_version(min_version="2.9"): - def __init__(self, fp, filename): + from werkzeug.datastructures import FileStorage - self.file = fp - self.filename = filename - self.name = 'upload' - self.list = None + class MockFieldStorage(FileStorage): + pass +else: - def __bool__(self): - return self.file is not None + class MockFieldStorage(cgi.FieldStorage): + + def __init__(self, fp, filename): + + self.file = fp + self.filename = filename + self.name = 'upload' + self.list = None + + def __bool__(self): + return self.file is not None def get_mock_file(contents): diff --git a/ckanext/validation/tests/test_form.py b/ckanext/validation/tests/test_form.py index cc548e97..b6f50e37 100644 --- a/ckanext/validation/tests/test_form.py +++ b/ckanext/validation/tests/test_form.py @@ -8,7 +8,7 @@ import ckantoolkit as t from ckantoolkit.tests.factories import Sysadmin, Dataset from ckantoolkit.tests.helpers import ( - call_action, + call_action, _get_test_app ) from ckanext.validation.tests.helpers import VALID_CSV, INVALID_CSV, mock_uploads @@ -94,8 +94,7 @@ def test_resource_form_create_json(self, app): assert dataset["resources"][0]["schema"] == value - @mock_uploads - def test_resource_form_create_upload(self, mock_open, app): + def test_resource_form_create_upload(self): dataset = Dataset() value = {"fields": [{"name": "code"}, {"name": "department"}]} @@ -110,14 +109,20 @@ def test_resource_form_create_upload(self, mock_open, app): "save": "", } - user = Sysadmin() - env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/new".format(dataset['id']), - extra_environ=env, - data=data, - ) + app = _get_test_app() + + @mock_uploads + def post(app, data, mock_open): + + user = Sysadmin() + env = {"REMOTE_USER": user["name"].encode("ascii")} + # TODO: url + app.post( + url="/dataset/{}/resource/new".format(dataset['id']), + extra_environ=env, + data=data, + ) + post(app, data) dataset = call_action("package_show", id=dataset["id"]) diff --git a/ckanext/validation/tests/test_jobs.py b/ckanext/validation/tests/test_jobs.py index 0ab4b384..6129a8a9 100644 --- a/ckanext/validation/tests/test_jobs.py +++ b/ckanext/validation/tests/test_jobs.py @@ -210,7 +210,6 @@ def test_job_local_paths_are_hidden(self, mock_open): invalid_file = get_mock_file(invalid_csv) mock_upload = MockFieldStorage(invalid_file, "invalid.csv") - resource = factories.Resource(format="csv", upload=mock_upload) invalid_stream = io.BufferedReader(io.BytesIO(invalid_csv.encode('utf8'))) diff --git a/ckanext/validation/tests/test_logic.py b/ckanext/validation/tests/test_logic.py index f3434733..b5c285b5 100644 --- a/ckanext/validation/tests/test_logic.py +++ b/ckanext/validation/tests/test_logic.py @@ -10,7 +10,6 @@ from ckan import model from ckan.tests.helpers import call_action, call_auth from ckan.tests import factories -from ckan.tests.helpers import _get_test_app import ckantoolkit as t @@ -469,10 +468,6 @@ def teardown_class(cls): @mock_uploads def test_validation_fails_on_upload(self, mock_open): - # This is needed to trigger a new environment reload that will reload the - # actions - _get_test_app() - invalid_file = get_mock_file(INVALID_CSV) mock_upload = MockFieldStorage(invalid_file, "invalid.csv") @@ -499,10 +494,6 @@ def test_validation_fails_on_upload(self, mock_open): @mock_uploads def test_validation_fails_no_validation_object_stored(self, mock_open): - # This is needed to trigger a new environment reload that will reload the - # actions - _get_test_app() - invalid_file = get_mock_file(INVALID_CSV) mock_upload = MockFieldStorage(invalid_file, "invalid.csv") @@ -530,10 +521,6 @@ def test_validation_fails_no_validation_object_stored(self, mock_open): @mock_uploads def test_validation_passes_on_upload(self, mock_open): - # This is needed to trigger a new environment reload that will reload the - # actions - _get_test_app() - valid_file = get_mock_file(VALID_CSV) mock_upload = MockFieldStorage(valid_file, "invalid.csv") @@ -590,8 +577,6 @@ def teardown_class(cls): @mock_uploads def test_validation_fails_on_upload(self, mock_open): - _get_test_app() - dataset = factories.Dataset(resources=[{"url": "https://example.com/data.csv"}]) invalid_file = get_mock_file(INVALID_CSV) @@ -618,8 +603,6 @@ def test_validation_fails_on_upload(self, mock_open): @mock_uploads def test_validation_fails_no_validation_object_stored(self, mock_open): - _get_test_app() - dataset = factories.Dataset(resources=[{"url": "https://example.com/data.csv"}]) invalid_file = get_mock_file(INVALID_CSV) @@ -646,8 +629,6 @@ def test_validation_fails_no_validation_object_stored(self, mock_open): @mock_uploads def test_validation_passes_on_upload(self, mock_open): - _get_test_app() - dataset = factories.Dataset(resources=[{"url": "https://example.com/data.csv"}]) valid_file = get_mock_file(VALID_CSV) From 1a9af610ca6d882049f8d4e45fa05f3bd46b6e21 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 27 Jul 2022 17:16:06 +0200 Subject: [PATCH 24/32] Drop CKAN 2.7 and 2.8 support --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8c990cf0..c9c1b8bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: needs: lint strategy: matrix: - ckan-version: [2.9, 2.9-py2, 2.8, 2.7] + ckan-version: [2.9, 2.9-py2] fail-fast: false name: CKAN ${{ matrix.ckan-version }} From 45ea928a102c7619199c5d4ca0b4ca34dfbc7450 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 28 Jul 2022 08:40:49 +0200 Subject: [PATCH 25/32] Ignore session param in mock call --- ckanext/validation/tests/test_jobs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckanext/validation/tests/test_jobs.py b/ckanext/validation/tests/test_jobs.py index 6129a8a9..48728abc 100644 --- a/ckanext/validation/tests/test_jobs.py +++ b/ckanext/validation/tests/test_jobs.py @@ -53,7 +53,7 @@ def test_job_run_no_schema(self, mock_get_action, mock_commit, mock_validate): run_validation_job(resource) mock_validate.assert_called_with( - "http://example.com/file.csv", format="csv", schema=None + "http://example.com/file.csv", format="csv", schema=None, http_session=mock.ANY, ) @mock.patch("ckanext.validation.jobs.validate", return_value=VALID_REPORT) @@ -81,7 +81,7 @@ def test_job_run_schema(self, mock_get_action, mock_commit, mock_validate): run_validation_job(resource) mock_validate.assert_called_with( - "http://example.com/file.csv", format="csv", schema=schema + "http://example.com/file.csv", format="csv", schema=schema, http_session=mock.ANY, ) @mock.patch("ckanext.validation.jobs.validate", return_value=VALID_REPORT) @@ -108,7 +108,7 @@ def test_job_run_uploaded_file( run_validation_job(resource) mock_validate.assert_called_with( - "/tmp/example/{}".format(resource["id"]), format="csv", schema=None + "/tmp/example/{}".format(resource["id"]), format="csv", schema=None, http_session=mock.ANY, ) @mock.patch("ckanext.validation.jobs.validate", return_value=VALID_REPORT) From 175568f95b557297b54b5c3e1a1ca1f2aa018d76 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 28 Jul 2022 08:57:23 +0200 Subject: [PATCH 26/32] Assert individual call args in mock call --- ckanext/validation/tests/test_jobs.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ckanext/validation/tests/test_jobs.py b/ckanext/validation/tests/test_jobs.py index 48728abc..9dea73bc 100644 --- a/ckanext/validation/tests/test_jobs.py +++ b/ckanext/validation/tests/test_jobs.py @@ -52,9 +52,9 @@ def test_job_run_no_schema(self, mock_get_action, mock_commit, mock_validate): run_validation_job(resource) - mock_validate.assert_called_with( - "http://example.com/file.csv", format="csv", schema=None, http_session=mock.ANY, - ) + assert mock_validate.call_args[0][0] == "http://example.com/file.csv" + assert mock_validate.call_args[1]["format"] == "csv" + assert mock_validate.call_args[1]["schema"] is None @mock.patch("ckanext.validation.jobs.validate", return_value=VALID_REPORT) @mock.patch.object(Session, "commit") @@ -80,9 +80,9 @@ def test_job_run_schema(self, mock_get_action, mock_commit, mock_validate): run_validation_job(resource) - mock_validate.assert_called_with( - "http://example.com/file.csv", format="csv", schema=schema, http_session=mock.ANY, - ) + assert mock_validate.call_args[0][0] == "http://example.com/file.csv" + assert mock_validate.call_args[1]["format"] == "csv" + assert mock_validate.call_args[1]["schema"] == schema @mock.patch("ckanext.validation.jobs.validate", return_value=VALID_REPORT) @mock.patch.object( @@ -107,9 +107,9 @@ def test_job_run_uploaded_file( run_validation_job(resource) - mock_validate.assert_called_with( - "/tmp/example/{}".format(resource["id"]), format="csv", schema=None, http_session=mock.ANY, - ) + assert mock_validate.call_args[0][0] == "/tmp/example/{}".format(resource["id"]) + assert mock_validate.call_args[1]["format"] == "csv" + assert mock_validate.call_args[1]["schema"] is None @mock.patch("ckanext.validation.jobs.validate", return_value=VALID_REPORT) def test_job_run_valid_stores_validation_object(self, mock_validate): @@ -210,6 +210,7 @@ def test_job_local_paths_are_hidden(self, mock_open): invalid_file = get_mock_file(invalid_csv) mock_upload = MockFieldStorage(invalid_file, "invalid.csv") + resource = factories.Resource(format="csv", upload=mock_upload) invalid_stream = io.BufferedReader(io.BytesIO(invalid_csv.encode('utf8'))) From 219d570647a3a4988fbead4b50de5a2eccafeb8b Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 28 Jul 2022 09:27:48 +0200 Subject: [PATCH 27/32] Cross-version post calls --- ckanext/validation/tests/test_form.py | 103 ++++++++++++++++++-------- 1 file changed, 73 insertions(+), 30 deletions(-) diff --git a/ckanext/validation/tests/test_form.py b/ckanext/validation/tests/test_form.py index b6f50e37..581b8db1 100644 --- a/ckanext/validation/tests/test_form.py +++ b/ckanext/validation/tests/test_form.py @@ -13,6 +13,39 @@ from ckanext.validation.tests.helpers import VALID_CSV, INVALID_CSV, mock_uploads +is_ckan29_or_higher = t.check_ckan_version(min_version="2.9") + + +def _post(app, url, extra_environ=None, data=None): + ''' Submit a POST request to 'app', + using either webtest or Flask syntax. + ''' + if hasattr(app, 'test_client'): + # Flask app + app.post( + url=url, extra_environ=extra_environ, data=data) + else: + app.post( + url, data, extra_environ=extra_environ) + + +def _new_resource_url(dataset_id): + + if is_ckan29_or_higher: + url = "/dataset/{}/resource/new".format(dataset_id) + else: + url = "/dataset/new_resource/{}".format(dataset_id) + return url + + +def _edit_resource_url(dataset_id, resource_id): + + if is_ckan29_or_higher: + url = "/dataset/{}/resource/{}/edit".format(dataset_id, resource_id) + else: + url = "/dataset/{}/resource_edit/{}".format(dataset_id, resource_id) + return url + def _get_resource_new_page_as_sysadmin(app, id): user = Sysadmin() @@ -58,9 +91,10 @@ def test_resource_form_create(self, app): user = Sysadmin() env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/new".format(dataset['id']), + + _post( + app, + url=_new_resource_url(dataset['id']), extra_environ=env, data=data, ) @@ -83,9 +117,10 @@ def test_resource_form_create_json(self, app): user = Sysadmin() env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/new".format(dataset['id']), + + _post( + app, + url=_new_resource_url(dataset['id']), extra_environ=env, data=data, ) @@ -116,9 +151,10 @@ def post(app, data, mock_open): user = Sysadmin() env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/new".format(dataset['id']), + + _post( + app, + url=_new_resource_url(dataset['id']), extra_environ=env, data=data, ) @@ -141,9 +177,10 @@ def test_resource_form_create_url(self, app): user = Sysadmin() env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/new".format(dataset['id']), + + _post( + app, + url=_new_resource_url(dataset['id']), extra_environ=env, data=data, ) @@ -173,9 +210,10 @@ def test_resource_form_update(self, app): user = Sysadmin() env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/{}/edit".format(dataset['id'], dataset['resources'][0]['id']), + + _post( + app, + url=_edit_resource_url(dataset['id'], dataset['resources'][0]['id']), extra_environ=env, data=data, ) @@ -202,9 +240,10 @@ def test_resource_form_update_json(self, app): user = Sysadmin() env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/{}/edit".format(dataset['id'], dataset['resources'][0]['id']), + + _post( + app, + url=_edit_resource_url(dataset['id'], dataset['resources'][0]['id']), extra_environ=env, data=data, ) @@ -230,9 +269,10 @@ def test_resource_form_update_url(self, app): user = Sysadmin() env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/{}/edit".format(dataset['id'], dataset['resources'][0]['id']), + + _post( + app, + url=_edit_resource_url(dataset['id'], dataset['resources'][0]['id']), extra_environ=env, data=data, ) @@ -261,9 +301,10 @@ def test_resource_form_update_upload(self, app): user = Sysadmin() env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/{}/edit".format(dataset['id'], dataset['resources'][0]['id']), + + _post( + app, + url=_edit_resource_url(dataset['id'], dataset['resources'][0]['id']), extra_environ=env, data=data, ) @@ -299,9 +340,10 @@ def test_resource_form_create(self, app): user = Sysadmin() env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/new".format(dataset['id']), + + _post( + app, + url=_new_resource_url(dataset['id']), extra_environ=env, data=data, ) @@ -340,9 +382,10 @@ def test_resource_form_update(self, app): user = Sysadmin() env = {"REMOTE_USER": user["name"].encode("ascii")} - # TODO: url - app.post( - url="/dataset/{}/resource/{}/edit".format(dataset['id'], dataset['resources'][0]['id']), + + _post( + app, + url=_edit_resource_url(dataset['id'], dataset['resources'][0]['id']), extra_environ=env, data=data, ) From c087ce4e5564a7befe851c47d8b32cb26a75e479 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 28 Jul 2022 10:10:58 +0200 Subject: [PATCH 28/32] Cross-version upload handling --- ckanext/validation/plugin/__init__.py | 3 +-- ckanext/validation/tests/test_form.py | 25 ++++++++++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/ckanext/validation/plugin/__init__.py b/ckanext/validation/plugin/__init__.py index e914198e..bbca635e 100644 --- a/ckanext/validation/plugin/__init__.py +++ b/ckanext/validation/plugin/__init__.py @@ -128,8 +128,7 @@ def _process_schema_fields(self, data_dict): schema_upload = data_dict.pop(u'schema_upload', None) schema_url = data_dict.pop(u'schema_url', None) schema_json = data_dict.pop(u'schema_json', None) - if bool(schema_upload) and \ - isinstance(schema_upload, ALLOWED_UPLOAD_TYPES): + if isinstance(schema_upload, ALLOWED_UPLOAD_TYPES): uploaded_file = _get_underlying_file(schema_upload) data_dict[u'schema'] = uploaded_file.read() elif schema_url: diff --git a/ckanext/validation/tests/test_form.py b/ckanext/validation/tests/test_form.py index 581b8db1..34d54ba0 100644 --- a/ckanext/validation/tests/test_form.py +++ b/ckanext/validation/tests/test_form.py @@ -4,6 +4,7 @@ import datetime import pytest +import six import ckantoolkit as t from ckantoolkit.tests.factories import Sysadmin, Dataset @@ -16,17 +17,19 @@ is_ckan29_or_higher = t.check_ckan_version(min_version="2.9") -def _post(app, url, extra_environ=None, data=None): +def _post(app, url, extra_environ=None, data=None, upload=None): ''' Submit a POST request to 'app', using either webtest or Flask syntax. ''' - if hasattr(app, 'test_client'): - # Flask app + if is_ckan29_or_higher: + if upload: + for entry in upload: + data[entry[0]] = (io.BytesIO(entry[2]), entry[1]) app.post( url=url, extra_environ=extra_environ, data=data) else: app.post( - url, data, extra_environ=extra_environ) + url, data, extra_environ=extra_environ, upload_files=upload) def _new_resource_url(dataset_id): @@ -133,17 +136,16 @@ def test_resource_form_create_upload(self): dataset = Dataset() value = {"fields": [{"name": "code"}, {"name": "department"}]} - json_value = io.BytesIO(json.dumps(value).encode('utf8')) - - upload = (json_value, "schema.json") + json_value = six.ensure_binary(json.dumps(value).encode('utf8')) data = { "url": "https://example.com/data.csv", - "schema_upload": upload, "id": "", "save": "", } + upload = ('schema_upload', 'schema.json', json_value) + app = _get_test_app() @mock_uploads @@ -157,6 +159,7 @@ def post(app, data, mock_open): url=_new_resource_url(dataset['id']), extra_environ=env, data=data, + upload=[upload] ) post(app, data) @@ -288,13 +291,12 @@ def test_resource_form_update_upload(self, app): ) value = {"fields": [{"name": "code"}, {"name": "department"}, {"name": "date"}]} - json_value = io.BytesIO(json.dumps(value).encode('utf8')) + json_value = six.ensure_binary(json.dumps(value).encode('utf8')) - upload = (json_value, "schema.json") + upload = ('schema_upload', 'schema.json', json_value) data = { "url": "https://example.com/data.csv", - "schema_upload": upload, "id": "", "save": "", } @@ -307,6 +309,7 @@ def test_resource_form_update_upload(self, app): url=_edit_resource_url(dataset['id'], dataset['resources'][0]['id']), extra_environ=env, data=data, + upload=[upload], ) dataset = call_action("package_show", id=dataset["id"]) From f7e8ed81b9683f07a3d7100bdf44c916ff629468 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 28 Jul 2022 10:11:40 +0200 Subject: [PATCH 29/32] Reenable 2.8 tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9c1b8bc..08d3374a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: needs: lint strategy: matrix: - ckan-version: [2.9, 2.9-py2] + ckan-version: [2.9, 2.9-py2, 2.8] fail-fast: false name: CKAN ${{ matrix.ckan-version }} From bc4696791a76fedf804d95b77cc6ae68064e73b0 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 28 Jul 2022 10:28:09 +0200 Subject: [PATCH 30/32] Bump six requirement --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4ffed1cc..c2f15ee9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -six +six>=1.12.0 ckantoolkit>=0.0.3 goodtables==1.5.1 -e git+https://github.com/ckan/ckanext-scheming.git#egg=ckanext-scheming From 9425f0e4420058a27747010e7b6f0a7e0551ad7f Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 28 Jul 2022 10:54:06 +0200 Subject: [PATCH 31/32] Update supported versions in README --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 099814d9..df59caab 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,10 @@ If you are eager to get started, jump to the [Installation](#installation) and [ ## Versions supported and requirements -This extension has been tested with CKAN 2.4 to 2.7. +This extension is currently tested in CKAN 2.8 and CKAN 2.9. It is strongly recommended to use it alongside [ckanext-scheming](https://github.com/ckan/ckanext-scheming) to define the necessary extra fields in the default CKAN schema. -If you want to use [asynchronous validation](#asynchronous-validation) with background jobs and are using CKAN 2.6 or lower, [ckanext-rq](https://github.com/ckan/ckanext-rq) is also needed. Please refer to both READMEs for installation instructions. - ## Installation From 9b44ee9deab74b3cf0aa77ce6bc745032c8381a6 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 28 Jul 2022 10:54:49 +0200 Subject: [PATCH 32/32] Update supported py versions in setup.py --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 7a729246..0df348f5 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,9 @@ # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ],