From fa66a9203c3217aa7fc96ace11355cdee52f9130 Mon Sep 17 00:00:00 2001 From: Mubangizi Allan Date: Fri, 28 Jun 2024 11:11:58 +0300 Subject: [PATCH] feat: add activity feed (#509) * setup app logger connection * add user activity endpoint * Add activity feed filters * add app and project data to response * downgrade pip to 24.1 * remove comments --- .github/workflows/test.yml | 2 +- api_docs.yml | 37 +++++++++++++++++- app/controllers/__init__.py | 1 + app/controllers/activity_feed.py | 67 ++++++++++++++++++++++++++++++++ app/helpers/activity_logger.py | 2 +- app/helpers/kube.py | 3 +- app/routes/__init__.py | 7 ++-- docker-compose.yml | 11 +++++- 8 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 app/controllers/activity_feed.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 883c12a7..4188f136 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade "pip<24.1" pip install pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi diff --git a/api_docs.yml b/api_docs.yml index 551849ee..481ad92a 100644 --- a/api_docs.yml +++ b/api_docs.yml @@ -720,6 +720,41 @@ paths: 500: description: "Internal Server Error" + "/activity_feed": + get: + tags: + - activity_feed + consumes: + - application/json + produces: + - application/json + parameters: + - in: header + name: Authorization + required: true + description: "Bearer [token]" + - in: query + name: user_id + type: string + description: user id + - in: query + name: page + type: integer + description: Page number + - in: query + name: per_page + type: integer + description: Number of items per page + responses: + 200: + description: "Success" + 404: + description: "User not found" + 400: + description: "Bad request" + 500: + description: "Internal Server Error" + "/clusters": post: tags: @@ -2756,7 +2791,7 @@ paths: description: "Bad request" 500: description: "Internal Server Error" - + delete: tags: - projects diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py index f32d5a19..12bf5065 100644 --- a/app/controllers/__init__.py +++ b/app/controllers/__init__.py @@ -29,3 +29,4 @@ BillingInvoiceView, BillingInvoiceNotificationView) from .system_status import SystemSummaryView from .project_users import ProjectUsersView, ProjectUsersTransferView, ProjectUsersHandleInviteView, ProjectFollowingView +from .activity_feed import ActivityFeedView diff --git a/app/controllers/activity_feed.py b/app/controllers/activity_feed.py new file mode 100644 index 00000000..78ecb6df --- /dev/null +++ b/app/controllers/activity_feed.py @@ -0,0 +1,67 @@ +from app.models.app import App +from app.schemas.app import AppSchema +from app.schemas.project import ProjectSchema +from flask import current_app +from flask_restful import Resource, request +from app.models.user import User +from app.models.project import Project +import requests +from flask_jwt_extended import jwt_required, get_jwt_identity + + +class ActivityFeedView(Resource): + @jwt_required + def get(self): + current_user_id = get_jwt_identity() + current_user = User.get_by_id(current_user_id) + project_schema = ProjectSchema() + app_schema = AppSchema() + + params = { + 'general': True, + 'operations': ['Create', 'Update', 'Delete'], + 'statuses': ['Success'], + 'models': ['Project', 'App', 'Database', ] + } + user_id = request.args.get('user_id', None) + if user_id: + user = User.get_by_id(user_id) + if not user: + return dict(status='fail', message='User not found'), 404 + params['user_id'] = user_id + + following = current_user.followed + if following: + params['user_ids'] = [user.id for user in following] + + LOGGER_APP_URL = current_app.config.get('LOGGER_APP_URL') + + user_feed = requests.get( + f"{LOGGER_APP_URL}/api/activities", + params=params, + headers={'Authorization': request.headers.get('Authorization')} + ) + + if user_feed.status_code != 200: + return dict(status='fail', message='Failed to fetch user feed'), 500 + + # get project or app details in each item in the feed and return them + user_feed = user_feed.json() + user_activities = user_feed.get('data').get('activity') + if not user_activities: + return dict(user_feed=user_feed), 200 + + for item in user_activities: + if item['model'] == 'Project': + project = Project.get_by_id(item['a_project_id']) + project_data, _ = project_schema.dump(project) + item['project'] = project_schema.dump(project_data)[0] + elif item['model'] == 'App': + app = App.get_by_id(item['a_app_id']) + app_data, _ = app_schema.dump(app) + item['app'] = app_schema.dump(app_data)[0] + elif item['model'] == 'Database': + pass + user_feed['data']['activity'] = user_activities + + return dict(user_feed=user_feed), 200 diff --git a/app/helpers/activity_logger.py b/app/helpers/activity_logger.py index f5c43eb1..860f8d7f 100644 --- a/app/helpers/activity_logger.py +++ b/app/helpers/activity_logger.py @@ -30,7 +30,7 @@ def log_activity(model: str, status: str, operation: str, description: str, a_us 'operation': operation, 'model': model, 'status': status, - 'description': description, + 'description': str(description), 'a_user_id': str(a_user_id) if a_user_id else None, 'a_db_id': str(a_db_id) if a_db_id else None, 'a_app_id': str(a_app_id) if a_app_id else None, diff --git a/app/helpers/kube.py b/app/helpers/kube.py index 7918a8a6..79f032e9 100644 --- a/app/helpers/kube.py +++ b/app/helpers/kube.py @@ -748,7 +748,8 @@ def enable_project(project: Project): if e.status != 404: log_activity('Project', status='Failed', operation='Enable', - description=f'Error enabling the project. {e.body}', + description=f'''Error enabling the project. { + e.body}''', a_project_id=project.id, a_cluster_id=project.cluster_id) return SimpleNamespace( diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 88d8e74c..0e7b925e 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -1,6 +1,6 @@ from flask_restful import Api from app.controllers import ( - IndexView, UsersView, UserLoginView, OAuthView, DeploymentsView, RolesView, InActiveUsersView,ProjectPinView, + IndexView, UsersView, UserLoginView, OAuthView, DeploymentsView, RolesView, InActiveUsersView, ProjectPinView, RolesDetailView, CreditAssignmentView, CreditAssignmentDetailView, CreditView, UserRolesView, UserDataSummaryView, ClustersView, ClusterDetailView, ClusterNamespacesView, ClusterNamespaceDetailView, ClusterNodesView, ClusterNodeDetailView, @@ -14,7 +14,7 @@ UserAdminUpdateView, AppRevertView, ProjectGetCostsView, TransactionRecordView, CreditTransactionRecordView, CreditPurchaseTransactionRecordView, BillingInvoiceView, BillingInvoiceNotificationView, SystemSummaryView, CreditDetailView, ProjectUsersView, ProjectUsersTransferView, AppReviseView, ProjectUsersHandleInviteView, ClusterProjectsView, ProjectDisableView, ProjectEnableView, AppRedeployView, AppDisableView, AppEnableView, - UserDisableView, UserEnableView, AppDockerWebhookListenerView, UserFollowersView, UserFollowView, ProjectFollowingView) + UserDisableView, UserEnableView, AppDockerWebhookListenerView, UserFollowersView, UserFollowView, ProjectFollowingView, ActivityFeedView) from app.controllers.app import AppRevisionsView from app.controllers.billing_invoice import BillingInvoiceDetailView from app.controllers.receipts import BillingReceiptsDetailView, BillingReceiptsView @@ -46,7 +46,8 @@ api.add_resource(UserFollowView, '/users//following') api.add_resource(UserFollowersView, '/users//followers') - +# Activity Feed +api.add_resource(ActivityFeedView, '/activity_feed') # Deployments api.add_resource(DeploymentsView, '/deployments', endpoint='deployments') diff --git a/docker-compose.yml b/docker-compose.yml index 56c5c72f..b793a924 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,6 +40,9 @@ services: dockerfile: Dockerfile image: ckwagaba/osprey-backend:latest container_name: flask-api + networks: + - cranecloud + - default environment: KUBE_HOST: KUBE_TOKEN: @@ -58,6 +61,7 @@ services: DATABASE_USER: postgres DATABASE_URI: ${DATABASE_URI:-postgresql://postgres:postgres@database:5432/cranecloud} TEST_DATABASE_URI: ${TEST_DATABASE_URI:-postgresql://postgres:postgres@database:5432/cranecloud_test} + LOGGER_APP_URL: http://app-logger:8000 ports: - "${FLASK_PORT:-5000}:5000" develop: @@ -87,7 +91,7 @@ services: MONGO_URI: mongodb://crane:password@crane-mongo-db:27017/admin DATABASE_URI: postgresql://postgres:postgres@database:5432/cranecloud ports: - - "4500:5000" + - "${CELERY_PORT:-4500}:5000" volumes: - .:/app links: @@ -98,3 +102,8 @@ volumes: db-data: cranemongodbdata: craneredisdbdata: + +networks: + cranecloud: + external: true + name: cranecloud_default