Skip to content

Criteria Class Validation #74

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions badgecheck/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
from input import detect_input_type
from graph import fetch_http_node, jsonld_compact_data
from validation import (detect_and_validate_node_class, identity_object_property_dependencies,
validate_expected_node_class, validate_id_property, validate_primitive_property, )
from task_types import (DETECT_AND_VALIDATE_NODE_CLASS, DETECT_INPUT_TYPE,
FETCH_HTTP_NODE, JSONLD_COMPACT_DATA, IDENTITY_OBJECT_PROPERTY_DEPENDENCIES,
VALIDATE_EXPECTED_NODE_CLASS, VALIDATE_ID_PROPERTY, VALIDATE_PRIMITIVE_PROPERTY, )
from validation import (criteria_property_dependencies, detect_and_validate_node_class,
evidence_property_dependencies, identity_object_property_dependencies,
validate_expected_node_class, validate_property, )
from task_types import (DETECT_AND_VALIDATE_NODE_CLASS, DETECT_INPUT_TYPE, CRITERIA_PROPERTY_DEPENDENCIES,
EVIDENCE_PROPERTY_DEPENDENCIES, FETCH_HTTP_NODE, JSONLD_COMPACT_DATA,
IDENTITY_OBJECT_PROPERTY_DEPENDENCIES, VALIDATE_EXPECTED_NODE_CLASS,
VALIDATE_PROPERTY, )


