Crud operations for hierarchy levels implemented

Only name changing implemented under hierarchy_levels url. Whole
hierarchy change should be performed throught environment update.
Cascade deletion added to hierarchy levels on environment deletion.
Order of hierarchy levels fixed for environments GET requests.
Module levels_hierarchy was renamed to hierarchy_levels.

Change-Id: I0642892b517357ebc95427617413048f4db6fba3
This commit is contained in:
Alexander Kislitsky 2016-08-18 18:57:13 +03:00
parent 7f3faa3336
commit f25a724ade
12 changed files with 396 additions and 121 deletions

View File

@ -19,6 +19,7 @@ from tuning_box import db
from tuning_box import errors
from tuning_box.library import components
from tuning_box.library import environments
from tuning_box.library import hierarchy_levels
from tuning_box.library import resource_definitions
from tuning_box.library import resource_overrides
from tuning_box.library import resource_values
@ -86,6 +87,17 @@ api.add_resource(
'/environments/<int:environment_id>'
)
# Hierarchy levels
api.add_resource(
hierarchy_levels.EnvironmentHierarchyLevelsCollection,
'/environments/<int:environment_id>/hierarchy_levels'
)
api.add_resource(
hierarchy_levels.EnvironmentHierarchyLevels,
'/environments/<int:environment_id>/hierarchy_levels/'
'<string:level>'
)
def handle_integrity_error(exc):
response = flask.jsonify(msg=exc.args[0])

View File

@ -16,11 +16,14 @@ import re
import flask
import flask_sqlalchemy
import sqlalchemy
import sqlalchemy.event
import sqlalchemy.ext.declarative as sa_decl
from sqlalchemy.orm import exc as orm_exc
from sqlalchemy import types
from tuning_box import errors
try:
from importlib import reload
except ImportError:
@ -152,8 +155,12 @@ class Environment(ModelMixin, db.Model):
class EnvironmentHierarchyLevel(ModelMixin, db.Model):
environment_id = fk(Environment)
environment = db.relationship(Environment, backref='hierarchy_levels')
environment_id = fk(Environment, ondelete='CASCADE')
environment = db.relationship(
Environment,
backref=sqlalchemy.orm.backref('hierarchy_levels',
cascade="all, delete-orphan")
)
name = db.Column(db.String(128))
@sa_decl.declared_attr
@ -245,3 +252,21 @@ def prefix_tables(module, prefix):
def unprefix_tables(module):
ModelMixin.table_prefix = ""
reload(module)
def get_or_404(cls, ident):
result = cls.query.get(ident)
if result is None:
raise errors.TuningboxNotFound(
"{0} not found by {1}".format(cls.__name__, ident)
)
return result
def find_or_404(cls, **attrs):
item = cls.query.filter_by(**attrs).one_or_none()
if not item:
raise errors.TuningboxNotFound(
"{0} not found by {1}".format(cls.__name__, attrs)
)
return item

View File

