Merge pull request #508 from sartography/validation-with-data-store-396

Validation with data store #396
This commit is contained in:
Dan Funk 2022-04-04 16:51:57 -04:00 committed by GitHub
commit 095105e3e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 299 additions and 120 deletions

View File

@ -1,5 +1,3 @@
from flask import g
from crc.api.common import ApiError
from crc.services.data_store_service import DataStoreBase
from crc.scripts.script import Script
@ -7,32 +5,35 @@ from crc.scripts.script import Script
class FileDataGet(Script, DataStoreBase):
def get_description(self):
return """Gets user data from the data store - takes only two keyword arguments arguments: 'file_id' and 'key' """
return """Gets user data from the data store - takes two keyword arguments arguments: 'file_id' and 'key' """
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
if self.validate_kw_args(**kwargs):
myargs = [kwargs['key']]
return True
self.validate_kw_args(**kwargs)
my_args = [kwargs['key']]
if 'default' in kwargs.keys():
my_args.append(kwargs['default'])
result = self.get_validate_common('file_data_get', None, None, kwargs['file_id'], *my_args)
return result
def validate_kw_args(self,**kwargs):
if kwargs.get('key',None) is None:
@staticmethod
def validate_kw_args(**kwargs):
if kwargs.get('key', None) is None:
raise ApiError(code="missing_argument",
message=f"The 'file_data_get' script requires a keyword argument of 'key'")
message=f"The 'file_data_get' script requires a keyword argument of 'key'")
if kwargs.get('file_id',None) is None:
if kwargs.get('file_id', None) is None:
raise ApiError(code="missing_argument",
message=f"The 'file_data_get' script requires a keyword argument of 'file_id'")
message=f"The 'file_data_get' script requires a keyword argument of 'file_id'")
return True
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
if self.validate_kw_args(**kwargs):
myargs = [kwargs['key']]
self.validate_kw_args(**kwargs)
my_args = [kwargs['key']]
if 'default' in kwargs.keys():
myargs.append(kwargs['default'])
my_args.append(kwargs['default'])
return self.get_data_common(None,
None,
'file_data_get',
kwargs['file_id'],
*myargs)
*my_args)

View File

