diff --git a/api/api.py b/api/api.py index db7a9dc70..d899a322e 100644 --- a/api/api.py +++ b/api/api.py @@ -1,23 +1,24 @@ import webapp2 import webapp2_extras.routes -from .download import Download -from .handlers.collectionshandler import CollectionsHandler -from .handlers.confighandler import Config, Version -from .handlers.containerhandler import ContainerHandler -from .handlers.dataexplorerhandler import DataExplorerHandler -from .handlers.devicehandler import DeviceHandler -from .handlers.grouphandler import GroupHandler -from .handlers.listhandler import FileListHandler, NotesListHandler, PermissionsListHandler, TagsListHandler -from .handlers.refererhandler import AnalysesHandler -from .handlers.reporthandler import ReportHandler -from .handlers.resolvehandler import ResolveHandler -from .handlers.roothandler import RootHandler -from .handlers.schemahandler import SchemaHandler -from .handlers.userhandler import UserHandler -from .jobs.handlers import BatchHandler, JobsHandler, JobHandler, GearsHandler, GearHandler, RulesHandler, RuleHandler -from .upload import Upload -from .web.base import RequestHandler +from .download import Download +from .handlers.abstractcontainerhandler import AbstractContainerHandler +from .handlers.collectionshandler import CollectionsHandler +from .handlers.confighandler import Config, Version +from .handlers.containerhandler import ContainerHandler +from .handlers.dataexplorerhandler import DataExplorerHandler +from .handlers.devicehandler import DeviceHandler +from .handlers.grouphandler import GroupHandler +from .handlers.listhandler import FileListHandler, NotesListHandler, PermissionsListHandler, TagsListHandler +from .handlers.refererhandler import AnalysesHandler +from .handlers.reporthandler import ReportHandler +from .handlers.resolvehandler import ResolveHandler +from .handlers.roothandler import RootHandler +from .handlers.schemahandler import SchemaHandler +from .handlers.userhandler import UserHandler +from .jobs.handlers import BatchHandler, JobsHandler, JobHandler, GearsHandler, GearHandler, RulesHandler, RuleHandler +from .upload import Upload +from .web.base import RequestHandler from . import config @@ -183,6 +184,11 @@ def prefix(path, routes): route('//rules/', RuleHandler, m=['GET', 'PUT', 'DELETE']), + # Abstract container + + route('/containers/', AbstractContainerHandler, h='handle'), + + # Groups route('/groups', GroupHandler, h='get_all', m=['GET']), diff --git a/api/handlers/abstractcontainerhandler.py b/api/handlers/abstractcontainerhandler.py new file mode 100644 index 000000000..011a4654a --- /dev/null +++ b/api/handlers/abstractcontainerhandler.py @@ -0,0 +1,66 @@ +from webapp2 import Request + +from .. import config +from ..web import base +from ..web.errors import APINotFoundException + + +# Efficiently search in multiple collections +CONTAINER_SEARCH_JS = r""" +(function searchContainer(_id) { + if (/^[a-f\d]{24}$/i.test(_id)) { + _id = ObjectId(_id); + } + return { + "groups": db.getCollection("groups").findOne({"_id" : _id}, {"_id": 1}), + "projects": db.getCollection("projects").findOne({"_id" : _id}, {"_id": 1}), + "sessions": db.getCollection("sessions").findOne({"_id" : _id}, {"_id": 1}), + "acquisitions": db.getCollection("acquisitions").findOne({"_id" : _id}, {"_id": 1}), + "analyses": db.getCollection("analyses").findOne({"_id" : _id}, {"_id": 1}), + "collections": db.getCollection("collections").findOne({"_id" : _id}, {"_id": 1}) + } +})("%s"); +""" + + +class AbstractContainerHandler(base.RequestHandler): + """ + Asbtract handler that removes the need to know a container's noun before performing an action. + """ + + # pylint: disable=unused-argument + def handle(self, cid, extra): + """ + Dispatch a request from /containers/x/... to its proper destination. + For example: + /containers/x/files --> x is a project ID --> /projects/x/files + """ + + # Run command; check result + command = config.db.command('eval', CONTAINER_SEARCH_JS % cid) + result = command.get('retval') + + if command.get('ok') != 1.0 or result is None: + self.abort(500, 'Error running db command') + + # Find which container type was found, if any + cont_name = None + for key in result.keys(): + if result[key] is not None: + cont_name = key + break + else: + raise APINotFoundException('No container ' + cid + ' found') + + # Create new request instance using destination URI (eg. replace containers with cont_name) + destination_environ = self.request.environ + for key in 'PATH_INFO', 'REQUEST_URI': + destination_environ[key] = destination_environ[key].replace('containers', cont_name, 1) + destination_request = Request(destination_environ) + + # Apply SciTranRequest attrs + destination_request.id = self.request.id + destination_request.logger = self.request.logger + + # Dispatch the destination request + self.app.router.dispatch(destination_request, self.response) diff --git a/api/web/errors.py b/api/web/errors.py index 9c515b8c0..3b4d04efc 100644 --- a/api/web/errors.py +++ b/api/web/errors.py @@ -59,4 +59,3 @@ class FileFormException(Exception): # Payload for a POST or PUT does not match input json schema class InputValidationException(Exception): pass - diff --git a/tests/integration_tests/python/test_containers.py b/tests/integration_tests/python/test_containers.py index e0134a9cb..fd45b8a76 100644 --- a/tests/integration_tests/python/test_containers.py +++ b/tests/integration_tests/python/test_containers.py @@ -1331,3 +1331,34 @@ def test_container_delete_tag(data_builder, default_payload, as_root, as_admin, # test that the (now) empty group can be deleted assert as_root.delete('/groups/' + group).ok +def test_abstract_containers(data_builder, as_admin, file_form): + group = data_builder.create_group() + project = data_builder.create_project() + session = data_builder.create_session() + acquisition = data_builder.create_acquisition() + analysis = as_admin.post('/sessions/' + session + '/analyses', files=file_form( + 'analysis.csv', meta={'label': 'no-job', 'inputs': [{'name': 'analysis.csv'}]})).json()['_id'] + collection = data_builder.create_collection() + + for cont in (collection, analysis, acquisition, session, project, group): + r = as_admin.post('/containers/' + cont + '/tags', json={'value': 'abstract1'}) + assert r.ok + + r = as_admin.get('/containers/' + cont) + assert r.ok + assert r.json()['tags'] == ['abstract1'] + + r = as_admin.put('/containers/' + cont + '/tags/abstract1', json={'value': 'abstract2'}) + assert r.ok + + r = as_admin.get('/containers/' + cont + '/tags/abstract2') + assert r.ok + assert r.json() == 'abstract2' + + # /analyses/x does not support DELETE (yet?) + for cont in (collection, acquisition, session, project, group): + r = as_admin.delete('/containers/' + cont) + assert r.ok + + r = as_admin.get('/containers/' + cont) + assert r.status_code == 404