Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save searches #895

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
9 changes: 8 additions & 1 deletion api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .handlers.reporthandler import ReportHandler
from .handlers.resolvehandler import ResolveHandler
from .handlers.roothandler import RootHandler
from .handlers.savesearchhandler import SaveSearchHandler
from .handlers.schemahandler import SchemaHandler
from .handlers.userhandler import UserHandler
from .jobs.handlers import BatchHandler, JobsHandler, JobHandler, GearsHandler, GearHandler, RulesHandler, RuleHandler
Expand Down Expand Up @@ -109,6 +110,12 @@ def prefix(path, routes):
route('/dataexplorer/search/nodes', DataExplorerHandler, h='get_nodes', m=['POST']),
route('/dataexplorer/index/fields', DataExplorerHandler, h='index_field_names', m=['POST']),

# Search Saving
route('/savesearches', SaveSearchHandler, m=['POST']),
route('/savesearches', SaveSearchHandler, h='get_all', m=['GET']),
route('/savesearches/<sid:{cid}>', SaveSearchHandler, m=['GET','DELETE']),
route('/savesearches/<sid:{cid}>', SaveSearchHandler, h='replace_search', m=['POST']),

# Users

route( '/users', UserHandler, h='get_all', m=['GET']),
Expand Down Expand Up @@ -233,7 +240,7 @@ def prefix(path, routes):

# Collections / Projects

