From 564d75d502145162f0bc52565295fdd7f231a3af Mon Sep 17 00:00:00 2001 From: jadmsaadaot <91914654+jadmsaadaot@users.noreply.github.com> Date: Thu, 3 Aug 2023 08:44:13 -0700 Subject: [PATCH 01/18] Add vimeo CSP (#1944) * Add vimeo CSP * Add supported platforms --- met-web/nginx/nginx.dev.conf | 33 +++++++++++++------ met-web/nginx/nginx.prod.conf | 16 ++++++--- met-web/nginx/nginx.test.conf | 22 ++++++++++--- .../form/EngagementWidgets/Video/Form.tsx | 2 +- 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/met-web/nginx/nginx.dev.conf b/met-web/nginx/nginx.dev.conf index 9e5297ded..d9d7eb1ad 100644 --- a/met-web/nginx/nginx.dev.conf +++ b/met-web/nginx/nginx.dev.conf @@ -39,16 +39,29 @@ http { return 405; } - # add in most common security headers - add_header Content-Security-Policy " - default-src 'self' https://kit.fontawesome.com https://ka-f.fontawesome.com data: blob: filesystem: 'unsafe-inline' 'unsafe-eval'; - script-src 'self' 'sha256-JXGej4mPACbE/fP5kuunldJEyMk62sNjNe85DtAcMoU=' https://kit.fontawesome.com https://ka-f.fontawesome.com https://www2.gov.bc.ca https://cdn.form.io https://api.mapbox.com https://www.youtube.com 'unsafe-eval'; - worker-src 'self' blob:; - img-src 'self' data: blob: https://citz-gdx.objectstore.gov.bc.ca; - style-src 'self' 'unsafe-inline'; - connect-src 'self' https://spt.apps.gov.bc.ca/com.snowplowanalytics.snowplow/tp2 https://epic-engage-analytics-api-dev.apps.gold.devops.gov.bc.ca https://met-analytics-api-dev.apps.gold.devops.gov.bc.ca https://epic-engage-oidc-dev.apps.gold.devops.gov.bc.ca https://met-oidc-dev.apps.gold.devops.gov.bc.ca https://kit.fontawesome.com https://ka-f.fontawesome.com https://citz-gdx.objectstore.gov.bc.ca https://api.mapbox.com https://governmentofbc.maps.arcgis.com https://tiles.arcgis.com https://www.arcgis.com https://noembed.com; - frame-src 'self' https://met-oidc-dev.apps.gold.devops.gov.bc.ca https://epic-engage-analytics-api-dev.apps.gold.devops.gov.bc.ca https://epic-engage-oidc-dev.apps.gold.devops.gov.bc.ca https://met-analytics-dev.apps.gold.devops.gov.bc.ca https://www.youtube.com; - frame-ancestors 'self' https://met-oidc-dev.apps.gold.devops.gov.bc.ca https://epic-engage-oidc-dev.apps.gold.devops.gov.bc.ca"; + # add in most common security headers + add_header Content-Security-Policy " + default-src 'self' https://kit.fontawesome.com https://ka-f.fontawesome.com data: blob: filesystem: + 'unsafe-inline' 'unsafe-eval'; + script-src 'self' 'sha256-JXGej4mPACbE/fP5kuunldJEyMk62sNjNe85DtAcMoU=' https://kit.fontawesome.com + https://ka-f.fontawesome.com https://www2.gov.bc.ca https://cdn.form.io https://api.mapbox.com + https://www.youtube.com https://player.vimeo.com 'unsafe-eval'; + worker-src 'self' blob:; + img-src 'self' data: blob: https://citz-gdx.objectstore.gov.bc.ca; + style-src 'self' 'unsafe-inline'; + connect-src 'self' https://spt.apps.gov.bc.ca/com.snowplowanalytics.snowplow/tp2 + https://epic-engage-analytics-api-dev.apps.gold.devops.gov.bc.ca + https://met-analytics-api-dev.apps.gold.devops.gov.bc.ca + https://epic-engage-oidc-dev.apps.gold.devops.gov.bc.ca + https://met-oidc-dev.apps.gold.devops.gov.bc.ca https://kit.fontawesome.com https://ka-f.fontawesome.com + https://citz-gdx.objectstore.gov.bc.ca https://api.mapbox.com https://governmentofbc.maps.arcgis.com + https://tiles.arcgis.com https://www.arcgis.com; + frame-src 'self' https://met-oidc-dev.apps.gold.devops.gov.bc.ca + https://epic-engage-analytics-api-dev.apps.gold.devops.gov.bc.ca + https://epic-engage-oidc-dev.apps.gold.devops.gov.bc.ca https://met-analytics-dev.apps.gold.devops.gov.bc.ca + https://www.youtube.com https://player.vimeo.com; + frame-ancestors 'self' https://met-oidc-dev.apps.gold.devops.gov.bc.ca + https://epic-engage-oidc-dev.apps.gold.devops.gov.bc.ca"; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; add_header X-Content-Type-Options "nosniff"; add_header X-XSS-Protection 1; diff --git a/met-web/nginx/nginx.prod.conf b/met-web/nginx/nginx.prod.conf index 4d13375c5..c9b8d2b75 100644 --- a/met-web/nginx/nginx.prod.conf +++ b/met-web/nginx/nginx.prod.conf @@ -41,13 +41,21 @@ http { # add in most common security headers add_header Content-Security-Policy " - default-src 'self' https://kit.fontawesome.com https://ka-f.fontawesome.com data: blob: filesystem: 'unsafe-inline' 'unsafe-eval'; - script-src 'self' 'sha256-JXGej4mPACbE/fP5kuunldJEyMk62sNjNe85DtAcMoU=' https://kit.fontawesome.com https://ka-f.fontawesome.com https://www2.gov.bc.ca https://cdn.form.io https://api.mapbox.com https://www.youtube.com 'unsafe-eval'; + default-src 'self' https://kit.fontawesome.com https://ka-f.fontawesome.com data: blob: filesystem: + 'unsafe-inline' 'unsafe-eval'; + script-src 'self' 'sha256-JXGej4mPACbE/fP5kuunldJEyMk62sNjNe85DtAcMoU=' https://kit.fontawesome.com + https://ka-f.fontawesome.com https://www2.gov.bc.ca https://cdn.form.io https://api.mapbox.com + https://www.youtube.com https://player.vimeo.com 'unsafe-eval'; worker-src 'self' blob:; img-src 'self' data: blob: https://citz-gdx.objectstore.gov.bc.ca; style-src 'self' 'unsafe-inline'; - connect-src 'self' https://spt.apps.gov.bc.ca/com.snowplowanalytics.snowplow/tp2 https://met-analytics-api.apps.gold.devops.gov.bc.ca https://met-oidc.apps.gold.devops.gov.bc.ca https://kit.fontawesome.com https://ka-f.fontawesome.com https://citz-gdx.objectstore.gov.bc.ca https://api.mapbox.com https://governmentofbc.maps.arcgis.com https://tiles.arcgis.com https://www.arcgis.com https://noembed.com; - frame-src 'self' https://met-oidc.apps.gold.devops.gov.bc.ca https://met-analytics.apps.gold.devops.gov.bc.ca https://www.youtube.com; + connect-src 'self' https://spt.apps.gov.bc.ca/com.snowplowanalytics.snowplow/tp2 + https://met-analytics-api.apps.gold.devops.gov.bc.ca + https://met-oidc.apps.gold.devops.gov.bc.ca https://kit.fontawesome.com https://ka-f.fontawesome.com + https://citz-gdx.objectstore.gov.bc.ca https://api.mapbox.com https://governmentofbc.maps.arcgis.com + https://tiles.arcgis.com https://www.arcgis.com; + frame-src 'self' https://met-oidc.apps.gold.devops.gov.bc.ca + https://met-analytics.apps.gold.devops.gov.bc.ca https://www.youtube.com https://player.vimeo.com; frame-ancestors 'self' https://met-oidc.apps.gold.devops.gov.bc.ca"; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; add_header X-Content-Type-Options "nosniff"; diff --git a/met-web/nginx/nginx.test.conf b/met-web/nginx/nginx.test.conf index 8900a4da0..f9a36471e 100644 --- a/met-web/nginx/nginx.test.conf +++ b/met-web/nginx/nginx.test.conf @@ -41,14 +41,26 @@ http { # add in most common security headers add_header Content-Security-Policy " - default-src 'self' https://kit.fontawesome.com https://ka-f.fontawesome.com data: blob: filesystem: 'unsafe-inline' 'unsafe-eval'; - script-src 'self' 'sha256-JXGej4mPACbE/fP5kuunldJEyMk62sNjNe85DtAcMoU=' https://kit.fontawesome.com https://ka-f.fontawesome.com https://www2.gov.bc.ca https://cdn.form.io https://api.mapbox.com https://www.youtube.com 'unsafe-eval'; + default-src 'self' https://kit.fontawesome.com https://ka-f.fontawesome.com data: blob: filesystem: + 'unsafe-inline' 'unsafe-eval'; + script-src 'self' 'sha256-JXGej4mPACbE/fP5kuunldJEyMk62sNjNe85DtAcMoU=' https://kit.fontawesome.com + https://ka-f.fontawesome.com https://www2.gov.bc.ca https://cdn.form.io https://api.mapbox.com + https://www.youtube.com https://player.vimeo.com 'unsafe-eval'; worker-src 'self' blob:; img-src 'self' data: blob: https://citz-gdx.objectstore.gov.bc.ca; style-src 'self' 'unsafe-inline'; - connect-src 'self' https://spt.apps.gov.bc.ca/com.snowplowanalytics.snowplow/tp2 https://epic-engage-analytics-api-test.apps.gold.devops.gov.bc.ca https://epic-engage-oidc-test.apps.gold.devops.gov.bc.ca https://met-analytics-api-test.apps.gold.devops.gov.bc.ca https://met-oidc-test.apps.gold.devops.gov.bc.ca https://kit.fontawesome.com https://ka-f.fontawesome.com https://citz-gdx.objectstore.gov.bc.ca https://api.mapbox.com https://governmentofbc.maps.arcgis.com https://tiles.arcgis.com https://www.arcgis.com https://noembed.com; - frame-src 'self' https://met-oidc-test.apps.gold.devops.gov.bc.ca https://epic-engage-oidc-test.apps.gold.devops.gov.bc.ca https://epic-engage-analytics-api-test.apps.gold.devops.gov.bc.ca https://met-analytics-test.apps.gold.devops.gov.bc.ca https://www.youtube.com; - frame-ancestors 'self' https://met-oidc-test.apps.gold.devops.gov.bc.ca https://epic-engage-oidc-test.apps.gold.devops.gov.bc.ca"; + connect-src 'self' https://spt.apps.gov.bc.ca/com.snowplowanalytics.snowplow/tp2 + https://epic-engage-analytics-api-test.apps.gold.devops.gov.bc.ca + https://epic-engage-oidc-test.apps.gold.devops.gov.bc.ca + https://met-analytics-api-test.apps.gold.devops.gov.bc.ca + https://met-oidc-test.apps.gold.devops.gov.bc.ca https://kit.fontawesome.com https://ka-f.fontawesome.com + https://citz-gdx.objectstore.gov.bc.ca https://api.mapbox.com https://governmentofbc.maps.arcgis.com + https://tiles.arcgis.com https://www.arcgis.com; + frame-src 'self' https://met-oidc-test.apps.gold.devops.gov.bc.ca + https://epic-engage-oidc-test.apps.gold.devops.gov.bc.ca https://epic-engage-analytics-api-test.apps.gold.devops.gov.bc.ca + https://met-analytics-test.apps.gold.devops.gov.bc.ca https://www.youtube.com https://player.vimeo.com; + frame-ancestors 'self' https://met-oidc-test.apps.gold.devops.gov.bc.ca + https://epic-engage-oidc-test.apps.gold.devops.gov.bc.ca"; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; add_header X-Content-Type-Options "nosniff"; add_header X-XSS-Protection 1; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx index ebc9b8bef..52d136795 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx @@ -163,7 +163,7 @@ const Form = () => { Video Link - The video must be hosted on one of the following platforms: + The video must be hosted on one of the following platforms: YouTube, Vimeo Date: Thu, 3 Aug 2023 11:46:06 -0400 Subject: [PATCH 02/18] fix two open tabs bug (#1951) --- met-web/src/components/landing/EngagementTile.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/met-web/src/components/landing/EngagementTile.tsx b/met-web/src/components/landing/EngagementTile.tsx index f360fd628..a28bba76a 100644 --- a/met-web/src/components/landing/EngagementTile.tsx +++ b/met-web/src/components/landing/EngagementTile.tsx @@ -123,7 +123,8 @@ const EngagementTile = ({ passedEngagement, engagementId }: EngagementTileProps) { + onClick={(event: React.MouseEvent) => { + event.stopPropagation(); window.open(engagementUrl, '_blank'); }} > @@ -133,7 +134,8 @@ const EngagementTile = ({ passedEngagement, engagementId }: EngagementTileProps) { + onClick={(event: React.MouseEvent) => { + event.stopPropagation(); window.open(engagementUrl, '_blank'); }} > From 16df7077c02561faebb661b6e41b42db7c4448a2 Mon Sep 17 00:00:00 2001 From: djnunez-aot <103138766+djnunez-aot@users.noreply.github.com> Date: Thu, 3 Aug 2023 11:46:31 -0400 Subject: [PATCH 03/18] Remove icons for dashboard on extra small screens (#1954) * push marginx * update margins to sides for mobile & make ultra small screen size(300 px) responsive * add higher fontsize for ultra small * remove comments * refactor to reduce cognitive complexity * update * remove icons for extra small devices --- .../SubmissionTrend/SubmissionTrend.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx b/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx index 0fa1d4590..2042cc4ec 100644 --- a/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx +++ b/met-web/src/components/publicDashboard/SubmissionTrend/SubmissionTrend.tsx @@ -173,7 +173,11 @@ const SubmissionTrend = ({ engagement, engagementIsLoading }: SubmissionTrendPro {...params} InputProps={{ ...params.InputProps, - endAdornment: , + endAdornment: isExtraSmall ? ( + <> + ) : ( + + ), }} /> )} @@ -199,7 +203,11 @@ const SubmissionTrend = ({ engagement, engagementIsLoading }: SubmissionTrendPro {...params} InputProps={{ ...params.InputProps, - endAdornment: , + endAdornment: isExtraSmall ? ( + <> + ) : ( + + ), }} /> )} From 4f6723baf9ba09dc422bf505a66d8824ed07a5a8 Mon Sep 17 00:00:00 2001 From: jadmsaadaot <91914654+jadmsaadaot@users.noreply.github.com> Date: Thu, 3 Aug 2023 08:58:23 -0700 Subject: [PATCH 04/18] tweaks to banner, image upload, engagement form (#1949) * tweaks to banner, image upload, engagement form * Remove unused imports --- met-web/src/App.tsx | 2 +- .../src/components/banner/BannerWithImage.tsx | 1 + .../WhoIsListening/AddContactDrawer.tsx | 2 +- .../engagement/view/EngagementInfoSection.tsx | 18 +++++++----------- .../src/components/imageUpload/Uploader.tsx | 7 ++++++- met-web/src/components/imageUpload/index.tsx | 11 +++++++++-- .../components/survey/submit/SurveyBanner.tsx | 2 +- 7 files changed, 26 insertions(+), 17 deletions(-) diff --git a/met-web/src/App.tsx b/met-web/src/App.tsx index 73f10d3a8..29412360e 100644 --- a/met-web/src/App.tsx +++ b/met-web/src/App.tsx @@ -32,8 +32,8 @@ const App = () => { const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated); const authenticationLoading = useAppSelector((state) => state.user.authentication.loading); const pathSegments = window.location.pathname.split('/'); - const basename = pathSegments[1]; const language = 'en'; // Default language is English, change as needed + const basename = pathSegments[1].toLowerCase(); const tenant: TenantState = useAppSelector((state) => state.tenant); diff --git a/met-web/src/components/banner/BannerWithImage.tsx b/met-web/src/components/banner/BannerWithImage.tsx index 0605a7477..d80da8c32 100644 --- a/met-web/src/components/banner/BannerWithImage.tsx +++ b/met-web/src/components/banner/BannerWithImage.tsx @@ -17,6 +17,7 @@ const BannerWithImage = ({ height, imageUrl, children }: BannerProps) => { height: height ? height : '38em', width: '100%', position: 'relative', + overflow: 'clip', }} > { handleAddFile={handleAddAvatarImage} savedImageUrl={contactToEdit?.avatar_url} savedImageName={contactToEdit?.avatar_filename} - helpText="Drag and drop an image here or click to select one" + helpText={'Drop an image here or click to select one.'} /> diff --git a/met-web/src/components/engagement/view/EngagementInfoSection.tsx b/met-web/src/components/engagement/view/EngagementInfoSection.tsx index 2877e4d43..a7b3eedf0 100644 --- a/met-web/src/components/engagement/view/EngagementInfoSection.tsx +++ b/met-web/src/components/engagement/view/EngagementInfoSection.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; -import { Grid, Typography, Stack } from '@mui/material'; -import { MetHeader1 } from 'components/common'; +import { Grid, Stack } from '@mui/material'; +import { MetHeader1, MetLabel } from 'components/common'; import { EngagementStatusChip } from '../status'; import { Editor } from 'react-draft-wysiwyg'; import dayjs from 'dayjs'; @@ -51,26 +51,22 @@ const EngagementInfoSection = ({ savedEngagement, children }: EngagementInfoSect backgroundColor: 'rgba(242, 242, 242, 0.95)', padding: '1em', margin: '1em', - maxWidth: '90%', }} m={{ lg: '3em 5em 0 3em', md: '3em', sm: '1em' }} + spacing={1} > {name} - + - - {EngagementDate} - + {EngagementDate} - + - - Status: - + Status: diff --git a/met-web/src/components/imageUpload/Uploader.tsx b/met-web/src/components/imageUpload/Uploader.tsx index 76eeb23bc..ea7534f56 100644 --- a/met-web/src/components/imageUpload/Uploader.tsx +++ b/met-web/src/components/imageUpload/Uploader.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useContext } from 'react'; import { Grid, Stack, Typography } from '@mui/material'; -import Dropzone from 'react-dropzone'; +import Dropzone, { Accept } from 'react-dropzone'; import { PrimaryButton, SecondaryButton } from 'components/common'; import { ImageUploadContext } from './imageUploadContext'; @@ -8,11 +8,13 @@ interface UploaderProps { margin?: number; helpText?: string; height?: string; + accept?: Accept; } const Uploader = ({ margin = 2, helpText = 'Drag and drop some files here, or click to select files', height = '10em', + accept = {}, }: UploaderProps) => { const { handleAddFile, @@ -88,6 +90,7 @@ const Uploader = ({ onClick={() => { setCropModalOpen(true); }} + size="small" > Crop @@ -99,11 +102,13 @@ const Uploader = ({ return ( { + if (acceptedFiles.length === 0) return; const createdObjectURL = URL.createObjectURL(acceptedFiles[0]); handleAddFile(acceptedFiles); setAddedImageFileUrl(createdObjectURL); setAddedImageFileName(acceptedFiles[0].name); }} + accept={accept} > {({ getRootProps, getInputProps }) => (
diff --git a/met-web/src/components/imageUpload/index.tsx b/met-web/src/components/imageUpload/index.tsx index b3821cf50..c4108ebb5 100644 --- a/met-web/src/components/imageUpload/index.tsx +++ b/met-web/src/components/imageUpload/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { CropModal } from './cropModal'; import { ImageUploadContextProvider } from './imageUploadContext'; import Uploader from './Uploader'; +import { Accept } from 'react-dropzone'; interface UploaderProps { margin?: number; @@ -11,15 +12,21 @@ interface UploaderProps { helpText?: string; height?: string; cropAspectRatio?: number; + accept?: Accept; } export const ImageUpload = ({ margin = 2, handleAddFile, savedImageUrl = '', savedImageName = '', - helpText = 'Drag and drop some files here, or click to select files', + helpText = 'Drag and drop an image here, or click to select an image from your device. Formats accepted are: .jpg, .jpeg, .png, .webp.', height = '10em', cropAspectRatio = 1, + accept = { + 'image/jpeg': [], + 'image/png': [], + 'image/webp': [], + }, }: UploaderProps) => { return ( - + ); diff --git a/met-web/src/components/survey/submit/SurveyBanner.tsx b/met-web/src/components/survey/submit/SurveyBanner.tsx index ce8f83150..eb3160cad 100644 --- a/met-web/src/components/survey/submit/SurveyBanner.tsx +++ b/met-web/src/components/survey/submit/SurveyBanner.tsx @@ -39,7 +39,7 @@ export const SurveyBanner = () => { } return ( - + ); From dee225d04811c716caef8c37d135095d7bedee04 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT <90332175+VineetBala-AOT@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:01:23 -0700 Subject: [PATCH 05/18] Changes for email verification template (#1960) * updates on engagement publish * update email template * adding action drop down * updated changes for User Management * access user details page for users without a role * updating variable name * updating the schema * updating as per review comments * updating schema * adding changes for clone and delete * fixing linting * update * fix for report setting on analytics * fixing lint * Changes for email verification template * fixing linting --- met-api/src/met_api/config.py | 2 ++ .../services/email_verification_service.py | 21 ++++++++++++------- met-api/templates/email_verification.html | 11 ++++------ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/met-api/src/met_api/config.py b/met-api/src/met_api/config.py index b5ce77f72..c9dc19863 100644 --- a/met-api/src/met_api/config.py +++ b/met-api/src/met_api/config.py @@ -97,6 +97,8 @@ class _Config(): # pylint: disable=too-few-public-methods SURVEY_PATH = os.getenv('SURVEY_PATH', '/surveys/submit/{survey_id}/{token}') SUBSCRIBE_PATH = os.getenv('SUBSCRIBE_PATH', '/engagements/{engagement_id}/subscribe/{token}') UNSUBSCRIBE_PATH = os.getenv('UNSUBSCRIBE_PATH', '/engagements/{engagement_id}/unsubscribe/{participant_id}') + ENGAGEMENT_PATH = os.getenv('ENGAGEMENT_PATH', '/engagements/{engagement_id}/view') + ENGAGEMENT_PATH_SLUG = os.getenv('ENGAGEMENT_PATH_SLUG', '/{slug}') # engagement dashboard path is used to pass the survey result to the public user. # The link is changed such that public user can access the comments page from the email and not the dashboard. ENGAGEMENT_DASHBOARD_PATH = os.getenv('ENGAGEMENT_DASHBOARD_PATH', '/engagements/{engagement_id}/comments') diff --git a/met-api/src/met_api/services/email_verification_service.py b/met-api/src/met_api/services/email_verification_service.py index 89fe66bd9..131a0353e 100644 --- a/met-api/src/met_api/services/email_verification_service.py +++ b/met-api/src/met_api/services/email_verification_service.py @@ -10,6 +10,7 @@ from met_api.models import Engagement as EngagementModel from met_api.models import EngagementSlug as EngagementSlugModel from met_api.models import Survey as SurveyModel +from met_api.models import Tenant as TenantModel from met_api.models.email_verification import EmailVerification from met_api.schemas.email_verification import EmailVerificationSchema from met_api.services.participant_service import ParticipantService @@ -157,32 +158,38 @@ def _render_survey_email_template(survey: SurveyModel, token): subject_template = current_app.config.get('VERIFICATION_EMAIL_SUBJECT') survey_path = current_app.config.get('SURVEY_PATH'). \ format(survey_id=survey.id, token=token) - dashboard_path = EmailVerificationService._get_dashboard_path(engagement) + engagement_path = EmailVerificationService._get_engagement_path(engagement) site_url = notification.get_tenant_site_url(engagement.tenant_id) + tenant_name = EmailVerificationService._get_tenant_name(engagement.tenant_id) args = { 'engagement_name': engagement_name, 'survey_url': f'{site_url}{survey_path}', - 'engagement_url': f'{site_url}{dashboard_path}', - 'end_date': datetime.strftime(engagement.end_date, EmailVerificationService.full_date_format), + 'engagement_url': f'{site_url}{engagement_path}', + 'tenant_name': tenant_name, } subject = subject_template.format(engagement_name=engagement_name) body = template.render( engagement_name=args.get('engagement_name'), survey_url=args.get('survey_url'), engagement_url=args.get('engagement_url'), - end_date=args.get('end_date'), + tenant_name=args.get('tenant_name'), ) return subject, body, args, template_id @staticmethod - def _get_dashboard_path(engagement: EngagementModel): + def _get_engagement_path(engagement: EngagementModel): engagement_slug = EngagementSlugModel.find_by_engagement_id(engagement.id) if engagement_slug: - return current_app.config.get('ENGAGEMENT_DASHBOARD_PATH_SLUG'). \ + return current_app.config.get('ENGAGEMENT_PATH_SLUG'). \ format(slug=engagement_slug.slug) - return current_app.config.get('ENGAGEMENT_DASHBOARD_PATH'). \ + return current_app.config.get('ENGAGEMENT_PATH'). \ format(engagement_id=engagement.id) + @staticmethod + def _get_tenant_name(tenant_id): + tenant = TenantModel.find_by_id(tenant_id) + return tenant.name + @staticmethod def validate_email_verification(email_verification: EmailVerificationSchema): """Validate an email verification.""" diff --git a/met-api/templates/email_verification.html b/met-api/templates/email_verification.html index 2ada0370b..0ea7000c9 100644 --- a/met-api/templates/email_verification.html +++ b/met-api/templates/email_verification.html @@ -1,11 +1,8 @@ -

Thank you for your interest in sharing your thoughts about {{ engagement_name }}.

-

Please click the link below to access the survey and share your comments. This link will expire in 24 hours and is only valid once.

+

Share your feedback about {{ engagement_name }}.

+

Please click the link below to provide your feedback. This link will expire in 24 hours and is only valid once. If the link has expired, you can request request access again by visiting {{ engagement_name }}.


-Access Survey -
-

You can view the survey results anytime here.

-

The comments will be available for viewing when the Engagement period is over, starting on {{ end_date }}.

+

Click here to provide your feedback.


Thank you,


-

The EAO Team

\ No newline at end of file +

The {{ tenant_name }} Team

\ No newline at end of file From 7c3e40ebb57c3851eebf772e4464bba5624d11be Mon Sep 17 00:00:00 2001 From: Aaron Forde <132496297+aforde-aot@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:27:32 -0700 Subject: [PATCH 06/18] Updating deploy.sh (#1956) --- .github/workflows/deploy.yml | 17 +++++++++++++++-- openshift/deploy.sh | 7 ++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 407032c4a..d38aa3271 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,12 +7,25 @@ on: description: "Environment (test/prod)" required: true default: "test" + project_type: + description: "Project Type (EAO/GDX)" + required: false + default: "GDX" # Default value is GDX defaults: run: shell: bash working-directory: ./openshift +env: + TAG_NAME: "test" + PROJECT_TYPE: "${{ github.event.inputs.project_type || 'GDX' }}" # If the project type is manually selected, use the input value; otherwise, use 'GDX' as default + + # Set OpenShift related variables based on PROJECT_TYPE + OPENSHIFT_LOGIN_REGISTRY: ${{ secrets.OPENSHIFT_LOGIN_REGISTRY }} + OPENSHIFT_SA_TOKEN: ${{ (github.event.inputs.project_type == 'EAO') && secrets.OPENSHIFT_SA_TOKEN_EAO || secrets.OPENSHIFT_SA_TOKEN }} + OPENSHIFT_REPOSITORY: ${{ (github.event.inputs.project_type == 'EAO') && secrets.OPENSHIFT_REPOSITORY_EAO || secrets.OPENSHIFT_REPOSITORY }} + jobs: met-deployment: runs-on: ubuntu-20.04 @@ -24,9 +37,9 @@ jobs: - name: Login Openshift shell: bash run: | - oc login --server=${{secrets.OPENSHIFT_LOGIN_REGISTRY}} --token=${{secrets.OPENSHIFT_SA_TOKEN}} + oc login --server=${{env.OPENSHIFT_LOGIN_REGISTRY}} --token=${{env.OPENSHIFT_SA_TOKEN}} - name: Tag Images and Rollout shell: bash run: | - sh deploy.sh ${{ github.event.inputs.environment }} \ No newline at end of file + sh deploy.sh ${{ github.event.inputs.environment }} ${{env.OPENSHIFT_REPOSITORY }} diff --git a/openshift/deploy.sh b/openshift/deploy.sh index 1c86c98c3..256735c12 100644 --- a/openshift/deploy.sh +++ b/openshift/deploy.sh @@ -1,4 +1,5 @@ -oc project e903c2-tools +oc project $2-tools + oc tag met-api:latest met-api:$1 oc tag notify-api:latest notify-api:$1 oc tag analytics-api:latest analytics-api:$1 @@ -7,5 +8,5 @@ oc tag met-web:latest met-web:$1 oc tag met-analytics:latest met-analytics:$1 oc tag dagster-etl:latest dagster-etl:$1 -oc rollout status dc/met-api -n e903c2-$1 -w -oc rollout status dc/met-web -n e903c2-$1 -w +oc rollout status dc/met-api -n $2-$1 -w +oc rollout status dc/met-web -n $2-$1 -w From 186541cabca970c46bb045eb1081609e610e181f Mon Sep 17 00:00:00 2001 From: djnunez-aot <103138766+djnunez-aot@users.noreply.github.com> Date: Thu, 3 Aug 2023 13:29:23 -0400 Subject: [PATCH 07/18] Hard coded text (#1950) * update email text * hard coded text * linting & adding tenant_name * add get_tenant name method to submission service --- .../met_api/services/submission_service.py | 91 +++++++++++++------ met-api/templates/email_rejected_comment.html | 36 +++++--- met-api/templates/email_verification.html | 2 +- .../review/emailPreview/EmailTemplates.tsx | 2 +- 4 files changed, 88 insertions(+), 43 deletions(-) diff --git a/met-api/src/met_api/services/submission_service.py b/met-api/src/met_api/services/submission_service.py index 88c97d59e..4ea0d994f 100644 --- a/met-api/src/met_api/services/submission_service.py +++ b/met-api/src/met_api/services/submission_service.py @@ -28,6 +28,7 @@ from met_api.utils import notification from met_api.utils.roles import Role from met_api.utils.template import Template +from met_api.models import Tenant as TenantModel class SubmissionService: @@ -58,7 +59,8 @@ def create(cls, token, submission: SubmissionSchema): # Creates a scoped session that will be committed when diposed or rolledback if a exception occurs with session_scope() as session: - email_verification = EmailVerificationService().verify(token, survey_id, None, session) + email_verification = EmailVerificationService().verify( + token, survey_id, None, session) participant_id = email_verification.get('participant_id') submission['participant_id'] = participant_id submission['created_by'] = participant_id @@ -66,7 +68,8 @@ def create(cls, token, submission: SubmissionSchema): submission_result = Submission.create(submission, session) submission['id'] = submission_result.id - comments = CommentService.extract_comments_from_survey(submission, survey) + comments = CommentService.extract_comments_from_survey( + submission, survey) CommentService().create_comments(comments, session) return submission_result @@ -85,8 +88,10 @@ def update_comments(cls, token, data: PublicSubmissionSchema): submission.comment_status_id = Status.Pending with session_scope() as session: - EmailVerificationService().verify(token, submission.survey_id, submission.id, session) - comments_result = [Comment.update(submission.id, comment, session) for comment in data.get('comments', [])] + EmailVerificationService().verify( + token, submission.survey_id, submission.id, session) + comments_result = [Comment.update( + submission.id, comment, session) for comment in data.get('comments', [])] Submission.update(SubmissionSchema().dump(submission), session) return comments_result @@ -96,7 +101,8 @@ def _validate_fields(submission): """Validate all fields.""" survey_id = submission.get('survey_id', None) survey: SurveyModel = SurveyModel.find_by_id(survey_id) - engagement: EngagementModel = EngagementModel.find_by_id(survey.engagement_id) + engagement: EngagementModel = EngagementModel.find_by_id( + survey.engagement_id) if not engagement: raise ValueError('Survey not linked to an Engagement') @@ -109,20 +115,26 @@ def review_comment(cls, submission_id, staff_review_details: dict, external_user user = StaffUserService.get_user_by_external_id(external_user_id) cls.validate_review(staff_review_details, user, submission_id) - reviewed_by = ' '.join([user.get('first_name', ''), user.get('last_name', '')]) + reviewed_by = ' '.join( + [user.get('first_name', ''), user.get('last_name', '')]) staff_review_details['reviewed_by'] = reviewed_by staff_review_details['user_id'] = user.get('id') with session_scope() as session: - should_send_email = SubmissionService._should_send_email(submission_id, staff_review_details) - submission = Submission.update_comment_status(submission_id, staff_review_details, session) + should_send_email = SubmissionService._should_send_email( + submission_id, staff_review_details) + submission = Submission.update_comment_status( + submission_id, staff_review_details, session) if staff_notes := staff_review_details.get('staff_note', []): - cls.add_or_update_staff_note(submission.survey_id, submission_id, staff_notes) + cls.add_or_update_staff_note( + submission.survey_id, submission_id, staff_notes) if should_send_email: - rejection_review_note = StaffNote.get_staff_note_by_type(submission_id, StaffNoteType.Review.name) - SubmissionService._trigger_email(rejection_review_note[0].note, session, submission) + rejection_review_note = StaffNote.get_staff_note_by_type( + submission_id, StaffNoteType.Review.name) + SubmissionService._trigger_email( + rejection_review_note[0].note, session, submission) session.commit() return SubmissionSchema().dump(submission) @@ -134,7 +146,8 @@ def _trigger_email(review_note, session, submission): 'submission_id': submission.id, 'type': EmailVerificationType.RejectedComment, }, session) - SubmissionService._send_rejected_email(submission, review_note, email_verification.get('verification_token')) + SubmissionService._send_rejected_email( + submission, review_note, email_verification.get('verification_token')) @classmethod def validate_review(cls, values: dict, user, submission_id): @@ -145,7 +158,8 @@ def validate_review(cls, values: dict, user, submission_id): has_threat = values.get('has_threat', None) rejected_reason_other = values.get('rejected_reason_other', None) - valid_statuses = [status.id for status in CommentStatus.get_comment_statuses()] + valid_statuses = [ + status.id for status in CommentStatus.get_comment_statuses()] if not user: raise ValueError('Invalid user.') @@ -162,7 +176,8 @@ def validate_review(cls, values: dict, user, submission_id): if not submission: raise ValueError('Invalid submission.') authorization.check_auth( - one_of_roles=(MembershipType.TEAM_MEMBER.name, Role.REVIEW_ALL_COMMENTS.value), + one_of_roles=(MembershipType.TEAM_MEMBER.name, + Role.REVIEW_ALL_COMMENTS.value), engagement_id=submission.engagement_id ) @@ -170,14 +185,21 @@ def validate_review(cls, values: dict, user, submission_id): def add_or_update_staff_note(cls, survey_id, submission_id, staff_notes): """Process staff note for a comment.""" for staff_note in staff_notes: - note = StaffNote.get_staff_note_by_type(submission_id, staff_note.get('note_type')) + note = StaffNote.get_staff_note_by_type( + submission_id, staff_note.get('note_type')) if note: note[0].note = staff_note['note'] note[0].flush() else: - doc = SubmissionService._create_staff_notes(survey_id, submission_id, staff_note) + doc = SubmissionService._create_staff_notes( + survey_id, submission_id, staff_note) doc.flush() + @staticmethod + def _get_tenant_name(tenant_id): + tenant = TenantModel.find_by_id(tenant_id) + return tenant.name + @staticmethod def _create_staff_notes(survey_id, submission_id, staff_note): doc: StaffNote = StaffNote() @@ -203,10 +225,12 @@ def _should_send_email(submission_id: int, staff_comment_details: dict) -> bool: if staff_comment_details.get('notify_email') is False: return False if staff_comment_details.get('status_id') == Status.Rejected.value: - has_review_note_changed = SubmissionService.is_review_note_changed(submission_id, staff_comment_details) + has_review_note_changed = SubmissionService.is_review_note_changed( + submission_id, staff_comment_details) if has_review_note_changed: return True - has_reason_changed = SubmissionService.is_rejection_reason_changed(submission_id, staff_comment_details) + has_reason_changed = SubmissionService.is_rejection_reason_changed( + submission_id, staff_comment_details) if has_reason_changed: return True return False @@ -229,7 +253,8 @@ def is_review_note_changed(submission_id: int, values: dict) -> bool: staff_notes = values.get('staff_note', []) for staff_note in staff_notes: if staff_note['note_type'] == StaffNoteType.Review.name: - note = StaffNote.get_staff_note_by_type(submission_id, StaffNoteType.Review.name) + note = StaffNote.get_staff_note_by_type( + submission_id, StaffNoteType.Review.name) if not note or note[0].note != staff_note.get('note'): return True return False @@ -256,7 +281,8 @@ def get_paginated( survey_id, pagination_options, search_text, - advanced_search_filters if any(advanced_search_filters.values()) else None + advanced_search_filters if any( + advanced_search_filters.values()) else None ) return { 'items': SubmissionSchema(many=True, exclude=['submission_json']).dump(items), @@ -269,16 +295,20 @@ def _send_rejected_email(submission: Submission, review_note, token) -> None: participant_id = submission.participant_id participant = ParticipantModel.find_by_id(participant_id) - template_id = current_app.config.get('REJECTED_EMAIL_TEMPLATE_ID', None) - subject, body, args = SubmissionService._render_email_template(submission, review_note, token) + template_id = current_app.config.get( + 'REJECTED_EMAIL_TEMPLATE_ID', None) + subject, body, args = SubmissionService._render_email_template( + submission, review_note, token) try: notification.send_email(subject=subject, - email=ParticipantModel.decode_email(participant.email_address), + email=ParticipantModel.decode_email( + participant.email_address), html_body=body, args=args, template_id=template_id) except Exception as exc: # noqa: B902 - current_app.logger.error(' None: @staticmethod def _render_email_template(submission: Submission, review_note, token): template = Template.get_template('email_rejected_comment.html') - engagement: EngagementModel = EngagementModel.find_by_id(submission.engagement_id) + engagement: EngagementModel = EngagementModel.find_by_id( + submission.engagement_id) survey: SurveyModel = SurveyModel.find_by_id(submission.survey_id) engagement_name = engagement.name survey_name = survey.name - + tenant_name = SubmissionService._get_tenant_name( + engagement.tenant_id) submission_path = current_app.config.get('SUBMISSION_PATH'). \ - format(engagement_id=submission.engagement_id, submission_id=submission.id, token=token) - submission_url = notification.get_tenant_site_url(engagement.tenant_id, submission_path) + format(engagement_id=submission.engagement_id, + submission_id=submission.id, token=token) + submission_url = notification.get_tenant_site_url( + engagement.tenant_id, submission_path) subject = current_app.config.get('REJECTED_EMAIL_SUBJECT'). \ format(engagement_name=engagement_name) args = { @@ -306,6 +340,7 @@ def _render_email_template(submission: Submission, review_note, token): 'submission_url': submission_url, 'review_note': review_note, 'end_date': datetime.strftime(engagement.end_date, EmailVerificationService.full_date_format), + 'tenant_name': tenant_name, } body = template.render( engagement_name=args.get('engagement_name'), diff --git a/met-api/templates/email_rejected_comment.html b/met-api/templates/email_rejected_comment.html index 538fd6c5a..954824a61 100644 --- a/met-api/templates/email_rejected_comment.html +++ b/met-api/templates/email_rejected_comment.html @@ -1,20 +1,30 @@ -

Thank you for taking the time to fill in our survey about {{ engagement_name }}.

-

We reviewed your comments and can't publish them on our public site for the following reason(s):

+

Thank you for taking the time to provide your feedback on {{ engagement_name }}.

+

We have reviewed your feedback and can't accept it for the following reason(s):


+

Your feedback contained:

+
    - {% if has_personal_info %} -
  • One or many of your comments contain personal information such as name, address, or other information that could identify you.
  • - {% endif %} - {% if has_profanity %} -
  • One or many of your comments contain swear words or profanities.
  • - {% endif %} - {% if has_other_reason %} -
  • One or many of your comments can't be published because of {{ other_reason }}.
  • - {% endif %} + {% if has_personal_info %} +
  • + One or many of your comments contain personal information such as name, + address, or other information that could identify you. +
  • + {% endif %} {% if has_profanity %} +
  • One or many of your comments contain swear words or profanities.
  • + {% endif %} {% if has_other_reason %} +
  • + One or many of your comments can't be published because of {{ other_reason + }}. +
  • + {% endif %}

-

Your comments will still be taken in consideration and send to the proponent for consideration but won't appear on our website.

+

You can edit and re-submit your feedback here.

+

+ The comment period is open until {{ end_date }}. You must re-submit + your feedback before the comment period closes. +


Thank you,


-

The EAO Team

\ No newline at end of file +

The {{tenant_name}} Team

diff --git a/met-api/templates/email_verification.html b/met-api/templates/email_verification.html index 0ea7000c9..a020c7882 100644 --- a/met-api/templates/email_verification.html +++ b/met-api/templates/email_verification.html @@ -5,4 +5,4 @@

Share your feedback about {{ engagement_name }}.


Thank you,


-

The {{ tenant_name }} Team

\ No newline at end of file +

The {{ tenant_name }} Team

diff --git a/met-web/src/components/comments/admin/review/emailPreview/EmailTemplates.tsx b/met-web/src/components/comments/admin/review/emailPreview/EmailTemplates.tsx index c0694a33b..a93b500ae 100644 --- a/met-web/src/components/comments/admin/review/emailPreview/EmailTemplates.tsx +++ b/met-web/src/components/comments/admin/review/emailPreview/EmailTemplates.tsx @@ -49,7 +49,7 @@ export const RejectEmailTemplate = ({ {`One or many of your comments can't be published because of (${otherReason}).`} + >{`One or many of your comments can't be published because of ${otherReason}.`} From 5d12a0e957c2ecd9cfe5672627fde635fab9aeeb Mon Sep 17 00:00:00 2001 From: djnunez-aot <103138766+djnunez-aot@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:01:08 -0400 Subject: [PATCH 08/18] Description fix for subscription drawer (#1963) * update email text * hard coded text * linting & adding tenant_name * add get_tenant name method to submission service * update text --- .../form/EngagementWidgets/Subscribe/SubscribeForm.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx index 022b095b8..ce09af3c7 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx @@ -26,10 +26,8 @@ const Form = () => { The email list will collect email addresses for a mailing list. A "double-opt-in" email will be - sent to confirm the subscription.Only the email addresses that have been double-opted-in will be - on the list. Please include the unsubscribe link provided on the Email List screen in every - future communication. Unsubscribed email addresses will be removed from the list. Please - downloaded the list before each communication. + sent to confirm the subscription. Only the email addresses that have been double-opted-in will + be on the list. From 545ae41c2ff4288da0344c81b0da4a6afe73377f Mon Sep 17 00:00:00 2001 From: jadmsaadaot <91914654+jadmsaadaot@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:15:09 -0700 Subject: [PATCH 09/18] Make widget title editable (#1961) * Make widget title updatable * Add widget title to all components * Show the widget.title in the engagement view * remove unsued imports * fix lint issues * fix flake8 issue * Fix failing tests --- met-api/src/met_api/models/widget.py | 14 +++ met-api/src/met_api/resources/widget.py | 23 +++- .../schemas/schemas/widget_update.json | 23 ++++ met-api/src/met_api/schemas/widget.py | 1 + .../src/met_api/services/widget_service.py | 18 +++ .../src/apiManager/apiSlices/widgets/index.ts | 19 ++- met-web/src/apiManager/endpoints/index.ts | 2 +- .../Documents/DocumentForm.tsx | 41 ++++--- .../Documents/DocumentOptionCard.tsx | 4 +- .../Events/EventsOptionCard.tsx | 4 +- .../form/EngagementWidgets/Events/Form.tsx | 11 +- .../form/EngagementWidgets/Map/Form.tsx | 8 +- .../EngagementWidgets/Map/MapOptionCard.tsx | 4 +- .../EngagementWidgets/Phases/PhasesForm.tsx | 113 +++++++++--------- .../Phases/PhasesOptionCard.tsx | 4 +- .../Subscribe/SubscribeForm.tsx | 21 ++-- .../Subscribe/SubscribeOptionCard.tsx | 4 +- .../form/EngagementWidgets/Video/Form.tsx | 14 +-- .../Video/VideoOptionCard.tsx | 4 +- .../WhoIsListening/WhoIsListeningForm.tsx | 5 +- .../WhoIsListeningOptionCard.tsx | 4 +- .../EngagementWidgets/WidgetCardSwitch.tsx | 14 +-- .../EngagementWidgets/WidgetDrawerContext.tsx | 5 + .../form/EngagementWidgets/WidgetTitle.tsx | 112 +++++++++++++++++ .../view/widgets/DocumentWidget.tsx | 2 +- .../view/widgets/Events/EventsWidget.tsx | 2 +- .../engagement/view/widgets/Map/MapWidget.tsx | 2 +- .../PhasesWidgetMobile/PhasesWidgetMobile.tsx | 2 +- .../view/widgets/PhasesWidget/index.tsx | 2 +- .../widgets/Subscribe/SubscribeWidget.tsx | 7 +- .../view/widgets/Video/VideoWidgetView.tsx | 2 +- .../view/widgets/WhoIsListeningWidget.tsx | 2 +- .../engagement/view/widgets/WidgetSwitch.tsx | 2 +- met-web/src/models/widget.tsx | 1 + .../components/engagement/engagement.test.tsx | 2 + met-web/tests/unit/components/factory.ts | 4 +- .../widgets/DocumentWidget.test.tsx | 3 + .../components/widgets/EventsWidget.test.tsx | 2 + .../components/widgets/MapWidget.test.tsx | 2 + .../components/widgets/PhasesWidget.test.tsx | 3 + .../widgets/WhoIsListeningWidget.test.tsx | 3 + 41 files changed, 379 insertions(+), 136 deletions(-) create mode 100644 met-api/src/met_api/schemas/schemas/widget_update.json create mode 100644 met-web/src/components/engagement/form/EngagementWidgets/WidgetTitle.tsx diff --git a/met-api/src/met_api/models/widget.py b/met-api/src/met_api/models/widget.py index b554abeda..9c55a0e63 100644 --- a/met-api/src/met_api/models/widget.py +++ b/met-api/src/met_api/models/widget.py @@ -4,6 +4,7 @@ """ from __future__ import annotations from datetime import datetime +from typing import Optional from sqlalchemy.sql.schema import ForeignKey @@ -65,6 +66,7 @@ def __create_new_widget_entity(widget): updated_date=datetime.utcnow(), created_by=widget.get('created_by', None), updated_by=widget.get('updated_by', None), + title=widget.get('title', None), ) @classmethod @@ -87,3 +89,15 @@ def update_widgets(cls, update_mappings: list) -> None: """Update widgets..""" db.session.bulk_update_mappings(Widget, update_mappings) db.session.commit() + + @classmethod + def update_widget(cls, engagement_id, widget_id, widget_data: dict) -> Optional[Widget]: + """Update widget.""" + query = Widget.query.filter_by(id=widget_id, engagement_id=engagement_id) + widget: Widget = query.first() + if not widget: + return None + widget_data['updated_date'] = datetime.utcnow() + query.update(widget_data) + db.session.commit() + return widget diff --git a/met-api/src/met_api/resources/widget.py b/met-api/src/met_api/resources/widget.py index 70a518565..26d90dabe 100644 --- a/met-api/src/met_api/resources/widget.py +++ b/met-api/src/met_api/resources/widget.py @@ -89,8 +89,8 @@ def patch(engagement_id): return {'message': err.error}, err.status_code -@cors_preflight('DELETE') -@API.route('/engagement//widget/') +@cors_preflight('DELETE, PATCH') +@API.route('//engagements/') class EngagementWidget(Resource): """Resource for managing widgets with engagements.""" @@ -107,6 +107,25 @@ def delete(engagement_id, widget_id): except ValueError as err: return str(err), HTTPStatus.INTERNAL_SERVER_ERROR + @staticmethod + @cross_origin(origins=allowedorigins()) + @_jwt.requires_auth + def patch(engagement_id, widget_id): + """Update widget.""" + try: + user_id = TokenInfo.get_id() + widget_data = request.get_json() + valid_format, errors = schema_utils.validate(widget_data, 'widget_update') + if not valid_format: + return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + + updated_widget = WidgetService().update_widget(engagement_id, widget_id, widget_data, user_id) + return updated_widget, HTTPStatus.OK + except (KeyError, ValueError) as err: + return str(err), HTTPStatus.INTERNAL_SERVER_ERROR + except ValidationError as err: + return str(err.messages), HTTPStatus.INTERNAL_SERVER_ERROR + @cors_preflight('POST,OPTIONS') @API.route('//items') diff --git a/met-api/src/met_api/schemas/schemas/widget_update.json b/met-api/src/met_api/schemas/schemas/widget_update.json new file mode 100644 index 000000000..1e5955ed3 --- /dev/null +++ b/met-api/src/met_api/schemas/schemas/widget_update.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://met.gov.bc.ca/.well_known/schemas/widget_update", + "type": "object", + "title": "The root schema", + "description": "The root schema comprises the entire JSON document.", + "default": {}, + "examples": [ + { + "title": "Who is Listening" + } + ], + "required": ["title"], + "properties": { + "title": { + "$id": "#/properties/title", + "type": "string", + "title": "Widget title", + "description": "The title of the widget.", + "examples": ["Who is Listening"] + } + } +} diff --git a/met-api/src/met_api/schemas/widget.py b/met-api/src/met_api/schemas/widget.py index d86147648..c2aec64d7 100644 --- a/met-api/src/met_api/schemas/widget.py +++ b/met-api/src/met_api/schemas/widget.py @@ -14,6 +14,7 @@ class Meta: # pylint: disable=too-few-public-methods unknown = EXCLUDE id = fields.Int(data_key='id') + title = fields.Str(data_key='title') widget_type_id = fields.Int(data_key='widget_type_id', required=True) engagement_id = fields.Int(data_key='engagement_id', required=True) created_by = fields.Str(data_key='created_by') diff --git a/met-api/src/met_api/services/widget_service.py b/met-api/src/met_api/services/widget_service.py index 52383a950..15b744fe4 100644 --- a/met-api/src/met_api/services/widget_service.py +++ b/met-api/src/met_api/services/widget_service.py @@ -65,6 +65,16 @@ def sort_widget(engagement_id, widgets: list, user_id=None): WidgetModel.update_widgets(widget_sort_mappings) + @staticmethod + def update_widget(engagement_id, widget_id: list, widget_data: dict, user_id=None): + """Sort widgets.""" + WidgetService._verify_widget(widget_id) + + widget_data['updated_by'] = user_id + + updated_widget = WidgetModel.update_widget(engagement_id, widget_id, widget_data) + return WidgetSchema().dump(updated_widget) + @staticmethod def _validate_widget_ids(engagement_id, widgets): """Validate if widget ids belong to the engagement.""" @@ -76,6 +86,14 @@ def _validate_widget_ids(engagement_id, widgets): error='Invalid widgets.', status_code=HTTPStatus.BAD_REQUEST) + @staticmethod + def _verify_widget(widget_id): + """Verify if widget exists.""" + widget = WidgetModel.get_widget_by_id(widget_id) + if not widget: + raise KeyError('Widget ' + widget_id + ' does not exist') + return widget + @staticmethod def create_widget_items_bulk(widget_items: list, user_id): """Create widget items in bulk.""" diff --git a/met-web/src/apiManager/apiSlices/widgets/index.ts b/met-web/src/apiManager/apiSlices/widgets/index.ts index 326b7b7c8..f5cf5e12a 100644 --- a/met-web/src/apiManager/apiSlices/widgets/index.ts +++ b/met-web/src/apiManager/apiSlices/widgets/index.ts @@ -25,6 +25,14 @@ export const widgetsApi = createApi({ }), invalidatesTags: ['Widgets'], }), + updateWidget: builder.mutation }>({ + query: ({ engagementId, id, data }) => ({ + url: `widgets/${id}/engagements/${engagementId}`, + method: 'PATCH', + body: data, + }), + invalidatesTags: ['Widgets'], + }), sortWidgets: builder.mutation({ query: ({ engagementId, widgets }) => ({ url: `widgets/engagement/${engagementId}/sort_index`, @@ -35,7 +43,7 @@ export const widgetsApi = createApi({ }), deleteWidget: builder.mutation({ query: ({ engagementId, widgetId }) => ({ - url: `widgets/engagement/${engagementId}/widget/${widgetId}`, + url: `widgets/${widgetId}/engagements/${engagementId}`, method: 'DELETE', }), invalidatesTags: (_result, _error, arg) => [{ type: 'Widgets', id: arg.widgetId }], @@ -47,5 +55,10 @@ export const widgetsApi = createApi({ // Export hooks for usage in functional components, which are // auto-generated based on the defined endpoints -export const { useLazyGetWidgetsQuery, useCreateWidgetMutation, useSortWidgetsMutation, useDeleteWidgetMutation } = - widgetsApi; +export const { + useLazyGetWidgetsQuery, + useCreateWidgetMutation, + useSortWidgetsMutation, + useDeleteWidgetMutation, + useUpdateWidgetMutation, +} = widgetsApi; diff --git a/met-web/src/apiManager/endpoints/index.ts b/met-web/src/apiManager/endpoints/index.ts index fb0ff1448..31cb26f15 100644 --- a/met-web/src/apiManager/endpoints/index.ts +++ b/met-web/src/apiManager/endpoints/index.ts @@ -84,7 +84,7 @@ const Endpoints = { Widgets: { GET_LIST: `${AppConfig.apiUrl}/widgets/engagement/engagement_id`, CREATE: `${AppConfig.apiUrl}/widgets/engagement/engagement_id`, - DELETE: `${AppConfig.apiUrl}/widgets/engagement/engagement_id/widget/widget_id`, + DELETE: `${AppConfig.apiUrl}/widgets/widget_id/engagements/engagement_id`, SORT: `${AppConfig.apiUrl}/widgets/engagement/engagement_id/sort_index`, }, Widget_items: { diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentForm.tsx index 6e86ba413..c1390f397 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentForm.tsx @@ -1,36 +1,41 @@ import React, { useContext } from 'react'; import { Divider, Grid } from '@mui/material'; -import { MetHeader3, PrimaryButton } from 'components/common'; +import { PrimaryButton } from 'components/common'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; import CreateFolderForm from './CreateFolderForm'; import DocumentsBlock from './DocumentsBlock'; +import { WidgetTitle } from '../WidgetTitle'; +import { DocumentsContext } from './DocumentsContext'; const DocumentForm = () => { const { handleWidgetDrawerOpen } = useContext(WidgetDrawerContext); + const { widget } = useContext(DocumentsContext); + + if (!widget) { + return null; + } return ( - <> - - - Documents - - + + + + + - - - + + + - - - + + + - - - handleWidgetDrawerOpen(false)}>{`Close`} - + + + handleWidgetDrawerOpen(false)}>{`Close`} - + ); }; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentOptionCard.tsx index e5f049e1b..578470ce4 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentOptionCard.tsx @@ -12,6 +12,7 @@ import { openNotification } from 'services/notificationService/notificationSlice import { optionCardStyle } from '../constants'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; +const Title = 'Documents'; const DocumentOptionCard = () => { const { widgets, loadWidgets, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); const { savedEngagement } = useContext(ActionContext); @@ -31,6 +32,7 @@ const DocumentOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Document, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -82,7 +84,7 @@ const DocumentOptionCard = () => { xs={8} > - Documents + {Title} Add documents and folders diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Events/EventsOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Events/EventsOptionCard.tsx index 3d95b543a..4c4690c44 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Events/EventsOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Events/EventsOptionCard.tsx @@ -12,6 +12,7 @@ import { optionCardStyle } from '../constants'; import { WidgetTabValues } from '../type'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; +const Title = 'Events'; const EventsOptionCard = () => { const { widgets, loadWidgets, handleWidgetDrawerOpen, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -32,6 +33,7 @@ const EventsOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Events, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -83,7 +85,7 @@ const EventsOptionCard = () => { xs={8} > - Events + {Title} diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Events/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Events/Form.tsx index 8ae4deda1..14a976b6e 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Events/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Events/Form.tsx @@ -1,18 +1,23 @@ import React, { useContext } from 'react'; import { Grid, Divider } from '@mui/material'; -import { PrimaryButton, MetHeader3, WidgetButton } from 'components/common'; +import { PrimaryButton, WidgetButton } from 'components/common'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { EventsContext } from './EventsContext'; import EventsInfoBlock from './EventsInfoBlock'; +import { WidgetTitle } from '../WidgetTitle'; const Form = () => { const { handleWidgetDrawerOpen } = useContext(WidgetDrawerContext); - const { setInPersonFormTabOpen, setVirtualSessionFormTabOpen } = useContext(EventsContext); + const { setInPersonFormTabOpen, setVirtualSessionFormTabOpen, widget } = useContext(EventsContext); + + if (!widget) { + return null; + } return ( - Events + diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Map/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Map/Form.tsx index f4e56711f..e70054147 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Map/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Map/Form.tsx @@ -2,7 +2,6 @@ import React, { useContext, useState, useEffect } from 'react'; import Divider from '@mui/material/Divider'; import { Grid, Typography, Stack, IconButton } from '@mui/material'; import { - MetHeader3, MetLabel, PrimaryButton, SecondaryButton, @@ -26,6 +25,7 @@ import LinkIcon from '@mui/icons-material/Link'; import { When } from 'react-if'; import HighlightOffIcon from '@mui/icons-material/HighlightOff'; import * as turf from '@turf/turf'; +import { WidgetTitle } from '../WidgetTitle'; const schema = yup .object({ @@ -163,10 +163,14 @@ const Form = () => { ); } + if (!widget) { + return null; + } + return ( - Map + diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Map/MapOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Map/MapOptionCard.tsx index 3d906580c..6e34da68b 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Map/MapOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Map/MapOptionCard.tsx @@ -12,6 +12,7 @@ import { WidgetTabValues } from '../type'; import LocationOnIcon from '@mui/icons-material/LocationOn'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; +const Title = 'Map'; const MapOptionCard = () => { const { widgets, loadWidgets, handleWidgetDrawerOpen, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -32,6 +33,7 @@ const MapOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Map, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -83,7 +85,7 @@ const MapOptionCard = () => { xs={8} > - Map + {Title} Add a map with the project location diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx index 78dbe3208..fba3305db 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx @@ -1,12 +1,13 @@ import React, { useContext, useEffect, useState } from 'react'; import { Autocomplete, Checkbox, Divider, FormControl, FormControlLabel, Grid, TextField } from '@mui/material'; -import { MetHeader3, MetLabel, PrimaryButton, SecondaryButton } from 'components/common'; +import { MetLabel, PrimaryButton, SecondaryButton } from 'components/common'; import { EngagementPhases } from 'models/engagementPhases'; import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; import { postWidgetItem } from 'services/widgetService'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { WidgetType } from 'models/widget'; +import { WidgetTitle } from '../WidgetTitle'; interface ISelectOptions { id: EngagementPhases; @@ -80,72 +81,68 @@ const PhasesForm = () => { }; return ( - <> - + + + + + + - The EA Process - + Engagement Phase + ( + + )} + isOptionEqualToValue={(option: ISelectOptions, value: ISelectOptions) => option.id == value.id} + getOptionLabel={(option: ISelectOptions) => option.label} + onChange={(_e: React.SyntheticEvent, option: ISelectOptions | null) => { + setSelectedOption(option); + setIsStandalone(false); + }} + /> - - - Engagement Phase - ( - + + { + setSelectedOption(null); + setIsStandalone(checked); }} /> - )} - isOptionEqualToValue={(option: ISelectOptions, value: ISelectOptions) => - option.id == value.id } - getOptionLabel={(option: ISelectOptions) => option.label} - onChange={(_e: React.SyntheticEvent, option: ISelectOptions | null) => { - setSelectedOption(option); - setIsStandalone(false); - }} + label="This engagement is a stand-alone engagement" /> - - - - { - setSelectedOption(null); - setIsStandalone(checked); - }} - /> - } - label="This engagement is a stand-alone engagement" - /> - - + + + + + + saveWidgetItem()} + >{`Save & Close`} - - - saveWidgetItem()} - >{`Save & Close`} - - - handleWidgetDrawerOpen(false)}>{`Cancel`} - + + handleWidgetDrawerOpen(false)}>{`Cancel`} - + ); }; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesOptionCard.tsx index d0d6c3487..256ae2245 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesOptionCard.tsx @@ -12,6 +12,7 @@ import { Else, If, Then } from 'react-if'; import ChatBubbleOutlineOutlinedIcon from '@mui/icons-material/ChatBubbleOutlineOutlined'; import { optionCardStyle } from '../constants'; +const Title = 'Environmental Assessment Process'; const PhasesOptionCard = () => { const { savedEngagement } = useContext(ActionContext); const { widgets, loadWidgets, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -31,6 +32,7 @@ const PhasesOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Phases, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -82,7 +84,7 @@ const PhasesOptionCard = () => { xs={8} > - Environmental Assessment Process + {Title} diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx index ce09af3c7..90ebfe038 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx @@ -1,27 +1,26 @@ import React, { useContext } from 'react'; -import { Grid, Divider, FormControlLabel, Checkbox } from '@mui/material'; -import { PrimaryButton, MetHeader3, WidgetButton, MetParagraph, MetLabel } from 'components/common'; +import { Grid, Divider } from '@mui/material'; +import { PrimaryButton, WidgetButton, MetParagraph } from 'components/common'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; -import BorderColorIcon from '@mui/icons-material/BorderColor'; import { SubscribeContext } from './SubscribeContext'; import { Subscribe_TYPE } from 'models/subscription'; +import { WidgetTitle } from '../WidgetTitle'; const Form = () => { const { handleWidgetDrawerOpen } = useContext(WidgetDrawerContext); - const { handleSubscribeDrawerOpen } = useContext(SubscribeContext); + const { handleSubscribeDrawerOpen, widget } = useContext(SubscribeContext); + + if (!widget) { + return null; + } return ( - - - Sign-up for updates - - - + - } label={Hide title} /> + diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeOptionCard.tsx index 8b5572b6f..5e63f540e 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeOptionCard.tsx @@ -12,6 +12,7 @@ import { optionCardStyle } from '../constants'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; import { WidgetTabValues } from '../type'; +const Title = 'Sign Up for Updates'; const SubscribeOptionCard = () => { const { widgets, loadWidgets, handleWidgetDrawerOpen, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -32,6 +33,7 @@ const SubscribeOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Subscribe, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -85,7 +87,7 @@ const SubscribeOptionCard = () => { xs={8} > - Sign Up for Updates + {Title} Offer members of the public to sign up for updates diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx index 52d136795..46f37b0c8 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx @@ -1,14 +1,7 @@ import React, { useContext, useEffect } from 'react'; import Divider from '@mui/material/Divider'; import { Grid } from '@mui/material'; -import { - MetDescription, - MetHeader3, - MetLabel, - MidScreenLoader, - PrimaryButton, - SecondaryButton, -} from 'components/common'; +import { MetDescription, MetLabel, MidScreenLoader, PrimaryButton, SecondaryButton } from 'components/common'; import { useForm, FormProvider, SubmitHandler } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; @@ -19,6 +12,7 @@ import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { VideoContext } from './VideoContext'; import { patchVideo, postVideo } from 'services/widgetService/VideoService'; import { updatedDiff } from 'deep-object-diff'; +import { WidgetTitle } from '../WidgetTitle'; const schema = yup .object({ @@ -120,7 +114,7 @@ const Form = () => { } }; - if (isLoadingVideoWidget) { + if (isLoadingVideoWidget || !widget) { return ( @@ -133,7 +127,7 @@ const Form = () => { return ( - Video + diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Video/VideoOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Video/VideoOptionCard.tsx index 9d50addc5..2abe5d671 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Video/VideoOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Video/VideoOptionCard.tsx @@ -12,6 +12,7 @@ import { WidgetTabValues } from '../type'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; import MovieIcon from '@mui/icons-material/Movie'; +const Title = 'Video'; const VideoOptionCard = () => { const { widgets, loadWidgets, handleWidgetDrawerOpen, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -32,6 +33,7 @@ const VideoOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Video, engagement_id: savedEngagement.id, + title: Title, }).unwrap(); await loadWidgets(); dispatch( @@ -83,7 +85,7 @@ const VideoOptionCard = () => { xs={8} > - Video + {Title} Add a link to a hosted video and link preview diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx index 677f6f932..cc46412f9 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx @@ -1,6 +1,6 @@ import React, { useContext, useState, useEffect } from 'react'; import { Autocomplete, Grid, TextField, Divider } from '@mui/material'; -import { MetLabel, PrimaryButton, SecondaryButton, MetHeader3 } from 'components/common'; +import { MetLabel, PrimaryButton, SecondaryButton } from 'components/common'; import { Contact } from 'models/contact'; import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; @@ -9,6 +9,7 @@ import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { WidgetType } from 'models/widget'; import ContactBlock from './ContactBlock'; import { WhoIsListeningContext } from './WhoIsListeningContext'; +import { WidgetTitle } from '../WidgetTitle'; const WhoIsListeningForm = () => { const { handleWidgetDrawerOpen, widgets, loadWidgets } = useContext(WidgetDrawerContext); @@ -86,7 +87,7 @@ const WhoIsListeningForm = () => { <> - Who is Listening + diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningOptionCard.tsx index 68da307b1..712a0d431 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningOptionCard.tsx @@ -12,6 +12,7 @@ import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; import { optionCardStyle } from '../constants'; +const Title = 'Who is Listening'; const WhoIsListeningOptionCard = () => { const { savedEngagement } = useContext(ActionContext); const { widgets, loadWidgets, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -31,6 +32,7 @@ const WhoIsListeningOptionCard = () => { await createWidget({ widget_type_id: WidgetType.WhoIsListening, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -83,7 +85,7 @@ const WhoIsListeningOptionCard = () => { xs={8} > - Who is Listening + {Title} Add contacts to this engagement diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx index 8dcef7729..43f6af81f 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx @@ -19,7 +19,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -32,7 +32,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -45,7 +45,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -58,7 +58,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -71,7 +71,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -84,7 +84,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -97,7 +97,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx index 616f0861a..249b5fe28 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx @@ -9,6 +9,7 @@ import { useDeleteWidgetMutation, useSortWidgetsMutation } from 'apiManager/apiS export interface WidgetDrawerContextProps { widgets: Widget[]; + setWidgets: React.Dispatch>; widgetDrawerOpen: boolean; handleWidgetDrawerOpen: (_open: boolean) => void; widgetDrawerTabValue: string; @@ -25,6 +26,9 @@ export type EngagementParams = { export const WidgetDrawerContext = createContext({ widgets: [], + setWidgets: () => { + return; + }, isWidgetsLoading: false, widgetDrawerOpen: false, handleWidgetDrawerOpen: (_open: boolean) => { @@ -103,6 +107,7 @@ export const WidgetDrawerProvider = ({ children }: { children: JSX.Element | JSX { + const [editing, setEditing] = React.useState(false); + const [title, setTitle] = React.useState(widget.title); + const [updateWidget] = useUpdateWidgetMutation(); + const dispatch = useAppDispatch(); + const { setWidgets } = useContext(WidgetDrawerContext); + const [isSaving, setIsSaving] = React.useState(false); + + const saveTitle = async () => { + if (title === widget.title) { + setEditing(false); + return; + } + try { + setIsSaving(true); + const response = await updateWidget({ + id: widget.id, + engagementId: widget.engagement_id, + data: { + title, + }, + }).unwrap(); + setWidgets((prevWidgets) => { + const updatedWidget = prevWidgets.find((prevWidget) => prevWidget.id === widget.id); + if (updatedWidget) { + updatedWidget.title = response?.title || ''; + } + return [...prevWidgets]; + }); + dispatch(openNotification({ severity: 'success', text: 'Widget title successfully updated' })); + setIsSaving(false); + setEditing(false); + } catch (error) { + setIsSaving(false); + dispatch(openNotification({ severity: 'error', text: 'Error occurred while updating widget title' })); + } + }; + + const handleTitleChange = (text: string) => { + if (!text) { + return; + } + + setTitle(text); + }; + + return ( + + + + handleTitleChange(e.target.value)} + inputProps={{ maxLength: 100 }} + fullWidth + /> + + + + + + { + saveTitle(); + }} + > + + + + + + + + + {widget.title} + { + setEditing(true); + }} + > + + + + + + ); +}; diff --git a/met-web/src/components/engagement/view/widgets/DocumentWidget.tsx b/met-web/src/components/engagement/view/widgets/DocumentWidget.tsx index 3b281a785..d23cdbf2c 100644 --- a/met-web/src/components/engagement/view/widgets/DocumentWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/DocumentWidget.tsx @@ -50,7 +50,7 @@ const DocumentWidget = ({ widget }: DocumentWidgetProps) => { <> - Documents + {widget.title} {documents.map((document: DocumentItem) => { diff --git a/met-web/src/components/engagement/view/widgets/Events/EventsWidget.tsx b/met-web/src/components/engagement/view/widgets/Events/EventsWidget.tsx index ac9875124..f126705f2 100644 --- a/met-web/src/components/engagement/view/widgets/Events/EventsWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/Events/EventsWidget.tsx @@ -73,7 +73,7 @@ const EventsWidget = ({ widget }: EventsWidgetProps) => { xs={12} paddingBottom={0} > - Events + {widget.title} {events.map((event: Event) => { diff --git a/met-web/src/components/engagement/view/widgets/Map/MapWidget.tsx b/met-web/src/components/engagement/view/widgets/Map/MapWidget.tsx index cf9545994..8d21e90ff 100644 --- a/met-web/src/components/engagement/view/widgets/Map/MapWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/Map/MapWidget.tsx @@ -79,7 +79,7 @@ const MapWidget = ({ widget }: MapWidgetProps) => { xs={12} paddingBottom={0} > - Map + {widget.title} diff --git a/met-web/src/components/engagement/view/widgets/PhasesWidget/PhasesWidgetMobile/PhasesWidgetMobile.tsx b/met-web/src/components/engagement/view/widgets/PhasesWidget/PhasesWidgetMobile/PhasesWidgetMobile.tsx index e6ae67fdb..8217f4613 100644 --- a/met-web/src/components/engagement/view/widgets/PhasesWidget/PhasesWidgetMobile/PhasesWidgetMobile.tsx +++ b/met-web/src/components/engagement/view/widgets/PhasesWidget/PhasesWidgetMobile/PhasesWidgetMobile.tsx @@ -65,7 +65,7 @@ export const PhasesWidgetMobile = () => { - The EA Process + {phasesWidget.title} diff --git a/met-web/src/components/engagement/view/widgets/PhasesWidget/index.tsx b/met-web/src/components/engagement/view/widgets/PhasesWidget/index.tsx index 6a07a999a..1a22c53bf 100644 --- a/met-web/src/components/engagement/view/widgets/PhasesWidget/index.tsx +++ b/met-web/src/components/engagement/view/widgets/PhasesWidget/index.tsx @@ -45,7 +45,7 @@ export const PhasesWidget = () => { - The Environmental Assessment Process + {phasesWidget.title} diff --git a/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx b/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx index c11cb0eb7..e22da3084 100644 --- a/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx @@ -9,8 +9,9 @@ import { createEmailVerification } from 'services/emailVerificationService'; import { createSubscription } from 'services/subscriptionService'; import { EmailVerificationType } from 'models/emailVerification'; import { SubscriptionType } from 'constants/subscriptionType'; +import { Widget } from 'models/widget'; -function SubscribeWidget() { +const SubscribeWidget = ({ widget }: { widget: Widget }) => { const dispatch = useAppDispatch(); const { savedEngagement, engagementMetadata } = useContext(ActionContext); const defaultType = engagementMetadata.project_id ? SubscriptionType.PROJECT : SubscriptionType.ENGAGEMENT; @@ -171,7 +172,7 @@ function SubscribeWidget() { /> - Sign Up for Updates + {widget.title} @@ -188,6 +189,6 @@ function SubscribeWidget() { ); -} +}; export default SubscribeWidget; diff --git a/met-web/src/components/engagement/view/widgets/Video/VideoWidgetView.tsx b/met-web/src/components/engagement/view/widgets/Video/VideoWidgetView.tsx index c2e414e37..be4b87d5a 100644 --- a/met-web/src/components/engagement/view/widgets/Video/VideoWidgetView.tsx +++ b/met-web/src/components/engagement/view/widgets/Video/VideoWidgetView.tsx @@ -78,7 +78,7 @@ const VideoWidgetView = ({ widget }: VideoWidgetProps) => { xs={12} paddingBottom={0} > - Video + {widget.title} diff --git a/met-web/src/components/engagement/view/widgets/WhoIsListeningWidget.tsx b/met-web/src/components/engagement/view/widgets/WhoIsListeningWidget.tsx index 9196728e3..fc543c33a 100644 --- a/met-web/src/components/engagement/view/widgets/WhoIsListeningWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/WhoIsListeningWidget.tsx @@ -73,7 +73,7 @@ const WhoIsListeningWidget = ({ widget }: WhoIsListeningWidgetProps) => { return ( - Who is Listening + {widget.title} {contacts.map((contact) => { diff --git a/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx b/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx index ee2de56ea..0924ffa79 100644 --- a/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx +++ b/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx @@ -28,7 +28,7 @@ export const WidgetSwitch = ({ widget }: WidgetSwitchProps) => { - + diff --git a/met-web/src/models/widget.tsx b/met-web/src/models/widget.tsx index 9bbdcde90..403348ec5 100644 --- a/met-web/src/models/widget.tsx +++ b/met-web/src/models/widget.tsx @@ -10,6 +10,7 @@ export interface Widget { widget_type_id: number; engagement_id: number; items: WidgetItem[]; + title: string; } export enum WidgetType { diff --git a/met-web/tests/unit/components/engagement/engagement.test.tsx b/met-web/tests/unit/components/engagement/engagement.test.tsx index 686afaf62..f8aba194f 100644 --- a/met-web/tests/unit/components/engagement/engagement.test.tsx +++ b/met-web/tests/unit/components/engagement/engagement.test.tsx @@ -41,6 +41,7 @@ const widgetItem: WidgetItem = { const whoIsListeningWidget: Widget = { id: 1, + title: 'Who is Listening', widget_type_id: WidgetType.WhoIsListening, engagement_id: 1, items: [widgetItem], @@ -48,6 +49,7 @@ const whoIsListeningWidget: Widget = { const engagementPhasesWidget: Widget = { id: 2, + title: 'Engagement Phases', widget_type_id: WidgetType.Phases, engagement_id: 1, items: [], diff --git a/met-web/tests/unit/components/factory.ts b/met-web/tests/unit/components/factory.ts index 46310bb77..f7f9397c8 100644 --- a/met-web/tests/unit/components/factory.ts +++ b/met-web/tests/unit/components/factory.ts @@ -88,7 +88,7 @@ const mockEventItem: EventItem = { const mockEvent: Event = { id: 1, - title: 'Jace', + title: 'Events', type: 'OPENHOUSE', sort_index: 1, widget_id: 1, @@ -106,6 +106,7 @@ const eventWidgetItem: WidgetItem = { const eventWidget: Widget = { id: 1, + title: 'Events', widget_type_id: WidgetType.Events, engagement_id: 1, items: [eventWidgetItem], @@ -120,6 +121,7 @@ const mapWidgetItem: WidgetItem = { const mapWidget: Widget = { id: 1, + title: 'Map', widget_type_id: WidgetType.Map, engagement_id: 1, items: [mapWidgetItem], diff --git a/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx b/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx index 251e7d70c..d0a19b3fa 100644 --- a/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx @@ -50,6 +50,7 @@ const mockFolder: DocumentItem = { const documentWidget: Widget = { id: 1, + title: 'Documents', widget_type_id: WidgetType.Document, engagement_id: 1, items: [], @@ -74,6 +75,7 @@ const mockCreateWidget = jest.fn(() => Promise.resolve(documentWidget)); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(documentWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], })); @@ -135,6 +137,7 @@ describe('Document widget in engagement page tests', () => { expect(mockCreateWidget).toHaveBeenNthCalledWith(1, { widget_type_id: WidgetType.Document, engagement_id: engagement.id, + title: documentWidget.title, }); expect(getWidgetsMock).toHaveBeenCalledTimes(2); expect(screen.getByText('Create Folder')).toBeVisible(); diff --git a/met-web/tests/unit/components/widgets/EventsWidget.test.tsx b/met-web/tests/unit/components/widgets/EventsWidget.test.tsx index 2ac900cba..c51894ca4 100644 --- a/met-web/tests/unit/components/widgets/EventsWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/EventsWidget.test.tsx @@ -62,6 +62,7 @@ const mockCreateWidget = jest.fn(() => Promise.resolve(eventWidget)); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(eventWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], })); @@ -118,6 +119,7 @@ describe('Event Widget tests', () => { expect(mockCreateWidget).toHaveBeenNthCalledWith(1, { widget_type_id: WidgetType.Events, engagement_id: draftEngagement.id, + title: mockEvent.title, }); expect(getWidgetsMock).toHaveBeenCalledTimes(2); expect(screen.getByText('Add In-Person Event')).toBeVisible(); diff --git a/met-web/tests/unit/components/widgets/MapWidget.test.tsx b/met-web/tests/unit/components/widgets/MapWidget.test.tsx index af79d1638..2d2dc1afe 100644 --- a/met-web/tests/unit/components/widgets/MapWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/MapWidget.test.tsx @@ -62,6 +62,7 @@ const mockCreateWidget = jest.fn(() => Promise.resolve(mapWidget)); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(mapWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], })); @@ -118,6 +119,7 @@ describe('Map Widget tests', () => { expect(mockCreateWidget).toHaveBeenNthCalledWith(1, { widget_type_id: WidgetType.Map, engagement_id: draftEngagement.id, + title: mapWidget.title, }); expect(getWidgetsMock).toHaveBeenCalledTimes(2); expect(screen.getByText('Upload Shapefile')).toBeVisible(); diff --git a/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx b/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx index d853f11c6..90e5e0185 100644 --- a/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx @@ -30,6 +30,7 @@ const phaseWidgetItem: WidgetItem = { const phasesWidget: Widget = { id: 2, + title: 'Environmental Assessment Process', widget_type_id: WidgetType.Phases, engagement_id: 1, items: [], @@ -58,6 +59,7 @@ const mockCreateWidget = jest.fn(() => Promise.resolve(phasesWidget)); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(phasesWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], })); @@ -112,6 +114,7 @@ describe('Phases widget tests', () => { expect(mockCreateWidget).toHaveBeenNthCalledWith(1, { widget_type_id: WidgetType.Phases, engagement_id: draftEngagement.id, + title: phasesWidget.title, }); expect(getWidgetsMock).toHaveBeenCalledTimes(2); diff --git a/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx b/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx index 7795d18c7..15b6b7137 100644 --- a/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx @@ -44,6 +44,7 @@ const contactWidgetItem: WidgetItem = { const whoIsListeningWidget: Widget = { id: 1, + title: 'Who is Listening', widget_type_id: WidgetType.WhoIsListening, engagement_id: 1, items: [contactWidgetItem], @@ -107,6 +108,7 @@ const mockCreateWidget = jest.fn(() => Promise.resolve(whoIsListeningWidget)); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(whoIsListeningWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], })); @@ -164,6 +166,7 @@ describe('Who is Listening widget tests', () => { expect(mockCreateWidget).toHaveBeenNthCalledWith(1, { widget_type_id: WidgetType.WhoIsListening, engagement_id: draftEngagement.id, + title: whoIsListeningWidget.title, }); expect(getWidgetsMock).toHaveBeenCalledTimes(2); expect(screen.getByText('Add This Contact')).toBeVisible(); From eb0c4da7a113bd5fdda822b9155feb63d6d57cb6 Mon Sep 17 00:00:00 2001 From: djnunez-aot <103138766+djnunez-aot@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:23:39 -0400 Subject: [PATCH 10/18] Subscribe Form Cards (#1966) --- .../src/met_api/models/widgets_subscribe.py | 2 +- .../src/met_api/resources/widget_subscribe.py | 30 +++-- .../services/widget_subscribe_service.py | 7 +- .../Subscribe/EmailListFormDrawer.tsx | 44 ++++++- .../Subscribe/SubscribeContext.tsx | 8 +- .../Subscribe/SubscribeForm.tsx | 57 +++++---- .../Subscribe/SubscribeInfoBlock.tsx | 116 ++++++++++++++++++ .../Subscribe/SubscribeInfoPaper.tsx | 108 ++++++++++++++++ met-web/src/models/subscription.ts | 20 ++- .../src/services/subscriptionService/index.ts | 14 +-- 10 files changed, 347 insertions(+), 59 deletions(-) create mode 100644 met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeInfoBlock.tsx create mode 100644 met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeInfoPaper.tsx diff --git a/met-api/src/met_api/models/widgets_subscribe.py b/met-api/src/met_api/models/widgets_subscribe.py index 9af5417cd..11a1017e2 100644 --- a/met-api/src/met_api/models/widgets_subscribe.py +++ b/met-api/src/met_api/models/widgets_subscribe.py @@ -40,7 +40,7 @@ def get_all_by_type(cls, type_, widget_id): return db.session.query(cls).filter_by(type=type_, widget_id=widget_id).all() @classmethod - def update_widget_events_bulk(cls, update_mappings: list) -> list[WidgetSubscribe]: + def update_widget_subscribes_bulk(cls, update_mappings: list) -> list[WidgetSubscribe]: """Save widget subscribe sorting.""" db.session.bulk_update_mappings(WidgetSubscribe, update_mappings) db.session.commit() diff --git a/met-api/src/met_api/resources/widget_subscribe.py b/met-api/src/met_api/resources/widget_subscribe.py index c9729aff0..180341b22 100644 --- a/met-api/src/met_api/resources/widget_subscribe.py +++ b/met-api/src/met_api/resources/widget_subscribe.py @@ -33,7 +33,7 @@ """ -@cors_preflight('GET, POST, OPTIONS, DELETE') +@cors_preflight('GET, POST, OPTIONS') @API.route('') class WidgetSubscribe(Resource): """Resource for managing a Widget Subscribe.""" @@ -59,17 +59,6 @@ def post(widget_id): except BusinessException as err: return str(err), err.status_code - @staticmethod - @cross_origin(origins=allowedorigins()) - def delete(widget_id, subscribe_id): - """Delete an subscribe .""" - try: - WidgetSubscribeService().delete_subscribe(subscribe_id, widget_id) - response, status = {}, HTTPStatus.OK - except BusinessException as err: - response, status = str(err), err.status_code - return response, status - @cors_preflight('GET,POST,OPTIONS') @API.route('//items', methods=['GET', 'DELETE', 'OPTIONS']) @@ -124,3 +113,20 @@ def patch(widget_id): return WidgetSubscribeSchema().dump(sort_widget_subscribe), HTTPStatus.OK except BusinessException as err: return str(err), err.status_code + + +@cors_preflight('DELETE') +@API.route('/', methods=['DELETE']) +class WidgetEvent(Resource): + """Resource for managing a Widget Events.""" + + @staticmethod + @cross_origin(origins=allowedorigins()) + def delete(widget_id, subscribe_id): + """Delete an subscribe .""" + try: + WidgetSubscribeService().delete_subscribe(subscribe_id, widget_id) + response, status = {}, HTTPStatus.OK + except BusinessException as err: + response, status = str(err), err.status_code + return response, status diff --git a/met-api/src/met_api/services/widget_subscribe_service.py b/met-api/src/met_api/services/widget_subscribe_service.py index 3a8eab822..0224e9b7f 100644 --- a/met-api/src/met_api/services/widget_subscribe_service.py +++ b/met-api/src/met_api/services/widget_subscribe_service.py @@ -115,13 +115,16 @@ def update_subscribe_item(widget_id, subscribe_id, item_id, request_json): raise BusinessException( error='Invalid widgets and subscribe', status_code=HTTPStatus.BAD_REQUEST) + subscribe_item: SubscribeItemsModel = SubscribeItemsModel.find_by_id( item_id) - if subscribe_item.widget_subscribes_id != subscribe_id: + if subscribe_item.widget_subscribe_id != subscribe_id: raise BusinessException( error='Invalid widgets and subscribe', status_code=HTTPStatus.BAD_REQUEST) - WidgetSubscribeService._update_from_dict(subscribe_item, request_json) + + WidgetSubscribeService._update_from_dict( + subscribe_item, request_json) subscribe_item.commit() return SubscribeItemsModel.find_by_id(item_id) diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/EmailListFormDrawer.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/EmailListFormDrawer.tsx index 861fb2b10..8203f7cd8 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/EmailListFormDrawer.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/EmailListFormDrawer.tsx @@ -14,11 +14,10 @@ import { Subscribe_TYPE, SubscribeForm } from 'models/subscription'; import RichTextEditor from 'components/common/RichTextEditor'; import { openNotification } from 'services/notificationService/notificationSlice'; import { getTextFromDraftJsContentState } from 'components/common/RichTextEditor/utils'; -import { postSubscribeForm } from 'services/subscriptionService'; +import { patchSubscribeForm, postSubscribeForm, PatchSubscribeProps } from 'services/subscriptionService'; const schema = yup .object({ - description: yup.string(), call_to_action_type: yup.string(), call_to_action_text: yup .string() @@ -39,23 +38,54 @@ const EmailListDrawer = () => { richEmailListDescription, setRichEmailListDescription, setSubscribe, + subscribeToEdit, + loadSubscribe, } = useContext(SubscribeContext); const [isCreating, setIsCreating] = useState(false); const [initialRichDescription, setInitialRichDescription] = useState(''); + const subscribeItem = subscribeToEdit ? subscribeToEdit.subscribe_items[0] : null; const dispatch = useAppDispatch(); const methods = useForm({ resolver: yupResolver(schema), }); useEffect(() => { - methods.setValue('description', ''); methods.setValue('call_to_action_type', 'link'); methods.setValue('call_to_action_text', 'Click here to sign up'); setInitialRichDescription(richEmailListDescription); }, []); + useEffect(() => { + methods.setValue('call_to_action_type', subscribeItem ? subscribeItem.call_to_action_type : 'link'); + methods.setValue( + 'call_to_action_text', + subscribeItem ? subscribeItem.call_to_action_text : 'Click here to sign up', + ); + setInitialRichDescription(subscribeItem ? subscribeItem.description : richEmailListDescription); + }, [subscribeToEdit]); + const { handleSubmit } = methods; + const updateEmailListForm = async (data: EmailList) => { + const validatedData = await schema.validate(data); + const { call_to_action_type, call_to_action_text } = validatedData; + if (subscribeToEdit && subscribeItem && widget) { + const subscribeUpdatesToPatch = { + description: richEmailListDescription, + call_to_action_type: call_to_action_type, + call_to_action_text: call_to_action_text, + } as PatchSubscribeProps; + + await patchSubscribeForm(widget.id, subscribeToEdit.id, subscribeItem.id, { + ...subscribeUpdatesToPatch, + }); + + loadSubscribe(); + + dispatch(openNotification({ severity: 'success', text: 'EmailListForm was successfully updated' })); + } + }; + const createEmailListForm = async (data: EmailList) => { const validatedData = await schema.validate(data); const { call_to_action_type, call_to_action_text } = validatedData; @@ -73,12 +103,18 @@ const EmailListDrawer = () => { ], }); - setSubscribe((prevWidgetForms: SubscribeForm[]) => [...prevWidgetForms, createdWidgetForm]); + setSubscribe((prevWidgetForms: SubscribeForm[]) => { + const filteredForms = prevWidgetForms.filter((form) => form.type !== Subscribe_TYPE.EMAIL_LIST); + return [...filteredForms, createdWidgetForm]; + }); } dispatch(openNotification({ severity: 'success', text: 'Email list form was successfully created' })); }; const saveForm = async (data: EmailList) => { + if (subscribeToEdit) { + return updateEmailListForm(data); + } return createEmailListForm(data); }; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeContext.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeContext.tsx index ceaf9b0bb..ca9fdf22e 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeContext.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeContext.tsx @@ -2,7 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import { useAppDispatch } from 'hooks'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { Widget, WidgetType } from 'models/widget'; -import { Subscribe, Subscribe_TYPE, SubscribeTypeLabel, SubscribeForm } from 'models/subscription'; +import { Subscribe_TYPE, SubscribeTypeLabel, SubscribeForm } from 'models/subscription'; import { getSubscriptionsForms, sortWidgetSubscribeForms } from 'services/subscriptionService'; import { openNotification } from 'services/notificationService/notificationSlice'; @@ -19,7 +19,7 @@ export interface SubscribeContextProps { setSubscribe: React.Dispatch>; setSubscribeToEdit: React.Dispatch>; handleSubscribeDrawerOpen: (_Subscribe: SubscribeTypeLabel, _open: boolean) => void; - updateWidgetSubscribeSorting: (widget_Subscribe: Subscribe[]) => void; + updateWidgetSubscribeSorting: (widget_Subscribe: SubscribeForm[]) => void; richEmailListDescription: string; setRichEmailListDescription: React.Dispatch>; richFormSignUpDescription: string; @@ -51,7 +51,7 @@ export const SubscribeContext = createContext({ handleSubscribeDrawerOpen: (_Subscribe: SubscribeTypeLabel, _open: boolean) => { /* empty default method */ }, - updateWidgetSubscribeSorting: (widget_Subscribe: Subscribe[]) => { + updateWidgetSubscribeSorting: (widget_Subscribe: SubscribeForm[]) => { /* empty default method */ }, richEmailListDescription: '', @@ -104,7 +104,7 @@ export const SubscribeProvider = ({ children }: { children: JSX.Element | JSX.El loadSubscribe(); }, [widget]); - const updateWidgetSubscribeSorting = async (resortedWidgetSubscribe: Subscribe[]) => { + const updateWidgetSubscribeSorting = async (resortedWidgetSubscribe: SubscribeForm[]) => { if (!widget) { return; } diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx index 90ebfe038..464c13319 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx @@ -5,10 +5,13 @@ import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { SubscribeContext } from './SubscribeContext'; import { Subscribe_TYPE } from 'models/subscription'; import { WidgetTitle } from '../WidgetTitle'; +import { When } from 'react-if'; +import SubscribeInfoBlock from './SubscribeInfoBlock'; const Form = () => { const { handleWidgetDrawerOpen } = useContext(WidgetDrawerContext); - const { handleSubscribeDrawerOpen, widget } = useContext(SubscribeContext); + const { handleSubscribeDrawerOpen, subscribe, widget } = useContext(SubscribeContext); + const subscribeFormExists = subscribe.length > 0; if (!widget) { return null; @@ -20,31 +23,39 @@ const Form = () => { + + + + + The email list will collect email addresses for a mailing list. A "double-opt-in" email will + be sent to confirm the subscription. Only the email addresses that have been double-opted-in + will be on the list. + + + + + The form sign-up will open the pre-defined form. The text and CTA for both are customizable. + + + + + + handleSubscribeDrawerOpen(Subscribe_TYPE.EMAIL_LIST, true)}> + Email List + + + + handleSubscribeDrawerOpen(Subscribe_TYPE.FORM, true)}> + Form Sign-up + + - + - - The email list will collect email addresses for a mailing list. A "double-opt-in" email will be - sent to confirm the subscription. Only the email addresses that have been double-opted-in will - be on the list. - - - - - The form sign-up will open the pre-defined form. The text and CTA for both are customizable. - - - - handleSubscribeDrawerOpen(Subscribe_TYPE.EMAIL_LIST, true)}> - Email List - - - - handleSubscribeDrawerOpen(Subscribe_TYPE.FORM, true)}> - Form Sign-up - + - + + { + const { subscribe, setSubscribe, isLoadingSubscribe, updateWidgetSubscribeSorting, widget } = + useContext(SubscribeContext); + const dispatch = useAppDispatch(); + const debounceUpdateWidgetSubscribeSorting = useRef( + debounce((widgetSubscribeToSort: SubscribeForm[]) => { + updateWidgetSubscribeSorting(widgetSubscribeToSort); + }, 800), + ).current; + + const moveSubscribeForm = (result: DropResult) => { + if (!result.destination) { + return; + } + + const items = reorder(subscribe, result.source.index, result.destination.index); + + setSubscribe(items); + + debounceUpdateWidgetSubscribeSorting(items); + }; + + if (isLoadingSubscribe) { + return ( + + + + + + ); + } + + const handleRemoveSubscribeForm = (subscribeFormId: number) => { + dispatch( + openNotificationModal({ + open: true, + data: { + header: 'Remove SubscribeForm', + subText: [ + { + text: 'You will be removing this subscribeForm from the engagement.', + }, + { + text: 'Do you want to remove this subscribeForm?', + }, + ], + handleConfirm: () => { + removeSubscribeForm(subscribeFormId); + }, + }, + type: 'confirm', + }), + ); + }; + + const removeSubscribeForm = async (subscribeFormId: number) => { + try { + if (widget) { + await deleteSubscribeForm(widget.id, subscribeFormId); + const newSubscribe = subscribe.filter((subscribeForm) => subscribeForm.id !== subscribeFormId); + setSubscribe([...newSubscribe]); + dispatch(openNotification({ severity: 'success', text: 'The subscribeForm was removed successfully' })); + } + } catch (error) { + dispatch( + openNotification({ severity: 'error', text: 'An error occurred while trying to remove subscribeForm' }), + ); + } + }; + + return ( + + + + {subscribe.map((subscribeForm: SubscribeForm, index) => { + return ( + + + + + + + + + + + ); + })} + + + + ); +}; + +export default SubscribeInfoBlock; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeInfoPaper.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeInfoPaper.tsx new file mode 100644 index 000000000..be9727e98 --- /dev/null +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeInfoPaper.tsx @@ -0,0 +1,108 @@ +import React, { useContext } from 'react'; +import { MetParagraph, MetWidgetPaper } from 'components/common'; +import { Grid, IconButton } from '@mui/material'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import HighlightOffIcon from '@mui/icons-material/HighlightOff'; +import EditIcon from '@mui/icons-material/Edit'; +import { When } from 'react-if'; +import { SubscribeForm } from 'models/subscription'; +import { SubscribeContext } from './SubscribeContext'; +import { Editor } from 'react-draft-wysiwyg'; +import { getEditorStateFromRaw } from 'components/common/RichTextEditor/utils'; +import { styled } from '@mui/system'; + +const EditorGrid = styled(Grid)` + padding-top: 0px !important; +`; +export interface SubscribeInfoPaperProps { + subscribeForm: SubscribeForm; + removeSubscribeForm: (_subscribeId: number) => void; +} + +const SubscribeInfoPaper = ({ subscribeForm, removeSubscribeForm, ...rest }: SubscribeInfoPaperProps) => { + const subscribeItem = subscribeForm.subscribe_items[0]; + const { setSubscribeToEdit, handleSubscribeDrawerOpen } = useContext(SubscribeContext); + + function capitalizeFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + return ( + + + + + + + + + + Email List + + + + Description: + + + + + + + + {capitalizeFirstLetter(subscribeItem.call_to_action_type)} + + + + {subscribeItem.call_to_action_text} + + + + + + + { + setSubscribeToEdit(subscribeForm); + handleSubscribeDrawerOpen(subscribeForm.type, true); + }} + /> + + + + removeSubscribeForm(subscribeForm.id)} + sx={{ padding: 1, margin: 0 }} + color="inherit" + aria-label="delete-icon" + > + + + + + + + ); +}; + +export default SubscribeInfoPaper; diff --git a/met-web/src/models/subscription.ts b/met-web/src/models/subscription.ts index ff32b1bab..f42407c4c 100644 --- a/met-web/src/models/subscription.ts +++ b/met-web/src/models/subscription.ts @@ -26,10 +26,24 @@ export const Subscribe_TYPE: { [x: string]: SubscribeTypeLabel } = { }; export interface SubscribeForm { + id: number; + title: string; + type: SubscribeTypeLabel; + sort_index: number; widget_id: number; + created_date: string; + updated_date: string; + subscribe_items: SubscribeFormItem[]; +} + +export interface SubscribeFormItem { + id: number; title?: string; - description?: string; - call_to_action_type?: string; - call_to_action_text?: string; + description: string; + call_to_action_type: 'link' | 'button'; + call_to_action_text: string; form_type: SubscribeTypeLabel; + created_date: string; + updated_date: string; + widget_subscribe: number; } diff --git a/met-web/src/services/subscriptionService/index.ts b/met-web/src/services/subscriptionService/index.ts index 6b811938e..afb213cc5 100644 --- a/met-web/src/services/subscriptionService/index.ts +++ b/met-web/src/services/subscriptionService/index.ts @@ -78,15 +78,9 @@ export const postSubscribeForm = async (widget_id: number, data: PostSubscribePr }; export interface PatchSubscribeProps { - widget_id: number; - title?: string; - type: SubscribeTypeLabel; - items: { - description?: string; - call_to_action_type?: string; - call_to_action_text?: string; - form_type: SubscribeTypeLabel; - }[]; + description?: string; + call_to_action_type?: string; + call_to_action_text?: string; } export const patchSubscribeForm = async ( @@ -133,7 +127,7 @@ export const deleteSubscribeForm = async (widget_id: number, subscribe_id: numbe } }; -export const sortWidgetSubscribeForms = async (widget_id: number, data: Subscribe[]): Promise => { +export const sortWidgetSubscribeForms = async (widget_id: number, data: SubscribeForm[]): Promise => { try { const url = replaceUrl(Endpoints.Subscription.SORT_FORMS, 'widget_id', String(widget_id)); const response = await http.PatchRequest(url, data); From c4b004f5f4765a7f504ec16373e624888cdf8ee4 Mon Sep 17 00:00:00 2001 From: jadmsaadaot <91914654+jadmsaadaot@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:37:07 -0700 Subject: [PATCH 11/18] Fix comments count (#1969) * Fix comments count * fix isort issue * fix lint issues * fix lint issues * fix lint issue --- met-api/src/met_api/constants/user.py | 1 + met-api/src/met_api/models/submission.py | 7 +++++-- met-api/src/met_api/schemas/engagement.py | 17 +++++++---------- met-api/src/met_api/schemas/survey.py | 17 ++++++++--------- met-api/src/met_api/schemas/utils.py | 16 ++++++++++++++++ 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/met-api/src/met_api/constants/user.py b/met-api/src/met_api/constants/user.py index 760192f72..219bcce3c 100644 --- a/met-api/src/met_api/constants/user.py +++ b/met-api/src/met_api/constants/user.py @@ -15,3 +15,4 @@ SYSTEM_USER = 1 +SYSTEM_REVIEWER = 'System' diff --git a/met-api/src/met_api/models/submission.py b/met-api/src/met_api/models/submission.py index 5ccfef073..1ae1fde29 100644 --- a/met-api/src/met_api/models/submission.py +++ b/met-api/src/met_api/models/submission.py @@ -3,14 +3,17 @@ Manages the Submission """ from __future__ import annotations + from datetime import datetime from typing import List + from sqlalchemy import ForeignKey from sqlalchemy.dialects import postgresql from met_api.constants.comment_status import Status -from met_api.models.survey import Survey +from met_api.constants.user import SYSTEM_REVIEWER from met_api.models.participant import Participant +from met_api.models.survey import Survey from met_api.schemas.submission import SubmissionSchema from .base_model import BaseModel @@ -63,7 +66,7 @@ def create(cls, submission: SubmissionSchema, session=None) -> Submission: const_review_date = None else: const_comment_status = Status.Approved.value - const_reviewed_by = 'System' + const_reviewed_by = SYSTEM_REVIEWER const_review_date = datetime.utcnow() new_submission = Submission( diff --git a/met-api/src/met_api/schemas/engagement.py b/met-api/src/met_api/schemas/engagement.py index 804d1e4f2..60f707c4c 100644 --- a/met-api/src/met_api/schemas/engagement.py +++ b/met-api/src/met_api/schemas/engagement.py @@ -7,11 +7,12 @@ from marshmallow import EXCLUDE, Schema, ValidationError, fields, validate, validates_schema -from met_api.constants.engagement_status import Status, SubmissionStatus from met_api.constants.comment_status import Status as CommentStatus +from met_api.constants.engagement_status import Status, SubmissionStatus +from met_api.schemas.engagement_status_block import EngagementStatusBlockSchema from met_api.schemas.engagement_survey import EngagementSurveySchema +from met_api.schemas.utils import count_comments_by_status from met_api.utils.datetime import local_datetime -from met_api.schemas.engagement_status_block import EngagementStatusBlockSchema from .engagement_status import EngagementStatusSchema @@ -61,18 +62,14 @@ def get_submissions_meta_data(self, obj): submissions = obj.surveys[0].submissions return { 'total': len(submissions), - 'pending': self._count_comments_by_status(submissions, CommentStatus.Pending.value), - 'approved': self._count_comments_by_status(submissions, CommentStatus.Approved.value), - 'rejected': self._count_comments_by_status(submissions, CommentStatus.Rejected.value), - 'needs_further_review': self._count_comments_by_status( + 'pending': count_comments_by_status(submissions, CommentStatus.Pending.value), + 'approved': count_comments_by_status(submissions, CommentStatus.Approved.value), + 'rejected': count_comments_by_status(submissions, CommentStatus.Rejected.value), + 'needs_further_review': count_comments_by_status( submissions, CommentStatus.Needs_further_review.value) } - def _count_comments_by_status(self, submissios, status): - return len([submission for submission in submissios - if submission.comment_status_id == status]) - def get_submission_status(self, obj): """Get the submission status of the engagement.""" if obj.status_id == Status.Draft.value or obj.status_id == Status.Scheduled.value: diff --git a/met-api/src/met_api/schemas/survey.py b/met-api/src/met_api/schemas/survey.py index 7d1a2ab18..9773dd9e6 100644 --- a/met-api/src/met_api/schemas/survey.py +++ b/met-api/src/met_api/schemas/survey.py @@ -4,8 +4,11 @@ """ from marshmallow import EXCLUDE, Schema, fields -from .engagement import EngagementSchema + from met_api.constants.comment_status import Status +from met_api.schemas.utils import count_comments_by_status + +from .engagement import EngagementSchema class SurveySchema(Schema): @@ -34,14 +37,10 @@ def get_comments_meta_data(self, obj): """Get the meta data of the comments made in the survey.""" return { 'total': len(obj.submissions), - 'pending': self._count_comments_by_status(obj.submissions, Status.Pending.value), - 'approved': self._count_comments_by_status(obj.submissions, Status.Approved.value), - 'rejected': self._count_comments_by_status(obj.submissions, Status.Rejected.value), - 'needs_further_review': self._count_comments_by_status( + 'pending': count_comments_by_status(obj.submissions, Status.Pending.value), + 'approved': count_comments_by_status(obj.submissions, Status.Approved.value), + 'rejected': count_comments_by_status(obj.submissions, Status.Rejected.value), + 'needs_further_review': count_comments_by_status( obj.submissions, Status.Needs_further_review.value) } - - def _count_comments_by_status(self, submissios, status): - return len([submission for submission in submissios - if submission.comment_status_id == status]) diff --git a/met-api/src/met_api/schemas/utils.py b/met-api/src/met_api/schemas/utils.py index a16e5562c..01361b3dd 100644 --- a/met-api/src/met_api/schemas/utils.py +++ b/met-api/src/met_api/schemas/utils.py @@ -20,6 +20,7 @@ from typing import Tuple from jsonschema import Draft7Validator, RefResolver, SchemaError, draft7_format_checker +from met_api.constants.user import SYSTEM_REVIEWER BASE_URI = 'https://met.gov.bc.ca/.well_known/schemas' @@ -115,3 +116,18 @@ def serialize(errors): for error in errors: error_message.append(error.message) return error_message + + +def count_comments_by_status(submissions, status): + """Count the comments by their status. + + :param submissions: List of submissions + :param status: Status of the comments + :return: Number of comments with the provided status + """ + return len([ + submission + for submission in submissions + if (submission.comment_status_id == status and + submission.reviewed_by != SYSTEM_REVIEWER) + ]) From d8c482b024446d497087c551d8ecf253b312bcd2 Mon Sep 17 00:00:00 2001 From: jadmsaadaot <91914654+jadmsaadaot@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:39:37 -0700 Subject: [PATCH 12/18] fill empty widget title (#1970) * fill empty widget title * update names --- met-api/migrations/versions/db737a0db061_.py | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 met-api/migrations/versions/db737a0db061_.py diff --git a/met-api/migrations/versions/db737a0db061_.py b/met-api/migrations/versions/db737a0db061_.py new file mode 100644 index 000000000..ec56787f0 --- /dev/null +++ b/met-api/migrations/versions/db737a0db061_.py @@ -0,0 +1,43 @@ +""" Fill empty widget titles +Revision ID: db737a0db061 +Revises: df73727dc6d9b7_add_sub_tbl +Create Date: 2023-08-04 14:11:01.993136 +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'db737a0db061' +down_revision = 'df73727dc6d9b7_add_sub_tbl' +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + # Execute an UPDATE statement to set 'title' based on 'widget_type_id' + op.execute(""" + UPDATE widget + SET title = + CASE + WHEN widget_type_id = 1 THEN 'Who is Listening' + WHEN widget_type_id = 2 THEN 'Documents' + WHEN widget_type_id = 3 THEN 'Environmental Assessment Process' + WHEN widget_type_id = 4 THEN 'Sign Up for Updates' + WHEN widget_type_id = 5 THEN 'Events' + WHEN widget_type_id = 6 THEN 'Map' + WHEN widget_type_id = 7 THEN 'Video' + ELSE 'Default Title' -- This is for any widget_type_id not covered above + END + WHERE title IS NULL; + """) + + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + # Execute an UPDATE statement to set 'title' to NULL for all rows + op.execute("UPDATE widget SET title = NULL;") + + # ### end Alembic commands ### From cd2fbd13bd0d514529bf9bf1e84d5dbf8bf47e28 Mon Sep 17 00:00:00 2001 From: jadmsaadaot <91914654+jadmsaadaot@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:22:11 -0700 Subject: [PATCH 13/18] Invalidate widgets when creating widget items (#1964) * invalidate cache when create items * remove deprecated tag for used method * Fix failing tests * remove duplication --- .../src/apiManager/apiSlices/widgets/index.ts | 16 ++++++++++++-- .../EngagementWidgets/Phases/PhasesForm.tsx | 6 ++++-- .../WhoIsListening/WhoIsListeningForm.tsx | 5 +++-- .../widgetService/DocumentService/index.tsx | 3 --- met-web/src/services/widgetService/index.tsx | 17 ++++++++++----- .../components/widgets/PhasesWidget.test.tsx | 21 ++++++++++++++----- .../widgets/WhoIsListeningWidget.test.tsx | 7 +++++++ 7 files changed, 56 insertions(+), 19 deletions(-) diff --git a/met-web/src/apiManager/apiSlices/widgets/index.ts b/met-web/src/apiManager/apiSlices/widgets/index.ts index f5cf5e12a..2578e8bf1 100644 --- a/met-web/src/apiManager/apiSlices/widgets/index.ts +++ b/met-web/src/apiManager/apiSlices/widgets/index.ts @@ -1,6 +1,6 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { AppConfig } from 'config'; -import { Widget } from 'models/widget'; +import { Widget, WidgetItem } from 'models/widget'; import { prepareHeaders } from 'apiManager//apiSlices/util'; // Define a service using a base URL and expected endpoints @@ -33,6 +33,17 @@ export const widgetsApi = createApi({ }), invalidatesTags: ['Widgets'], }), + createWidgetItems: builder.mutation< + WidgetItem[], + { widget_id: number; widget_items_data: Partial[] } + >({ + query: ({ widget_id, widget_items_data }) => ({ + url: `widgets/${widget_id}/items`, + method: 'POST', + body: widget_items_data, + }), + invalidatesTags: ['Widgets'], + }), sortWidgets: builder.mutation({ query: ({ engagementId, widgets }) => ({ url: `widgets/engagement/${engagementId}/sort_index`, @@ -58,7 +69,8 @@ export const widgetsApi = createApi({ export const { useLazyGetWidgetsQuery, useCreateWidgetMutation, + useUpdateWidgetMutation, useSortWidgetsMutation, useDeleteWidgetMutation, - useUpdateWidgetMutation, + useCreateWidgetItemsMutation, } = widgetsApi; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx index fba3305db..170696b46 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx @@ -4,9 +4,9 @@ import { MetLabel, PrimaryButton, SecondaryButton } from 'components/common'; import { EngagementPhases } from 'models/engagementPhases'; import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; -import { postWidgetItem } from 'services/widgetService'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { WidgetType } from 'models/widget'; +import { useCreateWidgetItemsMutation } from 'apiManager/apiSlices/widgets'; import { WidgetTitle } from '../WidgetTitle'; interface ISelectOptions { @@ -22,6 +22,8 @@ const PhasesForm = () => { const [savingWidgetItems, setSavingWidgetItems] = useState(false); const widget = widgets.filter((widget) => widget.widget_type_id === WidgetType.Phases)[0] || null; + const [createWidgetItems] = useCreateWidgetItemsMutation(); + useEffect(() => { if (widget && widget.items.length > 0) { setSelectedOption(options.find((o) => o.id === widget.items[0].widget_data_id) || null); @@ -67,7 +69,7 @@ const PhasesForm = () => { }; try { setSavingWidgetItems(true); - await postWidgetItem(widget.id, widgetsToUpdate); + await createWidgetItems({ widget_id: widget.id, widget_items_data: [widgetsToUpdate] }).unwrap(); await loadWidgets(); dispatch(openNotification({ severity: 'success', text: 'Widget successfully added' })); handleWidgetDrawerOpen(false); diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx index cc46412f9..de612e9a1 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx @@ -4,11 +4,11 @@ import { MetLabel, PrimaryButton, SecondaryButton } from 'components/common'; import { Contact } from 'models/contact'; import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; -import { postWidgetItems } from 'services/widgetService'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { WidgetType } from 'models/widget'; import ContactBlock from './ContactBlock'; import { WhoIsListeningContext } from './WhoIsListeningContext'; +import { useCreateWidgetItemsMutation } from 'apiManager/apiSlices/widgets'; import { WidgetTitle } from '../WidgetTitle'; const WhoIsListeningForm = () => { @@ -18,6 +18,7 @@ const WhoIsListeningForm = () => { const dispatch = useAppDispatch(); const [selectedContact, setSelectedContact] = useState(null); const [savingWidgetItems, setSavingWidgetItems] = useState(false); + const [createWidgetItems] = useCreateWidgetItemsMutation(); const widget = widgets.filter((widget) => widget.widget_type_id === WidgetType.WhoIsListening)[0] || null; useEffect(() => { @@ -69,7 +70,7 @@ const WhoIsListeningForm = () => { }); try { setSavingWidgetItems(true); - await postWidgetItems(widget.id, widgetsToUpdate); + await createWidgetItems({ widget_id: widget.id, widget_items_data: widgetsToUpdate }).unwrap(); await loadWidgets(); dispatch(openNotification({ severity: 'success', text: 'Widgets successfully added' })); handleWidgetDrawerOpen(false); diff --git a/met-web/src/services/widgetService/DocumentService/index.tsx b/met-web/src/services/widgetService/DocumentService/index.tsx index a1843a6f3..75e612772 100644 --- a/met-web/src/services/widgetService/DocumentService/index.tsx +++ b/met-web/src/services/widgetService/DocumentService/index.tsx @@ -3,9 +3,6 @@ import { DocumentItem, DocumentType } from 'models/document'; import Endpoints from 'apiManager/endpoints'; import { replaceAllInURL, replaceUrl } from 'helper'; -/** - * @deprecated The method was replaced by Redux RTK query to have caching behaviour - */ export const fetchDocuments = async (widget_id: number): Promise => { try { const url = replaceUrl(Endpoints.Documents.GET_LIST, 'widget_id', String(widget_id)); diff --git a/met-web/src/services/widgetService/index.tsx b/met-web/src/services/widgetService/index.tsx index 4dd2988a2..7bff37665 100644 --- a/met-web/src/services/widgetService/index.tsx +++ b/met-web/src/services/widgetService/index.tsx @@ -13,6 +13,9 @@ interface PostWidget { widget_type_id: number; engagement_id: number; } +/** + * @deprecated The method was replaced by Redux RTK query to have caching behaviour + */ export const postWidget = async (engagement_id: number, data: PostWidget): Promise => { try { const url = replaceUrl(Endpoints.Widgets.CREATE, 'engagement_id', String(engagement_id)); @@ -30,11 +33,9 @@ interface PostWidgetItemRequest { widget_id: number; widget_data_id: number; } -export const postWidgetItem = async (widget_id: number, data: PostWidgetItemRequest): Promise => { - const result = await postWidgetItems(widget_id, [data]); - return result[0]; -}; - +/** + * @deprecated The method was replaced by Redux RTK query to have caching behaviour + */ export const postWidgetItems = async (widget_id: number, data: PostWidgetItemRequest[]): Promise => { try { const url = replaceUrl(Endpoints.Widget_items.CREATE, 'widget_id', String(widget_id)); @@ -48,6 +49,9 @@ export const postWidgetItems = async (widget_id: number, data: PostWidgetItemReq } }; +/** + * @deprecated The method was replaced by Redux RTK query to have caching behaviour + */ export const removeWidget = async (engagement_id: number, widget_id: number): Promise => { try { const url = replaceAllInURL({ @@ -67,6 +71,9 @@ export const removeWidget = async (engagement_id: number, widget_id: number): Pr } }; +/** + * @deprecated The method was replaced by Redux RTK query to have caching behaviour + */ export const sortWidgets = async (engagement_id: number, data: Widget[]): Promise => { try { const url = replaceUrl(Endpoints.Widgets.SORT, 'engagement_id', String(engagement_id)); diff --git a/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx b/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx index 90e5e0185..f6976d1d5 100644 --- a/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx @@ -56,9 +56,16 @@ jest.mock('components/map', () => () => { }); const mockCreateWidget = jest.fn(() => Promise.resolve(phasesWidget)); +const mockCreateWidgetItems = jest.fn(() => Promise.resolve([phaseWidgetItem])); +const mockCreateWidgetItemsTrigger = jest.fn(() => { + return { + unwrap: mockCreateWidgetItems, + }; +}); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useCreateWidgetItemsMutation: () => [mockCreateWidgetItemsTrigger], useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(phasesWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], @@ -77,7 +84,7 @@ describe('Phases widget tests', () => { setupEnv(); }); - test('Phases widget is created when option is clicked', async () => { + test.only('Phases widget is created when option is clicked', async () => { useParamsMock.mockReturnValue({ engagementId: '1' }); getEngagementMock.mockReturnValueOnce( Promise.resolve({ @@ -86,9 +93,8 @@ describe('Phases widget tests', () => { }), ); getWidgetsMock.mockReturnValueOnce(Promise.resolve([])); - const postWidgetItemMock = jest.spyOn(widgetService, 'postWidgetItem'); mockCreateWidget.mockReturnValue(Promise.resolve(phasesWidget)); - postWidgetItemMock.mockReturnValue(Promise.resolve(phaseWidgetItem)); + mockCreateWidgetItems.mockReturnValue(Promise.resolve([phaseWidgetItem])); render(); await waitFor(() => { @@ -132,9 +138,14 @@ describe('Phases widget tests', () => { expect(saveWidgetButton).not.toBeVisible(); }); - expect(postWidgetItemMock).toHaveBeenNthCalledWith(1, phasesWidget.id, { + expect(mockCreateWidgetItemsTrigger).toHaveBeenNthCalledWith(1, { widget_id: phasesWidget.id, - widget_data_id: 0, + widget_items_data: [ + { + widget_id: phasesWidget.id, + widget_data_id: 0, + }, + ], }); }); }); diff --git a/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx b/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx index 15b6b7137..a64b51005 100644 --- a/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx @@ -105,9 +105,16 @@ jest.mock('@hello-pangea/dnd', () => ({ })); const mockCreateWidget = jest.fn(() => Promise.resolve(whoIsListeningWidget)); +const mockCreateWidgetItems = jest.fn(() => Promise.resolve(contactWidgetItem)); +const mockCreateWidgetItemsTrigger = jest.fn(() => { + return { + unwrap: mockCreateWidgetItems, + }; +}); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useCreateWidgetItemsMutation: () => [mockCreateWidgetItemsTrigger], useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(whoIsListeningWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], From 91368fb61b8b9523a8bfa731877389d78c684a9f Mon Sep 17 00:00:00 2001 From: jadmsaadaot <91914654+jadmsaadaot@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:22:21 -0700 Subject: [PATCH 14/18] Fix update survey block in engagement form (#1967) --- met-web/src/components/engagement/form/ActionContext.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/met-web/src/components/engagement/form/ActionContext.tsx b/met-web/src/components/engagement/form/ActionContext.tsx index c5c8d1f75..3b1b428db 100644 --- a/met-web/src/components/engagement/form/ActionContext.tsx +++ b/met-web/src/components/engagement/form/ActionContext.tsx @@ -215,8 +215,8 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { setSaving(true); try { const uploadedBannerImageFileName = await handleUploadBannerImage(); - const state = { ...savedEngagement }; - const engagementEditsToPatch = updatedDiff(state, { + + const engagementEditsToPatch = updatedDiff(savedEngagement, { ...engagement, banner_filename: uploadedBannerImageFileName, }) as PatchEngagementRequest; @@ -229,6 +229,9 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { const updatedEngagement = await patchEngagement({ ...engagementEditsToPatch, id: Number(engagementId), + status_block: engagement.status_block?.filter((_, index) => { + return engagementEditsToPatch.status_block?.[index]; + }), }); setEngagement(updatedEngagement); dispatch(openNotification({ severity: 'success', text: 'Engagement Updated Successfully' })); From dbbdf2aac12f2a8d207df126052e4e4522113731 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT <90332175+VineetBala-AOT@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:51:28 -0700 Subject: [PATCH 15/18] Changes for subscribe widget public view (#1971) * updates on engagement publish * update email template * adding action drop down * updated changes for User Management * access user details page for users without a role * updating variable name * updating the schema * updating as per review comments * updating schema * adding changes for clone and delete * fixing linting * update * fix for report setting on analytics * fixing lint * Changes for subscribe widget public view * adding changes for the model * updating link --- .../widgets/Subscribe/SubscribeWidget.tsx | 92 +++++++++++++++---- met-web/src/models/subscription.ts | 7 ++ 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx b/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx index e22da3084..a49ff8cc6 100644 --- a/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx @@ -1,7 +1,7 @@ -import React, { useState, useContext } from 'react'; +import React, { useEffect, useState, useContext } from 'react'; import { MetBody, MetHeader2, MetLabel, MetPaper, MetParagraph, PrimaryButton } from 'components/common'; import { ActionContext } from '../../ActionContext'; -import { Grid, Divider, Link, Typography, Box, RadioGroup, Radio, FormControlLabel } from '@mui/material'; +import { Grid, Divider, Link, Typography, Box, RadioGroup, Radio, FormControlLabel, Skeleton } from '@mui/material'; import { useAppDispatch } from 'hooks'; import { openNotificationModal } from 'services/notificationModalService/notificationModalSlice'; import EmailModal from 'components/common/Modals/EmailModal'; @@ -10,15 +10,24 @@ import { createSubscription } from 'services/subscriptionService'; import { EmailVerificationType } from 'models/emailVerification'; import { SubscriptionType } from 'constants/subscriptionType'; import { Widget } from 'models/widget'; +import { getSubscriptionsForms } from 'services/subscriptionService'; +import { WidgetType } from 'models/widget'; +import { openNotification } from 'services/notificationService/notificationSlice'; +import { Subscribe_TYPE, SubscribeForm, CallToActionType } from 'models/subscription'; +import { When } from 'react-if'; +import { getTextFromDraftJsContentState } from 'components/common/RichTextEditor/utils'; const SubscribeWidget = ({ widget }: { widget: Widget }) => { const dispatch = useAppDispatch(); - const { savedEngagement, engagementMetadata } = useContext(ActionContext); + const { savedEngagement, engagementMetadata, widgets } = useContext(ActionContext); const defaultType = engagementMetadata.project_id ? SubscriptionType.PROJECT : SubscriptionType.ENGAGEMENT; const [email, setEmail] = useState(''); const [open, setOpen] = useState(false); const [isSaving, setIsSaving] = useState(false); const [subscriptionType, setSubscriptionType] = useState(''); + const subscribeWidget = widgets.find((widget) => widget.widget_type_id === WidgetType.Subscribe); + const [subscribeItems, setSubscribeItems] = useState([]); + const [isLoadingSubscribeItems, setIsLoadingSubscribeItems] = useState(true); const sendEmail = async () => { try { @@ -90,10 +99,41 @@ const SubscribeWidget = ({ widget }: { widget: Widget }) => { } }; + const loadSubscribeItems = async () => { + if (!subscribeWidget) { + return; + } + try { + setIsLoadingSubscribeItems(true); + const loadedSubscribe = await getSubscriptionsForms(subscribeWidget.id); + setSubscribeItems(loadedSubscribe); + setIsLoadingSubscribeItems(false); + } catch (error) { + dispatch( + openNotification({ + severity: 'error', + text: 'An error occurred while trying to load the Subscribe Items', + }), + ); + } + }; + + useEffect(() => { + loadSubscribeItems(); + }, [widgets]); + const handleSubscriptionChange = (type: string) => { setSubscriptionType(type); }; + if (isLoadingSubscribeItems) { + return ( + + + + ); + } + return ( { } /> - - - {widget.title} - - - - - If you are interested in getting updates on public engagements at the EAO, you can sign up - below: - - - - setOpen(true)} sx={{ width: '100%' }}> - Sign Up for Updates - - - + {subscribeItems?.map((item) => { + return ( + + + + {widget.title} + + + + {getTextFromDraftJsContentState(item.subscribe_items[0].description)} + + + + setOpen(true)} sx={{ width: '100%' }}> + {item.subscribe_items[0].call_to_action_text} + + + + setOpen(true)} sx={{ cursor: 'pointer' }}> + {item.subscribe_items[0].call_to_action_text} + + + + + + ); + })} ); }; diff --git a/met-web/src/models/subscription.ts b/met-web/src/models/subscription.ts index f42407c4c..69d30eeef 100644 --- a/met-web/src/models/subscription.ts +++ b/met-web/src/models/subscription.ts @@ -1,5 +1,7 @@ export type SubscribeTypeLabel = 'EMAIL_LIST' | 'FORM'; +export type CallToActionTypes = 'link' | 'button'; + export interface Subscription { engagement_id: number; email_address: string; @@ -25,6 +27,11 @@ export const Subscribe_TYPE: { [x: string]: SubscribeTypeLabel } = { FORM: 'FORM', }; +export const CallToActionType: { [x: string]: CallToActionTypes } = { + LINK: 'link', + BUTTON: 'button', +}; + export interface SubscribeForm { id: number; title: string; From 8a68a6683e6c495adeaba957ef991af0402255f3 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT <90332175+VineetBala-AOT@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:25:08 -0700 Subject: [PATCH 16/18] Removing return to engagement link (#1974) * updates on engagement publish * update email template * adding action drop down * updated changes for User Management * access user details page for users without a role * updating variable name * updating the schema * updating as per review comments * updating schema * adding changes for clone and delete * fixing linting * update * fix for report setting on analytics * fixing lint * removing return to engagement link --- met-web/src/components/survey/edit/FormWrapped.tsx | 8 +------- met-web/src/components/survey/submit/EngagementLink.tsx | 7 +------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/met-web/src/components/survey/edit/FormWrapped.tsx b/met-web/src/components/survey/edit/FormWrapped.tsx index 9f727b171..e5ffd1873 100644 --- a/met-web/src/components/survey/edit/FormWrapped.tsx +++ b/met-web/src/components/survey/edit/FormWrapped.tsx @@ -1,6 +1,5 @@ import React, { useContext } from 'react'; -import { Grid, Link as MuiLink, Skeleton } from '@mui/material'; -import { Link } from 'react-router-dom'; +import { Grid, Skeleton } from '@mui/material'; import { Banner } from 'components/banner/Banner'; import { EditForm } from './EditForm'; import { ActionContext } from './ActionContext'; @@ -36,11 +35,6 @@ const FormWrapped = () => { alignItems="flex-start" m={{ lg: '0 8em 1em 3em', md: '2em', xs: '1em' }} > - - - {`<< Return to ${savedEngagement.name} Engagement`} - - diff --git a/met-web/src/components/survey/submit/EngagementLink.tsx b/met-web/src/components/survey/submit/EngagementLink.tsx index 72bacd1ee..e50ed6902 100644 --- a/met-web/src/components/survey/submit/EngagementLink.tsx +++ b/met-web/src/components/survey/submit/EngagementLink.tsx @@ -9,7 +9,7 @@ import { openNotificationModal } from 'services/notificationModalService/notific export const EngagementLink = () => { const dispatch = useDispatch(); - const { savedEngagement, isEngagementLoading, slug } = useContext(ActionContext); + const { savedEngagement, isEngagementLoading } = useContext(ActionContext); const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated); const navigate = useNavigate(); @@ -55,11 +55,6 @@ export const EngagementLink = () => { return ( <> - - - {`<< Return to ${savedEngagement.name} Engagement`} - - Date: Tue, 8 Aug 2023 12:53:09 -0700 Subject: [PATCH 17/18] auth check added (#1968) --- met-api/src/met_api/resources/engagement.py | 3 +- met-api/src/met_api/resources/survey.py | 1 - met-api/src/met_api/services/authorization.py | 36 +++++----- .../met_api/services/engagement_service.py | 23 ++++--- .../src/met_api/services/survey_service.py | 44 ++++++++++-- met-api/src/met_api/utils/roles.py | 3 +- met-api/tests/unit/api/test_engagement.py | 27 ++++++++ met-api/tests/unit/api/test_survey.py | 67 +++++++++++++++++-- .../tests/unit/services/test_engagement.py | 18 ++--- met-api/tests/unit/services/test_survey.py | 7 +- met-api/tests/utilities/factory_scenarios.py | 40 ++++++++++- met-api/tests/utilities/factory_utils.py | 5 +- 12 files changed, 224 insertions(+), 50 deletions(-) diff --git a/met-api/src/met_api/resources/engagement.py b/met-api/src/met_api/resources/engagement.py index 4a38c9d75..05b028104 100644 --- a/met-api/src/met_api/resources/engagement.py +++ b/met-api/src/met_api/resources/engagement.py @@ -46,8 +46,7 @@ class Engagement(Resource): def get(engagement_id): """Fetch a single engagement matching the provided id.""" try: - user_id = TokenInfo.get_id() - engagement_record = EngagementService().get_engagement(engagement_id, user_id) + engagement_record = EngagementService().get_engagement(engagement_id) if engagement_record: return engagement_record, HTTPStatus.OK diff --git a/met-api/src/met_api/resources/survey.py b/met-api/src/met_api/resources/survey.py index f726b1afd..25cb1ce5c 100644 --- a/met-api/src/met_api/resources/survey.py +++ b/met-api/src/met_api/resources/survey.py @@ -49,7 +49,6 @@ def get(survey_id): try: user_id = TokenInfo.get_id() if user_id: - # authenticated users have access to any survey/engagement status survey_record = SurveyService().get(survey_id) else: survey_record = SurveyService().get_open(survey_id) diff --git a/met-api/src/met_api/services/authorization.py b/met-api/src/met_api/services/authorization.py index 92d2e79e2..31144e870 100644 --- a/met-api/src/met_api/services/authorization.py +++ b/met-api/src/met_api/services/authorization.py @@ -15,28 +15,34 @@ def check_auth(**kwargs): """Check if user is authorized to perform action on the service.""" user_from_context: UserContext = kwargs['user_context'] - token_roles = user_from_context.roles - permitted_roles = kwargs.get('one_of_roles', []) - has_valid_roles = bool(set(token_roles) & set(permitted_roles)) + token_roles = set(user_from_context.roles) + permitted_roles = set(kwargs.get('one_of_roles', [])) + has_valid_roles = token_roles & permitted_roles if has_valid_roles: return - if MembershipType.TEAM_MEMBER.name in permitted_roles: + + team_permitted_roles = {MembershipType.TEAM_MEMBER.name, MembershipType.REVIEWER.name} & permitted_roles + + if team_permitted_roles: # check if he is a member of particular engagement. - is_a_member = _has_team_membership(kwargs, user_from_context) - if is_a_member: + has_valid_team_access = _has_team_membership(kwargs, user_from_context, team_permitted_roles) + if has_valid_team_access: return abort(403) -def _has_team_membership(kwargs, user_from_context) -> bool: - eng_id = kwargs.get('engagement_id', None) - external_id = user_from_context.sub - user = StaffUserModel.get_user_by_external_id(external_id) - if not eng_id or not user: +def _has_team_membership(kwargs, user_from_context, team_permitted_roles) -> bool: + eng_id = kwargs.get('engagement_id') + + if not eng_id: + return False + + user = StaffUserModel.get_user_by_external_id(user_from_context.sub) + + if not user: return False + memberships = MembershipModel.find_by_engagement_and_user_id(eng_id, user.id) - # TODO when multiple memberships are supported , iterate list and check role. - if memberships and memberships[0].type == MembershipType.TEAM_MEMBER: - return True - return False + + return any(membership.type.name in team_permitted_roles for membership in memberships) diff --git a/met-api/src/met_api/services/engagement_service.py b/met-api/src/met_api/services/engagement_service.py index 077743e44..eec2542b9 100644 --- a/met-api/src/met_api/services/engagement_service.py +++ b/met-api/src/met_api/services/engagement_service.py @@ -31,26 +31,33 @@ class EngagementService: otherdateformat = '%Y-%m-%d' @staticmethod - def get_engagement(engagement_id, user_id) -> EngagementSchema: + def get_engagement(engagement_id) -> EngagementSchema: """Get Engagement by the id.""" engagement_model: EngagementModel = EngagementModel.find_by_id(engagement_id) if engagement_model: - if user_id is None \ + if TokenInfo.get_id() is None \ and engagement_model.status_id not in (Status.Published.value, Status.Closed.value): # Non authenticated users only have access to published and closed engagements return None + if engagement_model.status_id in (Status.Draft.value, Status.Scheduled.value): + one_of_roles = ( + MembershipType.TEAM_MEMBER.name, + Role.VIEW_ENGAGEMENT.value + ) + authorization.check_auth(one_of_roles=one_of_roles, engagement_id=engagement_id) + engagement = EngagementSchema().dump(engagement_model) engagement['banner_url'] = ObjectStorageService.get_url(engagement_model.banner_filename) return engagement @classmethod def get_engagements_paginated( - cls, - external_user_id, - pagination_options: PaginationOptions, - search_options=None, - include_banner_url=False, + cls, + external_user_id, + pagination_options: PaginationOptions, + search_options=None, + include_banner_url=False, ): """Get engagements paginated.""" user_roles = TokenInfo.get_user_roles() @@ -199,7 +206,7 @@ def _save_or_update_eng_block(engagement_id, status_block): # see if there is one existing for the status ;if not create one survey_status = survey_block.get('survey_status') survey_block = survey_block.get('block_text') - status_block: EngagementStatusBlockModel = EngagementStatusBlockModel.\ + status_block: EngagementStatusBlockModel = EngagementStatusBlockModel. \ get_by_status(engagement_id, survey_status) if status_block: status_block.block_text = survey_block diff --git a/met-api/src/met_api/services/survey_service.py b/met-api/src/met_api/services/survey_service.py index a3a1e6dae..8db1f1923 100644 --- a/met-api/src/met_api/services/survey_service.py +++ b/met-api/src/met_api/services/survey_service.py @@ -13,11 +13,10 @@ from met_api.schemas.survey import SurveySchema from met_api.services import authorization from met_api.services.membership_service import MembershipService -from met_api.services.report_setting_service import ReportSettingService from met_api.services.object_storage_service import ObjectStorageService +from met_api.services.report_setting_service import ReportSettingService from met_api.utils.roles import Role from met_api.utils.token_info import TokenInfo - from ..exceptions.business_exception import BusinessException @@ -29,8 +28,32 @@ class SurveyService: @classmethod def get(cls, survey_id): - """Get survey by the id.""" - survey_model: SurveyModel = SurveyModel.find_by_id(survey_id) + """Get survey by the ID.""" + survey_model = SurveyModel.find_by_id(survey_id) + eng_id = None + one_of_roles = (Role.VIEW_SURVEYS.value,) + skip_auth = False + + if survey_model.is_hidden: + # Only Admins can view hidden surveys. + one_of_roles = (Role.VIEW_ALL_SURVEYS.value,) + elif survey_model.engagement_id: + engagement_model = EngagementModel.find_by_id(survey_model.engagement_id) + if engagement_model: + eng_id = engagement_model.id + if engagement_model.status_id == Status.Published.value: + # Published Engagement anyone can access. + skip_auth = True + else: + one_of_roles = ( + MembershipType.TEAM_MEMBER.name, + MembershipType.REVIEWER.name, + Role.VIEW_SURVEYS.value + ) + + if not skip_auth: + authorization.check_auth(one_of_roles=one_of_roles, engagement_id=eng_id) + survey = SurveySchema().dump(survey_model) return survey @@ -112,6 +135,13 @@ def clone(cls, data, survey_id): if not survey_to_clone: raise KeyError('Survey to clone was not found') + eng_id = None + if engagement_id := data.get('engagement_id', None): + engagement_model = EngagementModel.find_by_id(engagement_id) + eng_id = getattr(engagement_model, 'id', None) + + authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, + Role.CLONE_SURVEY.value), engagement_id=eng_id) cloned_survey = SurveyModel.create_survey({ 'name': data.get('name'), @@ -143,7 +173,7 @@ def update(cls, data: SurveySchema): engagement_id = survey.get('engagement_id', None) authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, - Role.EDIT_ALL_SURVEYS.value), engagement_id=engagement_id) + Role.EDIT_SURVEY.value), engagement_id=engagement_id) # check if user has edit all surveys access to edit template surveys as well user_roles = TokenInfo.get_user_roles() @@ -189,6 +219,8 @@ def validate_create_fields(data): def link(cls, survey_id, engagement_id): """Update survey.""" cls.validate_link_fields(survey_id, engagement_id) + authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, + Role.EDIT_SURVEY.value), engagement_id=engagement_id) return SurveyModel.link_survey(survey_id, engagement_id) @classmethod @@ -210,6 +242,8 @@ def validate_link_fields(cls, survey_id, engagement_id): def unlink(cls, survey_id, engagement_id): """Unlink survey.""" cls.validate_unlink_fields(survey_id, engagement_id) + authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, + Role.EDIT_SURVEY.value), engagement_id=engagement_id) return SurveyModel.unlink_survey(survey_id) @classmethod diff --git a/met-api/src/met_api/utils/roles.py b/met-api/src/met_api/utils/roles.py index 40e332c7c..28648136f 100644 --- a/met-api/src/met_api/utils/roles.py +++ b/met-api/src/met_api/utils/roles.py @@ -30,6 +30,7 @@ class Role(Enum): CREATE_ENGAGEMENT = 'create_engagement' VIEW_SURVEYS = 'view_surveys' CREATE_SURVEY = 'create_survey' + EDIT_SURVEY = 'edit_survey' CLONE_SURVEY = 'clone_survey' PUBLISH_ENGAGEMENT = 'publish_engagement' VIEW_ENGAGEMENT = 'view_engagement' @@ -41,7 +42,7 @@ class Role(Enum): ACCESS_DASHBOARD = 'access_dashboard' VIEW_MEMBERS = 'view_members' EDIT_MEMBERS = 'edit_members' - VIEW_ALL_SURVEYS = 'view_all_surveys' + VIEW_ALL_SURVEYS = 'view_all_surveys' # Super user can view all kind of surveys including hidden EDIT_ALL_SURVEYS = 'edit_all_surveys' EDIT_DRAFT_ENGAGEMENT = 'edit_draft_engagement' EDIT_SCHEDULED_ENGAGEMENT = 'edit_scheduled_engagement' diff --git a/met-api/tests/unit/api/test_engagement.py b/met-api/tests/unit/api/test_engagement.py index b4c84af5d..a83bbc2a7 100644 --- a/met-api/tests/unit/api/test_engagement.py +++ b/met-api/tests/unit/api/test_engagement.py @@ -18,6 +18,7 @@ """ import copy import json +from http import HTTPStatus import pytest from faker import Faker @@ -107,6 +108,32 @@ def test_get_engagements(client, jwt, session, engagement_info): # pylint:disab assert created_eng.get('content') == rv.json.get('content') +@pytest.mark.parametrize('engagement_info', [TestEngagementInfo.engagement_draft]) +def test_get_engagements_reviewer(client, jwt, session, engagement_info): # pylint:disable=unused-argument + """Assert reviewers access on an engagement.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + rv = client.post('/api/engagements/', data=json.dumps(engagement_info), + headers=headers, content_type=ContentType.JSON.value) + assert rv.status_code == HTTPStatus.OK.value + created_eng = rv.json + eng_id = created_eng.get('id') + staff_1 = dict(TestUserInfo.user_staff_1) + user = factory_staff_user_model(user_info=staff_1) + claims = copy.deepcopy(TestJwtClaims.reviewer_role.value) + claims['sub'] = str(user.external_id) + headers = factory_auth_header(jwt=jwt, claims=claims) + rv = client.get(f'/api/engagements/{eng_id}', + headers=headers, content_type=ContentType.JSON.value) + assert rv.status_code == HTTPStatus.FORBIDDEN.value + + factory_membership_model(user_id=user.id, engagement_id=eng_id, member_type='REVIEWER') + + # Reveiwer has no access to draft engagement + rv = client.get(f'/api/engagements/{eng_id}', + headers=headers, content_type=ContentType.JSON.value) + assert rv.status_code == HTTPStatus.FORBIDDEN.value + + @pytest.mark.parametrize('engagement_info', [TestEngagementInfo.engagement1]) def test_search_engagements_by_status(client, jwt, session, engagement_info): # pylint:disable=unused-argument diff --git a/met-api/tests/unit/api/test_survey.py b/met-api/tests/unit/api/test_survey.py index 28a9c1df6..3b4e5e42f 100644 --- a/met-api/tests/unit/api/test_survey.py +++ b/met-api/tests/unit/api/test_survey.py @@ -16,18 +16,24 @@ Test-Suite to ensure that the /Engagement endpoint is working as expected. """ +import copy import json from http import HTTPStatus import pytest from flask import current_app +from met_api.constants.engagement_status import Status +from met_api.models.engagement import Engagement as EngagementModel +from met_api.models.membership import Membership as MembershipModel from met_api.models.tenant import Tenant as TenantModel from met_api.utils.constants import TENANT_ID_HEADER -from met_api.utils.enums import ContentType -from tests.utilities.factory_scenarios import TestJwtClaims, TestSurveyInfo, TestTenantInfo +from met_api.utils.enums import ContentType, MembershipStatus +from tests.utilities.factory_scenarios import TestJwtClaims, TestSurveyInfo, TestTenantInfo, TestUserInfo from tests.utilities.factory_utils import ( - factory_auth_header, factory_engagement_model, factory_survey_model, factory_tenant_model, set_global_tenant) + factory_auth_header, factory_engagement_model, factory_membership_model, factory_staff_user_model, + factory_survey_model, factory_tenant_model, set_global_tenant) + surveys_url = '/api/surveys/' @@ -105,7 +111,7 @@ def test_survey_link(client, jwt, session): # pylint:disable=unused-argument """Assert that a survey can be POSTed.""" survey = factory_survey_model() survey_id = survey.id - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.no_role) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) eng = factory_engagement_model() eng_id = eng.id @@ -151,6 +157,59 @@ def test_get_hidden_survey_for_admins(client, jwt, session): # pylint:disable=u assert rv.json.get('total') == 1 +def test_get_survey_for_reviewer(client, jwt, session): # pylint:disable=unused-argument + """Assert reviewers different permission.""" + staff_1 = dict(TestUserInfo.user_staff_1) + user = factory_staff_user_model(user_info=staff_1) + claims = copy.deepcopy(TestJwtClaims.reviewer_role.value) + claims['sub'] = str(user.external_id) + headers = factory_auth_header(jwt=jwt, claims=claims) + set_global_tenant() + survey1 = factory_survey_model(TestSurveyInfo.survey1) + + # Attempt to access unlinked survey + rv = client.get(f'{surveys_url}{survey1.id}', + headers=headers, content_type=ContentType.JSON.value) + assert rv.status_code == 403 + + # Link to a draft engagement + eng: EngagementModel = factory_engagement_model(status=Status.Draft.value) + survey1.engagement_id = eng.id + survey1.commit() + + # Attempt to access survey linked to draft engagement + rv = client.get(f'{surveys_url}{survey1.id}', + headers=headers, content_type=ContentType.JSON.value) + assert rv.status_code == 403 + + # Add user as a reviewer in the team + factory_membership_model(user_id=user.id, engagement_id=eng.id, member_type='REVIEWER') + + # Assert Reviewer can see the survey since he is added to the team. + rv = client.get(f'{surveys_url}{survey1.id}', + headers=headers, content_type=ContentType.JSON.value) + assert rv.status_code == 200 + + # Deactivate membership + membership_model: MembershipModel = MembershipModel.find_by_engagement_and_user_id(eng.id, user.id) + membership_model[0].status = MembershipStatus.INACTIVE.value + membership_model[0].commit() + + rv = client.get(f'{surveys_url}{survey1.id}', + headers=headers, content_type=ContentType.JSON.value) + # Verify reviewer lost access after being removed from the team + assert rv.status_code == 403 + + # Publish the engagement + eng.status_id = Status.Published.value + eng.commit() + rv = client.get(f'{surveys_url}{survey1.id}', + headers=headers, content_type=ContentType.JSON.value) + + # Assert user can access the survey even when he is removed from the team since its published. + assert rv.status_code == 200 + + def test_get_hidden_survey_for_team_member(client, jwt, session): # pylint:disable=unused-argument """Assert that a hidden survey cannot be fetched by team members.""" headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.team_member_role) diff --git a/met-api/tests/unit/services/test_engagement.py b/met-api/tests/unit/services/test_engagement.py index 671722260..efc931973 100644 --- a/met-api/tests/unit/services/test_engagement.py +++ b/met-api/tests/unit/services/test_engagement.py @@ -21,20 +21,21 @@ from met_api.services import authorization from met_api.services.engagement_service import EngagementService -from tests.utilities.factory_scenarios import TestEngagementInfo, TestUserInfo -from tests.utilities.factory_utils import factory_engagement_model +from tests.utilities.factory_scenarios import TestEngagementInfo, TestJwtClaims +from tests.utilities.factory_utils import factory_engagement_model, factory_staff_user_model, patch_token_info fake = Faker() date_format = '%Y-%m-%d' -def test_create_engagement(session): # pylint:disable=unused-argument +def test_create_engagement(session, monkeypatch): # pylint:disable=unused-argument """Assert that an Org can be created.""" - user_id = TestUserInfo.user['id'] engagement_data = TestEngagementInfo.engagement1 saved_engagament = EngagementService().create_engagement(engagement_data) # fetch the engagement with id and assert - fetched_engagement = EngagementService().get_engagement(saved_engagament.id, user_id) + factory_staff_user_model() + patch_token_info(TestJwtClaims.staff_admin_role, monkeypatch) + fetched_engagement = EngagementService().get_engagement(saved_engagament.id) assert fetched_engagement.get('id') == saved_engagament.id assert fetched_engagement.get('name') == engagement_data.get('name') assert fetched_engagement.get('description') == engagement_data.get('description') @@ -42,13 +43,14 @@ def test_create_engagement(session): # pylint:disable=unused-argument assert fetched_engagement.get('end_date') -def test_create_engagement_with_survey_block(session): # pylint:disable=unused-argument +def test_create_engagement_with_survey_block(session, monkeypatch): # pylint:disable=unused-argument """Assert that an Org can be created.""" - user_id = TestUserInfo.user['id'] engagement_data = TestEngagementInfo.engagement2 saved_engagament = EngagementService().create_engagement(engagement_data) + factory_staff_user_model() + patch_token_info(TestJwtClaims.staff_admin_role, monkeypatch) # fetch the engagement with id and assert - fetched_engagement = EngagementService().get_engagement(saved_engagament.id, user_id) + fetched_engagement = EngagementService().get_engagement(saved_engagament.id) assert fetched_engagement.get('id') == saved_engagament.id assert fetched_engagement.get('name') == engagement_data.get('name') assert fetched_engagement.get('description') == engagement_data.get('description') diff --git a/met-api/tests/unit/services/test_survey.py b/met-api/tests/unit/services/test_survey.py index 2cb19731d..455572c55 100644 --- a/met-api/tests/unit/services/test_survey.py +++ b/met-api/tests/unit/services/test_survey.py @@ -17,15 +17,18 @@ """ from met_api.services.survey_service import SurveyService -from tests.utilities.factory_scenarios import TestSurveyInfo +from tests.utilities.factory_scenarios import TestJwtClaims, TestSurveyInfo +from tests.utilities.factory_utils import factory_staff_user_model, patch_token_info -def test_create_survey(session): # pylint:disable=unused-argument +def test_create_survey(session, monkeypatch,): # pylint:disable=unused-argument """Assert that a survey can be created.""" survey_data = { 'name': TestSurveyInfo.survey1.get('name'), 'display': TestSurveyInfo.survey1.get('form_json').get('display'), } + factory_staff_user_model() + patch_token_info(TestJwtClaims.staff_admin_role, monkeypatch) saved_survey = SurveyService().create(survey_data) # fetch the survey with id and assert fetched_survey = SurveyService().get(saved_survey.id) diff --git a/met-api/tests/utilities/factory_scenarios.py b/met-api/tests/utilities/factory_scenarios.py index cf7639c1b..061edf876 100644 --- a/met-api/tests/utilities/factory_scenarios.py +++ b/met-api/tests/utilities/factory_scenarios.py @@ -24,6 +24,8 @@ from met_api.config import get_named_config from met_api.constants.comment_status import Status as CommentStatus from met_api.constants.engagement_status import SubmissionStatus +from met_api.constants.engagement_status import Status as EngagementStatus + from met_api.constants.feedback import CommentType, FeedbackSourceType, RatingType from met_api.constants.widget import WidgetType from met_api.utils.enums import LoginSource @@ -158,7 +160,7 @@ class TestEngagementInfo(dict, Enum): 'banner_url': '', 'created_by': '123', 'updated_by': '123', - 'status': SubmissionStatus.Open.value, + 'status': EngagementStatus.Published.value, 'is_internal': False, 'description': 'My Test Engagement Description', 'rich_description': '"{\"blocks\":[{\"key\":\"2ku94\",\"text\":\"Rich Description Sample\",\"type\":\"unstyled\",\ @@ -168,6 +170,23 @@ class TestEngagementInfo(dict, Enum): \"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}"' } + engagement_draft = { + 'name': fake.name(), + 'start_date': (datetime.today() - timedelta(days=1)).strftime('%Y-%m-%d'), + 'end_date': (datetime.today() + timedelta(days=1)).strftime('%Y-%m-%d'), + 'banner_url': '', + 'created_by': '123', + 'updated_by': '123', + 'status': EngagementStatus.Draft.value, + 'is_internal': False, + 'description': 'My Test Engagement Description', + 'rich_description': '"{\"blocks\":[{\"key\":\"2ku94\",\"text\":\"Rich Description Sample\",\"type\":\"unstyled\",\ + \"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}"', + 'content': 'Content Sample', + 'rich_content': '"{\"blocks\":[{\"key\":\"fclgj\",\"text\":\"Rich Content Sample\",\"type\":\"unstyled\",\"depth\":0,\ + \"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}"' + } + engagement2 = { 'name': fake.name(), 'start_date': (datetime.today() - timedelta(days=1)).strftime('%Y-%m-%d'), @@ -270,7 +289,9 @@ class TestJwtClaims(dict, Enum): 'view_private_engagements', 'create_admin_user', 'view_all_surveys', + 'view_surveys', 'edit_all_surveys', + 'edit_survey', 'view_unapproved_comments', 'clone_survey', 'edit_members', @@ -299,6 +320,23 @@ class TestJwtClaims(dict, Enum): } } + reviewer_role = { + 'iss': CONFIG.JWT_OIDC_TEST_ISSUER, + 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', + 'idp_userid': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', + 'preferred_username': f'{fake.user_name()}@idir', + 'given_name': fake.first_name(), + 'family_name': fake.last_name(), + 'email': 'staff@gov.bc.ca', + 'identity_provider': LoginSource.IDIR.value, + 'realm_access': { + 'roles': [ + 'staff', + 'view_users', + ] + } + } + class TestWidgetInfo(dict, Enum): """Test scenarios of widget.""" diff --git a/met-api/tests/utilities/factory_utils.py b/met-api/tests/utilities/factory_utils.py index cd05dd7b1..b837f4301 100644 --- a/met-api/tests/utilities/factory_utils.py +++ b/met-api/tests/utilities/factory_utils.py @@ -44,7 +44,6 @@ TestReportSettingInfo, TestSubmissionInfo, TestSurveyInfo, TestTenantInfo, TestUserInfo, TestWidgetDocumentInfo, TestWidgetInfo, TestWidgetItemInfo) - CONFIG = get_named_config('testing') fake = Faker() @@ -176,12 +175,12 @@ def factory_participant_model(participant: dict = TestParticipantInfo.participan return participant -def factory_membership_model(user_id, engagement_id, member_type='TEAM_MEMBER'): +def factory_membership_model(user_id, engagement_id, member_type='TEAM_MEMBER', status=MembershipStatus.ACTIVE.value): """Produce a Membership model.""" membership = MembershipModel(user_id=user_id, engagement_id=engagement_id, type=member_type, - status=MembershipStatus.ACTIVE.value) + status=status) membership.created_by_id = user_id membership.save() From 45beb7e7037d87855f13b856f653b056981cf545 Mon Sep 17 00:00:00 2001 From: VineetBala-AOT <90332175+VineetBala-AOT@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:57:05 -0700 Subject: [PATCH 18/18] Removing role check for engagement/survey nav (#1977) * updates on engagement publish * update email template * adding action drop down * updated changes for User Management * access user details page for users without a role * updating variable name * updating the schema * updating as per review comments * updating schema * adding changes for clone and delete * fixing linting * update * fix for report setting on analytics * fixing lint * removing role check for engagement/survey nav --- met-web/src/components/layout/SideNav/SideNavElements.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/met-web/src/components/layout/SideNav/SideNavElements.tsx b/met-web/src/components/layout/SideNav/SideNavElements.tsx index 2446538f8..7c7368cfe 100644 --- a/met-web/src/components/layout/SideNav/SideNavElements.tsx +++ b/met-web/src/components/layout/SideNav/SideNavElements.tsx @@ -14,15 +14,15 @@ export const Routes: Route[] = [ name: 'Engagements', path: '/engagements', base: '/engagements', - authenticated: true, - allowedRoles: [USER_ROLES.VIEW_ENGAGEMENT, USER_ROLES.VIEW_ASSIGNED_ENGAGEMENTS], + authenticated: false, + allowedRoles: [], }, { name: 'Surveys', path: '/surveys', base: '/surveys', - authenticated: true, - allowedRoles: [USER_ROLES.VIEW_SURVEYS], + authenticated: false, + allowedRoles: [], }, { name: 'User Management',