diff --git a/setup.py b/setup.py index dd2d2d526..3256c994d 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ "elasticsearch-dsl==5.4.0", "elasticsearch==5.4.0", "future==0.18.2", - "jsonschema_serialize_fork==2.1.1", + "jsonschema[format]==4.4.0", "lucenequery==0.1", "passlib==1.7.2", "psutil==5.6.7", @@ -33,7 +33,7 @@ "rdflib-jsonld==0.6.0", "rdflib==4.2.2", "redis==3.5.3", - "requests==2.22.0", + "requests==2.27.1", "simplejson==3.17.0", "snovault-search==1.0.4", "transaction==3.0.0", diff --git a/src/snovault/schema_utils.py b/src/snovault/schema_utils.py index cbf41b58b..10cb52db1 100644 --- a/src/snovault/schema_utils.py +++ b/src/snovault/schema_utils.py @@ -8,12 +8,10 @@ import codecs import collections import copy -from jsonschema_serialize_fork import ( - Draft4Validator, - FormatChecker, - RefResolver, -) -from jsonschema_serialize_fork.exceptions import ValidationError +from snovault.schema_validation import SerializingSchemaValidator +from jsonschema import FormatChecker +from jsonschema import RefResolver +from jsonschema.exceptions import ValidationError from uuid import UUID from .util import ensurelist @@ -95,7 +93,6 @@ def linkTo(validator, linkTo, instance, schema): error = "%r is not of type %s" % (instance, ", ".join(reprs)) yield ValidationError(error) return - linkEnum = schema.get('linkEnum') if linkEnum is not None: if not validator.is_type(linkEnum, "array"): @@ -123,10 +120,6 @@ def linkTo(validator, linkTo, instance, schema): yield ValidationError(error) return - # And normalize the value to a uuid - if validator._serialize: - validator._validated[-1] = str(item.uuid) - def linkFrom(validator, linkFrom, instance, schema): # avoid circular import @@ -154,9 +147,6 @@ def linkFrom(validator, linkFrom, instance, schema): yield ValidationError(error) return else: - if validator._serialize: - lv = len(validator._validated) - # Look for an existing item; # if found use the schema for its type, # which may be a subtype of an abstract linkType @@ -210,16 +200,13 @@ def linkFrom(validator, linkFrom, instance, schema): for error in validator.descend(instance, subschema): yield error - if validator._serialize: - validated_instance = validator._validated[lv] - del validator._validated[lv:] - if uuid is not None: - validated_instance['uuid'] = uuid - elif 'uuid' in validated_instance: # where does this come from? - del validated_instance['uuid'] - if new_type is not None: - validated_instance['@type'] = [new_type] - validator._validated[-1] = validated_instance + validated_instance = instance + if uuid is not None: + validated_instance['uuid'] = uuid + elif 'uuid' in validated_instance: # where does this come from? + del validated_instance['uuid'] + if new_type is not None: + validated_instance['@type'] = [new_type] class IgnoreUnchanged(ValidationError): @@ -250,17 +237,6 @@ def permission(validator, permission, instance, schema): yield IgnoreUnchanged(error) -orig_uniqueItems = Draft4Validator.VALIDATORS['uniqueItems'] - - -def uniqueItems(validator, uI, instance, schema): - # Use serialized items if available - # (this gives the linkTo validator a chance to normalize paths into uuids) - if validator._serialize and validator._validated[-1]: - instance = validator._validated[-1] - yield from orig_uniqueItems(validator, uI, instance, schema) - - VALIDATOR_REGISTRY = {} @@ -281,8 +257,8 @@ def notSubmittable(validator, linkTo, instance, schema): yield ValidationError('submission disallowed') -class SchemaValidator(Draft4Validator): - VALIDATORS = Draft4Validator.VALIDATORS.copy() +class SchemaValidator(SerializingSchemaValidator): + VALIDATORS = SerializingSchemaValidator.VALIDATORS.copy() VALIDATORS['notSubmittable'] = notSubmittable # for backwards-compatibility VALIDATORS['calculatedProperty'] = notSubmittable @@ -290,7 +266,6 @@ class SchemaValidator(Draft4Validator): VALIDATORS['linkFrom'] = linkFrom VALIDATORS['permission'] = permission VALIDATORS['requestMethod'] = requestMethod - VALIDATORS['uniqueItems'] = uniqueItems VALIDATORS['validators'] = validators SERVER_DEFAULTS = SERVER_DEFAULTS @@ -311,13 +286,13 @@ def load_schema(filename): schema = mixinProperties(schema, resolver) # SchemaValidator is not thread safe for now - SchemaValidator(schema, resolver=resolver, serialize=True) + SchemaValidator(schema, resolver=resolver) return schema def validate(schema, data, current=None): resolver = NoRemoteResolver.from_schema(schema) - sv = SchemaValidator(schema, resolver=resolver, serialize=True, format_checker=format_checker) + sv = SchemaValidator(schema, resolver=resolver, format_checker=format_checker) validated, errors = sv.serialize(data) filtered_errors = [] @@ -336,6 +311,18 @@ def validate(schema, data, current=None): validated_value = validated_value[key] if validated_value == current_value: continue + # Also ignore requestMethod and permission errors from defaults. + if isinstance(error, IgnoreUnchanged): + current_value = data + try: + for key in error.path: + # If it's in original data then either user passed it in + # or it's from PATCH object with unchanged data. If it's + # unchanged then it's already been skipped above. + current_value = current_value[key] + except KeyError: + # If it's not in original data then it's filled in by defaults. + continue filtered_errors.append(error) return validated, filtered_errors diff --git a/src/snovault/schema_validation.py b/src/snovault/schema_validation.py new file mode 100644 index 000000000..6012f97f2 --- /dev/null +++ b/src/snovault/schema_validation.py @@ -0,0 +1,143 @@ +from copy import deepcopy +from jsonschema import Draft202012Validator +from jsonschema import validators +from jsonschema.exceptions import ValidationError +from pyramid.threadlocal import get_current_request +from pyramid.traversal import find_resource + + +NO_DEFAULT = object() + + +def get_resource_base(validator, linkTo): + from snovault import COLLECTIONS + request = get_current_request() + collections = request.registry[COLLECTIONS] + if validator.is_type(linkTo, 'string'): + resource_base = collections.get(linkTo, request.root) + else: + resource_base = request.root + return resource_base + + +def normalize_links(validator, links, linkTo): + resource_base = get_resource_base(validator, linkTo) + normalized_links = [] + errors = [] + for link in links: + try: + normalized_links.append( + str(find_resource(resource_base, link.replace(':', '%3A')).uuid) + ) + except KeyError: + errors.append( + ValidationError(f'Unable to resolve link: {link}') + ) + normalized_links.append( + link + ) + return normalized_links, errors + + +def should_mutate_properties(validator, instance): + if validator.is_type(instance, 'object'): + return True + return False + + +def get_items_or_empty_object(validator, subschema): + items = subschema.get('items', {}) + if validator.is_type(items, 'object'): + return items + return {} + + +def maybe_normalize_links_to_uuids(validator, property, subschema, instance): + errors = [] + if 'linkTo' in subschema: + link = instance.get(property) + if link: + normalized_links, errors = normalize_links( + validator, + [link], + subschema.get('linkTo'), + ) + instance[property] = normalized_links[0] + if 'linkTo' in get_items_or_empty_object(validator, subschema): + links = instance.get(property, []) + if links: + normalized_links, errors = normalize_links( + validator, + links, + subschema.get('items').get('linkTo'), + ) + instance[property] = normalized_links + for error in errors: + yield error + + +def set_defaults(validator, property, subschema, instance): + if 'default' in subschema: + instance.setdefault( + property, + deepcopy(subschema['default']) + ) + if 'serverDefault' in subschema: + server_default = validator.server_default( + instance, + subschema + ) + if server_default is not NO_DEFAULT: + instance.setdefault( + property, + server_default + ) + + +def extend_with_default(validator_class): + validate_properties = validator_class.VALIDATORS['properties'] + + def mutate_properties(validator, properties, instance, schema): + for property, subschema in properties.items(): + if not validator.is_type(subschema, 'object'): + continue + yield from maybe_normalize_links_to_uuids(validator, property, subschema, instance) + set_defaults(validator, property, subschema, instance) + + def before_properties_validation_hook(validator, properties, instance, schema): + if should_mutate_properties(validator, instance): + yield from mutate_properties(validator, properties, instance, schema) + yield from validate_properties(validator, properties, instance, schema) + + return validators.extend( + validator_class, {'properties': before_properties_validation_hook}, + ) + + +ExtendedValidator = extend_with_default(Draft202012Validator) + + +class SerializingSchemaValidator(ExtendedValidator): + + SERVER_DEFAULTS = {} + + def add_server_defaults(self, server_defaults): + self.SERVER_DEFAULTS.update(server_defaults) + return self + + def serialize(self, instance): + self._original_instance = instance + self._mutated_instance = deepcopy( + self._original_instance + ) + errors = list( + self.iter_errors( + self._mutated_instance + ) + ) + return self._mutated_instance, errors + + def server_default(self, instance, subschema): + factory_name = subschema['serverDefault'] + factory = self.SERVER_DEFAULTS[factory_name] + return factory(instance, subschema) diff --git a/src/snovault/schema_views.py b/src/snovault/schema_views.py index 1a6428bba..6fdca3169 100644 --- a/src/snovault/schema_views.py +++ b/src/snovault/schema_views.py @@ -78,8 +78,8 @@ def schemas_map(context, request): types = request.registry[TYPES] profiles_map = {} for type_info in types.by_item_type.values(): - if 'id' in type_info.schema: - profiles_map[type_info.name] = type_info.schema['id'] + if '$id' in type_info.schema: + profiles_map[type_info.name] = type_info.schema['$id'] profiles_map['@type'] = ['JSONSchemas'] return profiles_map diff --git a/src/snovault/tests/test_post_put_patch.py b/src/snovault/tests/test_post_put_patch.py index 30a930509..19101f3d2 100644 --- a/src/snovault/tests/test_post_put_patch.py +++ b/src/snovault/tests/test_post_put_patch.py @@ -233,7 +233,10 @@ def test_put_object_child_validation(content_with_child, testapp): }] } res = testapp.put_json(content_with_child['@id'], edit, status=422) - assert res.json['errors'][0]['name'] == [u'reverse', 0, u'target'] + assert ['reverse', 0, 'target'] in [ + x['name'] + for x in res.json['errors'] + ] def test_put_object_validates_child_references(content_with_child, testapp): diff --git a/src/snovault/tests/test_schema_utils.py b/src/snovault/tests/test_schema_utils.py index 28ac57c1c..334ac4626 100644 --- a/src/snovault/tests/test_schema_utils.py +++ b/src/snovault/tests/test_schema_utils.py @@ -16,9 +16,13 @@ def content(testapp): def test_uniqueItems_validates_normalized_links(content, threadlocals): schema = { - 'uniqueItems': True, - 'items': { - 'linkTo': 'TestingLinkTarget', + 'properties': { + 'some_links': { + 'uniqueItems': True, + 'items': { + 'linkTo': 'TestingLinkTarget', + } + } } } uuid = targets[0]['uuid'] @@ -26,7 +30,7 @@ def test_uniqueItems_validates_normalized_links(content, threadlocals): uuid, '/testing-link-targets/{}'.format(uuid), ] - validated, errors = validate(schema, data) + validated, errors = validate(schema, {'some_links': data}) assert len(errors) == 1 assert ( errors[0].message == "['{}', '{}'] has non-unique elements".format( diff --git a/src/snovault/tests/test_validator.py b/src/snovault/tests/test_validator.py new file mode 100644 index 000000000..30a5d713d --- /dev/null +++ b/src/snovault/tests/test_validator.py @@ -0,0 +1,312 @@ +import pytest + +from collections import OrderedDict + +from snovault.schema_validation import NO_DEFAULT +from snovault.schema_utils import SchemaValidator +from jsonschema.exceptions import ValidationError + + +validator_class = SchemaValidator + + +fake_schema = { + '$id': 'abc', + 'title': 'Fake', + 'description': 'Schema', + '$schema': 'https://json-schema.org/draft/2020-12/schema', + 'type': 'object', + 'required': ['award', 'lab'], + 'identifyingProperties': ['uuid'], + 'additionalProperties': False, + 'properties': { + 'uuid': { + 'type': 'string' + }, + 'award': { + 'type': 'string' + }, + 'lab': { + 'type': 'string' + } + } +} + + +def make_default(instance, subschema): + if instance.get('skip'): + return NO_DEFAULT + assert subschema == {'serverDefault': 'test'} + return 'bar' + + +def test_validator_serializes_default_properties(): + schema = { + 'properties': { + 'foo': { + 'default': 'bar' + } + } + } + result, errors = validator_class( + schema + ).serialize( + {} + ) + assert result == {'foo': 'bar'} + assert errors == [] + + +def test_validator_serializes_default_properties_in_items(): + schema = { + 'items': { + 'properties': { + 'foo': { + 'default': 'bar' + } + } + } + } + result, errors = validator_class( + schema, + ).serialize( + [ + {} + ] + ) + assert result == [{'foo': 'bar'}] + assert errors == [] + + +def test_validator_serializes_server_default_properties(): + schema = { + 'properties': { + 'foo': { + 'serverDefault': 'test' + } + } + } + result, errors = validator_class( + schema, + ).add_server_defaults( + { + 'test': make_default + } + ).serialize( + {} + ) + assert result == {'foo': 'bar'} + assert errors == [] + + +def test_validator_ignores_server_default_returning_no_default(): + schema = { + 'properties': { + 'foo': { + 'serverDefault': 'test' + }, + 'skip': {} + } + } + result, errors = validator_class( + schema, + ).add_server_defaults( + { + 'test': make_default + } + ).serialize( + { + 'skip': True + } + ) + assert result == {'skip': True} + assert errors == [] + + +def test_validator_serializes_server_default_properties_in_items(): + schema = { + 'items': { + 'properties': { + 'foo': { + 'serverDefault': 'test' + } + } + } + } + result, errors = SchemaValidator( + schema + ).add_server_defaults( + { + 'test': make_default + } + ).serialize( + [{}] + ) + assert result == [{'foo': 'bar'}] + assert errors == [] + + +def test_validator_serializes_properties_in_order_of_input(): + schema = { + 'properties': { + 'foo': {}, + 'bar': {}, + } + } + validator = validator_class( + schema + ) + value = { + 'bar': 1, + 'foo': 2 + } + result, errors = validator.serialize(value) + assert list(result) == ['bar', 'foo'] + assert errors == [] + + +def test_validator_returns_error(): + schema = { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string' + } + }, + 'required': ['name'] + } + result, errors = validator_class( + schema, + ).serialize( + { + 'name': 'abc' + } + ) + assert result == {'name': 'abc'} + assert not errors + result, errors = validator_class( + schema, + ).serialize( + { + 'name': 1 + } + ) + assert result == { + 'name': 1 + } + assert isinstance(errors[0], ValidationError) + assert errors[0].message == "1 is not of type 'string'" + result, errors = validator_class( + schema, + ).serialize( + {} + ) + assert result == {} + assert isinstance(errors[0], ValidationError) + assert errors[0].message == "'name' is a required property" + + +def test_validator_check_schema(): + validator_class.check_schema(fake_schema) + + +def test_validator_extend_with_default(): + from copy import deepcopy + from snovault.schema_validation import SerializingSchemaValidator + original_instance = { + 'x': 'y' + } + mutated_instance = deepcopy(original_instance) + assert original_instance == mutated_instance + schema = {'properties': {'foo': {'default': 'bar'}}} + SerializingSchemaValidator(schema).validate(mutated_instance) + assert original_instance == {'x': 'y'} + assert mutated_instance == {'x': 'y', 'foo': 'bar'} + + +def test_validator_extend_with_default_and_serialize(): + instance = { + 'x': 'y' + } + from snovault.schema_validation import SerializingSchemaValidator + schema = {'properties': {'foo': {'default': 'bar'}}} + result, errors = SerializingSchemaValidator(schema).serialize(instance) + assert instance == {'x': 'y'} + assert result == {'x': 'y', 'foo': 'bar'} + assert errors == [] + schema = { + 'properties': { + 'foo': { + 'default': 'bar' + }, + 'name': { + 'type': 'string' + } + }, + 'required': ['name'] + } + result, errors = SerializingSchemaValidator(schema).serialize( + { + 'foo': 'thing', + } + ) + assert result == {'foo': 'thing'} + assert errors[0].message == "'name' is a required property" + + +def test_validator_extend_with_server_default_and_serialize(): + instance = { + 'x': 'y' + } + from snovault.schema_validation import SerializingSchemaValidator + schema = {'properties': {'foo': {'serverDefault': 'test'}}} + result, errors = SerializingSchemaValidator( + schema, + ).add_server_defaults( + { + 'test': make_default + } + ).serialize(instance) + assert instance == {'x': 'y'} + assert result == {'x': 'y', 'foo': 'bar'} + assert errors == [] + schema = { + 'properties': { + 'foo': { + 'serverDefault': 'test' + }, + 'name': { + 'type': 'string' + } + }, + 'required': ['name'] + } + result, errors = SerializingSchemaValidator( + schema + ).add_server_defaults( + { + 'test': make_default + } + ).serialize( + { + 'foo': 'thing', + } + ) + assert result == {'foo': 'thing'} + assert errors[0].message == "'name' is a required property" + result, errors = SerializingSchemaValidator( + schema, + ).add_server_defaults( + { + 'test': make_default + } + ).serialize( + { + 'name': 'other thing', + } + ) + assert result == { + 'foo': 'bar', + 'name': 'other thing', + } + assert not errors diff --git a/src/snovault/tests/testing_views.py b/src/snovault/tests/testing_views.py index 90c55dbd1..e69ede788 100644 --- a/src/snovault/tests/testing_views.py +++ b/src/snovault/tests/testing_views.py @@ -418,7 +418,7 @@ class TestingDependencies(Item): item_type = 'testing_dependencies' schema = { 'type': 'object', - 'dependencies': { + 'dependentRequired': { 'dep1': ['dep2'], 'dep2': ['dep1'], }, diff --git a/src/snowflakes/schema_formats.py b/src/snowflakes/schema_formats.py index 39e376451..a2b5cf9e7 100644 --- a/src/snowflakes/schema_formats.py +++ b/src/snowflakes/schema_formats.py @@ -1,9 +1,10 @@ import re import rfc3987 -from jsonschema_serialize_fork import FormatChecker +from jsonschema import FormatChecker from pyramid.threadlocal import get_current_request from uuid import UUID + accession_re = re.compile(r'^SNO(SS|FL)[0-9][0-9][0-9][A-Z][A-Z][A-Z]$') test_accession_re = re.compile(r'^TST(SS|FL)[0-9][0-9][0-9]([0-9][0-9][0-9]|[A-Z][A-Z][A-Z])$') uuid_re = re.compile(r'(?i)\{?(?:[0-9a-f]{4}-?){8}\}?') diff --git a/src/snowflakes/schemas/access_key.json b/src/snowflakes/schemas/access_key.json index d02a84983..4d05b8706 100644 --- a/src/snowflakes/schemas/access_key.json +++ b/src/snowflakes/schemas/access_key.json @@ -1,7 +1,7 @@ { "title": "Admin access key", - "id": "/profiles/access_key_admin.json", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "/profiles/access_key_admin.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", "required": [], "additionalProperties": false, "mixinProperties": [ diff --git a/src/snowflakes/schemas/award.json b/src/snowflakes/schemas/award.json index 3c1426183..38cde1e12 100644 --- a/src/snowflakes/schemas/award.json +++ b/src/snowflakes/schemas/award.json @@ -1,7 +1,7 @@ { "title": "Grant", - "id": "/profiles/award.json", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "/profiles/award.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", "required": [ "name", "project", "rfa"], "identifyingProperties": ["uuid", "name", "title"], "additionalProperties": false, diff --git a/src/snowflakes/schemas/image.json b/src/snowflakes/schemas/image.json index 34945353e..eeeb8c80b 100644 --- a/src/snowflakes/schemas/image.json +++ b/src/snowflakes/schemas/image.json @@ -1,8 +1,8 @@ { "title": "Image", "description": "Schema for images embedded in page objects", - "id": "/profiles/image.json", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "/profiles/image.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": [ "attachment" ], "identifyingProperties": ["uuid"], diff --git a/src/snowflakes/schemas/lab.json b/src/snowflakes/schemas/lab.json index 0ddfab0b1..e619be345 100644 --- a/src/snowflakes/schemas/lab.json +++ b/src/snowflakes/schemas/lab.json @@ -1,7 +1,7 @@ { "title": "Lab", - "id": "/profiles/lab.json", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "/profiles/lab.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["name", "title"], "identifyingProperties": ["uuid", "title", "name"], diff --git a/src/snowflakes/schemas/mixins.json b/src/snowflakes/schemas/mixins.json index efc6c3494..a90ca5e30 100644 --- a/src/snowflakes/schemas/mixins.json +++ b/src/snowflakes/schemas/mixins.json @@ -1,12 +1,12 @@ { "title": "Mixin properties", "schema_version": { - "schema_version": { - "title": "Schema Version", - "comment": "Do not submit, value is assigned by the server. The version of the JSON schema that the server uses to validate the object. Schema version indicates generation of schema used to save version to to enable upgrade steps to work. Individual schemas should set the default.", - "type": "string", - "pattern": "^\\d+(\\.\\d+)*$", - "requestMethod": [] + "schema_version": { + "title": "Schema Version", + "comment": "Do not submit, value is assigned by the server. The version of the JSON schema that the server uses to validate the object. Schema version indicates generation of schema used to save version to enable upgrade steps to work. Individual schemas should set the default.", + "type": "string", + "pattern": "^\\d+(\\.\\d+)*$", + "requestMethod": [] } }, "uuid": { @@ -15,8 +15,7 @@ "type": "string", "format": "uuid", "serverDefault": "uuid4", - "permission": "import_items", - "requestMethod": "POST" + "requestMethod": ["POST"] } }, "standard_status": { diff --git a/src/snowflakes/schemas/page.json b/src/snowflakes/schemas/page.json index cc9a20a99..66107d18b 100644 --- a/src/snowflakes/schemas/page.json +++ b/src/snowflakes/schemas/page.json @@ -1,8 +1,8 @@ { "title": "Page", "description": "Schema for a portal page.", - "id": "/profiles/page.json", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "/profiles/page.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": [ "name", "title" ], "identifyingProperties": ["uuid", "name" ], diff --git a/src/snowflakes/schemas/snowball.json b/src/snowflakes/schemas/snowball.json index 0ef06e6d9..60af6e572 100644 --- a/src/snowflakes/schemas/snowball.json +++ b/src/snowflakes/schemas/snowball.json @@ -2,8 +2,8 @@ "title": "Snowball", "description": "Schema for submitting metadata for a Snowball with 1 or more snowflakes", "comment": "An snowball is a special case of snowset.", - "id": "/profiles/snowball.json", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "/profiles/snowball.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["award", "lab"], "identifyingProperties": ["uuid", "accession"], @@ -16,7 +16,7 @@ { "$ref": "mixins.json#/submitted" }, { "$ref": "snowset.json#/properties" } ], - "dependencies": { + "dependentSchemas": { "status": { "oneOf": [ { diff --git a/src/snowflakes/schemas/snowflake.json b/src/snowflakes/schemas/snowflake.json index aec07f048..ba7d24a11 100644 --- a/src/snowflakes/schemas/snowflake.json +++ b/src/snowflakes/schemas/snowflake.json @@ -1,8 +1,8 @@ { "title": "Snowflake ", "description": "Schema for submitting metadata for a snowflake", - "id": "/profiles/snowflake.json", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "/profiles/snowflake.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["snowset", "type","award", "lab"], "identifyingProperties": ["uuid", "accession"], @@ -26,7 +26,7 @@ "title": "Lab" } }, - "dependencies": { + "dependentSchemas": { "external_accession": { "not": { "required": ["accession"] diff --git a/src/snowflakes/schemas/snowfort.json b/src/snowflakes/schemas/snowfort.json index 088541f2f..0eed546bf 100644 --- a/src/snowflakes/schemas/snowfort.json +++ b/src/snowflakes/schemas/snowfort.json @@ -2,8 +2,8 @@ "title": "Snowfort", "description": "Schema for submitting metadata for a Snowfort with 1 or more snowflakes", "comment": "An snowfort is a special case of snowset.", - "id": "/profiles/snowfort.json", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "/profiles/snowfort.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["award", "lab"], "identifyingProperties": ["uuid", "accession"], @@ -16,7 +16,7 @@ { "$ref": "mixins.json#/submitted" }, { "$ref": "snowset.json#/properties" } ], - "dependencies": { + "dependentSchemas": { "status": { "oneOf": [ { diff --git a/src/snowflakes/schemas/snowset.json b/src/snowflakes/schemas/snowset.json index c393fff4c..6a56331c9 100644 --- a/src/snowflakes/schemas/snowset.json +++ b/src/snowflakes/schemas/snowset.json @@ -1,8 +1,8 @@ { "title": "Snowset", "description": "Abstract schema class for collections of snowflakes", - "id": "/profiles/snowset.json", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "/profiles/snowset.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": [ "award", "lab"], "identifyingProperties": ["uuid", "accession"], diff --git a/src/snowflakes/schemas/user.json b/src/snowflakes/schemas/user.json index 81a2c77c5..e7b48414a 100644 --- a/src/snowflakes/schemas/user.json +++ b/src/snowflakes/schemas/user.json @@ -1,7 +1,7 @@ { "title": "User", - "id": "/profiles/user.json", - "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "/profiles/user.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": [ "email", "first_name", "last_name"], "identifyingProperties": ["uuid", "email"], diff --git a/src/snowflakes/server_defaults.py b/src/snowflakes/server_defaults.py index 1253ceb5e..b5cc7c067 100644 --- a/src/snowflakes/server_defaults.py +++ b/src/snowflakes/server_defaults.py @@ -1,5 +1,5 @@ from datetime import datetime -from jsonschema_serialize_fork import NO_DEFAULT +from snovault.schema_validation import NO_DEFAULT from pyramid.threadlocal import get_current_request from string import ( digits, @@ -36,7 +36,6 @@ def userid(instance, subschema): @server_default def now(instance, subschema): - # from jsonschema_serialize_fork date-time format requires a timezone return datetime.utcnow().isoformat() + '+00:00' diff --git a/src/snowflakes/tests/test_permissions.py b/src/snowflakes/tests/test_permissions.py index a0f33261a..47e8ff5f3 100644 --- a/src/snowflakes/tests/test_permissions.py +++ b/src/snowflakes/tests/test_permissions.py @@ -215,7 +215,16 @@ def test_snowflake_accession_put_admin(testapp, snowflake): assert res.json['errors'][0]['description'] == 'uuid may not be changed' -def test_snowflake_accession_put_submitter(submitter_testapp, snowflake): +def test_snowflake_accession_put_submitter(testapp, submitter, submitter_testapp, snowflake): + # Manually add submitter. + testapp.patch_json( + snowflake['@id'], + { + 'submitted_by': submitter['@id'] + }, + status=200 + ) + testapp.get(snowflake['@id']).json snowflake_id = snowflake['@id'] old_accession = snowflake['accession'] # Can't resubmit @type/@id. diff --git a/src/snowflakes/tests/test_views.py b/src/snowflakes/tests/test_views.py index 0e5e96c8f..5e2bbe2b3 100644 --- a/src/snowflakes/tests/test_views.py +++ b/src/snowflakes/tests/test_views.py @@ -337,9 +337,9 @@ def test_index_data_workbook(workbook, testapp, indexer_testapp, item_type): @pytest.mark.parametrize('item_type', TYPE_LENGTH) def test_profiles(testapp, item_type): - from jsonschema_serialize_fork import Draft4Validator + from jsonschema import Draft202012Validator res = testapp.get('/profiles/%s.json' % item_type).maybe_follow(status=200) - errors = Draft4Validator.check_schema(res.json) + errors = Draft202012Validator.check_schema(res.json) assert not errors