diff --git a/refstack/api/constants.py b/refstack/api/constants.py index 95135e78..3f2c2208 100644 --- a/refstack/api/constants.py +++ b/refstack/api/constants.py @@ -47,3 +47,23 @@ SHARED_TEST_RUN = 'shared' # Roles ROLE_USER = 'user' ROLE_OWNER = 'owner' + +# Organization types. +# OpenStack Foundation +FOUNDATION = 0 +# User's private unofficial Vendor (allows creation and testing +# of user's products) +PRIVATE_VENDOR = 1 +# Vendor applied and waiting for official status. +PENDING_VENDOR = 2 +# Official Vendor approved by the Foundation. +OFFICIAL_VENDOR = 3 + +# Product object types. +CLOUD = 0 +SOFTWARE = 1 + +# Product specific types. +DISTRO = 0 +PUBLIC_CLOUD = 1 +HOSTED_PRIVATE_CLOUD = 2 diff --git a/refstack/db/api.py b/refstack/db/api.py index 17b8db8d..e03cc884 100644 --- a/refstack/db/api.py +++ b/refstack/db/api.py @@ -170,3 +170,43 @@ def add_user_to_group(user_openid, group_id, created_by_user): def remove_user_from_group(user_openid, group_id): """Remove specified user from specified group.""" return IMPL.remove_user_from_group(user_openid, group_id) + + +def add_organization(organization_info, creator): + """Add organization.""" + return IMPL.add_organization(organization_info, creator) + + +def update_organization(organization_info): + """Update organization.""" + return IMPL.update_organization(organization_info) + + +def get_organization(organization_id): + """Get organization by id.""" + return IMPL.get_organization(organization_id) + + +def delete_organization(organization_id): + """delete organization by id.""" + return IMPL.delete_organization(organization_id) + + +def add_product(product_info, creator): + """Add product from product_info dicionary with creator.""" + return IMPL.add_product(product_info, creator) + + +def update_product(product_info): + """Update product from prodict_info dicionary.""" + return IMPL.update_product(product_info) + + +def get_product(id): + """Get product by id.""" + return IMPL.get_product(id) + + +def delete_product(id): + """delete product by id.""" + return IMPL.delete_product(id) diff --git a/refstack/db/migrations/alembic/versions/19fded785b8c_create_organization_table.py b/refstack/db/migrations/alembic/versions/19fded785b8c_create_organization_table.py new file mode 100644 index 00000000..9958aad7 --- /dev/null +++ b/refstack/db/migrations/alembic/versions/19fded785b8c_create_organization_table.py @@ -0,0 +1,42 @@ +"""Create organization table. + +Revision ID: 19fded785b8c +Revises: 319ee8fe47c7 +Create Date: 2016-01-18 14:40:00 + +""" + +# revision identifiers, used by Alembic. +revision = '19fded785b8c' +down_revision = '319ee8fe47c7' +MYSQL_CHARSET = 'utf8' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + """Upgrade DB.""" + op.create_table( + 'organization', + sa.Column('updated_at', sa.DateTime()), + sa.Column('deleted_at', sa.DateTime()), + sa.Column('deleted', sa.Integer, default=0), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.String(36), nullable=False), + sa.Column('type', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('description', sa.Text()), + sa.Column('group_id', sa.String(36), nullable=False), + sa.Column('created_by_user', sa.String(128), nullable=False), + sa.Column('properties', sa.Text()), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ), + sa.ForeignKeyConstraint(['created_by_user'], ['user.openid'], ), + mysql_charset=MYSQL_CHARSET + ) + + +def downgrade(): + """Downgrade DB.""" + op.drop_table('organization') diff --git a/refstack/db/migrations/alembic/versions/7092392cbb8e_create_product_table.py b/refstack/db/migrations/alembic/versions/7092392cbb8e_create_product_table.py new file mode 100644 index 00000000..32069caf --- /dev/null +++ b/refstack/db/migrations/alembic/versions/7092392cbb8e_create_product_table.py @@ -0,0 +1,45 @@ +"""Create product table. + +Revision ID: 7092392cbb8e +Revises: 19fded785b8c +Create Date: 2016-01-18 16:10:00 + +""" + +# revision identifiers, used by Alembic. +revision = '7092392cbb8e' +down_revision = '19fded785b8c' +MYSQL_CHARSET = 'utf8' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + """Upgrade DB.""" + op.create_table( + 'product', + sa.Column('updated_at', sa.DateTime()), + sa.Column('deleted_at', sa.DateTime()), + sa.Column('deleted', sa.Integer, default=0), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('created_by_user', sa.String(128), nullable=False), + sa.Column('id', sa.String(36), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('description', sa.Text()), + sa.Column('product_id', sa.String(36), nullable=False), + sa.Column('type', sa.Integer(), nullable=False), + sa.Column('product_type', sa.Integer(), nullable=False), + sa.Column('public', sa.Boolean(), nullable=False), + sa.Column('organization_id', sa.String(36), nullable=False), + sa.Column('properties', sa.Text()), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['organization_id'], ['organization.id'], ), + sa.ForeignKeyConstraint(['created_by_user'], ['user.openid'], ), + mysql_charset=MYSQL_CHARSET + ) + + +def downgrade(): + """Downgrade DB.""" + op.drop_table('product') diff --git a/refstack/db/sqlalchemy/api.py b/refstack/db/sqlalchemy/api.py index d285492a..c0bf41b8 100644 --- a/refstack/db/sqlalchemy/api.py +++ b/refstack/db/sqlalchemy/api.py @@ -356,3 +356,130 @@ def remove_user_from_group(user_openid, group_id): filter_by(user_openid=user_openid). filter_by(group_id=group_id). delete(synchronize_session=False)) + + +def add_organization(organization_info, creator): + """Add organization.""" + session = get_session() + with session.begin(): + group = models.Group() + group.name = 'Group for %s' % organization_info['name'] + group.save(session=session) + group_id = group.id + + item = models.UserToGroup() + item.user_openid = creator + item.group_id = group_id + item.created_by_user = creator + item.save(session=session) + + organization = models.Organization() + organization.type = organization_info.get( + 'type', api_const.PRIVATE_VENDOR) + organization.name = organization_info['name'] + organization.description = organization_info.get('description') + organization.group_id = group_id + organization.created_by_user = creator + organization.properties = organization_info.get('properties') + organization.save(session=session) + + return _to_dict(organization) + + +def update_organization(organization_info): + """Update organization.""" + session = get_session() + _id = organization_info['id'] + organization = (session.query(models.Organization). + filter_by(id=_id).first()) + if organization is None: + raise NotFound('Organization with id %s not found' % _id) + + with session.begin(): + organization.type = organization_info.get( + 'type', organization.type) + organization.name = organization_info.get( + 'name', organization.name) + organization.description = organization_info.get( + 'description', organization.description) + organization.properties = organization_info.get( + 'properties', organization.properties) + organization.save(session=session) + + +def get_organization(organization_id): + """Get organization by id.""" + session = get_session() + organization = (session.query(models.Organization). + filter_by(id=organization_id).first()) + if organization is None: + raise NotFound('Organization with id %s not found' % organization_id) + return _to_dict(organization) + + +def delete_organization(organization_id): + """delete organization by id.""" + session = get_session() + with session.begin(): + (session.query(models.Product). + filter_by(organization_id=organization_id). + delete(synchronize_session=False)) + (session.query(models.Organization). + filter_by(id=organization_id). + delete(synchronize_session=False)) + + +def add_product(product_info, creator): + """Add product.""" + product = models.Product() + _id = six.text_type(uuid.uuid4()) + product.id = _id + product.type = product_info['type'] + product.product_type = product_info['product_type'] + product.product_id = product_info['product_id'] + product.name = product_info['name'] + product.description = product_info.get('description') + product.organization_id = product_info['organization_id'] + product.created_by_user = creator + product.public = product_info.get('public', False) + product.properties = product_info.get('properties') + + session = get_session() + with session.begin(): + product.save(session=session) + return _to_dict(product) + + +def update_product(product_info): + """Update product by product_id.""" + session = get_session() + _id = product_info.get('product_id') + product = session.query(models.Product).filter_by(product_id=_id).first() + if product is None: + raise NotFound('Product with product_id %s not found' % _id) + + product.name = product_info.get('name', product.name) + product.description = product_info.get('description', product.description) + product.public = product_info.get('public', product.public) + product.properties = product_info.get('properties', product.properties) + + with session.begin(): + product.save(session=session) + return _to_dict(product) + + +def get_product(id): + """Get product by id.""" + session = get_session() + product = session.query(models.Product).filter_by(id=id).first() + if product is None: + raise NotFound('Product with id "%s" not found' % id) + return _to_dict(product) + + +def delete_product(id): + """delete product by id.""" + session = get_session() + with session.begin(): + (session.query(models.Product).filter_by(id=id). + delete(synchronize_session=False)) diff --git a/refstack/db/sqlalchemy/models.py b/refstack/db/sqlalchemy/models.py index 9456b297..d024125b 100644 --- a/refstack/db/sqlalchemy/models.py +++ b/refstack/db/sqlalchemy/models.py @@ -193,3 +193,52 @@ class UserToGroup(BASE, RefStackBase): # pragma: no cover def default_allowed_keys(self): """Default keys.""" return 'user_openid', 'group_id' + + +class Organization(BASE, RefStackBase): # pragma: no cover + """Organization definition.""" + + __tablename__ = 'organization' + + id = sa.Column(sa.String(36), primary_key=True, + default=lambda: six.text_type(uuid.uuid4())) + type = sa.Column(sa.Integer, nullable=False) + name = sa.Column(sa.String(80), nullable=False) + description = sa.Column(sa.Text()) + group_id = sa.Column(sa.String(36), sa.ForeignKey('group.id'), + nullable=False) + created_by_user = sa.Column(sa.String(128), sa.ForeignKey('user.openid'), + nullable=False) + properties = sa.Column(sa.Text()) + + @property + def default_allowed_keys(self): + """Default keys.""" + return ('id', 'type', 'name', 'description', 'group_id', + 'created_by_user', 'properties') + + +class Product(BASE, RefStackBase): # pragma: no cover + """Product definition.""" + + __tablename__ = 'product' + + id = sa.Column(sa.Integer(), primary_key=True) + product_id = sa.Column(sa.String(36), nullable=False) + name = sa.Column(sa.String(80), nullable=False) + description = sa.Column(sa.Text()) + organization_id = sa.Column(sa.String(36), + sa.ForeignKey('organization.id'), + nullable=False) + created_by_user = sa.Column(sa.String(128), sa.ForeignKey('user.openid'), + nullable=False) + public = sa.Column(sa.Boolean(), nullable=False) + properties = sa.Column(sa.Text()) + type = sa.Column(sa.Integer(), nullable=False) + product_type = sa.Column(sa.Integer(), nullable=False) + + @property + def default_allowed_keys(self): + """Default keys.""" + return ('id', 'product_id', 'name', 'description', 'organization_id', + 'created_by_user', 'properties', 'type', 'product_type') diff --git a/refstack/tests/unit/test_db.py b/refstack/tests/unit/test_db.py index 4abff891..33aa2a80 100644 --- a/refstack/tests/unit/test_db.py +++ b/refstack/tests/unit/test_db.py @@ -638,3 +638,131 @@ class DBBackendTestCase(base.BaseTestCase): mock.call().filter_by(group_id='GUID'), mock.call().filter_by().delete(synchronize_session=False))) session.begin.assert_called_once_with() + + @mock.patch.object(api, 'get_session') + @mock.patch('refstack.db.sqlalchemy.models.Organization') + @mock.patch('refstack.db.sqlalchemy.models.Group') + @mock.patch('refstack.db.sqlalchemy.models.UserToGroup') + @mock.patch.object(api, '_to_dict', side_effect=lambda x: x) + def test_organization_add(self, mock_to_dict, mock_model_user_to_group, + mock_model_group, mock_model_organization, + mock_get_session): + + organization_info = {'name': 'a', 'description': 'b', 'type': 1} + session = mock_get_session.return_value + organization = mock_model_organization.return_value + result = api.add_organization(organization_info, 'user-123') + self.assertEqual(result, organization) + + group = mock_model_group.return_value + self.assertIsNotNone(group.id) + self.assertIsNotNone(organization.id) + self.assertIsNotNone(organization.group_id) + + mock_model_organization.assert_called_once_with() + mock_model_group.assert_called_once_with() + mock_model_user_to_group.assert_called_once_with() + mock_get_session.assert_called_once_with() + organization.save.assert_called_once_with(session=session) + group.save.assert_called_once_with(session=session) + user_to_group = mock_model_user_to_group.return_value + user_to_group.save.assert_called_once_with(session=session) + session.begin.assert_called_once_with() + + @mock.patch.object(api, 'get_session') + @mock.patch('refstack.db.sqlalchemy.models.Product') + @mock.patch.object(api, '_to_dict', side_effect=lambda x: x) + def test_product_add(self, mock_to_dict, mock_product, mock_get_session): + session = mock_get_session.return_value + product = mock_product.return_value + product_info = {'product_id': 'hash_or_guid', 'name': 'a', + 'organization_id': 'GUID0', 'type': 0, + 'product_type': 0} + result = api.add_product(product_info, 'user-123') + self.assertEqual(result, product) + + self.assertIsNotNone(product.id) + + mock_get_session.assert_called_once_with() + product.save.assert_called_once_with(session=session) + session.begin.assert_called_once_with() + + @mock.patch.object(api, 'get_session') + @mock.patch('refstack.db.sqlalchemy.models.Product') + def test_incomplete_product_add(self, mock_product, mock_get_session): + product_info = {} + self.assertRaises(KeyError, api.add_product, product_info, 'u') + + @mock.patch.object(api, 'get_session') + @mock.patch('refstack.db.sqlalchemy.models.Product.save') + def test_product_update(self, mock_product_save, mock_get_session): + session = mock_get_session.return_value + query = session.query.return_value + filtered = query.filter_by.return_value + product = models.Product() + product.product_id = '123' + filtered.first.return_value = product + + product_info = {'product_id': '098', 'name': 'a', 'description': 'b', + 'creator_openid': 'abc', 'organization_id': '1', + 'type': 0, 'product_type': 0} + api.update_product(product_info) + + self.assertEqual('123', product.product_id) + self.assertIsNone(product.created_by_user) + self.assertIsNone(product.organization_id) + self.assertIsNone(product.type) + self.assertIsNone(product.product_type) + + mock_get_session.assert_called_once_with() + mock_product_save.assert_called_once_with(session=session) + session.begin.assert_called_once_with() + + @mock.patch.object(api, 'get_session', + return_value=mock.Mock(name='session'),) + @mock.patch('refstack.db.sqlalchemy.models.Organization') + @mock.patch.object(api, '_to_dict', side_effect=lambda x: x) + def test_organization_get(self, mock_to_dict, mock_model, + mock_get_session): + organization_id = 12345 + session = mock_get_session.return_value + query = session.query.return_value + filtered = query.filter_by.return_value + organization = filtered.first.return_value + + result = api.get_organization(organization_id) + self.assertEqual(result, organization) + + session.query.assert_called_once_with(mock_model) + query.filter_by.assert_called_once_with(id=organization_id) + filtered.first.assert_called_once_with() + + @mock.patch.object(api, 'get_session', + return_value=mock.Mock(name='session'),) + @mock.patch('refstack.db.sqlalchemy.models.Product') + @mock.patch.object(api, '_to_dict', side_effect=lambda x: x) + def test_product_get(self, mock_to_dict, mock_model, mock_get_session): + _id = 12345 + session = mock_get_session.return_value + query = session.query.return_value + filtered = query.filter_by.return_value + product = filtered.first.return_value + + result = api.get_product(_id) + self.assertEqual(result, product) + + session.query.assert_called_once_with(mock_model) + query.filter_by.assert_called_once_with(id=_id) + filtered.first.assert_called_once_with() + + @mock.patch.object(api, 'get_session') + @mock.patch('refstack.db.sqlalchemy.api.models') + def test_product_delete(self, mock_models, mock_get_session): + session = mock_get_session.return_value + db.delete_product('product_id') + + session.query.assert_called_once_with(mock_models.Product) + session.query.return_value.filter_by.assert_has_calls(( + mock.call(id='product_id'), + mock.call().delete(synchronize_session=False))) + session.begin.assert_called_once_with()