From ab305436935552288fd8a8fdefb56f29c69bfa9a Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Fri, 25 Aug 2023 10:43:15 -0700 Subject: [PATCH 01/15] Changes to show all survey results to superusers --- analytics-api/src/analytics_api/resources/survey_result.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/analytics-api/src/analytics_api/resources/survey_result.py b/analytics-api/src/analytics_api/resources/survey_result.py index a07f57f38..7a3c6a9e2 100644 --- a/analytics-api/src/analytics_api/resources/survey_result.py +++ b/analytics-api/src/analytics_api/resources/survey_result.py @@ -22,6 +22,8 @@ from analytics_api.auth import jwt as _jwt from analytics_api.utils.roles import Role from analytics_api.services.survey_result import SurveyResultService +from analytics_api.utils.roles import Role +from analytics_api.utils.user_context import UserContext, user_context from analytics_api.utils.util import allowedorigins, cors_preflight @@ -40,6 +42,11 @@ class SurveyResultInternal(Resource): @_jwt.has_one_of_roles([Role.VIEW_ALL_SURVEY_RESULTS.value]) def get(engagement_id): """Fetch survey result for a single engagement id.""" + user_from_context: UserContext = kwargs['user_context'] + token_roles = set(user_from_context.roles) + can_view_all_survey_results = False + if dashboard_type == 'Internal': + can_view_all_survey_results = Role.VIEW_ALL_SURVEY_RESULTS.value in token_roles try: survey_result_record = SurveyResultService().get_survey_result(engagement_id, can_view_all_survey_results=True) From e1396c5f4095e0b0d2e0a0a861c4e9a1f7fe2b78 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Fri, 25 Aug 2023 17:31:24 -0700 Subject: [PATCH 02/15] removing hard coded values --- analytics-api/src/analytics_api/resources/survey_result.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analytics-api/src/analytics_api/resources/survey_result.py b/analytics-api/src/analytics_api/resources/survey_result.py index 7a3c6a9e2..2ba0bb5ae 100644 --- a/analytics-api/src/analytics_api/resources/survey_result.py +++ b/analytics-api/src/analytics_api/resources/survey_result.py @@ -24,7 +24,7 @@ from analytics_api.services.survey_result import SurveyResultService from analytics_api.utils.roles import Role from analytics_api.utils.user_context import UserContext, user_context -from analytics_api.utils.util import allowedorigins, cors_preflight +from analytics_api.utils.util import allowedorigins, cors_preflight, DashboardType API = Namespace('surveyresult', description='Endpoints for Survey result Management') @@ -45,7 +45,7 @@ def get(engagement_id): user_from_context: UserContext = kwargs['user_context'] token_roles = set(user_from_context.roles) can_view_all_survey_results = False - if dashboard_type == 'Internal': + if dashboard_type == DashboardType.INTERNAL.value: can_view_all_survey_results = Role.VIEW_ALL_SURVEY_RESULTS.value in token_roles try: survey_result_record = SurveyResultService().get_survey_result(engagement_id, From 5240be00ad929fee6af44681c772c2483befbb25 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Fri, 25 Aug 2023 17:42:27 -0700 Subject: [PATCH 03/15] fixing linting --- analytics-api/src/analytics_api/resources/survey_result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analytics-api/src/analytics_api/resources/survey_result.py b/analytics-api/src/analytics_api/resources/survey_result.py index 2ba0bb5ae..ff379a2b8 100644 --- a/analytics-api/src/analytics_api/resources/survey_result.py +++ b/analytics-api/src/analytics_api/resources/survey_result.py @@ -24,7 +24,7 @@ from analytics_api.services.survey_result import SurveyResultService from analytics_api.utils.roles import Role from analytics_api.utils.user_context import UserContext, user_context -from analytics_api.utils.util import allowedorigins, cors_preflight, DashboardType +from analytics_api.utils.util import DashboardType, allowedorigins, cors_preflight API = Namespace('surveyresult', description='Endpoints for Survey result Management') From 21f86c4c2e7e55cde39850d0edd084e67790923b Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Mon, 28 Aug 2023 10:52:22 -0700 Subject: [PATCH 04/15] splitting to seperate end points --- .../src/analytics_api/resources/survey_result.py | 9 +-------- .../src/analytics_api/services/survey_result.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/analytics-api/src/analytics_api/resources/survey_result.py b/analytics-api/src/analytics_api/resources/survey_result.py index ff379a2b8..a07f57f38 100644 --- a/analytics-api/src/analytics_api/resources/survey_result.py +++ b/analytics-api/src/analytics_api/resources/survey_result.py @@ -22,9 +22,7 @@ from analytics_api.auth import jwt as _jwt from analytics_api.utils.roles import Role from analytics_api.services.survey_result import SurveyResultService -from analytics_api.utils.roles import Role -from analytics_api.utils.user_context import UserContext, user_context -from analytics_api.utils.util import DashboardType, allowedorigins, cors_preflight +from analytics_api.utils.util import allowedorigins, cors_preflight API = Namespace('surveyresult', description='Endpoints for Survey result Management') @@ -42,11 +40,6 @@ class SurveyResultInternal(Resource): @_jwt.has_one_of_roles([Role.VIEW_ALL_SURVEY_RESULTS.value]) def get(engagement_id): """Fetch survey result for a single engagement id.""" - user_from_context: UserContext = kwargs['user_context'] - token_roles = set(user_from_context.roles) - can_view_all_survey_results = False - if dashboard_type == DashboardType.INTERNAL.value: - can_view_all_survey_results = Role.VIEW_ALL_SURVEY_RESULTS.value in token_roles try: survey_result_record = SurveyResultService().get_survey_result(engagement_id, can_view_all_survey_results=True) diff --git a/analytics-api/src/analytics_api/services/survey_result.py b/analytics-api/src/analytics_api/services/survey_result.py index 9bebe4d2b..1c1096cee 100644 --- a/analytics-api/src/analytics_api/services/survey_result.py +++ b/analytics-api/src/analytics_api/services/survey_result.py @@ -1,6 +1,8 @@ """Service for survey result management.""" from analytics_api.models.request_type_option import RequestTypeOption as RequestTypeOptionModel from analytics_api.schemas.survey_result import SurveyResultSchema +from analytics_api.utils.roles import Role +from analytics_api.utils.user_context import UserContext, user_context class SurveyResultService: # pylint: disable=too-few-public-methods @@ -9,8 +11,19 @@ class SurveyResultService: # pylint: disable=too-few-public-methods otherdateformat = '%Y-%m-%d' @staticmethod - def get_survey_result(engagement_id, can_view_all_survey_results) -> SurveyResultSchema: + @user_context + def get_survey_result_internal(engagement_id, **kwargs) -> SurveyResultSchema: """Get Survey result by the engagement id.""" + user_from_context: UserContext = kwargs['user_context'] + token_roles = set(user_from_context.roles) + can_view_all_survey_results = Role.VIEW_ALL_SURVEY_RESULTS.value in token_roles survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results) survey_result_schema = SurveyResultSchema(many=True) return survey_result_schema.dump(survey_result) + + @staticmethod + def get_survey_result_public(engagement_id) -> SurveyResultSchema: + """Get Survey result by the engagement id.""" + survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, False) + survey_result_schema = SurveyResultSchema(many=True) + return survey_result_schema.dump(survey_result) From a9a3cabc6bdd108a3f13a18d9f30712c0e4e0caf Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Mon, 28 Aug 2023 13:08:21 -0700 Subject: [PATCH 05/15] fixing auth check --- .../src/analytics_api/services/survey_result.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/analytics-api/src/analytics_api/services/survey_result.py b/analytics-api/src/analytics_api/services/survey_result.py index 1c1096cee..071dfb100 100644 --- a/analytics-api/src/analytics_api/services/survey_result.py +++ b/analytics-api/src/analytics_api/services/survey_result.py @@ -1,8 +1,6 @@ """Service for survey result management.""" from analytics_api.models.request_type_option import RequestTypeOption as RequestTypeOptionModel from analytics_api.schemas.survey_result import SurveyResultSchema -from analytics_api.utils.roles import Role -from analytics_api.utils.user_context import UserContext, user_context class SurveyResultService: # pylint: disable=too-few-public-methods @@ -11,19 +9,15 @@ class SurveyResultService: # pylint: disable=too-few-public-methods otherdateformat = '%Y-%m-%d' @staticmethod - @user_context - def get_survey_result_internal(engagement_id, **kwargs) -> SurveyResultSchema: + def get_survey_result_internal(engagement_id) -> SurveyResultSchema: """Get Survey result by the engagement id.""" - user_from_context: UserContext = kwargs['user_context'] - token_roles = set(user_from_context.roles) - can_view_all_survey_results = Role.VIEW_ALL_SURVEY_RESULTS.value in token_roles - survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results) + survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results = True) survey_result_schema = SurveyResultSchema(many=True) return survey_result_schema.dump(survey_result) @staticmethod def get_survey_result_public(engagement_id) -> SurveyResultSchema: """Get Survey result by the engagement id.""" - survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, False) + survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results = False) survey_result_schema = SurveyResultSchema(many=True) return survey_result_schema.dump(survey_result) From 483818baa2503bfbf0fc0a7b764a36e9f59358e7 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Mon, 28 Aug 2023 13:15:36 -0700 Subject: [PATCH 06/15] fixing linting --- analytics-api/src/analytics_api/services/survey_result.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analytics-api/src/analytics_api/services/survey_result.py b/analytics-api/src/analytics_api/services/survey_result.py index 071dfb100..2fced8f4a 100644 --- a/analytics-api/src/analytics_api/services/survey_result.py +++ b/analytics-api/src/analytics_api/services/survey_result.py @@ -11,13 +11,13 @@ class SurveyResultService: # pylint: disable=too-few-public-methods @staticmethod def get_survey_result_internal(engagement_id) -> SurveyResultSchema: """Get Survey result by the engagement id.""" - survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results = True) + survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results=True) survey_result_schema = SurveyResultSchema(many=True) return survey_result_schema.dump(survey_result) @staticmethod def get_survey_result_public(engagement_id) -> SurveyResultSchema: """Get Survey result by the engagement id.""" - survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results = False) + survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results=False) survey_result_schema = SurveyResultSchema(many=True) return survey_result_schema.dump(survey_result) From e15e0b57e47c54d3f8791aa20e4f878fad436845 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Mon, 28 Aug 2023 13:45:29 -0700 Subject: [PATCH 07/15] merging method in service --- .../src/analytics_api/services/survey_result.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/analytics-api/src/analytics_api/services/survey_result.py b/analytics-api/src/analytics_api/services/survey_result.py index 2fced8f4a..9bebe4d2b 100644 --- a/analytics-api/src/analytics_api/services/survey_result.py +++ b/analytics-api/src/analytics_api/services/survey_result.py @@ -9,15 +9,8 @@ class SurveyResultService: # pylint: disable=too-few-public-methods otherdateformat = '%Y-%m-%d' @staticmethod - def get_survey_result_internal(engagement_id) -> SurveyResultSchema: + def get_survey_result(engagement_id, can_view_all_survey_results) -> SurveyResultSchema: """Get Survey result by the engagement id.""" - survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results=True) - survey_result_schema = SurveyResultSchema(many=True) - return survey_result_schema.dump(survey_result) - - @staticmethod - def get_survey_result_public(engagement_id) -> SurveyResultSchema: - """Get Survey result by the engagement id.""" - survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results=False) + survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results) survey_result_schema = SurveyResultSchema(many=True) return survey_result_schema.dump(survey_result) From 111ce2f0dfb9be90f202aad2b81c7509c1451af1 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Mon, 28 Aug 2023 15:10:49 -0700 Subject: [PATCH 08/15] Handle no data error for graphs --- .../components/publicDashboard/ErrorBox.tsx | 26 +++++++++++++------ .../SubmissionTrend/SubmissionTrend.tsx | 2 +- .../publicDashboard/SurveyBar/index.tsx | 1 + 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/met-web/src/components/publicDashboard/ErrorBox.tsx b/met-web/src/components/publicDashboard/ErrorBox.tsx index 606dd6fae..09bdfccc8 100644 --- a/met-web/src/components/publicDashboard/ErrorBox.tsx +++ b/met-web/src/components/publicDashboard/ErrorBox.tsx @@ -3,24 +3,27 @@ import { Grid, IconButton, SxProps, Theme } from '@mui/material'; import { MetBody, MetHeader4, MetPaper } from 'components/common'; import LoopIcon from '@mui/icons-material/Loop'; import { DASHBOARD } from './constants'; +import { When } from 'react-if'; interface ErrorBoxProps { height?: number | string; onClick: () => void; sx?: SxProps; + noData?: boolean; } export const ErrorBox = ({ sx, onClick = () => { /*empty*/ }, + noData, }: ErrorBoxProps) => { return ( - - Error - Click to reload - onClick()}> - - - + + + Error + Click to reload + onClick()}> + + + + + + + No Data Available + + ); diff --git a/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx b/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx index 663272c27..37f32ba00 100644 --- a/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx +++ b/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx @@ -133,7 +133,7 @@ const SubmissionTrend = ({ engagement, engagementIsLoading }: SubmissionTrendPro } if (isError) { - return ; + return ; } return ( diff --git a/met-web/src/components/publicDashboard/SurveyBar/index.tsx b/met-web/src/components/publicDashboard/SurveyBar/index.tsx index 9a9d7c707..61163ad72 100644 --- a/met-web/src/components/publicDashboard/SurveyBar/index.tsx +++ b/met-web/src/components/publicDashboard/SurveyBar/index.tsx @@ -82,6 +82,7 @@ export const SurveyBar = ({ readComments, engagement, engagementIsLoading, dashb onClick={() => { fetchData(); }} + noData={noDataError} /> ); } From 70b69e5c2a15cd777c365caa59f6cb6fe93fccc9 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Wed, 30 Aug 2023 07:58:28 -0700 Subject: [PATCH 09/15] adding new nodata component --- .../components/publicDashboard/ErrorBox.tsx | 26 ++++++------------- .../SubmissionTrend/SubmissionTrend.tsx | 2 +- .../publicDashboard/SurveyBar/index.tsx | 1 - 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/met-web/src/components/publicDashboard/ErrorBox.tsx b/met-web/src/components/publicDashboard/ErrorBox.tsx index 09bdfccc8..606dd6fae 100644 --- a/met-web/src/components/publicDashboard/ErrorBox.tsx +++ b/met-web/src/components/publicDashboard/ErrorBox.tsx @@ -3,27 +3,24 @@ import { Grid, IconButton, SxProps, Theme } from '@mui/material'; import { MetBody, MetHeader4, MetPaper } from 'components/common'; import LoopIcon from '@mui/icons-material/Loop'; import { DASHBOARD } from './constants'; -import { When } from 'react-if'; interface ErrorBoxProps { height?: number | string; onClick: () => void; sx?: SxProps; - noData?: boolean; } export const ErrorBox = ({ sx, onClick = () => { /*empty*/ }, - noData, }: ErrorBoxProps) => { return ( - - - Error - Click to reload - onClick()}> - - - - - - - No Data Available - - + + Error + Click to reload + onClick()}> + + + ); diff --git a/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx b/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx index 37f32ba00..663272c27 100644 --- a/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx +++ b/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx @@ -133,7 +133,7 @@ const SubmissionTrend = ({ engagement, engagementIsLoading }: SubmissionTrendPro } if (isError) { - return ; + return ; } return ( diff --git a/met-web/src/components/publicDashboard/SurveyBar/index.tsx b/met-web/src/components/publicDashboard/SurveyBar/index.tsx index 61163ad72..9a9d7c707 100644 --- a/met-web/src/components/publicDashboard/SurveyBar/index.tsx +++ b/met-web/src/components/publicDashboard/SurveyBar/index.tsx @@ -82,7 +82,6 @@ export const SurveyBar = ({ readComments, engagement, engagementIsLoading, dashb onClick={() => { fetchData(); }} - noData={noDataError} /> ); } From 052516d5b404b5637fb6a3e45e070c9956f4222c Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Fri, 1 Sep 2023 12:23:20 -0700 Subject: [PATCH 10/15] adding new email for submission response --- met-api/src/met_api/services/submission_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/met-api/src/met_api/services/submission_service.py b/met-api/src/met_api/services/submission_service.py index 826b0cb08..2d12513a6 100644 --- a/met-api/src/met_api/services/submission_service.py +++ b/met-api/src/met_api/services/submission_service.py @@ -26,6 +26,7 @@ from met_api.services import authorization from met_api.services.comment_service import CommentService from met_api.services.email_verification_service import EmailVerificationService +from met_api.services.engagement_service import EngagementService from met_api.services.staff_user_service import StaffUserService from met_api.services.survey_service import SurveyService from met_api.utils import notification From 279fc3cfcf26c4496ccfd266a4b9e8825c7c31e2 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Fri, 1 Sep 2023 18:11:58 -0700 Subject: [PATCH 11/15] fixing linting and testing --- met-api/src/met_api/services/submission_service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/met-api/src/met_api/services/submission_service.py b/met-api/src/met_api/services/submission_service.py index 2d12513a6..826b0cb08 100644 --- a/met-api/src/met_api/services/submission_service.py +++ b/met-api/src/met_api/services/submission_service.py @@ -26,7 +26,6 @@ from met_api.services import authorization from met_api.services.comment_service import CommentService from met_api.services.email_verification_service import EmailVerificationService -from met_api.services.engagement_service import EngagementService from met_api.services.staff_user_service import StaffUserService from met_api.services.survey_service import SurveyService from met_api.utils import notification From d8a83c81b056fe170cb028bec78ad9fba4990a18 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Thu, 14 Sep 2023 07:52:49 -0700 Subject: [PATCH 12/15] Upgrades to Issue Tracking Table --- met-api/src/met_api/services/comment_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/met-api/src/met_api/services/comment_service.py b/met-api/src/met_api/services/comment_service.py index 48239f0f6..a7301f442 100644 --- a/met-api/src/met_api/services/comment_service.py +++ b/met-api/src/met_api/services/comment_service.py @@ -1,6 +1,8 @@ """Service for comment management.""" +from http import HTTPStatus import itertools +from met_api.exceptions.business_exception import BusinessException from met_api.constants.comment_status import Status from met_api.constants.membership_type import MembershipType from met_api.constants.export_comments import RejectionReason From f8df5822557d4eb0700676dae353e63cc576e3cb Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Thu, 14 Sep 2023 13:07:30 -0700 Subject: [PATCH 13/15] removing try catch --- met-api/src/met_api/services/comment_service.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/met-api/src/met_api/services/comment_service.py b/met-api/src/met_api/services/comment_service.py index a7301f442..48239f0f6 100644 --- a/met-api/src/met_api/services/comment_service.py +++ b/met-api/src/met_api/services/comment_service.py @@ -1,8 +1,6 @@ """Service for comment management.""" -from http import HTTPStatus import itertools -from met_api.exceptions.business_exception import BusinessException from met_api.constants.comment_status import Status from met_api.constants.membership_type import MembershipType from met_api.constants.export_comments import RejectionReason From f5b4b709239d98f5b886c919a567815d0e47a014 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Wed, 20 Sep 2023 13:04:58 -0700 Subject: [PATCH 14/15] Updated dagster user code deployment name --- .github/workflows/met-etl-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/met-etl-cd.yml b/.github/workflows/met-etl-cd.yml index 6f44afb2e..f9107b950 100644 --- a/.github/workflows/met-etl-cd.yml +++ b/.github/workflows/met-etl-cd.yml @@ -26,7 +26,7 @@ defaults: env: APP_NAME: "dagster-etl" - DEPLOYMENT_NAME: "dagster-dagster-user-deployments-k8s-example-user-code-1" + DEPLOYMENT_NAME: "dagster-dagster-user-deployments-etl" TAG_NAME: "dev" PROJECT_TYPE: "${{ github.event.inputs.project_type || 'EAO' }}" # If the project type is manually selected, use the input value; otherwise, use 'EAO' as default From 8efb47fbfb25a77b434fb5d28d62591a5720becb Mon Sep 17 00:00:00 2001 From: VineetBala-AOT Date: Thu, 21 Sep 2023 14:13:17 -0700 Subject: [PATCH 15/15] Restrict analytics for unpublish engagement --- .../constants/engagement_status.py | 25 +++++++++++ .../services/aggregator_service.py | 14 +++--- .../services/engagement_service.py | 17 +++++--- .../analytics_api/services/survey_result.py | 9 ++-- .../services/user_response_detail.py | 17 +++++--- .../utils/engagement_access_validator.py | 26 +++++++++++ .../src/analytics_api/utils/roles.py | 1 + .../src/analytics_api/utils/token_info.py | 43 +++++++++++++++++++ .../services/ops/engagement_etl_service.py | 5 +++ 9 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 analytics-api/src/analytics_api/constants/engagement_status.py create mode 100644 analytics-api/src/analytics_api/utils/engagement_access_validator.py create mode 100644 analytics-api/src/analytics_api/utils/token_info.py diff --git a/analytics-api/src/analytics_api/constants/engagement_status.py b/analytics-api/src/analytics_api/constants/engagement_status.py new file mode 100644 index 000000000..95dd69b0f --- /dev/null +++ b/analytics-api/src/analytics_api/constants/engagement_status.py @@ -0,0 +1,25 @@ +# Copyright © 2021 Province of British Columbia +# +# 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. +"""Constants of engagement status.""" +from enum import Enum + + +class Status(Enum): + """Enum of engagement status.""" + + Draft = 'Draft' + Published = 'Published' + Closed = 'Closed' + Scheduled = 'Scheduled' + Unpublished = 'Unpublished' diff --git a/analytics-api/src/analytics_api/services/aggregator_service.py b/analytics-api/src/analytics_api/services/aggregator_service.py index a38833aa2..94b0ffdeb 100644 --- a/analytics-api/src/analytics_api/services/aggregator_service.py +++ b/analytics-api/src/analytics_api/services/aggregator_service.py @@ -1,6 +1,7 @@ """Service to get counts for dashboard.""" from analytics_api.models.email_verification import EmailVerification as EmailVerificationModel from analytics_api.models.user_response_detail import UserResponseDetail as UserResponseDetailModel +from analytics_api.utils import engagement_access_validator class AggregatorService: # pylint: disable=too-few-public-methods @@ -11,11 +12,10 @@ class AggregatorService: # pylint: disable=too-few-public-methods @staticmethod def get_count(engagement_id, count_for=''): """Get total count for an engagement id.""" - total_count = 0 + if engagement_access_validator.check_engagement_access(engagement_id): + if count_for == 'email_verification': + return EmailVerificationModel.get_email_verification_count(engagement_id) + if count_for == 'survey_completed': + return UserResponseDetailModel.get_response_count(engagement_id) - if count_for == 'email_verification': - total_count = EmailVerificationModel.get_email_verification_count(engagement_id) - if count_for == 'survey_completed': - total_count = UserResponseDetailModel.get_response_count(engagement_id) - - return total_count + return 0 diff --git a/analytics-api/src/analytics_api/services/engagement_service.py b/analytics-api/src/analytics_api/services/engagement_service.py index 61ea3ebad..78ea231d2 100644 --- a/analytics-api/src/analytics_api/services/engagement_service.py +++ b/analytics-api/src/analytics_api/services/engagement_service.py @@ -2,6 +2,7 @@ from analytics_api.models.engagement import Engagement as EngagementModel from analytics_api.schemas.engagement import EngagementSchema from analytics_api.schemas.map_data import MapDataSchema +from analytics_api.utils import engagement_access_validator class EngagementService: # pylint: disable=too-few-public-methods @@ -12,13 +13,17 @@ class EngagementService: # pylint: disable=too-few-public-methods @staticmethod def get_engagement(engagement_id) -> EngagementSchema: """Get Engagement by the id.""" - engagement = EngagementModel.find_by_id(engagement_id) - engagement_schema = EngagementSchema() - return engagement_schema.dump(engagement) + if engagement_access_validator.check_engagement_access(engagement_id): + engagement = EngagementModel.find_by_id(engagement_id) + engagement_schema = EngagementSchema() + return engagement_schema.dump(engagement) + return {} @staticmethod def get_engagement_map_data(engagement_id) -> MapDataSchema: """Get Map data by the engagement id.""" - map_data = EngagementModel.get_engagement_map_data(engagement_id) - map_data_schema = MapDataSchema() - return map_data_schema.dump(map_data) + if engagement_access_validator.check_engagement_access(engagement_id): + map_data = EngagementModel.get_engagement_map_data(engagement_id) + map_data_schema = MapDataSchema() + return map_data_schema.dump(map_data) + return {} diff --git a/analytics-api/src/analytics_api/services/survey_result.py b/analytics-api/src/analytics_api/services/survey_result.py index 9bebe4d2b..40cccd80b 100644 --- a/analytics-api/src/analytics_api/services/survey_result.py +++ b/analytics-api/src/analytics_api/services/survey_result.py @@ -1,6 +1,7 @@ """Service for survey result management.""" from analytics_api.models.request_type_option import RequestTypeOption as RequestTypeOptionModel from analytics_api.schemas.survey_result import SurveyResultSchema +from analytics_api.utils import engagement_access_validator class SurveyResultService: # pylint: disable=too-few-public-methods @@ -11,6 +12,8 @@ class SurveyResultService: # pylint: disable=too-few-public-methods @staticmethod def get_survey_result(engagement_id, can_view_all_survey_results) -> SurveyResultSchema: """Get Survey result by the engagement id.""" - survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results) - survey_result_schema = SurveyResultSchema(many=True) - return survey_result_schema.dump(survey_result) + if engagement_access_validator.check_engagement_access(engagement_id): + survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results) + survey_result_schema = SurveyResultSchema(many=True) + return survey_result_schema.dump(survey_result) + return {} diff --git a/analytics-api/src/analytics_api/services/user_response_detail.py b/analytics-api/src/analytics_api/services/user_response_detail.py index 46e9caeef..6b7e6be96 100644 --- a/analytics-api/src/analytics_api/services/user_response_detail.py +++ b/analytics-api/src/analytics_api/services/user_response_detail.py @@ -1,5 +1,6 @@ """Service for user response detail management.""" from analytics_api.models.user_response_detail import UserResponseDetail as UserResponseDetailModel +from analytics_api.utils import engagement_access_validator class UserResponseDetailService: @@ -10,13 +11,17 @@ class UserResponseDetailService: @staticmethod def get_response_count_by_created_month(engagement_id, search_options=None): """Get user response count for an engagement id grouped by created month.""" - response_count_by_created_month = UserResponseDetailModel.get_response_count_by_created_month( - engagement_id, search_options) - return response_count_by_created_month + if engagement_access_validator.check_engagement_access(engagement_id): + response_count_by_created_month = UserResponseDetailModel.get_response_count_by_created_month( + engagement_id, search_options) + return response_count_by_created_month + return {} @staticmethod def get_response_count_by_created_week(engagement_id, search_options=None): """Get user response count for an engagement id grouped by created week.""" - response_count_by_created_week = UserResponseDetailModel.get_response_count_by_created_week( - engagement_id, search_options) - return response_count_by_created_week + if engagement_access_validator.check_engagement_access(engagement_id): + response_count_by_created_week = UserResponseDetailModel.get_response_count_by_created_week( + engagement_id, search_options) + return response_count_by_created_week + return {} diff --git a/analytics-api/src/analytics_api/utils/engagement_access_validator.py b/analytics-api/src/analytics_api/utils/engagement_access_validator.py new file mode 100644 index 000000000..8d27261fc --- /dev/null +++ b/analytics-api/src/analytics_api/utils/engagement_access_validator.py @@ -0,0 +1,26 @@ +"""Check Engagement Access Service.""" +from sqlalchemy import and_, exists +from sqlalchemy.sql.expression import true +from analytics_api.constants.engagement_status import Status +from analytics_api.models.db import db +from analytics_api.models.engagement import Engagement as EngagementModel +from analytics_api.utils.roles import Role +from analytics_api.utils.token_info import TokenInfo + + +def check_engagement_access(engagement_id): + """Check if user has access to get engagement details.""" + is_engagement_unpublished = db.session.query( + exists() + .where( + and_( + EngagementModel.source_engagement_id == engagement_id, + EngagementModel.is_active == true(), + EngagementModel.status_name == Status.Unpublished.value + ) + ) + ).scalar() + + user_roles = set(TokenInfo.get_user_roles()) + + return not is_engagement_unpublished or Role.ACCESS_DASHBOARD.value in user_roles diff --git a/analytics-api/src/analytics_api/utils/roles.py b/analytics-api/src/analytics_api/utils/roles.py index 07c82a899..9f06c69b0 100644 --- a/analytics-api/src/analytics_api/utils/roles.py +++ b/analytics-api/src/analytics_api/utils/roles.py @@ -19,3 +19,4 @@ class Role(Enum): """User Role.""" VIEW_ALL_SURVEY_RESULTS = 'view_all_survey_results' # Allows users to view results to all questions + ACCESS_DASHBOARD = 'access_dashboard' diff --git a/analytics-api/src/analytics_api/utils/token_info.py b/analytics-api/src/analytics_api/utils/token_info.py new file mode 100644 index 000000000..7b8797e3a --- /dev/null +++ b/analytics-api/src/analytics_api/utils/token_info.py @@ -0,0 +1,43 @@ +"""Helper for token decoding.""" +from flask import current_app, g + +from analytics_api.utils.roles import Role +from analytics_api.utils.user_context import UserContext, user_context + + +class TokenInfo: + """Token info.""" + + @staticmethod + @user_context + def get_id(**kwargs): + """Get the user identifier.""" + try: + user_from_context: UserContext = kwargs['user_context'] + return user_from_context.sub + except AttributeError: + return None + + @staticmethod + def get_user_data(): + """Get the user data.""" + token_info = g.jwt_oidc_token_info + user_data = { + 'external_id': token_info.get('sub', None), + 'first_name': token_info.get('given_name', None), + 'last_name': token_info.get('family_name', None), + 'email_address': token_info.get('email', None), + 'username': token_info.get('preferred_username', None), + 'identity_provider': token_info.get('identity_provider', ''), + 'roles': TokenInfo.get_user_roles(), + } + return user_data + + @staticmethod + def get_user_roles(): + """Get the user roles from token.""" + if not hasattr(g, 'jwt_oidc_token_info') or not g.jwt_oidc_token_info: + return [] + valid_roles = set(item.value for item in Role) + token_roles = current_app.config['JWT_ROLE_CALLBACK'](g.jwt_oidc_token_info) + return valid_roles.intersection(token_roles) diff --git a/met-etl/src/etl_project/services/ops/engagement_etl_service.py b/met-etl/src/etl_project/services/ops/engagement_etl_service.py index ed347d933..aa55dd702 100644 --- a/met-etl/src/etl_project/services/ops/engagement_etl_service.py +++ b/met-etl/src/etl_project/services/ops/engagement_etl_service.py @@ -1,6 +1,7 @@ from dagster import Out, Output, op from met_api.constants.engagement_status import Status as MetEngagementStatus from met_api.models.engagement import Engagement as MetEngagementModel +from met_api.models.engagement_status import EngagementStatus as EngagementStatusModel from met_api.models.widget_map import WidgetMap as MetWidgetMap from analytics_api.models.engagement import Engagement as EtlEngagementModel from sqlalchemy import func @@ -102,6 +103,9 @@ def load_engagement(context, new_engagements, updated_engagements, engagement_ne geojson=map_widget.geojson marker_label=map_widget.marker_label + engagement_status = met_session.query(EngagementStatusModel).filter( + EngagementStatusModel.id == engagement.status_id).first() + engagement_model = EtlEngagementModel(name=engagement.name, source_engagement_id=engagement.id, start_date=engagement.start_date, @@ -115,6 +119,7 @@ def load_engagement(context, new_engagements, updated_engagements, engagement_ne longitude=longitude, geojson=geojson, marker_label=marker_label, + status_name=engagement_status.status_name ) session.add(engagement_model) session.commit()