functions = {
FUNCTIONS = {
DETECT_AND_VALIDATE_NODE_CLASS: detect_and_validate_node_class,
DETECT_INPUT_TYPE: detect_input_type,
CRITERIA_PROPERTY_DEPENDENCIES: criteria_property_dependencies,
EVIDENCE_PROPERTY_DEPENDENCIES: evidence_property_dependencies,
FETCH_HTTP_NODE: fetch_http_node,
JSONLD_COMPACT_DATA: jsonld_compact_data,
IDENTITY_OBJECT_PROPERTY_DEPENDENCIES: identity_object_property_dependencies,
VALIDATE_EXPECTED_NODE_CLASS: validate_expected_node_class,
VALIDATE_ID_PROPERTY: validate_id_property,
VALIDATE_PRIMITIVE_PROPERTY: validate_primitive_property,
VALIDATE_PROPERTY: validate_property,
}


def task_named(key):
return functions[key]
return FUNCTIONS[key]
8 changes: 5 additions & 3 deletions badgecheck/tasks/task_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
"""
DETECT_AND_VALIDATE_NODE_CLASS = 'DETECT_AND_VALIDATE_NODE_CLASS'
VALIDATE_EXPECTED_NODE_CLASS = 'VALIDATE_EXPECTED_NODE_CLASS'
VALIDATE_ID_PROPERTY = 'VALIDATE_ID_PROPERTY'
VALIDATE_PRIMITIVE_PROPERTY = 'VALIDATE_PRIMITIVE_PROPERTY'
VALIDATE_PROPERTY = 'VALIDATE_PROPERTY'

# Class Level Validation Tasks
CRITERIA_PROPERTY_DEPENDENCIES = 'CRITERIA_PROPERTY_DEPENDENCIES'
IDENTITY_OBJECT_PROPERTY_DEPENDENCIES = 'IDENTITY_OBJECT_PROPERTY_DEPENDENCIES'
EVIDENCE_PROPERTY_DEPENDENCIES = 'EVIDENCE_PROPERTY_DEPENDENCIES'

CLASS_VALIDATION_TASKS = (IDENTITY_OBJECT_PROPERTY_DEPENDENCIES,)
CLASS_VALIDATION_TASKS = (CRITERIA_PROPERTY_DEPENDENCIES, EVIDENCE_PROPERTY_DEPENDENCIES,
IDENTITY_OBJECT_PROPERTY_DEPENDENCIES,)
4 changes: 4 additions & 0 deletions badgecheck/tasks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def is_empty_list(value):
return isinstance(value, (tuple, list,)) and len(value) == 0


def is_null_list(value):
return isinstance(value, (tuple, list,)) and all(val is None for val in value)


def abbreviate_value(value):
if len(str(value)) < 48:
return str(value)
Expand Down
229 changes: 135 additions & 94 deletions badgecheck/tasks/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
from ..exceptions import ValidationError
from ..state import get_node_by_id

from .task_types import (CLASS_VALIDATION_TASKS, FETCH_HTTP_NODE,
from .task_types import (CLASS_VALIDATION_TASKS, CRITERIA_PROPERTY_DEPENDENCIES,
EVIDENCE_PROPERTY_DEPENDENCIES, FETCH_HTTP_NODE,
IDENTITY_OBJECT_PROPERTY_DEPENDENCIES, VALIDATE_EXPECTED_NODE_CLASS,
VALIDATE_ID_PROPERTY, VALIDATE_PRIMITIVE_PROPERTY, )
VALIDATE_PROPERTY, VALIDATE_PROPERTY,)

from .utils import abbreviate_value, is_empty_list, task_result
from .utils import abbreviate_value, is_empty_list, is_null_list, task_result


class OBClasses(object):
Expand Down Expand Up @@ -46,7 +47,7 @@ class ValueTypes(object):
# TODO: EMAIL = 'EMAIL'
# TODO: TELEPHONE = 'TELEPHONE'

PRIMITIVES = (BOOLEAN, DATETIME, IDENTITY_HASH, IRI, MARKDOWN_TEXT, TEXT, URL)
PRIMITIVES = (BOOLEAN, DATETIME, ID, IDENTITY_HASH, IRI, MARKDOWN_TEXT, TEXT, URL)


class PrimitiveValueValidator(object):
Expand Down Expand Up @@ -120,9 +121,10 @@ def _validate_iri(cls, value):
re.match(urn_regex, value, re.IGNORECASE)
)

@staticmethod
def _validate_markdown_text(value):
raise NotImplementedError("TODO: Add validator")
@classmethod
def _validate_markdown_text(cls, value):
# TODO Assert no render errors if relevant?
return cls._validate_text

@staticmethod
def _validate_text(value):
Expand All @@ -141,10 +143,10 @@ def _validate_url(value):
return ret


def validate_primitive_property(state, task_meta):
def validate_property(state, task_meta):
"""
Validates presence and data type of a single property that is
expected to be one of the Open Badges Primitive data types.
expected to be one of the Open Badges Primitive data types or an ID.
"""
node_id = task_meta.get('node_id')
node = get_node_by_id(state, node_id)
Expand All @@ -154,44 +156,83 @@ def validate_primitive_property(state, task_meta):
prop_type = task_meta.get('prop_type')
prop_value = node.get(prop_name)
required = bool(task_meta.get('required'))
allow_many = task_meta.get('many')
actions = []

if prop_value is None and required:
return task_result(
False, "Required property {} not present in {} {}".format(
prop_name, node_class, node_id)
)
elif task_meta.get('many') and required and is_empty_list(prop_value):
return task_result(
False, "Required property {} contains no values in {} {}".format(
try:
prop_value = node[prop_name]
except KeyError:
if not required:
return task_result(
True, "Optional property {} not present in {} {}".format(
prop_name, node_class, node_id)
)

if prop_value is None and not required:
)
return task_result(
True, "Optional property {} not present in {} {}".format(
False, "Required property {} not present in {} {}".format(
prop_name, node_class, node_id)
)
)

if not isinstance(prop_value, (list, tuple,)):
values_to_test = [prop_value]
else:
values_to_test = prop_value

if required and (is_empty_list(values_to_test) or is_null_list(values_to_test)):
return task_result(
False, "Required property {} value {} is not acceptable in {} {}".format(
prop_name, abbreviate_value(prop_value), node_class, node_id)
)
if not required and (is_empty_list(values_to_test) or is_null_list(values_to_test)):
return task_result(True, "Optional property {} is null in {} {}".format(
prop_name, node_class, node_id
))
# TODO Return STRIP_PROPERTY action

if not allow_many and len(values_to_test) > 1:
return task_result(
False, "Property {} in {} {} has more than the single allowed value.".format(
prop_name, node_class, node_id
))

try:
for val in values_to_test:
value_check_function = PrimitiveValueValidator(prop_type)
if not required and not val:
continue
if not value_check_function(val):
raise ValidationError("{} property {} value {} not valid in {} {}".format(
prop_type, prop_name, abbreviate_value(val), node_class, node_id))
if prop_type != ValueTypes.ID:
for val in values_to_test:
value_check_function = PrimitiveValueValidator(prop_type)
if not value_check_function(val):
raise ValidationError("{} property {} value {} not valid in {} {}".format(
prop_type, prop_name, abbreviate_value(val), node_class, node_id))
else:
for val in values_to_test:
if not PrimitiveValueValidator(ValueTypes.IRI)(val):
raise ValidationError(
"ID-type property {} had value `{}` not in IRI format in {}.".format(
prop_name, abbreviate_value(val), node_id)
)

if not task_meta.get('fetch', False):
try:
target = get_node_by_id(state, val)
except IndexError:
if task_meta.get('allow_remote_url') and PrimitiveValueValidator(ValueTypes.URL)(val):
continue
raise ValidationError(
'Node {} has {} property value `{}` that appears not to be in URI format'.format(
node_id, prop_name, abbreviate_value(val)
))
actions.append(
add_task(VALIDATE_EXPECTED_NODE_CLASS, node_id=val,
expected_class=task_meta.get('expected_class')))
else:
actions.append(
add_task(FETCH_HTTP_NODE, url=val,
expected_class=task_meta.get('expected_class')))

except ValidationError as e:
return task_result(False, e.message)
return task_result(
True, "{} property {} valid in {} {}".format(
prop_type, prop_name, node_class, node_id
)
True, "{} property {} value {} valid in {} {}".format(
prop_type, prop_name, abbreviate_value(prop_value), node_class, node_id
), actions
)


Expand All @@ -211,7 +252,7 @@ def __init__(self, class_name):
# 'expected_class': OBClasses.VerificationObject, 'required': True},
{'prop_name': 'issuedOn', 'prop_type': ValueTypes.DATETIME, 'required': True},
{'prop_name': 'expires', 'prop_type': ValueTypes.DATETIME, 'required': False},
{'prop_name': 'image', 'prop_type': ValueTypes.URL, 'required': False}, # TODO: ValueTypes.DATA_URI_OR_URL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There may eventually be a merge conflict here, as in my recent PR #77 I switched this TODO to a test against ValueTypes.DATA_URI_OR_URL.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, trouble -- I'll do a manual merge when approved.

{'prop_name': 'image', 'prop_type': ValueTypes.URL, 'required': False},
{'prop_name': 'narrative', 'prop_type': ValueTypes.MARKDOWN_TEXT, 'required': False},
# TODO: {'prop_name': 'evidence', 'prop_type': ValueTypes.ID,
# 'expected_class': OBClasses.Evidence, 'many': True, 'fetch': False, required': True},
Expand All @@ -225,8 +266,9 @@ def __init__(self, class_name):
{'prop_name': 'name', 'prop_type': ValueTypes.TEXT, 'required': True},
{'prop_name': 'description', 'prop_type': ValueTypes.TEXT, 'required': True},
{'prop_name': 'image', 'prop_type': ValueTypes.URL, 'required': True}, # TODO: ValueTypes.DATA_URI_OR_URL
# TODO: {'prop_name': 'criteria', 'prop_type': ValueTypes.ID,
# 'expected_class': OBClasses.Criteria, 'fetch': False, 'required': True},
{'prop_name': 'criteria', 'prop_type': ValueTypes.ID,
'expected_class': OBClasses.Criteria, 'fetch': False,
'required': True, 'allow_remote_url': True},
# TODO: {'prop_name': 'alignment', 'prop_type': ValueTypes.ID,
# 'expected_class': OBClasses.AlignmentObject, 'many': True, 'fetch': False, required': False},
# TODO: {'prop_name': 'tags', 'prop_type': ValueTypes.TEXT, 'many': True, 'required': False},
Expand All @@ -250,6 +292,14 @@ def __init__(self, class_name):
# TODO: {'prop_name': 'revocationList', 'prop_type': ValueTypes.ID,
# 'expected_class': OBClasses.Revocationlist, 'fetch': True, 'required': False}, # TODO: Fetch only for relevant assertions?
)
elif class_name == OBClasses.Criteria:
self.validators = (
# TODO: {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE,
# 'required': False, 'default': OBClasses.Criteria},
{'prop_name': 'id', 'prop_type': ValueTypes.IRI, 'required': False},
{'prop_name': 'narrative', 'prop_type': ValueTypes.MARKDOWN_TEXT, 'required': False},
{'task_type': CRITERIA_PROPERTY_DEPENDENCIES}
)
elif class_name == OBClasses.IdentityObject:
self.validators = (
# TODO: {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, 'required': True},
Expand All @@ -263,7 +313,11 @@ def __init__(self, class_name):
# TODO: {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, 'required': False},
{'prop_name': 'id', 'prop_type': ValueTypes.IRI, 'required': False},
{'prop_name': 'narrative', 'prop_type': ValueTypes.MARKDOWN_TEXT, 'required': False},
# TODO {'task_type': EVIDENCE_PROPERTY_DEPENDENCIES}
{'prop_name': 'name', 'prop_type': ValueTypes.TEXT, 'required': False},
{'prop_name': 'description', 'prop_type': ValueTypes.TEXT, 'required': False},
{'prop_name': 'genre', 'prop_type': ValueTypes.TEXT, 'required': False},
{'prop_name': 'audience', 'prop_type': ValueTypes.TEXT, 'required': False},
{'task_type': EVIDENCE_PROPERTY_DEPENDENCIES}
)
else:
raise NotImplementedError("Chosen OBClass not implemented yet.")
Expand All @@ -275,12 +329,7 @@ def _get_validation_actions(node_id, node_class):
for validator in validators:
if validator.get('prop_type') in ValueTypes.PRIMITIVES:
actions.append(add_task(
VALIDATE_PRIMITIVE_PROPERTY, node_id=node_id,
node_class=node_class, **validator
))
elif validator.get('prop_type') == ValueTypes.ID:
actions.append(add_task(
VALIDATE_ID_PROPERTY, node_id=node_id,
VALIDATE_PROPERTY, node_id=node_id,
node_class=node_class, **validator
))
elif validator.get('task_type') in CLASS_VALIDATION_TASKS:
Expand Down Expand Up @@ -323,58 +372,6 @@ def validate_expected_node_class(state, task_meta):
)


def validate_id_property(state, task_meta):
node_id = task_meta.get('node_id')
node = get_node_by_id(state, node_id)
node_class = task_meta.get('node_class')
expected_class = task_meta.get('expected_class')

prop_name = task_meta.get('prop_name')
required = bool(task_meta.get('required'))
prop_value = node.get(prop_name)
actions = []

if prop_value is None and required:
return task_result(
False, "Required property {} not present in {} {}".format(
prop_name, node_class, node_id)
)

if not isinstance(prop_value, (list, tuple,)):
values_to_test = [prop_value]
else:
values_to_test = prop_value

try:
for val in values_to_test:
if not PrimitiveValueValidator(ValueTypes.IRI)(val):
raise ValidationError(
"ID-type property {} had value `{}` not in IRI format in {}.".format(
prop_name, abbreviate_value(val), node_id)
)

if not task_meta.get('fetch', False):
try:
target = get_node_by_id(state, val)
except IndexError:
if task_meta.get('allow_remote_url') and PrimitiveValueValidator(ValueTypes.URL)(val):
continue
raise ValidationError(
'Node {} has {} property value `{}` that appears not to be in URI format'.format(
node_id, prop_name, abbreviate_value(val)
))
actions.append(
add_task(VALIDATE_EXPECTED_NODE_CLASS, node_id=val, expected_class=expected_class))
else:
actions.append(add_task(FETCH_HTTP_NODE, url=val, expected_class=expected_class))
except ValidationError as e:
return task_result(False, e.message)

label = 'references are' if len(values_to_test) > 1 else 'reference is'
return task_result(True, "{} property {} {} valid in {} {}".format(
ValueTypes.ID, prop_name, label, node_class, node_id), actions)


"""
Class Validation Tasks
"""
Expand All @@ -399,3 +396,47 @@ def identity_object_property_dependencies(state, task_meta):
return task_result(False, "Email type identity must match email format.")

return task_result(True, "IdentityObject passes validation rules.")


def criteria_property_dependencies(state, task_meta):
node_id = task_meta.get('node_id')
node = get_node_by_id(state, node_id)
is_blank_id_node = bool(re.match(r'_:b\d+$', node_id))

if is_blank_id_node and not node.get('narrative'):
return task_result(False,
"Criteria node {} has no narrative. Either external id or narrative is required.".format(node_id)
)
elif is_blank_id_node:
return task_result(
True, "Criteria node {} is a narrative-based piece of evidence.".format(node_id)
)
elif not is_blank_id_node and node.get('narrative'):
return task_result(
True, "Criteria node {} has a URL and narrative."
)
# Case to handle no narrative but other props preventing compaction down to simple id string:
# {'id': 'http://example.com/1', 'name': 'Criteria Name'}
return task_result(True, "Criteria node {} has a URL.")


def evidence_property_dependencies(state, task_meta):
node_id = task_meta.get('node_id')
node = get_node_by_id(state, node_id)
is_blank_id_node = bool(re.match(r'_:b\d+$', node_id))

if is_blank_id_node and not node.get('narrative'):
return task_result(False,
"Evidence node {} has no narrative. Either external id or narrative is required.".format(node_id)
)
elif is_blank_id_node:
return task_result(
True, "Evidence node {} is a narrative-based piece of evidence.".format(node_id)
)
elif not is_blank_id_node and node.get('narrative'):
return task_result(
True, "Evidence node {} has a URL and narrative."
)
# Case to handle no narrative but other props preventing compaction down to simple id string:
# {'id': 'http://example.com/1', 'name': 'Evidence Name'}
return task_result(True, "Evidence node {} has a URL.")
Loading