From 060a4472bd3ba9de877e814b47f1b0bcb1da394d Mon Sep 17 00:00:00 2001 From: LanternNassi <71936382+LanternNassi@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:06:36 +0300 Subject: [PATCH] Add tags (#511) * Add tags * Fixes --- api_docs.yml | 112 +++++++++++++++++++++++++++ app/controllers/__init__.py | 1 + app/controllers/project_tags.py | 88 +++++++++++++++++++++ app/models/project.py | 1 + app/models/project_tag.py | 19 +++++ app/routes/__init__.py | 4 +- app/schemas/project_tags.py | 15 ++++ migrations/versions/2f06b8e0c98b_.py | 35 +++++++++ 8 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 app/controllers/project_tags.py create mode 100644 app/models/project_tag.py create mode 100644 app/schemas/project_tags.py create mode 100644 migrations/versions/2f06b8e0c98b_.py diff --git a/api_docs.yml b/api_docs.yml index 4f41cf20..9d7a4174 100644 --- a/api_docs.yml +++ b/api_docs.yml @@ -3641,6 +3641,118 @@ paths: 500: description: "Internal Server Error" + "/projects/tags/{tag_id}": + get: + tags: + - projects + consumes: + - application/json + produces: + - application/json + parameters: + - in: header + name: Authorization + required: true + description: "Bearer [token]" + - in: path + name: tag_id + required: true + type: string + responses: + 200: + description: "Success" + 404: + description: "Tag not found" + 400: + description: "Bad request" + 500: + description: "Internal Server Error" + + delete: + tags: + - projects + consumes: + - application/json + produces: + - application/json + parameters: + - in: header + name: Authorization + required: true + description: "Bearer [token]" + - in: path + name: tag_id + required: true + type: string + + responses: + 200: + description: "Success" + 404: + description: "User not found" + 400: + description: "Bad request" + 500: + description: "Internal Server Error" + + + "/projects/tags": + post: + tags: + - projects + consumes: + - application/json + parameters: + - in: header + name: Authorization + required: true + description: "Bearer [token]" + type: string + - in: body + name: tag + schema: + type: array + required: + - name + items: + type: string + + produces: + - application/json + responses: + 201: + description: "Success" + 400: + description: "Bad request" + 500: + description: "Internal Server Error" + + get: + tags: + - projects + consumes: + - application/json + produces: + - application/json + parameters: + - in: header + name: Authorization + required: true + description: "Bearer [token]" + type: string + - in: query + name: keywords + type: string + description: Enter keywords if searching + responses: + 200: + description: "Success" + 400: + description: "Bad request" + 500: + description: "Internal Server Error" + + components: securitySchemes: bearerAuth: diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py index 12bf5065..e72016f3 100644 --- a/app/controllers/__init__.py +++ b/app/controllers/__init__.py @@ -30,3 +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 diff --git a/app/controllers/project_tags.py b/app/controllers/project_tags.py new file mode 100644 index 00000000..23fa4bf0 --- /dev/null +++ b/app/controllers/project_tags.py @@ -0,0 +1,88 @@ + +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/models/project.py b/app/models/project.py index b2089390..768faf6d 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -38,3 +38,4 @@ class Project(ModelMixin): def is_followed_by(self, user): return any(follower.user_id == user.id for follower in self.followers) + diff --git a/app/models/project_tag.py b/app/models/project_tag.py new file mode 100644 index 00000000..c2ee89b0 --- /dev/null +++ b/app/models/project_tag.py @@ -0,0 +1,19 @@ +from sqlalchemy.dialects.postgresql import UUID +from app.models import db +from app.models.model_mixin import ModelMixin, SoftDeleteQuery +from sqlalchemy import text as sa_text + + + +class ProjectTag(ModelMixin): + __tablename__ = "project_tags" + query_class = SoftDeleteQuery + + id = db.Column(UUID(as_uuid=True), primary_key=True, + 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 0e7b925e..f1ae3502 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -14,6 +14,7 @@ UserAdminUpdateView, AppRevertView, ProjectGetCostsView, TransactionRecordView, CreditTransactionRecordView, CreditPurchaseTransactionRecordView, BillingInvoiceView, BillingInvoiceNotificationView, SystemSummaryView, CreditDetailView, ProjectUsersView, ProjectUsersTransferView, AppReviseView, ProjectUsersHandleInviteView, ClusterProjectsView, ProjectDisableView, ProjectEnableView, AppRedeployView, AppDisableView, AppEnableView, + ProjectTagsView,ProjectTagsDetailView, UserDisableView, UserEnableView, AppDockerWebhookListenerView, UserFollowersView, UserFollowView, ProjectFollowingView, ActivityFeedView) from app.controllers.app import AppRevisionsView from app.controllers.billing_invoice import BillingInvoiceDetailView @@ -149,7 +150,8 @@ 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/') # App routes api.add_resource(AppsView, '/apps') api.add_resource(AppDetailView, '/apps/') diff --git a/app/schemas/project_tags.py b/app/schemas/project_tags.py new file mode 100644 index 00000000..54448ac6 --- /dev/null +++ b/app/schemas/project_tags.py @@ -0,0 +1,15 @@ +from marshmallow import Schema, fields, validate, pre_load + + +class ProjectTagSchema(Schema): + + id = fields.UUID(dump_only=True) + name = fields.String(required=True, error_message={ + "required": "name is required"}, + validate=[ + validate.Regexp( + regex=r'^(?!\s*$)', error='name should be a valid string' + ), + ]) + date_created = fields.Date(dump_only=True) + diff --git a/migrations/versions/2f06b8e0c98b_.py b/migrations/versions/2f06b8e0c98b_.py new file mode 100644 index 00000000..481fdefd --- /dev/null +++ b/migrations/versions/2f06b8e0c98b_.py @@ -0,0 +1,35 @@ +"""empty message + +Revision ID: 2f06b8e0c98b +Revises: 5f6dfc8f1b35 +Create Date: 2024-06-27 20:02:34.415744 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '2f06b8e0c98b' +down_revision = '5f6dfc8f1b35' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('project_tags', + 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') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('project_tags') + # ### end Alembic commands ###