diff --git a/api_docs.yml b/api_docs.yml index 9d7a4174..81fcc33d 100644 --- a/api_docs.yml +++ b/api_docs.yml @@ -3641,10 +3641,10 @@ paths: 500: description: "Internal Server Error" - "/projects/tags/{tag_id}": + "/tags/{tag_id}": get: tags: - - projects + - tags consumes: - application/json produces: @@ -3670,7 +3670,7 @@ paths: delete: tags: - - projects + - tags consumes: - application/json produces: @@ -3696,10 +3696,10 @@ paths: description: "Internal Server Error" - "/projects/tags": + "/tags": post: tags: - - projects + - tags consumes: - application/json parameters: @@ -3729,7 +3729,7 @@ paths: get: tags: - - projects + - tags consumes: - application/json produces: diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py index e72016f3..ad99eb41 100644 --- a/app/controllers/__init__.py +++ b/app/controllers/__init__.py @@ -20,7 +20,7 @@ from .user_role import UserRolesView from .transactions import TransactionRecordView, TransactionRecordDetailView, CreditTransactionRecordView, CreditPurchaseTransactionRecordView from .project import ( - ProjectsView, ProjectDetailView, UserProjectsView, ProjectGetCostsView, ClusterProjectsView,ProjectPinView, + ProjectsView, ProjectDetailView, UserProjectsView, ProjectGetCostsView, ClusterProjectsView, ProjectPinView, ProjectDisableView, ProjectEnableView) from .app import (AppsView, ProjectAppsView, AppDetailView, AppLogsView, AppRevertView, AppReviseView, AppRedeployView, AppDisableView, AppEnableView, AppDockerWebhookListenerView) @@ -30,4 +30,4 @@ from .system_status import SystemSummaryView from .project_users import ProjectUsersView, ProjectUsersTransferView, ProjectUsersHandleInviteView, ProjectFollowingView from .activity_feed import ActivityFeedView -from .project_tags import ProjectTagsView, ProjectTagsDetailView +from .tags import TagsView, TagsDetailView diff --git a/app/controllers/project_tags.py b/app/controllers/project_tags.py deleted file mode 100644 index 23fa4bf0..00000000 --- a/app/controllers/project_tags.py +++ /dev/null @@ -1,88 +0,0 @@ - -from flask_restful import Resource, request -from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt_claims -from app.schemas.project_tags import ProjectTagSchema -from app.models.project_tag import ProjectTag -from app.helpers.decorators import admin_required - - - - -class ProjectTagsView(Resource): - - @jwt_required - def post(self): - tags_data = request.get_json() - - project_tag_schema = ProjectTagSchema() - - for _tag in tags_data: - validated_tag_data , errors = project_tag_schema.load({'name' : _tag}) - if not ProjectTag.find_first(name=validated_tag_data['name']): - tag = ProjectTag(**validated_tag_data) - tag.save() - - return dict( - status='success', - message='Tags saved successfully' - ) , 201 - - def get(self): - keywords = request.args.get('keywords', None) - - project_tag_schema = ProjectTagSchema(many=True) - - project_tags = ProjectTag.find_all() - - if keywords : - project_tags = ProjectTag.query.filter(ProjectTag.name.ilike(f'%{keywords}%')) - - tags_data = project_tag_schema.dump(project_tags) - - return dict( - status="success", - data=tags_data.data - ) , 200 - -class ProjectTagsDetailView(Resource): - def get(self, tag_id): - - project_tag_id_schema = ProjectTagSchema() - - project_tag = ProjectTag.get_by_id(tag_id) - - tags_data = project_tag_id_schema.dump(project_tag) - - return dict( - status="success", - data=tags_data.data - ) , 200 - - @admin_required - def delete(self, tag_id): - - current_user_roles = get_jwt_claims()['roles'] - - tag = ProjectTag.get_by_id(tag_id) - - deleted = tag.soft_delete() - - if not deleted: - return dict( - status='fail', - message='An error occured during deletion' - ) , 500 - - return dict( - status='success', - message=f"Tag {tag_id} successfully deleted" - ) , 200 - - - - - - - - - diff --git a/app/controllers/tags.py b/app/controllers/tags.py new file mode 100644 index 00000000..b841749c --- /dev/null +++ b/app/controllers/tags.py @@ -0,0 +1,92 @@ + +from flask_restful import Resource, request +from flask_jwt_extended import jwt_required +from app.schemas.tags import TagSchema +from app.models.tags import Tag +from app.helpers.decorators import admin_required + + +class TagsView(Resource): + + @jwt_required + def post(self): + tags_data = request.get_json() + tag_schema = TagSchema() + none_existing_tags = [] + + for tag in tags_data: + validated_tag_data, errors = tag_schema.load({'name': tag}) + if errors: + return dict(status="fail", message=errors), 400 + if not Tag.find_first(name=validated_tag_data['name']): + none_existing_tags.append(Tag(**validated_tag_data)) + + if none_existing_tags: + if Tag.bulk_save(none_existing_tags): + return dict( + status='success', + message='Tags saved successfully' + ), 201 + else: + return dict( + status='fail', + message='An error occurred while saving tags' + ), 500 + else: + return dict( + status='success', + message='No new tags to save' + ), 201 + + @jwt_required + def get(self): + keywords = request.args.get('keywords', None) + + tag_schema = TagSchema(many=True) + + tags = Tag.find_all() + print(tags) + if keywords: + tags = Tag.query.filter( + Tag.name.ilike(f'%{keywords}%')) + + tags_data = tag_schema.dump(tags) + + return dict( + status="success", + data=tags_data.data + ), 200 + + +class TagsDetailView(Resource): + + @jwt_required + def get(self, tag_id): + tag_id_schema = TagSchema() + + tag = Tag.get_by_id(tag_id) + + tags_data = tag_id_schema.dump(tag) + + return dict( + status="success", + data=tags_data.data + ), 200 + + @admin_required + def delete(self, tag_id): + + tag = Tag.get_by_id(tag_id) + + deleted = tag.soft_delete() + + if not deleted: + return dict( + status='fail', + message='An error occured during deletion' + ), 500 + + return dict( + status='success', + message=f"Tag {tag_id} successfully deleted" + ), 200 diff --git a/app/models/model_mixin.py b/app/models/model_mixin.py index e9fbd6b7..dd59ebcf 100644 --- a/app/models/model_mixin.py +++ b/app/models/model_mixin.py @@ -63,13 +63,13 @@ def soft_delete(self): db.session.rollback() return False - def soft_delete(self): + @classmethod + def bulk_save(cls, objects): try: - setattr(self, 'deleted', True) - setattr(self, 'name', f"{self.name}_deleted_{int(time.time())}") + db.session.bulk_save_objects(objects) db.session.commit() return True - except SQLAlchemyError as e: + except SQLAlchemyError: db.session.rollback() return False diff --git a/app/models/project_tag.py b/app/models/tags.py similarity index 76% rename from app/models/project_tag.py rename to app/models/tags.py index c2ee89b0..75b0b2a8 100644 --- a/app/models/project_tag.py +++ b/app/models/tags.py @@ -4,16 +4,12 @@ from sqlalchemy import text as sa_text - -class ProjectTag(ModelMixin): - __tablename__ = "project_tags" +class Tag(ModelMixin): + __tablename__ = "tag" query_class = SoftDeleteQuery id = db.Column(UUID(as_uuid=True), primary_key=True, - server_default=sa_text("uuid_generate_v4()")) + server_default=sa_text("uuid_generate_v4()")) name = db.Column(db.String) deleted = db.Column(db.Boolean, default=False) date_created = db.Column(db.DateTime, default=db.func.current_timestamp()) - - - diff --git a/app/routes/__init__.py b/app/routes/__init__.py index f1ae3502..028f6723 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -14,7 +14,7 @@ UserAdminUpdateView, AppRevertView, ProjectGetCostsView, TransactionRecordView, CreditTransactionRecordView, CreditPurchaseTransactionRecordView, BillingInvoiceView, BillingInvoiceNotificationView, SystemSummaryView, CreditDetailView, ProjectUsersView, ProjectUsersTransferView, AppReviseView, ProjectUsersHandleInviteView, ClusterProjectsView, ProjectDisableView, ProjectEnableView, AppRedeployView, AppDisableView, AppEnableView, - ProjectTagsView,ProjectTagsDetailView, + TagsView, TagsDetailView, UserDisableView, UserEnableView, AppDockerWebhookListenerView, UserFollowersView, UserFollowView, ProjectFollowingView, ActivityFeedView) from app.controllers.app import AppRevisionsView from app.controllers.billing_invoice import BillingInvoiceDetailView @@ -150,8 +150,9 @@ api.add_resource(ProjectPinView, '/projects//pin') # User Project routes api.add_resource(UserProjectsView, '/users//projects') -api.add_resource(ProjectTagsView, '/projects/tags') -api.add_resource(ProjectTagsDetailView, '/projects/tags/') +# Tags Routes +api.add_resource(TagsView, '/tags') +api.add_resource(TagsDetailView, '/tags/') # App routes api.add_resource(AppsView, '/apps') api.add_resource(AppDetailView, '/apps/') diff --git a/app/schemas/project_tags.py b/app/schemas/tags.py similarity index 89% rename from app/schemas/project_tags.py rename to app/schemas/tags.py index 54448ac6..63b530a1 100644 --- a/app/schemas/project_tags.py +++ b/app/schemas/tags.py @@ -1,7 +1,7 @@ from marshmallow import Schema, fields, validate, pre_load -class ProjectTagSchema(Schema): +class TagSchema(Schema): id = fields.UUID(dump_only=True) name = fields.String(required=True, error_message={ @@ -10,6 +10,5 @@ class ProjectTagSchema(Schema): validate.Regexp( regex=r'^(?!\s*$)', error='name should be a valid string' ), - ]) + ]) date_created = fields.Date(dump_only=True) - diff --git a/docker-compose.yml b/docker-compose.yml index b793a924..227e6a42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,8 +69,8 @@ services: - action: sync path: . target: /app - # volumes: - # - .:/app + volumes: + - .:/app depends_on: - database - crane-mongo-db diff --git a/migrations/versions/c7f9222b60b8_.py b/migrations/versions/c7f9222b60b8_.py new file mode 100644 index 00000000..31863d81 --- /dev/null +++ b/migrations/versions/c7f9222b60b8_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: c7f9222b60b8 +Revises: 2f06b8e0c98b +Create Date: 2024-07-04 08:52:22.750942 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'c7f9222b60b8' +down_revision = '2f06b8e0c98b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('tag', + sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('date_created', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.drop_table('project_tags') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('project_tags', + sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('deleted', sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.Column('date_created', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name='project_tags_pkey') + ) + op.drop_table('tag') + # ### end Alembic commands ###