diff --git a/api/api.py b/api/api.py index c2ff13c2c..eab02102b 100644 --- a/api/api.py +++ b/api/api.py @@ -7,6 +7,7 @@ from .handlers.confighandler import Config, Version from .handlers.containerhandler import ContainerHandler from .handlers.devicehandler import DeviceHandler +from .handlers.modalityhandler import ModalityHandler from .handlers.grouphandler import GroupHandler from .handlers.listhandler import AnalysesHandler, ListHandler, FileListHandler, NotesListHandler, PermissionsListHandler, TagsListHandler from .handlers.reporthandler import ReportHandler @@ -169,6 +170,14 @@ def prefix(path, routes): route('/', DeviceHandler, m=['GET']), ]), + # Modalities + + route( '/modalities', ModalityHandler, h='get_all', m=['GET']), + route( '/modalities', ModalityHandler, m=['POST']), + prefix('/modalities', [ + route('/', ModalityHandler, m=['GET', 'PUT', 'DELETE']), + ]), + # Groups @@ -245,7 +254,7 @@ def prefix(path, routes): route('/packfile', FileListHandler, h='packfile', m=['POST']), route('/packfile-end', FileListHandler, h='packfile_end'), route('/', FileListHandler, m=['POST']), - route('//', FileListHandler, m=['GET', 'DELETE']), + route('//', FileListHandler, m=['GET', 'DELETE', 'PUT']), route('///info', FileListHandler, h='get_info', m=['GET']), diff --git a/api/auth/listauth.py b/api/auth/listauth.py index 0828ba414..826ec61f7 100644 --- a/api/auth/listauth.py +++ b/api/auth/listauth.py @@ -20,6 +20,7 @@ def default_sublist(handler, container): access = _get_access(handler.uid, handler.user_site, container) def g(exec_op): def f(method, _id, query_params=None, payload=None, exclude_params=None): + log.debug('Im actually in here') if method == 'GET' and container.get('public', False): min_access = -1 elif method == 'GET': diff --git a/api/config.py b/api/config.py index eee4e401d..2b8e1c200 100644 --- a/api/config.py +++ b/api/config.py @@ -159,6 +159,7 @@ def apply_env_variables(config): 'file.json', 'group-new.json', 'group-update.json', + 'modality.json', 'note.json', 'packfile.json', 'permission.json', diff --git a/api/dao/liststorage.py b/api/dao/liststorage.py index ee68a204f..62b027684 100644 --- a/api/dao/liststorage.py +++ b/api/dao/liststorage.py @@ -3,6 +3,7 @@ import datetime from .. import config +from .. import util from . import consistencychecker, containerutil from . import APIStorageException, APIConflictException from .containerstorage import SessionStorage, AcquisitionStorage @@ -352,3 +353,32 @@ def inflate_job_info(analysis): analysis['job'] = job return analysis + + +class FileStorage(ListStorage): + + def update_file(self, _id, query_params, payload, replace_fields=False): + mod_elem = {} + update = {} + + # If we want to add to the classification lists rather than replace + # the entirity of the classification map, use $addToSet. + # This allows some endpoints to only make additive changes + if not replace_fields: + classification = payload.pop('classification', None) + if classification: + add_to_set = {} + for k,array in classification.items(): + add_to_set[self.list_name + '.$.classification.' + k] = array + update['$addToSet'] = add_to_set + payload = util.mongo_dict(payload) + + for k,v in payload.items(): + mod_elem[self.list_name + '.$.' + k] = v + query = { + '_id': _id, + self.list_name: {'$elemMatch': query_params} + } + update['$set'] = mod_elem + + return self.dbc.update_one(query, update) diff --git a/api/filetypes.json b/api/filetypes.json index d3e4c9306..477cfce3c 100644 --- a/api/filetypes.json +++ b/api/filetypes.json @@ -1,28 +1,29 @@ { - "bval": [ ".bval", ".bvals" ], - "bvec": [ ".bvec", ".bvecs" ], - "dicom": [ ".dcm", ".dcm.zip", ".dicom.zip" ], - "ismrmrd": [ ".h5", ".hdf5" ], - "parrec": [ ".parrec.zip", ".par-rec.zip" ], - "gephysio": [ ".gephysio.zip" ], - "MATLAB data": [ ".mat" ], - "MGH data": [ ".mgh", ".mgz", ".mgh.gz" ], - "nifti": [ ".nii.gz", ".nii" ], - "pfile": [ ".7.gz", ".7", ".7.zip" ], - "PsychoPy data": [ ".psydat" ], - "qa": [ ".qa.png", ".qa.json", ".qa.html" ], + "BVAL": [ ".bval", ".bvals" ], + "BVEC": [ ".bvec", ".bvecs" ], + "DICOM": [ ".dcm", ".dcm.zip", ".dicom.zip" ], + "GE Physio": [ ".gephysio.zip" ], + "ISMRMRD": [ ".h5", ".hdf5" ], + "MATLAB Data": [ ".mat" ], + "MGH Data": [ ".mgh", ".mgz", ".mgh.gz" ], + "NIfTI": [ ".nii.gz", ".nii" ], + "PAR/REC": [ ".parrec.zip", ".par-rec.zip" ], + "PFile": [ ".7.gz", ".7", ".7.zip" ], + "PsychoPy Data": [ ".psydat" ], + "QC": [ ".qa.png", ".qa.json", ".qa.html", ".qc.png", ".qc.json", ".qc.html" ], - "archive": [ ".zip", ".tbz2", ".tar.gz", ".tbz", ".tar.bz2", ".tgz", ".tar", ".txz", ".tar.xz" ], - "document": [ ".docx", ".doc" ], - "image": [ ".jpg", ".tif", ".jpeg", ".gif", ".bmp", ".png", ".tiff" ], - "markup": [ ".html", ".htm", ".xml" ], - "markdown": [ ".md", ".markdown" ], - "log": [ ".log" ], - "pdf": [ ".pdf" ], - "presentation": [ ".ppt", ".pptx" ], - "source code": [ ".c", ".py", ".cpp", ".js", ".m", ".json", ".java", ".php", ".css", ".toml", ".yaml", ".yml" ], - "spreadsheet": [ ".xls", ".xlsx" ], - "tabular data": [ ".csv.gz", ".csv" ], - "text": [ ".txt" ], - "video": [ ".mpeg", ".mpg", ".mov", ".mp4", ".m4v", ".mts" ] + "Archive": [ ".zip", ".tbz2", ".tar.gz", ".tbz", ".tar.bz2", ".tgz", ".tar", ".txz", ".tar.xz" ], + "Audio": [ ".mp3", ".wav", ".wave" ], + "Document": [ ".docx", ".doc" ], + "Image": [ ".jpg", ".tif", ".jpeg", ".gif", ".bmp", ".png", ".tiff" ], + "Log File": [ ".log" ], + "Markdown": [ ".md", ".markdown" ], + "Markup": [ ".html", ".htm", ".xml" ], + "PDF": [ ".pdf" ], + "Plain Text": [ ".txt" ], + "Presentation": [ ".ppt", ".pptx" ], + "Source Code": [ ".c", ".py", ".cpp", ".js", ".m", ".json", ".java", ".php", ".css", ".toml", ".yaml", ".yml" ], + "Spreadsheet": [ ".xls", ".xlsx" ], + "Tabular Data": [ ".csv.gz", ".csv", ".tsv.gz", ".tsv" ], + "Video": [ ".mpeg", ".mpg", ".mov", ".mp4", ".m4v", ".mts" ] } diff --git a/api/handlers/listhandler.py b/api/handlers/listhandler.py index e4f34312b..79382a8ba 100644 --- a/api/handlers/listhandler.py +++ b/api/handlers/listhandler.py @@ -18,6 +18,7 @@ from ..dao import liststorage from ..dao import APIStorageException from ..dao import hierarchy +from .modalityhandler import ModalityHandler from ..web.request import log_access, AccessType @@ -38,7 +39,7 @@ def initialize_list_configurations(): 'input_schema_file': 'tag.json' }, 'files': { - 'storage': liststorage.ListStorage, + 'storage': liststorage.FileStorage, 'permchecker': listauth.default_sublist, 'use_object_id': True, 'storage_schema_file': 'file.json', @@ -502,6 +503,38 @@ def post(self, cont_name, list_name, **kwargs): return upload.process_upload(self.request, upload.Strategy.targeted, container_type=cont_name, id_=_id, origin=self.origin) + def put(self, cont_name, list_name, **kwargs): + _id = kwargs.pop('cid') + permchecker, storage, mongo_validator, payload_validator, keycheck = self._initialize_request(cont_name, list_name, _id, query_params=kwargs) + + payload = self.request.json_body + payload_validator(payload, 'PUT') + if not set(payload.keys()).issubset({'info', 'modality', 'classification'}): + self.abort(400, 'Can only update info, modality an classification keys on file.') + + classification = payload.get('classification') + if classification: + modality = payload.get('modality') + if not modality: + file_obj = storage.exec_op('GET', _id, query_params=kwargs) + modality = file_obj.get('modality') + + if not ModalityHandler.check_classification(modality, classification): + self.abort(400, 'Classification does not match allowable values for modality {}.'.format(modality)) + + rf = self.is_true('replace_fields') + + try: + keycheck(mongo_validator(permchecker(noop)))('PUT', _id=_id, query_params=kwargs, payload=payload) + result = storage.update_file(_id, kwargs, payload, replace_fields=rf) + except APIStorageException as e: + self.abort(400, e.message) + # abort if the query of the update wasn't able to find any matching documents + if result.matched_count == 0: + self.abort(404, 'Element not updated in list {} of container {} {}'.format(storage.list_name, storage.cont_name, _id)) + else: + return {'modified':result.modified_count} + def delete(self, cont_name, list_name, **kwargs): # Overriding base class delete to audit action before completion _id = kwargs.pop('cid') diff --git a/api/handlers/modalityhandler.py b/api/handlers/modalityhandler.py new file mode 100644 index 000000000..fa2f5f271 --- /dev/null +++ b/api/handlers/modalityhandler.py @@ -0,0 +1,112 @@ +from ..web import base +from .. import config +from ..auth import require_login, require_superuser +from ..dao import containerstorage, APINotFoundException +# from ..validators import validate_data + +log = config.log + + +class ModalityHandler(base.RequestHandler): + + def __init__(self, request=None, response=None): + super(ModalityHandler, self).__init__(request, response) + self.storage = containerstorage.ContainerStorage('modalities', use_object_id=False) + + @require_login + def get(self, modality_name): + return self.storage.get_container(modality_name) + + @require_login + def get_all(self): + return self.storage.get_all_el(None, None, None) + + @require_superuser + def post(self): + payload = self.request.json_body + # Clean this up when validate_data method is fixed to use new schemas + # POST unnecessary, used to avoid run-time modification of schema + #validate_data(payload, 'modality.json', 'input', 'POST', optional=True) + + result = self.storage.create_el(payload) + if result.acknowledged: + return {'_id': result.inserted_id} + else: + self.abort(400, 'Modality not inserted') + + @require_superuser + def put(self, modality_name): + payload = self.request.json_body + # Clean this up when validate_data method is fixed to use new schemas + # POST unnecessary, used to avoid run-time modification of schema + #validate_data(payload, 'modality.json', 'input', 'POST', optional=True) + + result = self.storage.update_el(modality_name, payload) + if result.matched_count == 1: + return {'modified': result.modified_count} + else: + raise APINotFoundException('Modality with name {} not found, modality not updated'.format(modality_name)) + + @require_superuser + def delete(self, modality_name): + result = self.storage.delete_el(modality_name) + if result.deleted_count == 1: + return {'deleted': result.deleted_count} + else: + raise APINotFoundException('Modality with name {} not found, modality not deleted'.format(modality_name)) + + @staticmethod + def check_classification(modality_name, classification_map): + """ + Given a modality name and a proposed classification map, + ensure: + - that a modality exists with that name and has a classification + map + - all keys in the classification_map exist in the + `classifications` map on the modality object + - all the values in the arrays in the classification_map + exist in the modality's classifications map + + For example: + Modality = { + "_id" = "Example_modality", + "classifications": { + "Example1": ["Blue", "Green"] + "Example2": ["one", "two"] + } + } + + Returns True: + classification_map = { + "Example1": ["Blue"], + "custom": ["anything"] + } + + Returns False: + classification_map = { + "Example1": ["Red"], # "Red" is not allowed + "Example2": ["one", "two"] + } + """ + try: + modality = containerstorage.ContainerStorage('modalities', use_object_id=False).get_container(modality_name) + except APINotFoundException: + if classification_map.keys() == ['custom']: + # for unknown modalities allow only list of custom values + return True + else: + return False + + classifications = modality.get('classifications') + if not classifications: + return False + + for k,array in classification_map.iteritems(): + if k == 'custom': + # any unique value is allowed in custom list + continue + possible_values = classifications.get(k, []) + if not set(array).issubset(set(possible_values)): + return False + + return True diff --git a/api/jobs/rules.py b/api/jobs/rules.py index 6a3ac3864..0e721eccc 100644 --- a/api/jobs/rules.py +++ b/api/jobs/rules.py @@ -73,16 +73,16 @@ def lower(x): elif match_type == 'file.name': return fnmatch.fnmatch(file_['name'].lower(), match_param.lower()) - # Match any of the file's measurements - elif match_type == 'file.measurements': - try: - if match_param: - return match_param.lower() in map(lower, file_.get('measurements', [])) - else: - return False - except KeyError: - _log_file_key_error(file_, container, 'has no measurements key') + # Match any of the file's classifications + elif match_type == 'file.classification': + for v in file_.get('classification', {}).itervalues(): + try: + if match_param and match_param.lower() in map(lower, v): + return True + except KeyError: + _log_file_key_error(file_, container, 'has no measurements key') return False + return False # Match the container having any file (including this one) with this type elif match_type == 'container.has-type': diff --git a/api/placer.py b/api/placer.py index 7b3bd666b..7b351bab6 100644 --- a/api/placer.py +++ b/api/placer.py @@ -242,15 +242,16 @@ def check(self): job = Job.get(self.context.get('job_id')) input_names = [{'name': v.name} for v in job.inputs.itervalues()] - measurement = self.metadata.get(self.container_type, {}).pop('measurement', None) + classification = self.metadata.get(self.container_type, {}).pop('measurement', None) info = self.metadata.get(self.container_type,{}).pop('metadata', None) modality = self.metadata.get(self.container_type, {}).pop('instrument', None) - if measurement or info or modality: + if classification or info or modality: files_ = self.metadata[self.container_type].get('files', []) files_ += input_names for f in files_: - if measurement: - f['measurements'] = [measurement] + if classification: + custom = {'custom': [classification]} + f['classification'] = custom if info: f['info'] = info if modality: @@ -502,7 +503,7 @@ def finalize(self): # OPPORTUNITY: packfile endpoint could be extended someday to take additional metadata. 'modality': None, - 'measurements': [], + 'classification': {}, 'tags': [], 'info': {}, diff --git a/api/upload.py b/api/upload.py index 31370c3ba..4c9eef88e 100644 --- a/api/upload.py +++ b/api/upload.py @@ -127,7 +127,7 @@ def process_upload(request, strategy, container_type=None, id_=None, origin=None 'type': None, 'modality': None, - 'measurements': [], + 'classification': {}, 'tags': [], 'info': {} } diff --git a/bin/database.py b/bin/database.py index d05695511..e4fbd9e63 100755 --- a/bin/database.py +++ b/bin/database.py @@ -948,6 +948,58 @@ def upgrade_to_26(): process_cursor(cursor, upgrade_to_26_closure) +def upgrade_to_22(): + """ + Change file `measurement` field to `classification` map + Place all measurements in `custom` key on map + """ + + def update_project_template(template): + for a in template.get('acquisitions', []): + new_file_templates = [] + for f in a.get('files', []): + if f.get('measurement'): + measurements = f.pop('measurement') + f['classification'] = {'custom': measurement} + + return template + + + def change_to_classification(cont_list, cont_name): + for container in cont_list: + + if cont_name == 'projects' and container.get('template'): + new_template = update_project_template(json.loads(container.get('template'))) + update['$set'] = {'template': new_template} + + query = {'_id': container['_id']} + update = {} + + files = container.get('files') + if files is not None: + updated_files = [] + for file_ in files: + if 'measurements' in file_: + measurements = file_.pop('measurements', []) + custom = {'custom': measurements} + file_['classification'] = custom + + updated_files.append(file_) + if update.get('$set'): + update['$set']['files'] = updated_files + else: + update['$set'] = {'files': updated_files} + + result = config.db[cont_name].update_one(query, update) + + query = {'files.measurements': { '$exists': True}} + + change_to_classification(config.db.collections.find(query), 'collections') + change_to_classification(config.db.projects.find(query), 'projects') + change_to_classification(config.db.sessions.find(query), 'sessions') + change_to_classification(config.db.acquisitions.find(query), 'acquisitions') + + def upgrade_schema(): """ Upgrades db to the current schema version diff --git a/raml/api.raml b/raml/api.raml index 70a90b32a..32438df4a 100644 --- a/raml/api.raml +++ b/raml/api.raml @@ -50,6 +50,8 @@ resourceTypes: /jobs: !include resources/jobs.raml /gears: !include resources/gears.raml /devices: !include resources/devices.raml +/rules: !include resources/rules.raml +/modalities: !include resources/modalities.raml /groups: !include resources/groups.raml /collections: !include resources/collections.raml /sessions: !include resources/sessions.raml diff --git a/raml/examples/file_info_list.json b/raml/examples/file_info_list.json index cea54456c..ef97d7f48 100755 --- a/raml/examples/file_info_list.json +++ b/raml/examples/file_info_list.json @@ -8,7 +8,7 @@ "hash": "v0-sha384-12188e00a26650b2baa3f0195337dcf504f4362bb2136eef0cdbefb57159356b1355a0402fca0ab5ab081f21c305e5c2", "name": "cortical_surface_right_hemisphere.obj", "tags": [], - "measurements": [], + "classification": {}, "modified": "2016-10-18T15:26:35.701000+00:00", "instrument": null, "size": 21804112, @@ -23,7 +23,7 @@ "hash": "v0-sha384-12188e00a26650b2baa3f0195337dcf504f4362bb2136eef0cdbefb57159356b1355a0402fca0ab5ab081f21c305e5c2", "name": "cortical_surface_right_hemisphere.obj", "tags": [], - "measurements": [], + "classification": {}, "modified": "2016-10-18T17:45:17.776000+00:00", "instrument": null, "metadata": {}, diff --git a/raml/examples/input/modality-new.json b/raml/examples/input/modality-new.json new file mode 100644 index 000000000..19aebadf6 --- /dev/null +++ b/raml/examples/input/modality-new.json @@ -0,0 +1,8 @@ +{ + "_id": "MR", + "classifications": { + "Contrast": ["B0", "B1", "T1", "T2", "T2*", "PD", "MT", "ASL", "Perfusion", "Diffusion", "Spectroscopy", "Susceptibility", "Velocity", "Fingerprinting"], + "Intent": ["Structural", "Functional", "Localizer", "Shim", "Calibration"], + "Features": ["Quantitative", "Multi-Shell", "Multi-Echo", "Multi-Flip", "Multi-Band", "Steady-State", "3D", "Compressed-Sensing", "Eddy-Current-Corrected", "Fieldmap-Corrected", "Gradient-Unwarped", "Motion-Corrected", "Physio-Corrected"] + } +} diff --git a/raml/examples/input/modality-update.json b/raml/examples/input/modality-update.json new file mode 100644 index 000000000..9e5f424d7 --- /dev/null +++ b/raml/examples/input/modality-update.json @@ -0,0 +1,7 @@ +{ + "classifications": { + "Contrast": ["B0", "B1", "T1", "T2", "T2*", "PD", "MT", "ASL", "Perfusion", "Diffusion", "Spectroscopy", "Susceptibility", "Velocity", "Fingerprinting"], + "Intent": ["Structural", "Functional", "Localizer", "Shim", "Calibration"], + "Features": ["Quantitative", "Multi-Shell", "Multi-Echo", "Multi-Flip", "Multi-Band", "Steady-State", "3D", "Compressed-Sensing", "Eddy-Current-Corrected", "Fieldmap-Corrected", "Gradient-Unwarped", "Motion-Corrected", "Physio-Corrected"] + } +} diff --git a/raml/examples/output/acquisition-list.json b/raml/examples/output/acquisition-list.json index 8a1992578..e7307091c 100644 --- a/raml/examples/output/acquisition-list.json +++ b/raml/examples/output/acquisition-list.json @@ -9,7 +9,7 @@ "name": "Admin Import" }, "mimetype": "application/zip", - "measurements": [], + "classification": {}, "hash": "v0-sha384-dd3c97bfe0ad1fcba75ae6718c6e81038c59af4f447f5db194d52732efa4f955b28455db02eb64cad3e4e55f11e3679f", "name": "4784_1_1_localizer_dicom.zip", "tags": [], @@ -49,7 +49,7 @@ "name": "Admin Import" }, "mimetype": "application/zip", - "measurements": [], + "classification": {}, "hash": "v0-sha384-ca055fb36845db86e4278cf6e185f8674d11a96f4b29af27e401fc495cc82ef6b53a5729c3757713064649dc71c8c725", "name": "4784_3_1_t1_dicom.zip", "tags": [], @@ -89,7 +89,7 @@ "name": "Admin Import" }, "mimetype": "application/zip", - "measurements": [], + "classification": {}, "hash": "v0-sha384-537e42b1dd8f1feef9844fbfb4f60461361e71cafa7055556097e9d0b9f7fac68c8f234ed126af9412bd43a548948847", "name": "4784_5_1_fmri_dicom.zip", "tags": [], diff --git a/raml/examples/output/acquisition.json b/raml/examples/output/acquisition.json index 7a0fad57c..ce6135b26 100644 --- a/raml/examples/output/acquisition.json +++ b/raml/examples/output/acquisition.json @@ -8,7 +8,7 @@ "name": "Admin Import" }, "mimetype": "application/zip", - "measurements": [], + "classification": {}, "hash": "v0-sha384-dd3c97bfe0ad1fcba75ae6718c6e81038c59af4f447f5db194d52732efa4f955b28455db02eb64cad3e4e55f11e3679f", "name": "4784_1_1_localizer_dicom.zip", "tags": [], diff --git a/raml/examples/output/analysis-item.json b/raml/examples/output/analysis-item.json index ad1b73e7f..495e5c333 100644 --- a/raml/examples/output/analysis-item.json +++ b/raml/examples/output/analysis-item.json @@ -8,7 +8,7 @@ "hash": "v0-sha384-12188e00a26650b2baa3f0195337dcf504f4362bb2136eef0cdbefb57159356b1355a0402fca0ab5ab081f21c305e5c2", "name": "cortical_surface_right_hemisphere.obj", "tags": [], - "measurements": [], + "classification": {}, "modified": "2016-10-18T15:26:35.701000+00:00", "instrument": null, "input": true, @@ -24,7 +24,7 @@ "hash": "v0-sha384-12188e00a26650b2baa3f0195337dcf504f4362bb2136eef0cdbefb57159356b1355a0402fca0ab5ab081f21c305e5c2", "name": "cortical_surface_right_hemisphere.obj", "tags": [], - "measurements": [], + "classification": {}, "modified": "2016-10-18T17:45:17.776000+00:00", "instrument": null, "output": true, diff --git a/raml/examples/output/modality-list.json b/raml/examples/output/modality-list.json new file mode 100644 index 000000000..59671c57a --- /dev/null +++ b/raml/examples/output/modality-list.json @@ -0,0 +1,10 @@ +[ + { + "_id": "MR", + "classifications": { + "Contrast": ["B0", "B1", "T1", "T2", "T2*", "PD", "MT", "ASL", "Perfusion", "Diffusion", "Spectroscopy", "Susceptibility", "Velocity", "Fingerprinting"], + "Intent": ["Structural", "Functional", "Localizer", "Shim", "Calibration"], + "Features": ["Quantitative", "Multi-Shell", "Multi-Echo", "Multi-Flip", "Multi-Band", "Steady-State", "3D", "Compressed-Sensing", "Eddy-Current-Corrected", "Fieldmap-Corrected", "Gradient-Unwarped", "Motion-Corrected", "Physio-Corrected"] + } + } +] diff --git a/raml/examples/output/modality.json b/raml/examples/output/modality.json new file mode 100644 index 000000000..19aebadf6 --- /dev/null +++ b/raml/examples/output/modality.json @@ -0,0 +1,8 @@ +{ + "_id": "MR", + "classifications": { + "Contrast": ["B0", "B1", "T1", "T2", "T2*", "PD", "MT", "ASL", "Perfusion", "Diffusion", "Spectroscopy", "Susceptibility", "Velocity", "Fingerprinting"], + "Intent": ["Structural", "Functional", "Localizer", "Shim", "Calibration"], + "Features": ["Quantitative", "Multi-Shell", "Multi-Echo", "Multi-Flip", "Multi-Band", "Steady-State", "3D", "Compressed-Sensing", "Eddy-Current-Corrected", "Fieldmap-Corrected", "Gradient-Unwarped", "Motion-Corrected", "Physio-Corrected"] + } +} diff --git a/raml/resources/modalities.raml b/raml/resources/modalities.raml new file mode 100644 index 000000000..a138eb029 --- /dev/null +++ b/raml/resources/modalities.raml @@ -0,0 +1,77 @@ +description: Utility functions for modalities and their acceptable classifications. +get: + description: | + List all modalities and their classifications. + Requires login. + responses: + 200: + body: + application/json: + example: !include ../examples/output/modality-list.json + schema: !include ../schemas/output/modality-list.json +post: + description: | + Insert a modality type and its classifications map. + Requires admin. + body: + application/json: + example: !include ../examples/input/modality-new.json + schema: !include ../schemas/input/modality.json + responses: + 200: + body: + application/json: + example: | + { + "inserted": "1" + } + +/{ModalityName}: + description: Perform actions with a specific modality + uriParameters: + ModalityName: + type: string + get: + description: | + Get modality details. + Requires login. + responses: + 200: + body: + application/json: + example: !include ../examples/output/modality.json + schema: !include ../schemas/output/modality.json + 404: + description: Modality not found + put: + description: | + Replace a modality's classifications map. + Requires admin. + body: + application/json: + example: !include ../examples/input/modality-update.json + schema: !include ../schemas/input/modality.json + responses: + 200: + body: + application/json: + example: | + { + "modified": "1" + } + 404: + description: Modality not found + delete: + description: | + Remove a modality. + Requires admin. + responses: + 200: + body: + application/json: + example: | + { + "modified": "1" + } + 404: + description: Modality not found diff --git a/raml/schemas/definitions/analysis.json b/raml/schemas/definitions/analysis.json index 729e815fb..c97c544dc 100644 --- a/raml/schemas/definitions/analysis.json +++ b/raml/schemas/definitions/analysis.json @@ -49,7 +49,7 @@ {"type":"null"} ] }, - "measurements": {"$ref":"../definitions/file.json#/definitions/measurements"}, + "classification": {"$ref":"../definitions/file.json#/definitions/classification"}, "tags": {"$ref":"../definitions/file.json#/definitions/tags"}, "info": {"$ref":"../definitions/file.json#/definitions/info"}, "origin":{"$ref":"../definitions/file.json#/definitions/origin"}, diff --git a/raml/schemas/definitions/file.json b/raml/schemas/definitions/file.json index c2e4c9a51..c9a698f3a 100644 --- a/raml/schemas/definitions/file.json +++ b/raml/schemas/definitions/file.json @@ -5,10 +5,8 @@ "file-type": { "type": "string" }, "mimetype": { "type": "string" }, "modality": { "type": "string" }, - "measurements": { - "items": { "type": "string"}, - "type": "array", - "uniqueItems": true + "classification": { + "type": "object" }, "tags": { "items": { "type": "string"}, @@ -37,7 +35,7 @@ "type": {"$ref":"#/definitions/file-type"}, "mimetype": {"$ref":"#/definitions/mimetype"}, "modality": {"$ref":"#/definitions/modality"}, - "measurements": {"$ref":"#/definitions/measurements"}, + "classification": {"$ref":"#/definitions/classification"}, "tags": {"$ref":"#/definitions/tags"}, "info": {"$ref":"#/definitions/info"} }, @@ -55,7 +53,7 @@ {"type":"null"} ] }, - "measurements": {"$ref":"#/definitions/measurements"}, + "classification": {"$ref":"#/definitions/classification"}, "tags": {"$ref":"#/definitions/tags"}, "info": {"$ref":"#/definitions/info"}, "origin":{"$ref":"#/definitions/origin"}, diff --git a/raml/schemas/definitions/modality.json b/raml/schemas/definitions/modality.json new file mode 100644 index 000000000..cc8cab0bf --- /dev/null +++ b/raml/schemas/definitions/modality.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "definitions": { + "_id": { + "maxLength": 64, + "minLength": 2, + "pattern": "^[0-9a-zA-Z_-]*$" + }, + "classifications": { + "type": "object", + "patternProperties": { + "^[0-9a-zA-Z_-]*$":{ + "type": "array", + "items": { + "type": "string" + } + }, + }, + }, + "modality":{ + "type": "object", + "properties": { + "_id": {"$ref":"#/definitions/_id"}, + "classifications": {"$ref":"#/definitions/classifications"} + }, + "additionalProperties": false + } + } +} diff --git a/raml/schemas/input/modality.json b/raml/schemas/input/modality.json new file mode 100644 index 000000000..416053171 --- /dev/null +++ b/raml/schemas/input/modality.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf":[{"$ref":"../definitions/modality.json#/definitions/modality"}], + "required": ["classifications"] +} diff --git a/raml/schemas/mongo/file.json b/raml/schemas/mongo/file.json index 4e077d85a..4d86f66ec 100644 --- a/raml/schemas/mongo/file.json +++ b/raml/schemas/mongo/file.json @@ -10,10 +10,8 @@ "size": { "type": "integer" }, "hash": { "type": "string" }, "modality": { "type": "string" }, - "measurements": { - "items": { "type": "string"}, - "type": "array", - "uniqueItems": true + "classification": { + "type": "object" }, "tags": { "items": { "type": "string"}, diff --git a/raml/schemas/output/modality-list.json b/raml/schemas/output/modality-list.json new file mode 100644 index 000000000..08340eff8 --- /dev/null +++ b/raml/schemas/output/modality-list.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type":"array", + "items":{ + "allOf":[{"$ref":"../definitions/modality.json#/definitions/modality"}], + "required":["_id", "classifications"] + } +} diff --git a/raml/schemas/output/modality.json b/raml/schemas/output/modality.json new file mode 100644 index 000000000..5edcd7149 --- /dev/null +++ b/raml/schemas/output/modality.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf":[{"$ref":"../definitions/modality.json#/definitions/modality"}], + "required": ["_id", "classifications"] +} diff --git a/test/integration_tests/abao/abao_test_hooks.js b/test/integration_tests/abao/abao_test_hooks.js index a0413e962..243de77c3 100644 --- a/test/integration_tests/abao/abao_test_hooks.js +++ b/test/integration_tests/abao/abao_test_hooks.js @@ -24,6 +24,7 @@ var test_project_tag = 'test-project-tag'; var delete_project_id = ''; var device_id = 'bootstrapper_Bootstrapper' var injected_api_key = 'XZpXI40Uk85eozjQkU1zHJ6yZHpix+j0mo1TMeGZ4dPzIqVPVGPmyfeK' +var modality_name = 'MR' // Tests we're skipping, fix these @@ -1478,3 +1479,53 @@ hooks.before("GET /devices/{DeviceId} -> 404", function(test, done) { test.request.params.DeviceId = 'bad_device_id'; done(); }); + +hooks.before("GET /modalities/{ModalityName} -> 200", function(test, done) { + test.request.params.ModalityName = modality_name; + done(); +}); + +hooks.before("GET /modalities/{ModalityName} -> 404", function(test, done) { + test.request.params.ModalityName = 'bad_modality_name'; + done(); +}); + +hooks.before("POST /modalities/ -> 200", function(test, done) { + test.request.body = { + "_id":"MR", + "classifications": { + "intent": ["functional"] + } + } + done(); +}); + +hooks.before("PUT /modalities/{ModalityName} -> 200", function(test, done) { + test.request.params.ModalityName = modality_name; + test.request.body = { + "classifications": { + "intent": ["localizer"] + } + } + done(); +}); + +hooks.before("PUT /modalities/{ModalityName} -> 404", function(test, done) { + test.request.params.ModalityName = 'bad_modality_name'; + test.request.body = { + "classifications": { + "intent": ["localizer"] + } + } + done(); +}); + +hooks.before("DELETE /modalities/{ModalityName} -> 200", function(test, done) { + test.request.params.ModalityName = modality_name; + done(); +}); + +hooks.before("DELETE /modalities/{ModalityName} -> 404", function(test, done) { + test.request.params.ModalityName = 'bad_modality_name'; + done(); +}); diff --git a/test/unit_tests/python/test_rules.py b/test/unit_tests/python/test_rules.py index ca32b7fce..c05da8a59 100644 --- a/test/unit_tests/python/test_rules.py +++ b/test/unit_tests/python/test_rules.py @@ -66,8 +66,8 @@ def test_eval_match_file_name_match_relative(): result = rules.eval_match(*args) assert result == False -def test_eval_match_file_measurements(): - part = rulePart(match_type='file.measurements', file_={'measurements': ['a', 'diffusion', 'b'] }) +def test_eval_match_file_classification(): + part = rulePart(match_type='file.classification', file_={'classification': {'custom': ['a', 'diffusion', 'b'] }}) args = part.gen(match_param='diffusion') result = rules.eval_match(*args)