diff --git a/glare/api/v1/resource.py b/glare/api/v1/resource.py index 5129214..e28c680 100644 --- a/glare/api/v1/resource.py +++ b/glare/api/v1/resource.py @@ -320,7 +320,7 @@ class ArtifactsController(api_versioning.VersionedResource): if not values.get('name'): msg = _("Name must be specified at creation.") raise exc.BadRequest(msg) - for field in ('visibility', 'status'): + for field in ('visibility', 'status', 'display_type_name'): if field in values: msg = _("%s is not allowed in a request at creation.") % field raise exc.BadRequest(msg) @@ -385,7 +385,8 @@ class ArtifactsController(api_versioning.VersionedResource): artifacts = artifacts_data["artifacts"] result = {'artifacts': artifacts, 'type_name': type_name, - 'total_count': artifacts_data['total_count']} + 'total_count': artifacts_data['total_count'], + 'display_type_name': artifacts_data['display_type_name']} if len(artifacts) != 0 and len(artifacts) == limit: result['next_marker'] = artifacts[-1]['id'] return result @@ -545,7 +546,8 @@ class ResponseSerializer(api_versioning.VersionedResource, 'artifacts': af_list['artifacts'], 'first': '/artifacts/%s' % type_name, 'schema': '/schemas/%s' % type_name, - 'total_count': af_list['total_count'] + 'total_count': af_list['total_count'], + 'display_type_name': af_list['display_type_name'] } if query: body['first'] = '%s?%s' % (body['first'], query) diff --git a/glare/db/migration/alembic_migrations/versions/005_added_display_name.py b/glare/db/migration/alembic_migrations/versions/005_added_display_name.py new file mode 100644 index 0000000..0d0fbe2 --- /dev/null +++ b/glare/db/migration/alembic_migrations/versions/005_added_display_name.py @@ -0,0 +1,45 @@ +# Copyright 2018 OpenStack Foundation. +# +# 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. + +"""added display name + +Revision ID: 005 +Revises: 004 +Create Date: 2018-03-13 14:32:33.765690 + +""" + +# revision identifiers, used by Alembic. +revision = '005' +down_revision = '004' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('glare_artifacts', sa.Column('display_type_name', + sa.String(255), + nullable=True)) + op.create_index('ix_glare_artifact_display_name', + 'glare_artifacts', + ['display_type_name'] + ) + + +def downgrade(): + with op.batch_alter_table('glare_artifacts') as batch_op: + batch_op.drop_index('ix_glare_artifact_display_name') + batch_op.drop_column('display_type_name') diff --git a/glare/db/sqlalchemy/api.py b/glare/db/sqlalchemy/api.py index 454995c..f38bbf4 100644 --- a/glare/db/sqlalchemy/api.py +++ b/glare/db/sqlalchemy/api.py @@ -50,7 +50,8 @@ options.set_defaults(CONF) BASE_ARTIFACT_PROPERTIES = ('id', 'visibility', 'created_at', 'updated_at', 'activated_at', 'owner', 'status', 'description', - 'name', 'type_name', 'version') + 'name', 'type_name', 'version', + 'display_type_name') _FACADE = None _LOCK = threading.Lock() diff --git a/glare/db/sqlalchemy/models.py b/glare/db/sqlalchemy/models.py index cd6dbd0..f02972f 100644 --- a/glare/db/sqlalchemy/models.py +++ b/glare/db/sqlalchemy/models.py @@ -99,6 +99,7 @@ class Artifact(BASE, ArtifactBase): Index('ix_glare_artifact_status', 'status'), Index('ix_glare_artifact_owner', 'owner'), Index('ix_glare_artifact_visibility', 'visibility'), + Index('ix_glare_artifact_display_name', 'display_type_name'), {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8'}) __protected_attributes__ = set(["created_at", "updated_at"]) @@ -122,6 +123,7 @@ class Artifact(BASE, ArtifactBase): updated_at = Column(DateTime, default=lambda: timeutils.utcnow(), nullable=False, onupdate=lambda: timeutils.utcnow()) activated_at = Column(DateTime) + display_type_name = Column(String(255), nullable=True) def to_dict(self): d = super(Artifact, self).to_dict() diff --git a/glare/engine.py b/glare/engine.py index 813d496..f6c7d07 100644 --- a/glare/engine.py +++ b/glare/engine.py @@ -349,6 +349,9 @@ class Engine(object): context, filters, marker, limit, sort, latest, list_all_artifacts) artifacts_data["artifacts"] = [af.to_dict() for af in artifacts_data["artifacts"]] + artifacts_data['display_type_name'] = \ + artifact_type.get_display_type_name() + return artifacts_data @staticmethod diff --git a/glare/objects/all.py b/glare/objects/all.py index 9c92ead..08e8d2e 100644 --- a/glare/objects/all.py +++ b/glare/objects/all.py @@ -30,6 +30,14 @@ class All(base.BaseArtifact): 'type_name': Field(fields.StringField, description="Name of artifact type.", sortable=True), + 'display_type_name': Field(fields.StringField, + description="Display name of " + "artifact type.", + sortable=True, + filter_ops=(wrappers.FILTER_LIKE, + wrappers.FILTER_EQ, + wrappers.FILTER_NEQ, + wrappers.FILTER_IN)) } @classmethod @@ -51,6 +59,10 @@ class All(base.BaseArtifact): def get_type_name(cls): return "all" + @classmethod + def get_display_type_name(cls): + return "All Artifacts" + def to_dict(self): # Use specific method of artifact type to convert it to dict values = self.obj_to_primitive()['versioned_object.data'] diff --git a/glare/objects/base.py b/glare/objects/base.py index 7d06f9f..b44b4ef 100644 --- a/glare/objects/base.py +++ b/glare/objects/base.py @@ -245,6 +245,16 @@ Possible values: """ raise NotImplementedError() + @classmethod + def get_display_type_name(cls): + """ + Provides verbose Artifact type name which any external user can + understand easily. + + :return: general purpose name for Artifact + """ + return None + def create(self, context): """Create new artifact in Glare repo. @@ -253,6 +263,7 @@ Possible values: """ values = self.obj_changes_to_primitive() values['type_name'] = self.get_type_name() + values['display_type_name'] = self.get_display_type_name() LOG.debug("Sending request to create artifact of type '%(type_name)s'." " New values are %(values)s", diff --git a/glare/objects/heat_environment.py b/glare/objects/heat_environment.py index 4d0bd9c..cb49b27 100644 --- a/glare/objects/heat_environment.py +++ b/glare/objects/heat_environment.py @@ -29,3 +29,7 @@ class HeatEnvironment(base.BaseArtifact): @classmethod def get_type_name(cls): return "heat_environments" + + @classmethod + def get_display_type_name(cls): + return "Heat Environments" diff --git a/glare/objects/heat_template.py b/glare/objects/heat_template.py index df32dac..21918f7 100644 --- a/glare/objects/heat_template.py +++ b/glare/objects/heat_template.py @@ -47,3 +47,7 @@ class HeatTemplate(base.BaseArtifact): @classmethod def get_type_name(cls): return "heat_templates" + + @classmethod + def get_display_type_name(cls): + return "Heat Templates" diff --git a/glare/objects/image.py b/glare/objects/image.py index c7728cd..f49cfc4 100644 --- a/glare/objects/image.py +++ b/glare/objects/image.py @@ -88,3 +88,7 @@ class Image(base.BaseArtifact): @classmethod def get_type_name(cls): return "images" + + @classmethod + def get_display_type_name(cls): + return "Images" diff --git a/glare/objects/murano_package.py b/glare/objects/murano_package.py index fb6c718..496b708 100644 --- a/glare/objects/murano_package.py +++ b/glare/objects/murano_package.py @@ -58,3 +58,7 @@ class MuranoPackage(base.BaseArtifact): @classmethod def get_type_name(cls): return "murano_packages" + + @classmethod + def get_display_type_name(cls): + return "Murano packages" diff --git a/glare/objects/secret.py b/glare/objects/secret.py index 921c763..132f0c5 100644 --- a/glare/objects/secret.py +++ b/glare/objects/secret.py @@ -34,6 +34,10 @@ class Secret(base_artifact.BaseArtifact): def get_type_name(cls): return "secrets" + @classmethod + def get_display_type_name(cls): + return "Secrets" + fields = { 'payload': Blob( # The encrypted secret data description="The secret's data to be stored" diff --git a/glare/objects/tosca_template.py b/glare/objects/tosca_template.py index 8b28570..5adf692 100644 --- a/glare/objects/tosca_template.py +++ b/glare/objects/tosca_template.py @@ -33,3 +33,7 @@ class TOSCATemplate(base.BaseArtifact): @classmethod def get_type_name(cls): return "tosca_templates" + + @classmethod + def get_display_type_name(cls): + return "TOSCA Templates" diff --git a/glare/tests/functional/test_all.py b/glare/tests/functional/test_all.py index 01491bc..e437417 100644 --- a/glare/tests/functional/test_all.py +++ b/glare/tests/functional/test_all.py @@ -61,6 +61,30 @@ class TestAll(base.TestArtifact): self.assertEqual(54, len(res)) self.assertEqual(sorted(res, key=lambda x: x['type_name']), res) + # get all artifacts Sorted in Asc order based on display_type_name + url = '/all?sort=display_type_name:asc&limit=100' + res = self.get(url=url, status=200)['artifacts'] + self.assertEqual(54, len(res)) + self.assertEqual(sorted(res, key=lambda x: x['display_type_name']), + res) + + # get all artifacts sorted in desc order based on display_type_name + url = '/all?sort=display_type_name:desc&limit=100' + res = self.get(url=url, status=200)['artifacts'] + self.assertEqual(54, len(res)) + self.assertEqual(sorted(res, key=lambda x: x['display_type_name'], + reverse=True), res) + + # get Heat Template like only + url = '/all?display_type_name=like:Heat%&sort=display_type_name:asc' + res = self.get(url=url, status=200)['artifacts'] + self.assertEqual(18, len(res)) + for art in res: + self.assertEqual('Heat', art['display_type_name'][:4]) + + # TODO(kushalagrawal): Need to Add test case for display_type_name with + # null once https://bugs.launchpad.net/glare/+bug/1741400 is resolved + def test_all_readonlyness(self): self.create_artifact(data={'name': 'all'}, type_name='all', status=403) art = self.create_artifact(data={'name': 'image'}, type_name='images') diff --git a/glare/tests/functional/test_database_store.py b/glare/tests/functional/test_database_store.py index c5bf089..5b9da19 100644 --- a/glare/tests/functional/test_database_store.py +++ b/glare/tests/functional/test_database_store.py @@ -45,7 +45,8 @@ default_store = database 'artifacts': [], 'schema': '/schemas/sample_artifact', 'type_name': 'sample_artifact', - 'total_count': 0} + 'total_count': 0, + 'display_type_name': 'Sample Artifact'} self.assertEqual(expected, response) # Create a test artifact diff --git a/glare/tests/functional/test_sample_artifact.py b/glare/tests/functional/test_sample_artifact.py index 06ad15b..391db2f 100644 --- a/glare/tests/functional/test_sample_artifact.py +++ b/glare/tests/functional/test_sample_artifact.py @@ -653,6 +653,12 @@ class TestList(base.TestArtifact): self.assertEqual(art1, result['artifacts'][0]) self.assertEqual(response_url, result['first']) + def test_list_response_attributes(self): + url = '/sample_artifact' + res = self.get(url=url, status=200) + self.assertEqual(res['total_count'], 0) + self.assertEqual(res['display_type_name'], "Sample Artifact") + class TestBlobs(base.TestArtifact): def test_blob_dicts(self): @@ -663,7 +669,8 @@ class TestBlobs(base.TestArtifact): 'artifacts': [], 'schema': '/schemas/sample_artifact', 'type_name': 'sample_artifact', - 'total_count': 0} + 'total_count': 0, + 'display_type_name': 'Sample Artifact'} self.assertEqual(expected, response) # Create a test artifact @@ -1225,6 +1232,10 @@ class TestArtifactOps(base.TestArtifact): self.create_artifact(data={"name": "test_af", "string_required": "test_str"}) + # Check we cannot create data with display_type_name. + self.create_artifact(data={"display_type_name": "Sample Artifact", + "name": "Invalid_data"}, status=400) + def test_activate(self): # create artifact to update private_art = self.create_artifact( diff --git a/glare/tests/functional/test_schemas.py b/glare/tests/functional/test_schemas.py index 3eea734..3d2f2a3 100644 --- a/glare/tests/functional/test_schemas.py +++ b/glare/tests/functional/test_schemas.py @@ -932,6 +932,11 @@ fixtures = { u'maxLength': 255, u'sortable': True, u'type': [u'string', u'null']}, + u'display_type_name': { + u'description': u'Display name of artifact type.', + u'filter_ops': [u'like', u'eq', u'neq', u'in'], + u'glareType': u'String', u'maxLength': 255, + u'sortable': True, u'type': [u'string', u'null']} }), u'required': [u'name'], diff --git a/glare/tests/hooks_artifact.py b/glare/tests/hooks_artifact.py index a0c11f2..ca44c8e 100644 --- a/glare/tests/hooks_artifact.py +++ b/glare/tests/hooks_artifact.py @@ -74,6 +74,10 @@ class HookChecker(base.BaseArtifact): def get_type_name(cls): return "hooks_artifact" + @classmethod + def get_display_type_name(cls): + return "Hooks Artifact" + @classmethod def pre_create_hook(cls, context, af): # create a temporary file and set the path to artifact field diff --git a/glare/tests/sample_artifact.py b/glare/tests/sample_artifact.py index 28bdb15..3ef731b 100644 --- a/glare/tests/sample_artifact.py +++ b/glare/tests/sample_artifact.py @@ -139,6 +139,10 @@ class SampleArtifact(base_artifact.BaseArtifact): def get_type_name(cls): return "sample_artifact" + @classmethod + def get_display_type_name(cls): + return "Sample Artifact" + def to_dict(self): res = self.obj_to_primitive()['versioned_object.data'] res['__some_meta_information__'] = res['name'].upper() diff --git a/glare/tests/unit/db/migrations/test_migrations.py b/glare/tests/unit/db/migrations/test_migrations.py index 977910c..dfe0b9a 100644 --- a/glare/tests/unit/db/migrations/test_migrations.py +++ b/glare/tests/unit/db/migrations/test_migrations.py @@ -247,6 +247,33 @@ class GlareMigrationsCheckers(object): self.assert_table(engine, 'glare_quotas', quota_indices, quota_columns) + def _check_005(self, engine, data): + artifacts_indices = [('ix_glare_artifact_name_and_version', + ['name', 'version_prefix', 'version_suffix']), + ('ix_glare_artifact_type', + ['type_name']), + ('ix_glare_artifact_status', ['status']), + ('ix_glare_artifact_visibility', ['visibility']), + ('ix_glare_artifact_owner', ['owner']), + ('ix_glare_artifact_display_name', + ['display_type_name'])] + artifacts_columns = ['id', + 'name', + 'type_name', + 'version_prefix', + 'version_suffix', + 'version_meta', + 'description', + 'visibility', + 'status', + 'owner', + 'created_at', + 'updated_at', + 'activated_at', + 'display_type_name'] + self.assert_table(engine, 'glare_artifacts', artifacts_indices, + artifacts_columns) + class TestMigrationsMySQL(GlareMigrationsCheckers, WalkVersionsMixin, diff --git a/glare/tests/unpacking_artifact.py b/glare/tests/unpacking_artifact.py index 44a5df6..c9c0051 100644 --- a/glare/tests/unpacking_artifact.py +++ b/glare/tests/unpacking_artifact.py @@ -37,6 +37,10 @@ class Unpacker(base.BaseArtifact): def get_type_name(cls): return "unpacking_artifact" + @classmethod + def get_display_type_name(cls): + return "Unpacking Artifact" + @classmethod def pre_upload_hook(cls, context, af, field_name, blob_key, fd): flobj = io.BytesIO(fd.read(cls.MAX_BLOB_SIZE))