Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ Documentation of release versions of `nacc-form-validator`
* Updates `_validate_compute_gds` to remove partial GDS score calculation
* Updates `_check_subschema_valid` to accept an optional record parameter, defaults to the document - used for rules that require the previous record
* Updates `compare_with` to support the `abs` operator
* Updates `compatibility` error message to be more verbose
* Refactors the tests to be more modularized so that they're more manageable
* Refactors logic for `compare_values` by moving it to its own utility method
* Adds `get_previous_record` method to grab previous record from Datastore, which can grab the previous record or the previous record where a specific field is non-empty
* Updates `_validate_temporalrules` to also support the `ignore_empty` parameter
* Adds support for comparing against the previous record in `compare_with`
* Adds new rule `compare_age` to handle rules that need to compare ages relative to a date
* Adds custom operator `count_exact` to `json_logic.py`
* Fixes issue where `datastore` was not being set for the temp validator in `_check_subschema_valid`, causing nested conditions with previous records to not evaluate correctly

## 0.3.0

Expand Down
8 changes: 4 additions & 4 deletions nacc_form_validator/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class ErrorDefs:
INVALID_DATE_MIN = ErrorDefinition(0x1005, "min")
FILLED_TRUE = ErrorDefinition(0x1006, "filled")
FILLED_FALSE = ErrorDefinition(0x1007, "filled")
COMPATIBILITY_TRUE = ErrorDefinition(0x1008, "compatibility")
COMPATIBILITY_FALSE = ErrorDefinition(0x1009, "compatibility")
COMPATIBILITY = ErrorDefinition(0x1008, "compatibility")
COMPATIBILITY_ELSE = ErrorDefinition(0x1009, "compatibility")
TEMPORAL = ErrorDefinition(0x2000, "temporalrules")
NO_PRIMARY_KEY = ErrorDefinition(0x2001, "temporalrules")
NO_PREV_VISIT = ErrorDefinition(0x2002, "temporalrules")
Expand Down Expand Up @@ -84,9 +84,9 @@ def __set_custom_error_codes(self):
0x1007:
"must be empty",
0x1008:
"{1} for {2} - compatibility rule no: {0}",
"{1} for if {2} then {3} - compatibility rule no: {0}",
0x1009:
"{1} for {2} - compatibility rule no: {0}",
"{1} for if {2} else {3} - compatibility rule no: {0}",
0x2000:
"{1} in current visit for {2} in previous visit - temporal rule no: {0}",
0x2001:
Expand Down
19 changes: 14 additions & 5 deletions nacc_form_validator/nacc_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,12 @@ def _check_subschema_valid(
allow_unknown=True,
error_handler=CustomErrorHandler(subschema),
)

# pass the same datastore
if self.primary_key and self.datastore:
temp_validator.primary_key = self.primary_key
temp_validator.datastore = self.datastore

if operator == "OR":
valid = valid or temp_validator.validate(record,
normalize=False)
Expand Down Expand Up @@ -600,29 +606,32 @@ def _validate_compatibility(self, constraints: List[Mapping], field: str,
else_conds = constraint.get(SchemaDefs.ELSE, None)

# Check if dependencies satisfied the If clause
error_def = ErrorDefs.COMPATIBILITY_FALSE
error_def = ErrorDefs.COMPATIBILITY
errors = None
valid, _ = self._check_subschema_valid(if_conds, if_operator)

# If the If clause valid, validate the Then clause
if valid:
valid, errors = self._check_subschema_valid(
then_conds, then_operator)
error_def = ErrorDefs.COMPATIBILITY_TRUE

# Otherwise validate the else clause, if they exist
elif else_conds:
valid, errors = self._check_subschema_valid(
else_conds, else_operator)
error_def = ErrorDefs.COMPATIBILITY_FALSE
error_def = ErrorDefs.COMPATIBILITY_ELSE
else: # if the If condition is not satisfied, do nothing
pass

# Something in the then/else clause failed - report errors
if errors:
for error in errors.items():
self._error(field, error_def, rule_no, str(error),
if_conds)
if error_def == ErrorDefs.COMPATIBILITY:
self._error(field, error_def, rule_no, str(error),
if_conds, then_conds)
else:
self._error(field, error_def, rule_no, str(error),
if_conds, else_conds)

# pylint: disable=(too-many-locals)
def _validate_temporalrules(self, temporalrules: List[Mapping], field: str,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_nacc_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ def test_lots_of_rules(create_nacc_validator):

# invalid cases - compatibility
assert not nv.validate({'adcid': 0, 'prevenrl': 1, 'oldadcid': None})
assert nv.errors == {'oldadcid': ["('oldadcid', ['null value not allowed']) for {'prevenrl': {'allowed': [1]}} - compatibility rule no: 0"]}
assert nv.errors == {'oldadcid': ["('oldadcid', ['null value not allowed']) for if {'prevenrl': {'allowed': [1]}} then {'oldadcid': {'nullable': False}} - compatibility rule no: 0"]}
assert not nv.validate({'adcid': 0, 'prevenrl': 0, 'oldadcid': 1})
assert nv.errors == {'oldadcid': ["('oldadcid', ['must be empty']) for {'prevenrl': {'allowed': [0, 9]}} - compatibility rule no: 1"]}
assert nv.errors == {'oldadcid': ["('oldadcid', ['must be empty']) for if {'prevenrl': {'allowed': [0, 9]}} then {'oldadcid': {'nullable': True, 'filled': False}} - compatibility rule no: 1"]}

# invalid cases, logic (adcid != oldadcid)
assert not nv.validate({'adcid': 0, 'prevenrl': 1, 'oldadcid': 0})
Expand Down
30 changes: 30 additions & 0 deletions tests/test_nacc_validator_datastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,36 @@ def test_compare_with_previous_different_variable():
nv.reset_record_cache()
assert nv.validate({'patient_id': 'PatientID1', 'visit_num': 2, 'birthyear': 1950})

def test_temporal_check_with_nested_compare_with_previous_record():
""" Test when compare_with previous_record nested inside a temporalrules """
schema = {
"patient_id": {"type": "string"},
"visit_num": {"type": "integer"},
"birthyr": {
"type": "integer",
"temporalrules": [
{
"index": 0,
"previous": {"birthyr": {"forbidden": [-1]}},
"current": {
"birthyr": {
"compare_with": {
"comparator": "==",
"base": "birthyr",
"previous_record": True
}
}
}
}
]
}
}
nv = create_nacc_validator_with_ds(schema, 'patient_id', 'visit_num')
assert nv.validate({'patient_id': 'PatientID1', 'visit_num': 4, 'birthyr': 1950})

assert not nv.validate({'patient_id': 'PatientID1', 'visit_num': 4, 'birthyr': 1951})
assert nv.errors == {'birthyr': ['(\'birthyr\', ["input value doesn\'t satisfy the condition birthyr == birthyr (previous record)"]) in current visit for {\'birthyr\': {\'forbidden\': [-1]}} in previous visit - temporal rule no: 0']}

def test_check_with_rxnorm():
""" Test checking drugID is a valid RXCUI """
schema = {
Expand Down
Loading