@ -1,5 +1,3 @@
from flask import g
from crc.api.common import ApiError
from crc.services.data_store_service import DataStoreBase
from crc.scripts.script import Script
@ -9,16 +7,23 @@ from crc.services.user_file_service import UserFileService
class FileDataSet(Script, DataStoreBase):
def get_description(self):
return """Sets data the data store - takes three keyword arguments arguments: 'file_id' and 'key' and 'value'"""
return """Sets data the data store - takes three keyword arguments arguments: 'file_id', 'key' and 'value'"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
if self.validate_kw_args(**kwargs):
myargs = [kwargs['key'],kwargs['value']]
fileid = kwargs['file_id']
del(kwargs['file_id'])
return True
self.validate_kw_args(**kwargs)
my_args = [kwargs['key'], kwargs['value']]
file_id = kwargs['file_id']
result = self.set_validate_common(task.id,
study_id,
workflow_id,
'file_data_set',
None,
file_id,
*my_args)
return result
def validate_kw_args(self, **kwargs):
@staticmethod
def validate_kw_args(**kwargs):
if kwargs.get('key', None) is None:
raise ApiError(code="missing_argument",
message=f"The 'file_data_get' script requires a keyword argument of 'key'")
@ -32,28 +37,26 @@ class FileDataSet(Script, DataStoreBase):
if kwargs['key'] == 'irb_code' and not DocumentService.is_allowed_document(kwargs.get('value')):
raise ApiError("invalid_form_field_key",
"When setting an irb_code, the form field id must match a known document in the "
"irb_docunents.xslx reference file. This code is not found in that file '%s'" %
"irb_documents.xlsx reference file. This code is not found in that file '%s'" %
kwargs.get('value'))
return True
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
if self.validate_kw_args(**kwargs):
myargs = [kwargs['key'],kwargs['value']]
self.validate_kw_args(**kwargs)
my_args = [kwargs['key'], kwargs['value']]
try:
fileid = int(kwargs['file_id'])
except:
except Exception:
raise ApiError("invalid_file_id",
"Attempting to update DataStore for an invalid fileid '%s'" % kwargs['file_id'])
"Attempting to update DataStore for an invalid file_id '%s'" % kwargs['file_id'])
del(kwargs['file_id'])
if kwargs['key'] == 'irb_code':
irb_doc_code = kwargs['value']
UserFileService.update_irb_code(fileid, irb_doc_code)
return self.set_data_common(task.id,
None,
None,
@ -61,6 +64,5 @@ class FileDataSet(Script, DataStoreBase):
None,
'file_data_set',
fileid,
*myargs,
*my_args,
**kwargs)

View File

@ -2,12 +2,12 @@ from crc.services.data_store_service import DataStoreBase
from crc.scripts.script import Script
class StudyDataGet(Script,DataStoreBase):
class StudyDataGet(Script, DataStoreBase):
def get_description(self):
return """Gets study data from the data store."""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
return self.do_task(task, study_id, workflow_id, *args, **kwargs)
return self.get_validate_common('study_data_get', study_id, None, None, *args)
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
return self.get_data_common(study_id,
@ -15,4 +15,3 @@ class StudyDataGet(Script,DataStoreBase):
'study_data_get',
None,
*args)

View File

@ -2,17 +2,19 @@ from crc.services.data_store_service import DataStoreBase
from crc.scripts.script import Script
class StudyDataSet(Script,DataStoreBase):
class StudyDataSet(Script, DataStoreBase):
def get_description(self):
return """Sets study data from the data store. Takes two positional arguments key and value"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
self.set_validate_common(study_id,
workflow_id,
None,
'study_data_set',
None,
*args)
result = self.set_validate_common(task.id,
study_id,
workflow_id,
'study_data_set',
None,
None,
*args)
return result
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
return self.set_data_common(task.id,
@ -24,9 +26,3 @@ class StudyDataSet(Script,DataStoreBase):
None,
*args,
**kwargs)

View File

