diff --git a/api/dao/containerstorage.py b/api/dao/containerstorage.py index 673c9182c18428e2328236bd394cd01cab87835d..16ad589d4f147a8875424ca2debfecd496adb7a7 100644 --- a/api/dao/containerstorage.py +++ b/api/dao/containerstorage.py @@ -1,6 +1,5 @@ import bson.errors import bson.objectid -import json import pymongo.errors from .. import util @@ -222,20 +221,6 @@ class ProjectStorage(ContainerStorage): def __init__(self): super(ProjectStorage,self).__init__('projects', use_object_id=True) - def _from_mongo(self, cont): - if cont: - template = cont.get('template') - if template: - cont['template'] = json.loads(template) - return super(ProjectStorage,self)._from_mongo(cont) - - def _to_mongo(self, payload): - if payload: - template = payload.get('template') - if template: - payload['template'] = json.dumps(template) - return super(ProjectStorage,self)._to_mongo(payload) - def update_el(self, _id, payload, unset_payload=None, recursive=False, r_payload=None, replace_metadata=False): result = super(ProjectStorage, self).update_el(_id, payload, unset_payload=unset_payload, recursive=recursive, r_payload=r_payload, replace_metadata=replace_metadata) diff --git a/api/dao/hierarchy.py b/api/dao/hierarchy.py index aeb54d8d20ad91ab4a54c08e9cf6ee0975821a40..469492ec3010110cfc6890f3725643973bcbdd7a 100644 --- a/api/dao/hierarchy.py +++ b/api/dao/hierarchy.py @@ -3,7 +3,6 @@ import copy import datetime import dateutil.parser import difflib -from jsonschema import Draft4Validator, ValidationError import pymongo import re @@ -118,32 +117,77 @@ def is_session_compliant(session, template): Given a project-level session template and a session, returns True/False if the session is in compliance with the template """ + + def check_req(cont, req_k, req_v): + """ + Return True if container satisfies specific requirement. + """ + cont_v = cont.get(req_k) + if cont_v: + if isinstance(req_v, dict): + for k,v in req_v.iteritems(): + if not check_req(cont_v, k, v): + return False + elif isinstance(cont_v, list): + found_in_list = False + for v in cont_v: + if bool(re.match(req_v, cont_v)): + found_in_list = True + break + if not found_in_list: + return False + else: + # Assume regex for now + if not bool(re.match(req_v, cont_v)): + return False + else: + return False + return True + + + def check_cont(cont, reqs): + """ + Return True if container satisfies requirements. + Return False otherwise. + """ + + for req_k, req_v in reqs.iteritems(): + if req_k == 'files': + file_reqs = req_v + min_count = file_reqs.pop('minimum') + count = 0 + for f in cont.get('files', []): + if not check_cont(f, req_v): + # Didn't find a match, on to the next one + continue + else: + count += 1 + if count >= min_count: + break + if count < min_count: + return False + + else: + if not check_req(cont, req_k, req_v): + return False + return True + + s_requirements = template.get('session') a_requirements = template.get('acquisitions') - f_requirements = template.get('files') - - acquisitions = [] - if a_requirements or f_requirements: - if session.get('_id'): - # Only grab acquisitions when not validating a newly created session - acquisitions = list(config.db.acquisitions.find({'session': session['_id']})) if s_requirements: - validator = Draft4Validator(s_requirements.get('schema')) - try: - validator.validate(session) - except ValidationError: + if not check_cont(session, s_requirements): return False if a_requirements: + acquisitions = list(config.db.acquisitions.find({'session': session['_id']})) for req in a_requirements: - validator = Draft4Validator(req.get('schema')) - min_count = req.get('minimum') + min_count = req.pop('minimum') count = 0 for a in acquisitions: - try: - validator.validate(a) - except ValidationError: + if not check_cont(a, req): + # Didn't find a match, on to the next one continue else: count += 1 @@ -151,25 +195,6 @@ def is_session_compliant(session, template): break if count < min_count: return False - - if f_requirements: - files_ = [f for a in acquisitions for f in a.get('files', [])] - for req in f_requirements: - validator = Draft4Validator(req.get('schema')) - min_count = req.get('minimum') - count = 0 - for f in files_: - try: - validator.validate(a) - except ValidationError: - continue - else: - count += 1 - if count >= min_count: - break - if count < min_count: - return False - return True def upsert_fileinfo(cont_name, _id, fileinfo): @@ -412,7 +437,6 @@ def upsert_bottom_up_hierarchy(metadata, user=None, site=None): def upsert_top_down_hierarchy(metadata, type_='label', user=None, site=None): - log.debug('I know my type is {}'.format(type_)) group = metadata['group'] project = metadata['project'] session = metadata.get('session') @@ -491,7 +515,6 @@ def _update_container_nulls(base_query, update, container_type): q.update(base_query) q['$or'] = [{k: {'$exists': False}}, {k: None}] u = {'$set': {k: v}} - log.debug('the query is {} and the update is {}'.format(q,u)) bulk.find(q).update_one(u) bulk.execute() return config.db[coll_name].find_one(base_query) diff --git a/bin/database.py b/bin/database.py index c710e3f8ff988fe4e41fefa6240eca0a48016608..9d122e351e93ef12718f8257cc854ef6a04db4fd 100755 --- a/bin/database.py +++ b/bin/database.py @@ -542,17 +542,20 @@ def upgrade_to_21(): Acquisition fields `instrument` and `measurement` removed """ - - - - # def update_project_template(template): - # for a in template.get('acquisitions', []): - # properties = a['schema']['properties'] - # if 'measurement' in properties: - # m_req = properties.pop('measurement') - # a['files']['schema'] - - # return template + def update_project_template(template): + new_template = {'acquisitions': []} + for a in template.get('acquisitions', []): + new_a = {'minimum': a['minimum']} + properties = a['schema']['properties'] + if 'measurement' in properties: + m_req = properties.pop('measurement') + new_a['files']=[{'measurement': m_req['pattern'], 'minimum': 1}] + if 'label' in properties: + l_req = properties.pop('label') + new_a['label'] = l_req['pattern'] + new_template['acquisitions'].append(new_a) + + return new_template def dm_v2_updates(cont_list, cont_name): for container in cont_list: @@ -561,7 +564,9 @@ def upgrade_to_21(): update = {'$rename': {'metadata': 'info'}} if cont_name == 'projects' and container.get('template'): - pass + new_template = update_project_template(json.loads(container.get('template'))) + update['$set'] = {'template': new_template} + if cont_name == 'sessions': update['$rename'].update({'subject.metadata': 'subject.info'}) @@ -597,7 +602,7 @@ def upgrade_to_21(): dm_v2_updates(config.db.collections.find(query), 'collections') query['$or'].append({'template': { '$exists': True}}) - dm_v2_updates(config.db.projects.find(query), 'projects') + dm_v2_updates(config.db.projects.find({}), 'projects') query['$or'].append({'subject': { '$exists': True}}) dm_v2_updates(config.db.sessions.find(query), 'sessions') diff --git a/raml/schemas/definitions/project-template.json b/raml/schemas/definitions/project-template.json index d70bfb700010e0aedc308b19479107aff0576a86..b45e8c316dd3ce210aeb8e777387a2a8b856a67c 100644 --- a/raml/schemas/definitions/project-template.json +++ b/raml/schemas/definitions/project-template.json @@ -9,29 +9,19 @@ {"required": ["maximum"]} ], "properties": { - "schema": {"$ref": "http://json-schema.org/draft-04/schema"}, "minimum": {"type": "integer", "minimum": 0}, "maximum": {"type": "integer", "minimum": 0} - }, - "required": ["schema"] + } } }, "properties": { "session": { - "properties": { - "schema": {"$ref": "http://json-schema.org/draft-04/schema"} - }, - "required": ["schema"] + "type": "object" }, "acquisitions": { "type": "array", "minItems": 1, "items": {"$ref": "#/definitions/requirement"} - }, - "files": { - "type": "array", - "minItems": 1, - "items": {"$ref": "#/definitions/requirement"} } }, "additionalProperties": false diff --git a/test/integration_tests/postman/integration_tests.postman_collection b/test/integration_tests/postman/integration_tests.postman_collection index a6f651ee53d6c8231bedf2c581b0bca73d855fbf..43a654db98e23dd9d889f209cc52978a13a63880 100644 --- a/test/integration_tests/postman/integration_tests.postman_collection +++ b/test/integration_tests/postman/integration_tests.postman_collection @@ -38,8 +38,7 @@ "body": { "mode": "raw", "raw": "{\"_id\":\"jane.doe@gmail.com\",\"firstname\":\"Jane\",\"lastname\":\"Doe\",\"email\":\"jane.doe@gmail.com\"}" - }, - "description": "List users\n\n" + } }, "response": [] }, @@ -76,8 +75,7 @@ "body": { "mode": "raw", "raw": "{\"_id\":\"jane.doe@gmail.com\", \"lastname\":\"Doe\",\"email\":\"jane.doe@gmail.com\"}" - }, - "description": "\n\n" + } }, "response": [] }, @@ -121,8 +119,7 @@ "enabled": true } ] - }, - "description": "" + } }, "response": [] }, @@ -155,8 +152,7 @@ "body": { "mode": "raw", "raw": "{\n\"_id\":\"test-group\"\n}" - }, - "description": "" + } }, "response": [] }, @@ -207,8 +203,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -253,8 +248,7 @@ "body": { "mode": "raw", "raw": "{\"_id\":\"jane.doe@gmail.com\",\"firstname\":\"Jane\",\"lastname\":\"Doe\",\"email\":\"jane.doe@gmail.com\"}" - }, - "description": "" + } }, "response": [] }, @@ -296,8 +290,7 @@ "body": { "mode": "raw", "raw": "{\"_id\":\"jane.doe@gmail.com\",\"firstname\":\"Jane\",\"lastname\":\"Doe\",\"email\":\"jane.doe@gmail.com\"}" - }, - "description": "" + } }, "response": [] }, @@ -327,8 +320,7 @@ "body": { "mode": "raw", "raw": "{\n\t\"category\": \"converter\",\n\t\"input\": { },\n\t\"name\": \"test_gear\",\n\t\"manifest\": {\n\t\t\"name\": \"test_gear\",\n\t\t\"inputs\": {\n\t\t\t\"any text file <= 100 KB\": {\n\t\t\t\t\"base\": \"file\",\n\t\t\t\t\"name\": {\n\t\t\t\t\t\"pattern\": \"^.*.txt$\"\n\t\t\t\t},\n\t\t\t\t\"size\": {\n\t\t\t\t\t\"maximum\": 100000\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"description\": \"A gear to test the API\",\n\t\t\"license\": \"Other\",\n\t\t\"author\": \"\",\n\t\t\"url\": \"https://unknown.example\",\n\t\t\"label\": \"An exported gear\",\n\t\t\"source\": \"https://unknown.example\",\n\t\t\"config\": {\n\t\t\t\"two-digit multiple of ten\": {\n\t\t\t\t\"exclusiveMaximum\": true,\n\t\t\t\t\"type\": \"number\",\n\t\t\t\t\"multipleOf\": 10,\n\t\t\t\t\"maximum\": 100\n\t\t\t}\n\t\t}\n\t}\n}" - }, - "description": "Create or update a gear.\n\"Name\" field of gear must match \"GearName\" uri parameter\nIf no existing gear is found, one will be created\nOtherwise, the specified gear will be updated\n\n\nParameters:\n\nGearName: Name of the gear to interact with\n\n" + } }, "response": [] }, @@ -389,8 +381,7 @@ "body": { "mode": "raw", "raw": "{\n \"gear\": \"test-case-gear\",\n \"inputs\": {\n \"dicom\": {\n \"type\": \"acquisition\",\n \"id\": \"{{test-acquisition-1-id}}\",\n \"name\" : \"1_1_dicom.zip\"\n }\n },\n \"config\": {\n\t\t\"two-digit multiple of ten\": 20\n\t},\n \"destination\": {\n \"type\": \"acquisition\",\n \"id\": \"{{test-acquisition-1-id}}\"\n },\n \"tags\": [\n \"ad-hoc\"\n ]\n}\n" - }, - "description": "" + } }, "response": [] }, @@ -425,8 +416,7 @@ "body": { "mode": "raw", "raw": "{\n \"label\":\"test-collection-1\"\n}" - }, - "description": "" + } }, "response": [] }, @@ -457,8 +447,7 @@ "body": { "mode": "raw", "raw": "{\n \"contents\":{\n \t\"operation\":\"add\",\n \t\"nodes\":[\n \t\t{\n \t\t\t\"level\":\"session\",\n \t\t\t\"_id\":\"{{test-session-1-id}}\"\n \t\t}\n \t]\n }\n}" - }, - "description": "" + } }, "response": [] }, @@ -498,8 +487,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -530,8 +518,7 @@ "body": { "mode": "raw", "raw": "{\n \"label\":\"test-collection-2\"\n}" - }, - "description": "" + } }, "response": [] }, @@ -571,8 +558,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -612,8 +598,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -645,8 +630,7 @@ "body": { "mode": "raw", "raw": "{\"text\":\"test note\"}" - }, - "description": "" + } }, "response": [] }, @@ -678,8 +662,7 @@ "body": { "mode": "raw", "raw": "{\"text\":\"test note\"}" - }, - "description": "" + } }, "response": [] }, @@ -711,8 +694,7 @@ "body": { "mode": "raw", "raw": "{\"text\":\"test note\"}" - }, - "description": "" + } }, "response": [] }, @@ -744,8 +726,7 @@ "body": { "mode": "raw", "raw": "{\n \"text\":\"test note\"\n}" - }, - "description": "" + } }, "response": [] }, @@ -785,8 +766,7 @@ "body": { "mode": "raw", "raw": "{\n \"analysis\": {\n \"label\": \"Test Analysis 1\"\n },\n \"job\" : {\n \"gear\": \"test-case-gear\",\n \"inputs\": {\n \"dicom\": {\n \"type\": \"acquisition\",\n \"id\": \"{{test-acquisition-1-id}}\",\n \"name\" : \"test-1.dcm\"\n }\n },\n \"tags\": [\"example\"]\n }\n}\n" - }, - "description": "" + } }, "response": [] }, @@ -835,8 +815,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -874,8 +853,7 @@ "body": { "mode": "raw", "raw": "" - }, - "description": "" + } }, "response": [] }, @@ -924,8 +902,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -971,8 +948,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -1021,8 +997,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -1069,8 +1044,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -1119,8 +1093,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -1166,8 +1139,7 @@ "value": "" } ] - }, - "description": "" + } }, "response": [] }, @@ -1199,8 +1171,7 @@ "body": { "mode": "raw", "raw": "{\"text\":\"test note\"}" - }, - "description": "" + } }, "response": [] }, @@ -1232,8 +1203,7 @@ "body": { "mode": "raw", "raw": "{\"text\":\"test note\"}" - }, - "description": "" + } }, "response": [] }, @@ -1265,8 +1235,7 @@ "body": { "mode": "raw", "raw": "{\"text\":\"test note\"}" - }, - "description": "" + } }, "response": [] }, @@ -1298,8 +1267,7 @@ "body": { "mode": "raw", "raw": "{\"text\":\"test note\"}" - }, - "description": "" + } }, "response": [] }, @@ -1334,8 +1302,7 @@ "body": { "mode": "raw", "raw": " {\n \"group\": \"test-group\",\n \"label\": \"Project with template\",\n \"public\": false\n }" - }, - "description": "Create the project for testing session templates" + } }, "response": [] }, @@ -1370,8 +1337,7 @@ "body": { "mode": "raw", "raw": " {\n \"subject\": {\n \"code\": \"ex8945\"\n },\n \"label\": \"Compliant Session\",\n \"project\": \"{{ST-project-id}}\",\n \"public\": false\n }" - }, - "description": "Create a session that will be compliant with the project-level session template" + } }, "response": [] }, @@ -1406,8 +1372,7 @@ "body": { "mode": "raw", "raw": " {\n \"subject\": {\n \"code\": \"ex9849\"\n },\n \"label\": \"Non-compliant Session\",\n \"project\": \"{{ST-project-id}}\",\n \"public\": false\n }" - }, - "description": "Create a session that will NOT be compliant with the project-level session template" + } }, "response": [] }, @@ -1441,9 +1406,8 @@ ], "body": { "mode": "raw", - "raw": "{\n \"label\": \"c-acquisition-1-t1\",\n \"session\":\"{{ST-compliant-session-id}}\",\n \"public\": false,\n \"measurement\": \"localizer\"\n}" - }, - "description": "Create an acquisition for the compliant session" + "raw": "{\n \"label\": \"c-acquisition-1-t1\",\n \"session\":\"{{ST-compliant-session-id}}\",\n \"public\": false\n}" + } }, "response": [] }, @@ -1477,9 +1441,8 @@ ], "body": { "mode": "raw", - "raw": "{\n \"label\": \"c-acquisition-2\",\n \"session\":\"{{ST-compliant-session-id}}\",\n \"public\": false,\n \"measurement\": \"localizer\"\n}" - }, - "description": "Create an acquisition for the compliant session" + "raw": "{\n \"label\": \"c-acquisition-2-t1\",\n \"session\":\"{{ST-compliant-session-id}}\",\n \"public\": false\n}" + } }, "response": [] }, @@ -1513,9 +1476,8 @@ ], "body": { "mode": "raw", - "raw": "{\n \"label\": \"nc-acquisition-1\",\n \"session\":\"{{ST-noncompliant-session-id}}\",\n \"public\": false,\n \"measurement\": \"localizer\"\n}" - }, - "description": "Create an acquisition for the noncompliant session" + "raw": "{\n \"label\": \"nc-acquisition-1-t1\",\n \"session\":\"{{ST-noncompliant-session-id}}\",\n \"public\": false\n}" + } }, "response": [] }, @@ -1545,13 +1507,17 @@ "key": "Authorization", "value": "scitran-user {{test_user_api_key}}", "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" } ], "body": { "mode": "raw", - "raw": "{\n \"session\": {\n \"schema\": {\n \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n \"properties\": {\n \"subject\": {\n \"type\": \"object\",\n \"properties\": {\n \"code\": {\n \"type\": \"string\",\n \"pattern\": \"^ex\" \n }\n },\n \"required\": [\"code\"]\n }\n },\n \"required\": [\"subject\"]\n }\n },\n \"acquisitions\": [\n {\n \"schema\": {\n \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"measurement\": {\n \"type\": \"string\",\n \"pattern\": \"^(?i)localizer$\" \n }\n },\n \"required\": [\"measurement\"]\n },\n \"minimum\": 2\n },\n {\n \"schema\": {\n \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"measurement\": {\n \"type\": \"string\",\n \"pattern\": \"^(?i)localizer$\" \n },\n \"label\": {\n \"type\": \"string\",\n \"pattern\": \"t1\"\n }\n },\n \"required\": [\"label\", \"measurement\"]\n },\n \"minimum\": 1\n }\n ]\n}" - }, - "description": "Should modify project and it's sessions. Both sessions should have a \"project-has-template\" flag as \"true\" and the session setup to be compliant should have \"compliant\" flag set to True." + "raw": "{\n \"session\": {\n \"subject\": {\n \"code\" : \"^ex\"\n }\n },\n \"acquisitions\": [\n {\n \"label\": \"t1\",\n \"minimum\": 2\n }\n ]\n}" + } }, "response": [] }, @@ -1588,8 +1554,7 @@ "body": { "mode": "raw", "raw": "" - }, - "description": "" + } }, "response": [] }, @@ -1626,8 +1591,7 @@ "body": { "mode": "raw", "raw": "" - }, - "description": "" + } }, "response": [] }, @@ -1657,13 +1621,17 @@ "key": "Authorization", "value": "scitran-user {{test_user_api_key}}", "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" } ], "body": { "mode": "raw", - "raw": "{\n \"label\": \"nc-acquisition-2-t1\",\n \"session\":\"{{ST-noncompliant-session-id}}\",\n \"public\": false,\n \"measurement\": \"localizer\"\n}" - }, - "description": "This should make the session compliant" + "raw": "{\n \"label\": \"nc-acquisition-2-t1\",\n \"session\":\"{{ST-noncompliant-session-id}}\",\n \"public\": false\n}" + } }, "response": [] }, @@ -1700,8 +1668,7 @@ "body": { "mode": "raw", "raw": "" - }, - "description": "" + } }, "response": [] }, @@ -1728,13 +1695,17 @@ "key": "Authorization", "value": "scitran-user {{test_user_api_key}}", "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" } ], "body": { "mode": "raw", "raw": " {\n \"subject\": {\n \"code\": \"bad-subject-code\"\n }\n }" - }, - "description": "Create a session that will NOT be compliant with the project-level session template" + } }, "response": [] }, @@ -1771,10 +1742,9 @@ "body": { "mode": "raw", "raw": "" - }, - "description": "" + } }, "response": [] } ] -} \ No newline at end of file +}