From eebe77b20f7022391eb701ca1ef7a67260535fac Mon Sep 17 00:00:00 2001 From: hardik Date: Thu, 4 Aug 2016 17:45:13 +0530 Subject: [PATCH] Filtering support for actions TODO: Add more tests. Change-Id: I110c2073b82c4ffb3a1f02e152937451395a1e87 Implements: blueprint mistral-items-filtering --- mistral/api/controllers/v2/action.py | 18 +- .../api/controllers/v2/action_execution.py | 40 ++--- mistral/api/controllers/v2/cron_trigger.py | 3 +- mistral/api/controllers/v2/environment.py | 3 +- mistral/api/controllers/v2/execution.py | 3 +- mistral/api/controllers/v2/task.py | 7 +- mistral/api/controllers/v2/workbook.py | 17 +- mistral/api/controllers/v2/workflow.py | 18 +- mistral/db/v2/sqlalchemy/api.py | 19 +-- mistral/db/v2/sqlalchemy/filters.py | 63 +++++++ .../unit/db/v2/test_sqlalchemy_db_api.py | 161 ++++++++++++++++++ mistral/utils/filter_utils.py | 91 ++++++++++ .../tests/api/v2/test_actions.py | 157 +++++++++++++++++ 13 files changed, 510 insertions(+), 90 deletions(-) create mode 100644 mistral/db/v2/sqlalchemy/filters.py create mode 100644 mistral/utils/filter_utils.py diff --git a/mistral/api/controllers/v2/action.py b/mistral/api/controllers/v2/action.py index 4279ddfb..eecf0cd7 100644 --- a/mistral/api/controllers/v2/action.py +++ b/mistral/api/controllers/v2/action.py @@ -29,6 +29,7 @@ from mistral import context from mistral.db.v2 import api as db_api from mistral import exceptions as exc from mistral.services import actions +from mistral.utils import filter_utils from mistral.utils import rest_utils from mistral.workbook import parser as spec_parser @@ -138,11 +139,11 @@ class ActionsController(rest.RestController, hooks.HookController): @wsme_pecan.wsexpose(resources.Actions, types.uuid, int, types.uniquelist, types.list, types.uniquelist, wtypes.text, wtypes.text, resources.SCOPE_TYPES, wtypes.text, - types.uniquelist, wtypes.text, wtypes.text, - wtypes.text, bool, wtypes.text) + wtypes.text, wtypes.text, wtypes.text, wtypes.text, + wtypes.text) def get_all(self, marker=None, limit=None, sort_keys='name', sort_dirs='asc', fields='', created_at=None, name=None, - scope=None, tag=None, tags=None, updated_at=None, + scope=None, tags=None, updated_at=None, description=None, definition=None, is_system=None, input=None): """Return all actions. @@ -168,9 +169,6 @@ class ActionsController(rest.RestController, hooks.HookController): :param input: Optional. Keep only resources with a specific input. :param description: Optional. Keep only resources with a specific description. - :param tag: Optional. Keep only resources with a specific tag. If it is - used with 'tags', it will be appended to the list of - matching tags. :param tags: Optional. Keep only resources containing specific tags. :param created_at: Optional. Keep only resources created at a specific time and date. @@ -182,13 +180,7 @@ class ActionsController(rest.RestController, hooks.HookController): """ acl.enforce('actions:list', context.ctx()) - if tag is not None: - if tags is None: - tags = [tag] - else: - tags.append(tag) - - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( created_at=created_at, name=name, scope=scope, diff --git a/mistral/api/controllers/v2/action_execution.py b/mistral/api/controllers/v2/action_execution.py index b3f8763e..54fb7d06 100644 --- a/mistral/api/controllers/v2/action_execution.py +++ b/mistral/api/controllers/v2/action_execution.py @@ -27,6 +27,7 @@ from mistral import context from mistral.db.v2 import api as db_api from mistral.engine.rpc_backend import rpc from mistral import exceptions as exc +from mistral.utils import filter_utils from mistral.utils import rest_utils from mistral.workflow import states from mistral.workflow import utils as wf_utils @@ -178,13 +179,12 @@ class ActionExecutionsController(rest.RestController): @wsme_pecan.wsexpose(resources.ActionExecutions, types.uuid, int, types.uniquelist, types.list, types.uniquelist, wtypes.text, wtypes.text, wtypes.text, - types.uniquelist, wtypes.text, wtypes.text, - wtypes.text, types.uuid, wtypes.text, wtypes.text, - bool, types.jsontype, types.jsontype, types.jsontype, - wtypes.text) + wtypes.text, wtypes.text, wtypes.text, types.uuid, + wtypes.text, wtypes.text, bool, types.jsontype, + types.jsontype, types.jsontype, wtypes.text) def get_all(self, marker=None, limit=None, sort_keys='created_at', sort_dirs='asc', fields='', created_at=None, name=None, - tag=None, tags=None, updated_at=None, workflow_name=None, + tags=None, updated_at=None, workflow_name=None, task_name=None, task_execution_id=None, state=None, state_info=None, accepted=None, input=None, output=None, params=None, description=None): @@ -224,9 +224,6 @@ class ActionExecutionsController(rest.RestController): :param params: Optional. Keep only resources with specific parameters. :param description: Optional. Keep only resources with a specific description. - :param tag: Optional. Keep only resources with a specific tag. If it is - used with 'tags', it will be appended to the list of - matching tags. :param tags: Optional. Keep only resources containing specific tags. :param created_at: Optional. Keep only resources created at a specific time and date. @@ -235,13 +232,7 @@ class ActionExecutionsController(rest.RestController): """ acl.enforce('action_executions:list', context.ctx()) - if tag is not None: - if tags is None: - tags = [tag] - else: - tags.append(tag) - - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( created_at=created_at, name=name, tags=tags, @@ -299,13 +290,13 @@ class ActionExecutionsController(rest.RestController): class TasksActionExecutionController(rest.RestController): @wsme_pecan.wsexpose(resources.ActionExecutions, types.uuid, types.uuid, int, types.uniquelist, types.list, types.uniquelist, - wtypes.text, wtypes.text, types.uniquelist, + wtypes.text, types.uniquelist, wtypes.text, wtypes.text, wtypes.text, wtypes.text, wtypes.text, - wtypes.text, wtypes.text, bool, types.jsontype, - types.jsontype, types.jsontype, wtypes.text) + wtypes.text, bool, types.jsontype, types.jsontype, + types.jsontype, wtypes.text) def get_all(self, task_execution_id, marker=None, limit=None, sort_keys='created_at', sort_dirs='asc', fields='', - created_at=None, name=None, tag=None, tags=None, + created_at=None, name=None, tags=None, updated_at=None, workflow_name=None, task_name=None, state=None, state_info=None, accepted=None, input=None, output=None, params=None, description=None): @@ -345,9 +336,6 @@ class TasksActionExecutionController(rest.RestController): :param params: Optional. Keep only resources with specific parameters. :param description: Optional. Keep only resources with a specific description. - :param tag: Optional. Keep only resources with a specific tag. If it is - used with 'tags', it will be appended to the list of - matching tags. :param tags: Optional. Keep only resources containing specific tags. :param created_at: Optional. Keep only resources created at a specific time and date. @@ -356,13 +344,7 @@ class TasksActionExecutionController(rest.RestController): """ acl.enforce('action_executions:list', context.ctx()) - if tag is not None: - if tags is None: - tags = [tag] - else: - tags.append(tag) - - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( created_at=created_at, name=name, tags=tags, diff --git a/mistral/api/controllers/v2/cron_trigger.py b/mistral/api/controllers/v2/cron_trigger.py index 63da8669..711b7703 100644 --- a/mistral/api/controllers/v2/cron_trigger.py +++ b/mistral/api/controllers/v2/cron_trigger.py @@ -23,6 +23,7 @@ from mistral.api.controllers.v2 import types from mistral import context from mistral.db.v2 import api as db_api from mistral.services import triggers +from mistral.utils import filter_utils from mistral.utils import rest_utils LOG = logging.getLogger(__name__) @@ -130,7 +131,7 @@ class CronTriggersController(rest.RestController): """ acl.enforce('cron_triggers:list', context.ctx()) - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( created_at=created_at, name=name, updated_at=updated_at, diff --git a/mistral/api/controllers/v2/environment.py b/mistral/api/controllers/v2/environment.py index 9cfad7a4..c6653703 100644 --- a/mistral/api/controllers/v2/environment.py +++ b/mistral/api/controllers/v2/environment.py @@ -25,6 +25,7 @@ from mistral.api.controllers.v2 import types from mistral import context from mistral.db.v2 import api as db_api from mistral import exceptions as exceptions +from mistral.utils import filter_utils from mistral.utils import rest_utils @@ -71,7 +72,7 @@ class EnvironmentController(rest.RestController): """ acl.enforce('environments:list', context.ctx()) - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( created_at=created_at, name=name, updated_at=updated_at, diff --git a/mistral/api/controllers/v2/execution.py b/mistral/api/controllers/v2/execution.py index edd8e630..e775e72f 100644 --- a/mistral/api/controllers/v2/execution.py +++ b/mistral/api/controllers/v2/execution.py @@ -29,6 +29,7 @@ from mistral.db.v2 import api as db_api from mistral.engine.rpc_backend import rpc from mistral import exceptions as exc from mistral.services import workflows as wf_service +from mistral.utils import filter_utils from mistral.utils import rest_utils from mistral.workflow import states @@ -260,7 +261,7 @@ class ExecutionsController(rest.RestController): """ acl.enforce('executions:list', context.ctx()) - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( created_at=created_at, workflow_name=workflow_name, workflow_id=workflow_id, diff --git a/mistral/api/controllers/v2/task.py b/mistral/api/controllers/v2/task.py index c9c02c8c..2832a789 100644 --- a/mistral/api/controllers/v2/task.py +++ b/mistral/api/controllers/v2/task.py @@ -28,6 +28,7 @@ from mistral import context from mistral.db.v2 import api as db_api from mistral.engine.rpc_backend import rpc from mistral import exceptions as exc +from mistral.utils import filter_utils from mistral.utils import rest_utils from mistral.workbook import parser as spec_parser from mistral.workflow import data_flow @@ -93,7 +94,7 @@ class TaskExecutionsController(rest.RestController): """ acl.enforce('executions:list', context.ctx()) - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( task_execution_id=task_execution_id, created_at=created_at, workflow_name=workflow_name, @@ -196,7 +197,7 @@ class TasksController(rest.RestController): """ acl.enforce('tasks:list', context.ctx()) - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( created_at=created_at, workflow_name=workflow_name, workflow_id=workflow_id, @@ -339,7 +340,7 @@ class ExecutionTasksController(rest.RestController): """ acl.enforce('tasks:list', context.ctx()) - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( workflow_execution_id=workflow_execution_id, created_at=created_at, workflow_name=workflow_name, diff --git a/mistral/api/controllers/v2/workbook.py b/mistral/api/controllers/v2/workbook.py index 1d2466e7..68047d51 100644 --- a/mistral/api/controllers/v2/workbook.py +++ b/mistral/api/controllers/v2/workbook.py @@ -28,6 +28,7 @@ from mistral.api.hooks import content_type as ct_hook from mistral import context from mistral.db.v2 import api as db_api from mistral.services import workbooks +from mistral.utils import filter_utils from mistral.utils import rest_utils from mistral.workbook import parser as spec_parser @@ -95,11 +96,10 @@ class WorkbooksController(rest.RestController, hooks.HookController): @wsme_pecan.wsexpose(resources.Workbooks, types.uuid, int, types.uniquelist, types.list, types.uniquelist, wtypes.text, wtypes.text, wtypes.text, - resources.SCOPE_TYPES, wtypes.text, types.uniquelist, - wtypes.text) + resources.SCOPE_TYPES, wtypes.text, wtypes.text) def get_all(self, marker=None, limit=None, sort_keys='created_at', sort_dirs='asc', fields='', created_at=None, - definition=None, name=None, scope=None, tag=None, tags=None, + definition=None, name=None, scope=None, tags=None, updated_at=None): """Return a list of workbooks. @@ -119,9 +119,6 @@ class WorkbooksController(rest.RestController, hooks.HookController): :param name: Optional. Keep only resources with a specific name. :param definition: Optional. Keep only resources with a specific definition. - :param tag: Optional. Keep only resources with a specific tag. If it is - used with 'tags', it will be appended to the list of - matching tags. :param tags: Optional. Keep only resources containing specific tags. :param scope: Optional. Keep only resources with a specific scope. :param created_at: Optional. Keep only resources created at a specific @@ -134,13 +131,7 @@ class WorkbooksController(rest.RestController, hooks.HookController): """ acl.enforce('workbooks:list', context.ctx()) - if tag is not None: - if tags is None: - tags = [tag] - else: - tags.append(tag) - - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( created_at=created_at, definition=definition, name=name, diff --git a/mistral/api/controllers/v2/workflow.py b/mistral/api/controllers/v2/workflow.py index 2f3a14dd..c5a6e5e2 100644 --- a/mistral/api/controllers/v2/workflow.py +++ b/mistral/api/controllers/v2/workflow.py @@ -32,6 +32,7 @@ from mistral import context from mistral.db.v2 import api as db_api from mistral import exceptions as exc from mistral.services import workflows +from mistral.utils import filter_utils from mistral.utils import rest_utils from mistral.workbook import parser as spec_parser @@ -169,11 +170,11 @@ class WorkflowsController(rest.RestController, hooks.HookController): @wsme_pecan.wsexpose(resources.Workflows, types.uuid, int, types.uniquelist, types.list, types.uniquelist, wtypes.text, wtypes.text, wtypes.text, wtypes.text, - types.uniquelist, resources.SCOPE_TYPES, types.uuid, - wtypes.text, wtypes.text) + resources.SCOPE_TYPES, types.uuid, wtypes.text, + wtypes.text) def get_all(self, marker=None, limit=None, sort_keys='created_at', sort_dirs='asc', fields='', name=None, input=None, - definition=None, tag=None, tags=None, scope=None, + definition=None, tags=None, scope=None, project_id=None, created_at=None, updated_at=None): """Return a list of workflows. @@ -194,9 +195,6 @@ class WorkflowsController(rest.RestController, hooks.HookController): :param input: Optional. Keep only resources with a specific input. :param definition: Optional. Keep only resources with a specific definition. - :param tag: Optional. Keep only resources with a specific tag. If it is - used with 'tags', it will be appended to the list of - matching tags. :param tags: Optional. Keep only resources containing specific tags. :param scope: Optional. Keep only resources with a specific scope. :param project_id: Optional. The same as the requester project_id @@ -208,13 +206,7 @@ class WorkflowsController(rest.RestController, hooks.HookController): """ acl.enforce('workflows:list', context.ctx()) - if tag is not None: - if tags is None: - tags = [tag] - else: - tags.append(tag) - - filters = rest_utils.filters_to_dict( + filters = filter_utils.create_filters_from_request_params( created_at=created_at, name=name, scope=scope, diff --git a/mistral/db/v2/sqlalchemy/api.py b/mistral/db/v2/sqlalchemy/api.py index 0f307fc6..49f9f570 100644 --- a/mistral/db/v2/sqlalchemy/api.py +++ b/mistral/db/v2/sqlalchemy/api.py @@ -30,6 +30,7 @@ from sqlalchemy.sql.expression import Insert from mistral.db.sqlalchemy import base as b from mistral.db.sqlalchemy import model_base as mb from mistral.db.sqlalchemy import sqlite_lock +from mistral.db.v2.sqlalchemy import filters as db_filters from mistral.db.v2.sqlalchemy import models from mistral import exceptions as exc from mistral.services import security @@ -173,7 +174,7 @@ def _delete_all(model, session=None, **kwargs): def _get_collection(model, insecure=False, limit=None, marker=None, - sort_keys=None, sort_dirs=None, fields=None, **kwargs): + sort_keys=None, sort_dirs=None, fields=None, **filters): columns = ( tuple([getattr(model, f) for f in fields if hasattr(model, f)]) if fields else () @@ -181,21 +182,7 @@ def _get_collection(model, insecure=False, limit=None, marker=None, query = (b.model_query(model, *columns) if insecure else _secure_query(model, *columns)) - query = query.filter_by(**kwargs) - - tags = kwargs.pop('tags', None) - - # To match the tag list, a resource must contain at least all of the - # tags present in the filter parameter. - if tags: - tag_attr = getattr(model, 'tags') - - if len(tags) == 1: - expr = tag_attr.contains(tags) - else: - expr = sa.and_(*[tag_attr.contains(tag) for tag in tags]) - - query = query.filter(expr) + query = db_filters.apply_filters(query, model, **filters) query = _paginate_query( model, diff --git a/mistral/db/v2/sqlalchemy/filters.py b/mistral/db/v2/sqlalchemy/filters.py new file mode 100644 index 00000000..724baa77 --- /dev/null +++ b/mistral/db/v2/sqlalchemy/filters.py @@ -0,0 +1,63 @@ +# Copyright 2016 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six +import sqlalchemy as sa + + +def apply_filters(query, model, **filters): + filter_dict = {} + + for key, value in six.iteritems(filters): + column_attr = getattr(model, key) + if isinstance(value, dict): + if 'in' in value: + query = query.filter(column_attr.in_(value['in'])) + elif 'nin' in value: + query = query.filter(~column_attr.in_(value['nin'])) + elif 'neq' in value: + query = query.filter(column_attr != value['neq']) + elif 'gt' in value: + query = query.filter(column_attr > value['gt']) + elif 'gte' in value: + query = query.filter(column_attr >= value['gte']) + elif 'lt' in value: + query = query.filter(column_attr < value['lt']) + elif 'lte' in value: + query = query.filter(column_attr <= value['lte']) + elif 'eq' in value: + query = query.filter(column_attr == value['eq']) + else: + filter_dict[key] = value + + # We need to handle tag case seprately. As tag datatype is MutableList. + # TODO(hparekh): Need to think how can we get rid of this. + tags = filters.pop('tags', None) + + # To match the tag list, a resource must contain at least all of the + # tags present in the filter parameter. + if tags: + tag_attr = getattr(model, 'tags') + + if not isinstance(tags, list): + expr = tag_attr.contains(tags) + else: + expr = sa.and_(*[tag_attr.contains(tag) for tag in tags]) + + query = query.filter(expr) + + if filter_dict: + query = query.filter_by(**filter_dict) + + return query diff --git a/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py b/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py index 77d39602..97857c15 100644 --- a/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py +++ b/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py @@ -26,6 +26,7 @@ from mistral.db.v2.sqlalchemy import models as db_models from mistral import exceptions as exc from mistral.services import security from mistral.tests.unit import base as test_base +from mistral.utils import filter_utils user_context = test_base.get_context(default=False) @@ -567,6 +568,15 @@ ACTION_DEFINITIONS = [ 'attributes': None, 'project_id': '' }, + { + 'name': 'action3', + 'description': 'Action #3', + 'is_system': False, + 'tags': ['mc', 'abc'], + 'action_class': 'mypackage.my_module.Action3', + 'attributes': None, + 'project_id': '' + }, ] @@ -605,6 +615,157 @@ class ActionDefinitionTest(SQLAlchemyTest): ACTION_DEFINITIONS[0] ) + def test_filter_action_definitions_by_equal_value(self): + db_api.create_action_definition(ACTION_DEFINITIONS[0]) + db_api.create_action_definition(ACTION_DEFINITIONS[1]) + + created2 = db_api.create_action_definition(ACTION_DEFINITIONS[2]) + _filter = filter_utils.create_or_update_filter( + 'is_system', + False, + 'eq' + ) + fetched = db_api.get_action_definitions(**_filter) + + self.assertEqual(1, len(fetched)) + self.assertEqual(created2, fetched[0]) + + def test_filter_action_definitions_by_notEqual_value(self): + created0 = db_api.create_action_definition(ACTION_DEFINITIONS[0]) + created1 = db_api.create_action_definition(ACTION_DEFINITIONS[1]) + + db_api.create_action_definition(ACTION_DEFINITIONS[2]) + + _filter = filter_utils.create_or_update_filter( + 'is_system', + False, + 'neq' + ) + fetched = db_api.get_action_definitions(**_filter) + + self.assertEqual(2, len(fetched)) + self.assertEqual(created0, fetched[0]) + self.assertEqual(created1, fetched[1]) + + def test_filter_action_definitions_by_greaterThan_value(self): + created0 = db_api.create_action_definition(ACTION_DEFINITIONS[0]) + created1 = db_api.create_action_definition(ACTION_DEFINITIONS[1]) + created2 = db_api.create_action_definition(ACTION_DEFINITIONS[2]) + + _filter = filter_utils.create_or_update_filter( + 'created_at', + created0['created_at'], + 'gt' + ) + fetched = db_api.get_action_definitions(**_filter) + + self.assertEqual(2, len(fetched)) + self.assertEqual(created1, fetched[0]) + self.assertEqual(created2, fetched[1]) + + def test_filter_action_definitions_by_greaterThanEqual_value(self): + created0 = db_api.create_action_definition(ACTION_DEFINITIONS[0]) + created1 = db_api.create_action_definition(ACTION_DEFINITIONS[1]) + created2 = db_api.create_action_definition(ACTION_DEFINITIONS[2]) + + _filter = filter_utils.create_or_update_filter( + 'created_at', + created0['created_at'], + 'gte' + ) + fetched = db_api.get_action_definitions(**_filter) + + self.assertEqual(3, len(fetched)) + self.assertEqual(created0, fetched[0]) + self.assertEqual(created1, fetched[1]) + self.assertEqual(created2, fetched[2]) + + def test_filter_action_definitions_by_lessThan_value(self): + created0 = db_api.create_action_definition(ACTION_DEFINITIONS[0]) + created1 = db_api.create_action_definition(ACTION_DEFINITIONS[1]) + created2 = db_api.create_action_definition(ACTION_DEFINITIONS[2]) + + _filter = filter_utils.create_or_update_filter( + 'created_at', + created2['created_at'], + 'lt' + ) + fetched = db_api.get_action_definitions(**_filter) + + self.assertEqual(2, len(fetched)) + self.assertEqual(created0, fetched[0]) + self.assertEqual(created1, fetched[1]) + + def test_filter_action_definitions_by_lessThanEqual_value(self): + created0 = db_api.create_action_definition(ACTION_DEFINITIONS[0]) + created1 = db_api.create_action_definition(ACTION_DEFINITIONS[1]) + created2 = db_api.create_action_definition(ACTION_DEFINITIONS[2]) + + _filter = filter_utils.create_or_update_filter( + 'created_at', + created2['created_at'], + 'lte' + ) + fetched = db_api.get_action_definitions(**_filter) + + self.assertEqual(3, len(fetched)) + self.assertEqual(created0, fetched[0]) + self.assertEqual(created1, fetched[1]) + self.assertEqual(created2, fetched[2]) + + def test_filter_action_definitions_by_values_in_list(self): + created0 = db_api.create_action_definition(ACTION_DEFINITIONS[0]) + created1 = db_api.create_action_definition(ACTION_DEFINITIONS[1]) + + db_api.create_action_definition(ACTION_DEFINITIONS[2]) + + _filter = filter_utils.create_or_update_filter( + 'created_at', + [created0['created_at'], created1['created_at']], + 'in' + ) + fetched = db_api.get_action_definitions(**_filter) + + self.assertEqual(2, len(fetched)) + self.assertEqual(created0, fetched[0]) + self.assertEqual(created1, fetched[1]) + + def test_filter_action_definitions_by_values_notin_list(self): + created0 = db_api.create_action_definition(ACTION_DEFINITIONS[0]) + created1 = db_api.create_action_definition(ACTION_DEFINITIONS[1]) + created2 = db_api.create_action_definition(ACTION_DEFINITIONS[2]) + + _filter = filter_utils.create_or_update_filter( + 'created_at', + [created0['created_at'], created1['created_at']], + 'nin' + ) + fetched = db_api.get_action_definitions(**_filter) + + self.assertEqual(1, len(fetched)) + self.assertEqual(created2, fetched[0]) + + def test_filter_action_definitions_by_multiple_columns(self): + created0 = db_api.create_action_definition(ACTION_DEFINITIONS[0]) + created1 = db_api.create_action_definition(ACTION_DEFINITIONS[1]) + + db_api.create_action_definition(ACTION_DEFINITIONS[2]) + + _filter = filter_utils.create_or_update_filter( + 'created_at', + [created0['created_at'], created1['created_at']], + 'in' + ) + _filter = filter_utils.create_or_update_filter( + 'is_system', + True, + 'neq', + _filter + ) + fetched = db_api.get_action_definitions(**_filter) + + self.assertEqual(0, len(fetched)) + def test_update_action_definition_with_name(self): created = db_api.create_action_definition(ACTION_DEFINITIONS[0]) diff --git a/mistral/utils/filter_utils.py b/mistral/utils/filter_utils.py new file mode 100644 index 00000000..4d697b26 --- /dev/null +++ b/mistral/utils/filter_utils.py @@ -0,0 +1,91 @@ +# Copyright 2016 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + + +def create_filters_from_request_params(**params): + """Create filters from REST request parameters. + + :param req_params: REST request parameters. + :return: filters dictionary. + """ + filters = {} + for column, data in six.iteritems(params): + if data is not None: + if isinstance(data, six.string_types): + f_type, value = _extract_filter_type_and_value(data) + create_or_update_filter(column, value, f_type, filters) + else: + create_or_update_filter(column, data, _filter=filters) + return filters + + +def create_or_update_filter(column, value, filter_type='eq', _filter=None): + """Create or Update filter. + + :param column: Column name by which user want to filter. + :param value: Column value. + :param filter_type: filter type. Filter type can be + 'eq', 'neq', 'gt', 'gte', 'lte', 'in', + 'lt', 'nin'. Default is 'eq'. + :parma _filter: Optional. If provided same filter dictionary will + be updated. + :return: filter dictionary. + + """ + if _filter is None: + _filter = {} + _filter[column] = {filter_type: value} + + return _filter + + +def _extract_filter_type_and_value(data): + """Extract filter type and its value from the data. + + :param data: REST parameter value from which filter type and + value can be get. It should be in format of + 'filter_type:value'. + :return: filter type and value. + """ + if data.startswith("in:"): + value = list(six.text_type(data[3:]).split(",")) + filter_type = 'in' + elif data.startswith("nin:"): + value = list(six.text_type(data[4:]).split(",")) + filter_type = 'nin' + elif data.startswith("neq:"): + value = six.text_type(data[4:]) + filter_type = 'neq' + elif data.startswith("gt:"): + value = six.text_type(data[3:]) + filter_type = 'gt' + elif data.startswith("gte:"): + value = six.text_type(data[4:]) + filter_type = 'gte' + elif data.startswith("lt:"): + value = six.text_type(data[3:]) + filter_type = 'lt' + elif data.startswith("lte:"): + value = six.text_type(data[4:]) + filter_type = 'lte' + elif data.startswith("eq:"): + value = six.text_type(data[3:]) + filter_type = 'eq' + else: + value = data + filter_type = 'eq' + + return filter_type, value diff --git a/mistral_tempest_tests/tests/api/v2/test_actions.py b/mistral_tempest_tests/tests/api/v2/test_actions.py index 7aede1ca..55374ee9 100644 --- a/mistral_tempest_tests/tests/api/v2/test_actions.py +++ b/mistral_tempest_tests/tests/api/v2/test_actions.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime from oslo_log import log as logging from tempest.lib import exceptions from tempest import test @@ -125,6 +126,162 @@ class ActionTestsV2(base.TestCase): context.resp_body.get('faultstring') ) + @test.attr(type='smoke') + def test_get_list_actions_equalto_filter(self): + resp, body = self.client.create_action('action_v2.yaml') + self.assertEqual(201, resp.status) + + resp, body = self.client.get_list_obj( + 'actions?is_system=False' + ) + + self.assertEqual(200, resp.status) + self.assertNotEqual([], body['actions']) + + for act in body['actions']: + self.assertFalse(act['is_system']) + + @test.attr(type='smoke') + def test_get_list_actions_notEqualto_filter(self): + resp, body = self.client.create_action('action_v2.yaml') + self.assertEqual(201, resp.status) + + resp, body = self.client.get_list_obj( + 'actions?is_system=neq:False' + ) + + self.assertEqual(200, resp.status) + self.assertNotEqual([], body['actions']) + + for act in body['actions']: + self.assertTrue(act['is_system']) + + @test.attr(type='smoke') + def test_get_list_actions_inList_filter(self): + resp, body = self.client.create_action('action_v2.yaml') + self.assertEqual(201, resp.status) + + created_acts = [action['name'] for action in body['actions']] + _, body = self.client.get_object('actions', created_acts[0]) + time = body['created_at'] + resp, body = self.client.get_list_obj( + 'actions?created_at=in:' + time.replace(' ', '%20') + ) + + self.assertEqual(200, resp.status) + action_names = [action['name'] for action in body['actions']] + self.assertListEqual(created_acts, action_names) + + @test.attr(type='smoke') + def test_get_list_actions_notinList_filter(self): + resp, body = self.client.create_action('action_v2.yaml') + self.assertEqual(201, resp.status) + + created_acts = [action['name'] for action in body['actions']] + _, body = self.client.get_object('actions', created_acts[0]) + time = body['created_at'] + resp, body = self.client.get_list_obj( + 'actions?created_at=nin:' + time.replace(' ', '%20') + ) + + self.assertEqual(200, resp.status) + action_names = [action['name'] for action in body['actions']] + for act in created_acts: + self.assertNotIn(act, action_names) + + @test.attr(type='smoke') + def test_get_list_actions_greaterThan_filter(self): + time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + resp, body = self.client.get_list_obj( + 'actions?created_at=gt:' + time.replace(' ', '%20') + ) + + self.assertEqual(200, resp.status) + self.assertEqual([], body['actions']) + + @test.attr(type='smoke') + def test_get_list_actions_greaterThanEqualto_filter(self): + resp, body = self.client.create_action('action_v2.yaml') + self.assertEqual(201, resp.status) + + created_acts = [action['name'] for action in body['actions']] + _, body = self.client.get_object('actions', created_acts[0]) + time = body['created_at'] + resp, body = self.client.get_list_obj( + 'actions?created_at=gte:' + time.replace(' ', '%20') + ) + + actions = [action['name'] for action in body['actions']] + self.assertEqual(200, resp.status) + self.assertIn(created_acts[0], actions) + + @test.attr(type='smoke') + def test_get_list_actions_lessThan_filter(self): + resp, body = self.client.create_action('action_v2.yaml') + self.assertEqual(201, resp.status) + + created_acts = [action['name'] for action in body['actions']] + _, body = self.client.get_object('actions', created_acts[0]) + time = body['created_at'] + resp, body = self.client.get_list_obj( + 'actions?created_at=lt:' + time.replace(' ', '%20') + ) + + actions = [action['name'] for action in body['actions']] + self.assertEqual(200, resp.status) + self.assertNotIn(created_acts[0], actions) + + @test.attr(type='smoke') + def test_get_list_actions_lessThanEqualto_filter(self): + resp, body = self.client.create_action('action_v2.yaml') + self.assertEqual(201, resp.status) + + created_acts = [action['name'] for action in body['actions']] + _, body = self.client.get_object('actions', created_acts[0]) + time = body['created_at'] + resp, body = self.client.get_list_obj( + 'actions?created_at=lte:' + time.replace(' ', '%20') + ) + + actions = [action['name'] for action in body['actions']] + self.assertEqual(200, resp.status) + self.assertIn(created_acts[0], actions) + + @test.attr(type='smoke') + def test_get_list_actions_multiple_filter(self): + resp, body = self.client.create_action('action_v2.yaml') + self.assertEqual(201, resp.status) + + created_acts = [action['name'] for action in body['actions']] + _, body = self.client.get_object('actions', created_acts[0]) + time = body['created_at'] + resp, body = self.client.get_list_obj( + 'actions?created_at=lte:' + time.replace(' ', '%20') + + '&is_system=False' + ) + + actions = [action['name'] for action in body['actions']] + self.assertEqual(200, resp.status) + self.assertIn(created_acts[0], actions) + + @test.attr(type='negative') + def test_get_list_actions_invalid_filter(self): + self.assertRaises( + exceptions.BadRequest, + self.client.get_list_obj, + 'actions?is_system2016-02-23%2008:51:26' + ) + @test.attr(type='sanity') def test_create_and_delete_few_actions(self): resp, body = self.client.create_action('action_v2.yaml')