@ -9,7 +9,7 @@ class UserDataGet(Script, DataStoreBase):
return """Gets user data from the data store - takes only one argument 'key' """
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
self.do_task(task, study_id, workflow_id, *args, **kwargs)
return self.get_validate_common('user_data_get', None, g.user.uid, None, *args)
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
return self.get_data_common(None,

View File

@ -4,17 +4,18 @@ from crc.services.data_store_service import DataStoreBase
from crc.scripts.script import Script
class UserDataSet(Script,DataStoreBase):
class UserDataSet(Script, DataStoreBase):
def get_description(self):
return """Sets user data to the data store these are positional arguments key and value.
example: user_data_set('mykey','myvalue')
example: user_data_set('my_key','my_value')
"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
self.set_validate_common(None,
self.set_validate_common(task.id,
study_id,
workflow_id,
g.user.uid,
'user_data_set',
g.user.uid,
None,
*args)
@ -28,6 +29,3 @@ class UserDataSet(Script,DataStoreBase):
None,
*args,
**kwargs)

View File

@ -2,45 +2,58 @@ from crc import session
from crc.api.common import ApiError
from crc.models.data_store import DataStoreModel
from crc.models.workflow import WorkflowModel
from datetime import datetime
from flask import g
class DataStoreBase(object):
def overwritten(self, value, prev_value):
if prev_value is None:
overwritten = False
else:
if prev_value == value:
overwritten = False
else:
overwritten = True
return overwritten
def set_validate_common(self, study_id, workflow_id, user_id, script_name, file_id, *args):
def set_validate_common(self, task_id, study_id, workflow_id, script_name, user_id, file_id, *args):
self.check_args_2(args, script_name)
workflow = session.query(WorkflowModel).filter(WorkflowModel.id == workflow_id).first()
self.get_prev_value(study_id=study_id, user_id=user_id, file_id=file_id, key=args[0])
if script_name == 'study_data_set':
record = {'task_id': task_id, 'study_id': study_id, 'workflow_id': workflow_id, args[0]: args[1]}
elif script_name == 'file_data_set':
record = {'task_id': task_id, 'study_id': study_id, 'workflow_id': workflow_id, 'file_id': file_id, args[0]: args[1]}
elif script_name == 'user_data_set':
record = {'task_id': task_id, 'study_id': study_id, 'workflow_id': workflow_id, 'user_id': user_id, args[0]: args[1]}
g.validation_data_store.append(record)
return record
def check_args(self, args, maxlen=1, script_name='study_data_get'):
def get_validate_common(self, script_name, study_id=None, user_id=None, file_id=None, *args):
# This method uses a temporary validation_data_store that is only available for the current validation request.
# This allows us to set data_store values during validation that don't affect the real data_store.
# For data_store `gets`, we first look in the temporary validation_data_store.
# If we don't find an entry in validation_data_store, we look in the real data_store.
if script_name == 'study_data_get':
# If it's in the validation data store, return it
for record in g.validation_data_store:
if 'study_id' in record and record['study_id'] == study_id and args[0] in record:
return record[args[0]]
# If not in validation_data_store, look for in the actual data_store
return self.get_data_common(study_id, user_id, 'study_data_get', file_id, *args)
elif script_name == 'file_data_get':
for record in g.validation_data_store:
if 'file_id' in record and record['file_id'] == file_id and args[0] in record:
return record[args[0]]
return self.get_data_common(study_id, user_id, 'file_data_get', file_id, *args)
elif script_name == 'user_data_get':
for record in g.validation_data_store:
if 'user_id' in record and record['user_id'] == user_id and args[0] in record:
return record[args[0]]
return self.get_data_common(study_id, user_id, 'user_data_get', file_id, *args)
@staticmethod
def check_args(args, maxlen=1, script_name='study_data_get'):
if len(args) < 1 or len(args) > maxlen:
raise ApiError(code="missing_argument",
message=f"The {script_name} script takes either one or two arguments, "
f"starting with the key and an optional default")
def check_args_2(self, args, script_name='study_data_set'):
@staticmethod
def check_args_2(args, script_name='study_data_set'):
if len(args) != 2:
raise ApiError(code="missing_argument",
message=f"The {script_name} script takes two arguments, starting with the key and a "
"value for the key")
def get_prev_value(self, study_id, user_id, key, file_id):
study = session.query(DataStoreModel).filter_by(study_id=study_id,
user_id=user_id,
file_id=file_id,
key=key).first()
return study
message=f"The {script_name} script takes two arguments, key and value, in that order.")
def set_data_common(self,
task_spec,
@ -54,47 +67,38 @@ class DataStoreBase(object):
**kwargs):
self.check_args_2(args, script_name=script_name)
study = self.get_prev_value(study_id=study_id,
user_id=user_id,
file_id=file_id,
key=args[0])
if workflow_spec_id is None and workflow_id is not None:
workflow = session.query(WorkflowModel).filter(WorkflowModel.id == workflow_id).first()
workflow_spec_id = workflow.workflow_spec_id
if study is not None:
prev_value = study.value
else:
prev_value = None
study = DataStoreModel(key=args[0], value=args[1],
study_id=study_id,
task_spec=task_spec,
user_id=user_id, # Make this available to any User
file_id=file_id,
workflow_id=workflow_id,
spec_id=workflow_spec_id)
study.value = args[1]
study.last_updated = datetime.utcnow()
overwritten = self.overwritten(study.value, prev_value)
session.add(study)
dsm = DataStoreModel(key=args[0],
value=args[1],
study_id=study_id,
task_spec=task_spec,
user_id=user_id, # Make this available to any User
file_id=file_id,
workflow_id=workflow_id,
spec_id=workflow_spec_id)
session.add(dsm)
session.commit()
return {'new_value': study.value,
'old_value': prev_value,
'overwritten': overwritten}
return dsm.value
def get_data_common(self, study_id, user_id, script_name, file_id=None, *args):
self.check_args(args, 2, script_name)
study = session.query(DataStoreModel).filter_by(study_id=study_id,
user_id=user_id,
file_id=file_id,
key=args[
0]).first()
if study:
return study.value
record = session.query(DataStoreModel).filter_by(study_id=study_id,
user_id=user_id,
file_id=file_id,
key=args[0]).first()
if record:
return record.value
else:
return args[1]
# This is a possible default value passed in from the data_store get methods
if len(args) == 2:
return args[1]
def get_multi_common(self, study_id, user_id, file_id=None):
study = session.query(DataStoreModel).filter_by(study_id=study_id,
user_id=user_id,
file_id=file_id)
return study
@staticmethod
def get_multi_common(study_id, user_id, file_id=None):
results = session.query(DataStoreModel).filter_by(study_id=study_id,
user_id=user_id,
file_id=file_id)
return results

View File

@ -41,6 +41,8 @@ from crc.services.user_service import UserService
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_spec_service import WorkflowSpecService
from flask import g
class WorkflowService(object):
TASK_ACTION_COMPLETE = "COMPLETE"
@ -73,7 +75,7 @@ class WorkflowService(object):
db.session.add(UserModel(uid="test"))
db.session.commit()
user = db.session.query(UserModel).filter_by(uid="test").first()
if validate_study_id:
if validate_study_id is not None:
study = db.session.query(StudyModel).filter_by(id=validate_study_id).first()
else:
study = db.session.query(StudyModel).filter_by(user_uid=user.uid).first()
@ -175,6 +177,8 @@ class WorkflowService(object):
spec, only completing the required fields, rather than everything.
"""
g.validation_data_store = []
workflow_model = WorkflowService.make_test_workflow(spec_id, validate_study_id)
try:
processor = WorkflowProcessor(workflow_model, validate_only=True)

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_d24c44d" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<bpmn:process id="Process_DataStoreValidation" name="Data Store Validation" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0nstzm8</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0nstzm8" sourceRef="StartEvent_1" targetRef="Activity_SetDataStore" />
<bpmn:scriptTask id="Activity_SetDataStore" name="Set Data Store">
<bpmn:incoming>Flow_0nstzm8</bpmn:incoming>
<bpmn:outgoing>Flow_08r3ga0</bpmn:outgoing>
<bpmn:script>study_data_set('study_data_key', 'study_data_value')
file_data_set(file_id=1, key='file_data_key', value='file_data_value')
user_data_set('user_data_key', 'user_data_value')</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_08r3ga0" sourceRef="Activity_SetDataStore" targetRef="Activity_GetDataStore" />
<bpmn:scriptTask id="Activity_GetDataStore" name="Get Data Store">
<bpmn:incoming>Flow_08r3ga0</bpmn:incoming>
<bpmn:outgoing>Flow_02l0u2v</bpmn:outgoing>
<bpmn:script>
previous_study_data_value = study_data_get('previous_study_data_key')
previous_file_data_value = file_data_get(file_id=1, key='previous_file_data_key')
previous_user_data_value = user_data_get('previous_user_data_key')
study_data_value = study_data_get('study_data_key')
file_data_value = file_data_get(file_id=1, key='file_data_key')
user_data_value = user_data_get('user_data_key')
bad_study_data_value = study_data_get('bad_study_data_key', 'bad_study_data_value')
bad_file_data_value = file_data_get(file_id=1, key='bad_file_data_key', default='bad_file_data_value')
bad_user_data_value = user_data_get('bad_user_data_key', 'bad_user_data_value')
</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_02l0u2v" sourceRef="Activity_GetDataStore" targetRef="Activity_DisplayDataStore" />
<bpmn:manualTask id="Activity_DisplayDataStore" name="Display Data Store">
<bpmn:documentation>## Data
# Study
{{ study_data_value }}</bpmn:documentation>
<bpmn:incoming>Flow_02l0u2v</bpmn:incoming>
<bpmn:outgoing>Flow_047lt2r</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:endEvent id="Event_1it98kr">
<bpmn:incoming>Flow_047lt2r</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_047lt2r" sourceRef="Activity_DisplayDataStore" targetRef="Event_1it98kr" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_DataStoreValidation">
<bpmndi:BPMNEdge id="Flow_047lt2r_di" bpmnElement="Flow_047lt2r">
<di:waypoint x="690" y="117" />
<di:waypoint x="752" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_02l0u2v_di" bpmnElement="Flow_02l0u2v">
<di:waypoint x="530" y="117" />
<di:waypoint x="590" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_08r3ga0_di" bpmnElement="Flow_08r3ga0">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0nstzm8_di" bpmnElement="Flow_0nstzm8">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_18fyqwb_di" bpmnElement="Activity_SetDataStore">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0u3tlod_di" bpmnElement="Activity_GetDataStore">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0s05yw5_di" bpmnElement="Activity_DisplayDataStore">
<dc:Bounds x="590" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1it98kr_di" bpmnElement="Event_1it98kr">
<dc:Bounds x="752" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,94 @@
from tests.base_test import BaseTest
from crc import session
from crc.models.data_store import DataStoreModel
from crc.models.file import FileModel, FileType
from crc.models.study import StudyModel
from crc.models.user import UserModel
from crc.services.workflow_service import WorkflowService
from flask import g
class TestDataStoreValidation(BaseTest):
@staticmethod
def add_test_file():
file_model = FileModel(
name='my_test_file',
type=FileType.pdf,
content_type='application/pdf'
)
session.add(file_model)
session.commit()
file_id = session.query(FileModel.id).filter(FileModel.name == 'my_test_file').scalar()
return file_id
@staticmethod
def add_previous_data_stores(user, study, spec_model, file_id):
dsm = DataStoreModel(
key='previous_study_data_key',
workflow_id=None,
study_id=study.id,
task_spec=None,
spec_id=spec_model.id,
user_id=None,
file_id=None,
value='previous_study_data_value'
)
session.add(dsm)
dsm = DataStoreModel(
key='previous_user_data_key',
workflow_id=None,
study_id=None,
task_spec=None,
spec_id=spec_model.id,
user_id=user.uid,
file_id=None,
value='previous_user_data_value'
)
session.add(dsm)
dsm = DataStoreModel(
key='previous_file_data_key',
workflow_id=None,
study_id=None,
task_spec=None,
spec_id=spec_model.id,
user_id=None,
file_id=file_id,
value='previous_file_data_value'
)
session.add(dsm)
session.commit()
def test_data_store_validation(self):
# add_studies also adds test users
self.add_studies()
user = session.query(UserModel).first()
g.user = user
study = session.query(StudyModel).first()
spec_model = self.load_test_spec('data_store_validation')
file_id = self.add_test_file()
self.add_previous_data_stores(user, study, spec_model, file_id)
result = WorkflowService.test_spec(spec_model.id, validate_study_id=study.id)
self.assertIn('previous_study_data_value', result)
self.assertEqual('previous_study_data_value', result['previous_study_data_value'])
self.assertIn('previous_file_data_value', result)
self.assertEqual('previous_file_data_value', result['previous_file_data_value'])
self.assertIn('previous_user_data_value', result)
self.assertEqual('previous_user_data_value', result['previous_user_data_value'])
self.assertIn('study_data_value', result)
self.assertEqual('study_data_value', result['study_data_value'])
self.assertIn('file_data_value', result)
self.assertEqual('file_data_value', result['file_data_value'])
self.assertIn('user_data_value', result)
self.assertEqual('user_data_value', result['user_data_value'])
self.assertIn('bad_study_data_value', result)
self.assertEqual('bad_study_data_value', result['bad_study_data_value'])
self.assertIn('bad_file_data_value', result)
self.assertEqual('bad_file_data_value', result['bad_file_data_value'])
self.assertIn('bad_user_data_value', result)
self.assertEqual('bad_user_data_value', result['bad_user_data_value'])