prefix('/<cont_name:collections|projects>', [
prefix('/<cont_name:collections|projects|savesearches>', [
prefix('/<cid:{cid}>', [
route('/<list_name:permissions>', PermissionsListHandler, m=['POST']),
route('/<list_name:permissions>/<_id:{uid}>', PermissionsListHandler, m=['GET', 'PUT', 'DELETE']),
Expand Down
2 changes: 2 additions & 0 deletions api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def apply_env_variables(config):
'project-update.json',
'rule-new.json',
'rule-update.json',
'search-input.json',
'session.json',
'session-update.json',
'subject.json',
Expand Down Expand Up @@ -228,6 +229,7 @@ def initialize_db():
# TODO review all indexes
db.users.create_index('api_key.key')
db.projects.create_index([('gid', 1), ('name', 1)])
db.savesearches.create_index('creator')
db.sessions.create_index('project')
db.sessions.create_index('uid')
db.sessions.create_index('created')
Expand Down
20 changes: 18 additions & 2 deletions api/dao/containerstorage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import datetime

import pymongo
import bson
import copy

Expand All @@ -10,7 +10,7 @@
from ..jobs.jobs import Job
from ..jobs.queue import Queue
from ..jobs.rules import copy_site_rules_for_project
from ..web.errors import APIStorageException, APINotFoundException
from ..web.errors import APIStorageException, APINotFoundException, APIConflictException
from .basecontainerstorage import ContainerStorage

log = config.log
Expand Down Expand Up @@ -434,3 +434,19 @@ def inflate_job_info(self, analysis):

analysis['job'] = job
return analysis

class SearchStorage(ContainerStorage):

def __init__(self):
super(SearchStorage, self).__init__('savesearches', use_object_id=True)

def create_el(self, payload):
try:
result = self.dbc.insert_one(payload)
except pymongo.errors.DuplicateKeyError:
raise APIConflictException('Object with id {} already exists.'.format(payload['_id']))
return result

def replace_el(self, search):
self.delete_el(search['_id'])
return self.create_el(search)
1 change: 1 addition & 0 deletions api/dao/containerutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'project': 'projects',
'session': 'sessions',
'user': 'users',
'savesearch': 'savesearches',
}
PLURAL_TO_SINGULAR = {p: s for s, p in SINGULAR_TO_PLURAL.iteritems()}

Expand Down
1 change: 1 addition & 0 deletions api/handlers/listhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def initialize_list_configurations():
'acquisitions': copy.deepcopy(container_default_configurations),
'collections': copy.deepcopy(container_default_configurations),
'analyses': copy.deepcopy(container_default_configurations),
'savesearches': copy.deepcopy(container_default_configurations),
}
# preload the Storage instances for all configurations
for cont_name, cont_config in list_container_configurations.iteritems():
Expand Down
99 changes: 99 additions & 0 deletions api/handlers/savesearchhandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import bson
from ast import literal_eval
from ..web import base
from .. import config, validators
from ..auth import require_login
from ..dao.containerstorage import SearchStorage

from ..auth import groupauth
from ..dao import noop


log = config.log
storage = SearchStorage()


def string_filters(payload):
if payload.get('search') and payload['search'].get('filters'):
filters = []
for filter_ in payload['search'].get('filters',[]):
filters.append(str(filter_))
payload['search']['filters'] = filters
return payload

def unstring_filters(payload):
if payload['search'].get('filters'):
filters= []
for filter_ in payload['search'].get('filters',[]):
filters.append(literal_eval(filter_))
payload['search']['filters']= filters
return payload

class SaveSearchHandler(base.RequestHandler):

def __init__(self, request=None, response=None):
super(SaveSearchHandler, self).__init__(request, response)

@require_login
def post(self):
payload = self.request.json_body
validators.validate_data(payload, 'search-input.json', 'input', 'POST')
payload = string_filters(payload)
payload['permissions'] = [{"_id": self.uid, "access": "admin"}]
payload['creator'] = self.uid
result = storage.create_el(payload)
if result.acknowledged:
if result.inserted_id:
return {'_id': result.inserted_id}
else:
self.abort(404, 'Search not created')

def get_all(self):
log.debug(self.uid)
return storage.get_all_el({}, {'_id': self.uid}, {'label': 1})

def get(self, sid):
result = storage.get_el(sid)
if result is None:
self.abort(404, 'Element {} not found'.format(sid))
unstring_filters(result)
return result

def delete(self, sid):
search = storage.get_container(sid)
permchecker = groupauth.default(self, search)
result = permchecker(storage.exec_op)('DELETE', sid)
if result.deleted_count == 1:
return {'deleted': result.deleted_count}
else:
self.abort(404, 'Group {} not removed'.format(sid))
return result

def replace_search(self, sid):
payload = self.request.json_body
payload = self._scrub_replace(payload)
validators.validate_data(payload, 'search-input.json', 'input', 'POST')
payload = string_filters(payload)
payload['_id'] = bson.ObjectId(sid)
search = storage.get_container(sid)
payload['permissions'] = search['permissions']
permchecker = groupauth.default(self, search)
permchecker(noop)('DELETE', sid)
result = storage.replace_el(payload)
if result.acknowledged:
if result.inserted_id:
return {'_id': result.inserted_id}
return {"hi" : "bye"}

def _scrub_replace(self, payload):
'''
Function to turn a search returned from a GET to a legal post/replace
'''
if payload.get('_id'):
del(payload['_id'])
if payload.get('permissions'):
del(payload['permissions'])
if payload.get('creator'):
del(payload['creator'])
return payload

1 change: 1 addition & 0 deletions raml/api.raml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ resourceTypes:
/acquisitions: !include resources/acquisitions.raml
/projects: !include resources/projects.raml
/report: !include resources/report.raml
/savesearches: !include resources/savesearch.raml
7 changes: 7 additions & 0 deletions raml/examples/input/search-input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"search": {
"return_type": "file",
"filters": [{"terms": {"file.type":["nifti"]}}]
},
"label" : "Test Search"
}
4 changes: 4 additions & 0 deletions raml/examples/output/search-list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[{
"label": "Test Search",
"_id": "57e452791cff88b85f9f9c23"
}]
10 changes: 10 additions & 0 deletions raml/examples/output/search-output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"label": "Test Search",
"_id": "57e452791cff88b85f9f9c23",
"search": {
"return_type": "file",
"filters": [{"terms": {"file.type":["nifti"]}}]
},
"permissions": [{"access": "admin", "_id": "[email protected]"}],
"creator": "[email protected]"
}
38 changes: 38 additions & 0 deletions raml/resources/savesearch.raml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
type: container
get:
description: List all saved searches user has access to.
responses:
200:
body:
application/json:
example: !include ../examples/output/search-list.json
schema: !include ../schemas/output/search-list.json
post:
body:
application/json:
schema: !include ../schemas/input/search-input.json
/{SearchId}:
type: container
get:
responses:
200:
body:
application/json:
schema: !include ../schemas/output/search-output.json
post:
description: Replace saved search with a new search
body:
application/json:
schema: !include ../schemas/input/search-input.json
/permissions:
type: permissions-list
/{UserId}:
type: permissions-item
delete:
description: Delete a saved search
responses:
200:
body:
application/json:
schema: !include ../schemas/output/container-delete.json
example: !include ../examples/output/container-delete.json
46 changes: 46 additions & 0 deletions raml/schemas/definitions/search.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions":{
"filter" : {
"type" : "object"

},
"search" : {
"type": "object",
"properties": {
"filters": {
"type" : "array",
"items" : {"$ref": "#/definitions/filter"}
},
"search_string": {"type" : "string"},
"all_data": {"type": "boolean"},
"return_type": {"enum": ["session", "acquisition", "analysis", "file"]}
},
"additionalProperties": false,
"required": ["return_type"]
},
"search-input":{
"type": "object",
"properties": {
"label": {"$ref": "../definitions/container.json#/definitions/label"},
"search": {"$ref": "#/definitions/search"}
},
"additionalProperties": false
},
"search-output":{
"type": "object",
"properties": {
"_id": {"$ref":"../definitions/objectid.json#"},
"label": {"$ref": "../definitions/container.json#/definitions/label"},
"creator": {"$ref":"../definitions/container.json#/definitions/uid"},
"created": {"$ref":"../definitions/created-modified.json#/definitions/created"},
"modified": {"$ref":"../definitions/created-modified.json#/definitions/modified"},
"permissions": {
"type":"array",
"items":{"$ref":"../definitions/permission.json#/definitions/permission-output-default-required"}
},
"search": {"$ref":"#/definitions/search"}
}
}
}
}
6 changes: 6 additions & 0 deletions raml/schemas/input/search-input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Search",
"type": "object",
"allOf":[{"$ref":"../definitions/search.json#/definitions/search-input"}]
}
11 changes: 11 additions & 0 deletions raml/schemas/output/search-list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type":"array",
"items":{
"type":"object",
"allOf":[{"$ref":"../definitions/search.json#/definitions/search-output"}],
"required":[
"_id", "label"
]
}
}
8 changes: 8 additions & 0 deletions raml/schemas/output/search-output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type":"object",
"allOf":[{"$ref":"../definitions/search.json#/definitions/search-output"}],
"required":[
"_id", "label", "permissions", "search"
]
}
Loading