@ -14,7 +14,7 @@ from sqlalchemy.orm import exc as sa_exc
from tuning_box import db
from tuning_box import errors
from tuning_box.library import levels_hierarchy
from tuning_box.library import hierarchy_levels
def load_objects(model, ids):
@ -75,7 +75,7 @@ def get_resource_definition(id_or_name, environment_id):
def get_resource_values(environment, levels, res_def):
level_value = levels_hierarchy.get_environment_level_value(
level_value = hierarchy_levels.get_environment_level_value(
environment, levels)
res_values = db.ResourceValues.query.filter_by(
environment_id=environment.id,

View File

@ -29,7 +29,15 @@ class EnvironmentsCollection(flask_restful.Resource):
method_decorators = [flask_restful.marshal_with(environment_fields)]
def get(self):
return db.Environment.query.all()
envs = db.Environment.query.all()
result = []
for env in envs:
hierarchy_levels = db.EnvironmentHierarchyLevel.\
get_for_environment(env)
# Proper order of levels can't be provided by ORM backref
result.append({'id': env.id, 'components': env.components,
'hierarchy_levels': hierarchy_levels})
return result, 200
def _check_components(self, components):
identities = set()
@ -72,7 +80,12 @@ class Environment(flask_restful.Resource):
method_decorators = [flask_restful.marshal_with(environment_fields)]
def get(self, environment_id):
return db.Environment.query.get_or_404(environment_id)
env = db.Environment.query.get_or_404(environment_id)
hierarchy_levels = db.EnvironmentHierarchyLevel.\
get_for_environment(env)
# Proper order of levels can't be provided by ORM backref
return {'id': env.id, 'components': env.components,
'hierarchy_levels': hierarchy_levels}, 200
def _update_components(self, environment, components):
if components is not None:
@ -80,14 +93,29 @@ class Environment(flask_restful.Resource):
db.Component, components)
environment.components = new_components
def _update_hierarchy_levels(self, environment, hierarchy_levels):
if hierarchy_levels is not None:
new_hierarchy_levels = library.load_objects_by_id_or_name(
db.EnvironmentHierarchyLevel, hierarchy_levels)
parent = None
def _update_hierarchy_levels(self, environment, hierarchy_levels_names):
if hierarchy_levels_names is not None:
old_hierarchy_levels = db.EnvironmentHierarchyLevel.query.filter(
db.EnvironmentHierarchyLevel.environment_id == environment.id
).all()
new_hierarchy_levels = []
for level_name in hierarchy_levels_names:
level = db.get_or_create(
db.EnvironmentHierarchyLevel,
name=level_name,
environment=environment
)
new_hierarchy_levels.append(level)
parent_id = None
for level in new_hierarchy_levels:
level.parent = parent
parent = level
level.parent_id = parent_id
parent_id = level.id
for old_level in old_hierarchy_levels:
if old_level not in new_hierarchy_levels:
db.db.session.delete(old_level)
environment.hierarchy_levels = new_hierarchy_levels
@db.with_transaction

View File

@ -0,0 +1,85 @@
# 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 flask
import werkzeug
import flask_restful
from flask_restful import fields
from tuning_box import db
def iter_environment_level_values(environment, levels):
env_levels = db.EnvironmentHierarchyLevel.get_for_environment(environment)
level_pairs = zip(env_levels, levels)
for env_level, (level_name, level_value) in level_pairs:
if env_level.name != level_name:
raise werkzeug.exceptions.BadRequest(
"Unexpected level name '{0}'. Expected '{1}'.".format(
level_name, env_level.name))
level_value_db = db.get_or_create(
db.EnvironmentHierarchyLevelValue,
level=env_level,
value=level_value,
)
yield level_value_db
def get_environment_level_value(environment, levels):
level_value = None
for level_value in iter_environment_level_values(environment, levels):
pass
return level_value
environment_hierarchy_level_fields = {
'name': fields.String,
'environment_id': fields.Integer,
'parent': fields.String(attribute='parent.name')
}
class EnvironmentHierarchyLevelsCollection(flask_restful.Resource):
method_decorators = [
flask_restful.marshal_with(environment_hierarchy_level_fields)
]
def get(self, environment_id):
env = db.get_or_404(db.Environment, environment_id)
return db.EnvironmentHierarchyLevel.get_for_environment(env)
class EnvironmentHierarchyLevels(flask_restful.Resource):
method_decorators = [
flask_restful.marshal_with(environment_hierarchy_level_fields)
]
def get(self, environment_id, level):
level = db.find_or_404(db.EnvironmentHierarchyLevel,
environment_id=environment_id,
name=level)
return level
@db.with_transaction
def _do_update(self, environment_id, level):
level = db.find_or_404(db.EnvironmentHierarchyLevel,
environment_id=environment_id,
name=level)
level.name = flask.request.json.get('name', level.name)
def put(self, environment_id, level):
return self.patch(environment_id, level)
def patch(self, environment_id, level):
self._do_update(environment_id, level)
return None, 204

View File

@ -1,38 +0,0 @@
# 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 werkzeug
from tuning_box import db
def iter_environment_level_values(environment, levels):
env_levels = db.EnvironmentHierarchyLevel.get_for_environment(environment)
level_pairs = zip(env_levels, levels)
for env_level, (level_name, level_value) in level_pairs:
if env_level.name != level_name:
raise werkzeug.exceptions.BadRequest(
"Unexpected level name '{0}'. Expected '{1}'.".format(
level_name, env_level.name))
level_value_db = db.get_or_create(
db.EnvironmentHierarchyLevelValue,
level=env_level,
value=level_value,
)
yield level_value_db
def get_environment_level_value(environment, levels):
level_value = None
for level_value in iter_environment_level_values(environment, levels):
pass
return level_value

View File

@ -15,7 +15,7 @@ import flask_restful
from tuning_box import db
from tuning_box import library
from tuning_box.library import levels_hierarchy
from tuning_box.library import hierarchy_levels
from tuning_box.library import resource_keys_operation
@ -35,7 +35,7 @@ class ResourceOverrides(flask_restful.Resource):
resource_id_or_name=res_def.id,
), code=308)
level_value = levels_hierarchy.get_environment_level_value(
level_value = hierarchy_levels.get_environment_level_value(
environment, levels)
esv = db.get_or_create(
db.ResourceValues,
@ -61,7 +61,7 @@ class ResourceOverrides(flask_restful.Resource):
)
return flask.redirect(url, code=308)
level_value = levels_hierarchy.get_environment_level_value(
level_value = hierarchy_levels.get_environment_level_value(
environment, levels)
res_values = db.ResourceValues.query.filter_by(
resource_definition=res_def,

View File

@ -16,7 +16,7 @@ import itertools
from tuning_box import db
from tuning_box import library
from tuning_box.library import levels_hierarchy
from tuning_box.library import hierarchy_levels
from tuning_box.library import resource_keys_operation
@ -37,7 +37,7 @@ class ResourceValues(flask_restful.Resource):
resource_id_or_name=res_def.id,
), code=308)
level_value = levels_hierarchy.get_environment_level_value(
level_value = hierarchy_levels.get_environment_level_value(
environment, levels)
esv = db.get_or_create(
db.ResourceValues,
@ -66,7 +66,7 @@ class ResourceValues(flask_restful.Resource):
url += '?' + qs
return flask.redirect(url, code=308)
level_values = list(levels_hierarchy.iter_environment_level_values(
level_values = list(hierarchy_levels.iter_environment_level_values(
environment, levels))
if 'effective' in flask.request.args:

View File

@ -0,0 +1,59 @@
# 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.
"""Level cascade deletion on environment removal
Revision ID: adf671eddeb4
Revises: a86472389a70
Create Date: 2016-08-19 16:39:46.745113
"""
# revision identifiers, used by Alembic.
revision = 'adf671eddeb4'
down_revision = 'a86472389a70'
branch_labels = None
depends_on = None
from alembic import context
from alembic import op
def upgrade():
table_prefix = context.config.get_main_option('table_prefix')
table_name = table_prefix + 'environment_hierarchy_level'
with op.batch_alter_table(table_name) as batch:
batch.drop_constraint(
table_prefix + 'environment_hierarchy_level_environment_id_fkey',
type_='foreignkey'
)
batch.create_foreign_key(
table_prefix + 'environment_hierarchy_level_environment_id_fkey',
table_prefix + 'environment',
['environment_id'], ['id'], ondelete='CASCADE'
)
def downgrade():
table_prefix = context.config.get_main_option('table_prefix')
table_name = table_prefix + 'environment_hierarchy_level'
with op.batch_alter_table(table_name) as batch:
batch.drop_constraint(
table_prefix + 'environment_hierarchy_level_environment_id_fkey',
type_='foreignkey'
)
batch.create_foreign_key(
table_prefix + 'environment_hierarchy_level_environment_id_fkey',
table_prefix + 'environment',
['environment_id'], ['id']
)

View File

@ -166,11 +166,24 @@ class TestEnvironments(BaseTest):
def test_delete_environment(self):
self._fixture()
env_id = 9
res = self.client.delete(self.object_url.format(env_id))
self.assertEqual(res.status_code, 204)
env_url = self.object_url.format(env_id)
res = self.client.get(env_url)
self.assertEqual(200, res.status_code)
levels = ['lvl1', 'lvl2']
self.assertEqual(levels, res.json['hierarchy_levels'])
res = self.client.delete(env_url)
self.assertEqual(204, res.status_code)
self.assertEqual(res.data, b'')
self._assert_not_in_db(db.Environment, 9)
with self.app.app_context():
for name in levels:
obj = db.EnvironmentHierarchyLevel.query.filter(
db.EnvironmentHierarchyLevel.name == name
).first()
self.assertIsNone(obj)
def test_delete_environment_404(self):
env_id = 9
res = self.client.delete(self.object_url.format(env_id))
@ -260,11 +273,42 @@ class TestEnvironments(BaseTest):
self.assertEqual(expected_levels, actual['hierarchy_levels'])
self.check_hierarchy_levels(actual['hierarchy_levels'])
def test_put_environment_level_not_found(self):
def test_put_environment_hierarchy_levels_reverse(self):
self._fixture()
env_id = 9
env_url = self.object_url.format(env_id)
initial = self.client.get(env_url).json
expected_levels = initial['hierarchy_levels']
expected_levels.reverse()
# Updating hierarchy levels
res = self.client.put(
self.object_url.format(env_id),
data={'hierarchy_levels': [None]}
env_url,
data={'hierarchy_levels': expected_levels}
)
self.assertEqual(404, res.status_code)
self.assertEqual(204, res.status_code)
actual = self.client.get(env_url).json
self.assertEqual(expected_levels, actual['hierarchy_levels'])
self.check_hierarchy_levels(actual['hierarchy_levels'])
def test_put_environment_hierarchy_levels_with_new_level(self):
self._fixture()
env_id = 9
env_url = self.object_url.format(env_id)
initial = self.client.get(env_url).json
expected_levels = ['root'] + initial['hierarchy_levels']
res = self.client.put(
env_url,
data={'hierarchy_levels': expected_levels}
)
self.assertEqual(204, res.status_code)
res = self.client.get('/environments/9/hierarchy_levels')
self.assertEqual(200, res.status_code)
res = self.client.get(env_url)
self.assertEqual(200, res.status_code)
actual = res.json
self.assertEqual(expected_levels, actual['hierarchy_levels'])
self.check_hierarchy_levels(actual['hierarchy_levels'])

View File

@ -0,0 +1,118 @@
# 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 werkzeug
from tuning_box import db
from tuning_box.library import hierarchy_levels
from tuning_box.tests.test_app import BaseTest
class TestLevelsHierarchy(BaseTest):
collection_url = '/environments/{0}/hierarchy_levels'
object_url = collection_url + '/{1}'
def test_get_environment_level_value_root(self):
self._fixture()
with self.app.app_context(), db.db.session.begin():
level_value = hierarchy_levels.get_environment_level_value(
db.Environment(id=9),
[],
)
self.assertIsNone(level_value)
def test_get_environment_level_value_deep(self):
self._fixture()
with self.app.app_context(), db.db.session.begin():
level_value = hierarchy_levels.get_environment_level_value(
db.Environment(id=9),
[('lvl1', 'val1'), ('lvl2', 'val2')],
)
self.assertIsNotNone(level_value)
self.assertEqual(level_value.level.name, 'lvl2')
self.assertEqual(level_value.value, 'val2')
level = level_value.level.parent
self.assertIsNotNone(level)
self.assertEqual(level.name, 'lvl1')
self.assertIsNone(level.parent)
def test_get_environment_level_value_bad_level(self):
self._fixture()
with self.app.app_context(), db.db.session.begin():
exc = self.assertRaises(
werkzeug.exceptions.BadRequest,
hierarchy_levels.get_environment_level_value,
db.Environment(id=9),
[('lvlx', 'val1')],
)
self.assertEqual(
exc.description,
"Unexpected level name 'lvlx'. Expected 'lvl1'.",
)
def test_get_hierarchy_levels(self):
self._fixture()
environment_id = 9
expected_levels = ['lvl1', 'lvl2']
res = self.client.get(self.collection_url.format(environment_id))
self.assertEqual(200, res.status_code)
self.assertEqual(expected_levels, [d['name'] for d in res.json])
def test_get_hierarchy_levels_not_found(self):
environment_id = 9
res = self.client.get(self.collection_url.format(environment_id))
self.assertEqual(404, res.status_code)
def test_get_hierarchy_level(self):
self._fixture()
environment_id = 9
levels = ['lvl1', 'lvl2']
for level in levels:
res = self.client.get(self.object_url.format(environment_id,
level))
self.assertEqual(200, res.status_code)
self.assertEqual(level, res.json['name'])
def test_get_hierarchy_level_not_found(self):
levels = ['lvl1', 'lvl2']
for level in levels:
res = self.client.get(self.object_url.format(9, level))
self.assertEqual(404, res.status_code)
def test_put_hierarchy_level(self):
self._fixture()
environment_id = 9
level = 'lvl1'
new_name = 'new_{0}'.format(level)
res = self.client.put(self.object_url.format(environment_id, level),
data={'name': new_name})
self.assertEqual(204, res.status_code)
res = self.client.get(self.object_url.format(environment_id, new_name))
self.assertEqual(200, res.status_code)
self.assertEqual(new_name, res.json['name'])
def test_put_hierarchy_level_not_found(self):
self._fixture()
environment_id = 9
res = self.client.put(self.object_url.format(environment_id, 'xx'),
data={'name': 'new_name'})
self.assertEqual(404, res.status_code)
res = self.client.put(self.object_url.format(1, 'lvl1'),
data={'name': 'new_name'})
self.assertEqual(404, res.status_code)
res = self.client.put(self.object_url.format(1, 'xx'),
data={'name': 'new_name'})
self.assertEqual(404, res.status_code)

View File

@ -1,58 +0,0 @@
# 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 werkzeug
from tuning_box import db
from tuning_box.library import levels_hierarchy
from tuning_box.tests.test_app import BaseTest
class TestLevelsHierarchy(BaseTest):
def test_get_environment_level_value_root(self):
self._fixture()
with self.app.app_context(), db.db.session.begin():
level_value = levels_hierarchy.get_environment_level_value(
db.Environment(id=9),
[],
)
self.assertIsNone(level_value)
def test_get_environment_level_value_deep(self):
self._fixture()
with self.app.app_context(), db.db.session.begin():
level_value = levels_hierarchy.get_environment_level_value(
db.Environment(id=9),
[('lvl1', 'val1'), ('lvl2', 'val2')],
)
self.assertIsNotNone(level_value)
self.assertEqual(level_value.level.name, 'lvl2')
self.assertEqual(level_value.value, 'val2')
level = level_value.level.parent
self.assertIsNotNone(level)
self.assertEqual(level.name, 'lvl1')
self.assertIsNone(level.parent)
def test_get_environment_level_value_bad_level(self):
self._fixture()
with self.app.app_context(), db.db.session.begin():
exc = self.assertRaises(
werkzeug.exceptions.BadRequest,
levels_hierarchy.get_environment_level_value,
db.Environment(id=9),
[('lvlx', 'val1')],
)
self.assertEqual(
exc.description,
"Unexpected level name 'lvlx'. Expected 'lvl1'.",
)