From 534a87260896efce05947c2a9a0d91c18cfc4312 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 22 Aug 2024 17:45:19 -0700 Subject: [PATCH 01/10] DESENG-676: Combine "summary" and "custom" content into engagement content model --- met-api/migrations/versions/bd493dbd9e0e_.py | 112 ++++++++++ ...0d07ab_add_tables_for_dynamic_eng_pages.py | 9 +- .../constants/engagement_content_type.py | 33 --- met-api/src/met_api/models/__init__.py | 2 - .../src/met_api/models/engagement_content.py | 16 +- .../models/engagement_custom_content.py | 47 ----- .../models/engagement_summary_content.py | 55 ----- met-api/src/met_api/resources/__init__.py | 4 - .../met_api/resources/engagement_content.py | 7 +- .../engagement_content_translation.py | 3 +- .../resources/engagement_custom_content.py | 72 ------- .../resources/engagement_summary_content.py | 72 ------- .../src/met_api/schemas/engagement_content.py | 6 +- .../schemas/engagement_custom_content.py | 20 -- .../schemas/engagement_summary_content.py | 20 -- .../schemas/schemas/engagement_content.json | 11 +- .../schemas/engagement_content_update.json | 24 --- .../services/engagement_content_service.py | 41 +--- .../engagement_custom_content_service.py | 53 ----- .../met_api/services/engagement_service.py | 18 +- .../engagement_summary_content_service.py | 53 ----- .../engagement_translation_service.py | 15 +- met-api/src/met_api/utils/enums.py | 9 +- .../tests/unit/api/test_engagement_content.py | 28 +-- .../api/test_engagement_custom_content.py | 146 ------------- .../api/test_engagement_summary_content.py | 122 ----------- met-api/tests/utilities/factory_scenarios.py | 17 +- met-api/tests/utilities/factory_utils.py | 4 +- met-web/src/apiManager/endpoints/index.ts | 10 - .../engagement/form/ActionContext.tsx | 14 +- .../EngagementContent/ContentTabModal.tsx | 45 +--- .../EngagementContent/ContentTabs.tsx | 194 ++++++------------ .../EngagementContent/CustomTabContent.tsx | 50 ----- .../EngagementContentContext.tsx | 75 +------ .../{SummaryTabContent.tsx => TabContent.tsx} | 41 ++-- .../EngagementTabsContext.tsx | 118 ----------- .../engagement/old-view/ActionContext.tsx | 47 ++--- .../engagement/old-view/EngagementContent.tsx | 8 +- .../old-view/ScheduleModal/index.tsx | 4 +- .../public/view/EngagementContentTabs.tsx | 30 +-- .../public/view/EngagementLoader.tsx | 49 +---- met-web/src/models/engagementContent.ts | 15 +- met-web/src/models/engagementCustomContent.ts | 7 - .../src/models/engagementSummaryContent.ts | 7 - .../engagementContentService/index.ts | 7 +- .../services/engagementCustomService/index.ts | 59 ------ .../engagementSummaryService/index.ts | 58 ------ .../engagement/EngagementFormUserTab.test.tsx | 2 - .../edit/EngagementForm.Edit.Two.test.tsx | 1 - .../engagement/old-engagement.test.tsx | 26 ++- met-web/tests/unit/components/factory.ts | 4 +- 51 files changed, 339 insertions(+), 1551 deletions(-) create mode 100644 met-api/migrations/versions/bd493dbd9e0e_.py delete mode 100644 met-api/src/met_api/constants/engagement_content_type.py delete mode 100644 met-api/src/met_api/models/engagement_custom_content.py delete mode 100644 met-api/src/met_api/models/engagement_summary_content.py delete mode 100644 met-api/src/met_api/resources/engagement_custom_content.py delete mode 100644 met-api/src/met_api/resources/engagement_summary_content.py delete mode 100644 met-api/src/met_api/schemas/engagement_custom_content.py delete mode 100644 met-api/src/met_api/schemas/engagement_summary_content.py delete mode 100644 met-api/src/met_api/schemas/schemas/engagement_content_update.json delete mode 100644 met-api/src/met_api/services/engagement_custom_content_service.py delete mode 100644 met-api/src/met_api/services/engagement_summary_content_service.py delete mode 100644 met-api/tests/unit/api/test_engagement_custom_content.py delete mode 100644 met-api/tests/unit/api/test_engagement_summary_content.py delete mode 100644 met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/CustomTabContent.tsx rename met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/{SummaryTabContent.tsx => TabContent.tsx} (63%) delete mode 100644 met-web/src/models/engagementCustomContent.ts delete mode 100644 met-web/src/models/engagementSummaryContent.ts delete mode 100644 met-web/src/services/engagementCustomService/index.ts delete mode 100644 met-web/src/services/engagementSummaryService/index.ts diff --git a/met-api/migrations/versions/bd493dbd9e0e_.py b/met-api/migrations/versions/bd493dbd9e0e_.py new file mode 100644 index 000000000..f4caada0b --- /dev/null +++ b/met-api/migrations/versions/bd493dbd9e0e_.py @@ -0,0 +1,112 @@ +"""empty message + +Revision ID: bd493dbd9e0e +Revises: 901a6724bca2 +Create Date: 2024-08-21 10:06:44.377763 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +from sqlalchemy.sql import table, column +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'bd493dbd9e0e' +down_revision = '901a6724bca2' +branch_labels = None +depends_on = None + + +def upgrade(): + # Create new columns in the engagement_content table + op.add_column('engagement_content', sa.Column('text_content', sa.Text(), nullable=True)) + op.add_column('engagement_content', sa.Column('json_content', sa.JSON(), nullable=True)) + + # Reference the existing tables + + engagement_content = table('engagement_content', + column('id', sa.Integer), + column('text_content', sa.Text), + column('json_content', sa.JSON) + ) + + engagement_summary_content = table('engagement_summary_content', + column('engagement_content_id', sa.Integer), + column('content', sa.Text), + column('rich_content', sa.JSON) + ) + + engagement_custom_content = table('engagement_custom_content', + column('engagement_content_id', sa.Integer), + column('custom_text_content', sa.Text), + column('custom_json_content', sa.JSON) + ) + + # Copy data from the old tables to the new columns in engagement_content + op.execute( + engagement_content.update() + .where(engagement_content.c.id == engagement_summary_content.c.engagement_content_id) + .values({ + 'text_content': engagement_summary_content.c.content, + 'json_content': engagement_summary_content.c.rich_content + }) + ) + + op.execute( + engagement_content.update() + .where(engagement_content.c.id == engagement_custom_content.c.engagement_content_id) + .values({ + 'text_content': engagement_custom_content.c.custom_text_content, + 'json_content': engagement_custom_content.c.custom_json_content + }) + ) + + # Drop old tables + op.drop_table('engagement_custom_content') + op.drop_table('engagement_summary_content') + + # Drop the content_type column and icon name as they're no longer needed + op.drop_column('engagement_content', 'content_type') + op.drop_column('engagement_content', 'icon_name') + + +def downgrade(): + # Recreate the old tables + op.create_table('engagement_summary_content', + sa.Column('created_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.Column('updated_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('content', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('rich_content', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), + sa.Column('engagement_content_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('engagement_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('created_by', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('updated_by', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['engagement_content_id'], ['engagement_content.id'], name='engagement_summary_content_engagement_content_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['engagement_id'], ['engagement.id'], name='engagement_summary_content_engagement_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='engagement_summary_content_pkey') + ) + + op.create_table('engagement_custom_content', + sa.Column('created_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.Column('updated_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('custom_text_content', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('custom_json_content', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), + sa.Column('engagement_content_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('engagement_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('created_by', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('updated_by', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['engagement_content_id'], ['engagement_content.id'], name='engagement_custom_content_engagement_content_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['engagement_id'], ['engagement.id'], name='engagement_custom_content_engagement_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='engagement_custom_content_pkey') + ) + + # Drop new columns in engagement_content table + op.drop_column('engagement_content', 'json_content') + op.drop_column('engagement_content', 'text_content') + + # Re-add the content_type and icon_name columns + op.add_column('engagement_content', sa.Column('icon_name', sa.Text(), autoincrement=False, nullable=True)) + op.add_column('engagement_content', sa.Column('content_type', postgresql.ENUM('Summary', 'Custom', name='engagementcontenttype'), autoincrement=False, nullable=False)) diff --git a/met-api/migrations/versions/e2625b0d07ab_add_tables_for_dynamic_eng_pages.py b/met-api/migrations/versions/e2625b0d07ab_add_tables_for_dynamic_eng_pages.py index 14f8ff695..040f695e7 100644 --- a/met-api/migrations/versions/e2625b0d07ab_add_tables_for_dynamic_eng_pages.py +++ b/met-api/migrations/versions/e2625b0d07ab_add_tables_for_dynamic_eng_pages.py @@ -11,9 +11,6 @@ from sqlalchemy import text from sqlalchemy.dialects import postgresql -from met_api.constants.engagement_content_type import EngagementContentType -from met_api.utils.enums import ContentTitle - # revision identifiers, used by Alembic. revision = 'e2625b0d07ab' down_revision = '37176ea4708d' @@ -90,9 +87,9 @@ def upgrade(): """ ), { - 'title': ContentTitle.DEFAULT.value, - 'icon_name': ContentTitle.DEFAULT_ICON.value, - 'content_type': EngagementContentType(1).name, + 'title': "Summary", + 'icon_name': "n/a", + 'content_type': "n/a", 'engagement_id': eng_id, }, ) diff --git a/met-api/src/met_api/constants/engagement_content_type.py b/met-api/src/met_api/constants/engagement_content_type.py deleted file mode 100644 index 82f89a9f7..000000000 --- a/met-api/src/met_api/constants/engagement_content_type.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright © 2021 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Constants of engagement content type. - -Each value in this corresponds to a specific section or content element. -""" -from enum import Enum, IntEnum - - -class EngagementContentType(IntEnum): - """Enum of engagement content type.""" - - Summary = 1 - Custom = 2 - - -class EngagementContentDefaultValues(Enum): - """Default enum of engagement content type.""" - - Title = 'Summary' - Icon = 'faRectangleList' - Type = 'Summary' diff --git a/met-api/src/met_api/models/__init__.py b/met-api/src/met_api/models/__init__.py index ae5a15033..075d7518b 100644 --- a/met-api/src/met_api/models/__init__.py +++ b/met-api/src/met_api/models/__init__.py @@ -24,8 +24,6 @@ from .engagement_status import EngagementStatus from .engagement_status_block import EngagementStatusBlock from .engagement_settings import EngagementSettingsModel -from .engagement_custom_content import EngagementCustom -from .engagement_summary_content import EngagementSummary from .event_item import EventItem from .subscribe_item import SubscribeItem from .feedback import Feedback diff --git a/met-api/src/met_api/models/engagement_content.py b/met-api/src/met_api/models/engagement_content.py index 870fe8652..743df1e02 100644 --- a/met-api/src/met_api/models/engagement_content.py +++ b/met-api/src/met_api/models/engagement_content.py @@ -9,7 +9,6 @@ from typing import Optional from sqlalchemy.sql.schema import ForeignKey -from met_api.constants.engagement_content_type import EngagementContentType from .base_model import BaseModel from .db import db @@ -21,24 +20,23 @@ class EngagementContent(BaseModel): __tablename__ = 'engagement_content' id = db.Column(db.Integer, primary_key=True, autoincrement=True) title = db.Column(db.String(50), unique=False, nullable=False) - icon_name = db.Column(db.Text, unique=False, nullable=True) - content_type = db.Column(db.Enum(EngagementContentType), nullable=False, - default=EngagementContentType.Summary) + text_content = db.Column(db.Text, unique=False, nullable=True) + json_content = db.Column(db.JSON, unique=False, nullable=True) engagement_id = db.Column(db.Integer, ForeignKey('engagement.id', ondelete='CASCADE')) sort_index = db.Column(db.Integer, nullable=False, default=1) is_internal = db.Column(db.Boolean, nullable=False) @classmethod - def get_contents_by_engagement_id(cls, engagement_id): - """Get contents by engagement id.""" + def find_by_engagement_id(cls, engagement_id): + """Get content by engagement id.""" return db.session.query(EngagementContent)\ .filter(EngagementContent.engagement_id == engagement_id)\ .order_by(EngagementContent.sort_index.asc())\ .all() @classmethod - def update_engagement_contents(cls, update_mappings: list) -> None: - """Update contents.""" + def bulk_update_engagement_content(cls, update_mappings: list) -> None: + """Update content.""" db.session.bulk_update_mappings(EngagementContent, update_mappings) db.session.commit() @@ -49,7 +47,7 @@ def save_engagement_content(cls, content: list) -> None: @classmethod def remove_engagement_content(cls, engagement_id, engagement_content_id,) -> EngagementContent: - """Remove engagement content from engagement.""" + """Remove content from an engagement.""" engagement_content = EngagementContent.query.filter_by(id=engagement_content_id, engagement_id=engagement_id).delete() db.session.commit() diff --git a/met-api/src/met_api/models/engagement_custom_content.py b/met-api/src/met_api/models/engagement_custom_content.py deleted file mode 100644 index 9f32409f5..000000000 --- a/met-api/src/met_api/models/engagement_custom_content.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Engagement custom model class. - -Manages the engagement custom content -""" - -from __future__ import annotations - -from sqlalchemy.dialects.postgresql import JSON -from sqlalchemy.sql.schema import ForeignKey - -from .base_model import BaseModel -from .db import db - - -class EngagementCustom(BaseModel): - """Definition of the Engagement custom content entity.""" - - __tablename__ = 'engagement_custom_content' - id = db.Column(db.Integer, primary_key=True, autoincrement=True) - custom_text_content = db.Column(db.Text, unique=False, nullable=True) - custom_json_content = db.Column(JSON, unique=False, nullable=True) - engagement_content_id = db.Column(db.Integer, ForeignKey('engagement_content.id', ondelete='CASCADE')) - engagement_id = db.Column(db.Integer, ForeignKey('engagement.id', ondelete='CASCADE')) - - @classmethod - def get_custom_content(cls, content_id) -> list[EngagementCustom]: - """Get engagement custom content.""" - custom_content = db.session.query(EngagementCustom) \ - .filter(EngagementCustom.engagement_content_id == content_id) \ - .all() - return custom_content - - @classmethod - def update_custom_content(cls, content_id, custom_content_data: dict) -> EngagementCustom: - """Update engagement custom content.""" - query = EngagementCustom.query.filter_by(engagement_content_id=content_id) - custom_content: EngagementCustom = query.first() - if not custom_content: - return custom_content_data - query.update(custom_content_data) - db.session.commit() - return custom_content - - @classmethod - def save_engagement_custom_content(cls, custom_content: list) -> None: - """Save custom content.""" - db.session.bulk_save_objects(custom_content) diff --git a/met-api/src/met_api/models/engagement_summary_content.py b/met-api/src/met_api/models/engagement_summary_content.py deleted file mode 100644 index 2eb52270f..000000000 --- a/met-api/src/met_api/models/engagement_summary_content.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Engagement summary model class. - -Manages the engagement summary content -""" - -from __future__ import annotations - -from sqlalchemy.dialects.postgresql import JSON -from sqlalchemy.sql.schema import ForeignKey - -from .base_model import BaseModel -from .db import db - - -class EngagementSummary(BaseModel): - """Definition of the Engagement summary content entity.""" - - __tablename__ = 'engagement_summary_content' - id = db.Column(db.Integer, primary_key=True, autoincrement=True) - content = db.Column(db.Text, unique=False, nullable=False) - rich_content = db.Column(JSON, unique=False, nullable=False) - engagement_content_id = db.Column(db.Integer, ForeignKey('engagement_content.id', ondelete='CASCADE')) - engagement_id = db.Column(db.Integer, ForeignKey('engagement.id', ondelete='CASCADE')) - - @classmethod - def get_summary_content(cls, content_id) -> list[EngagementSummary]: - """Get engagement summary content.""" - summary_content = db.session.query(EngagementSummary) \ - .filter(EngagementSummary.engagement_content_id == content_id) \ - .all() - return summary_content - - @classmethod - def get_summary_content_by_engagement_id(cls, engagement_id) -> list[EngagementSummary]: - """Get engagement summary content by engagement id.""" - summary_content = db.session.query(EngagementSummary) \ - .filter(EngagementSummary.engagement_id == engagement_id) \ - .first() - return summary_content - - @classmethod - def update_summary_content(cls, content_id, summary_content_data: dict) -> EngagementSummary: - """Update engagement summary content.""" - query = EngagementSummary.query.filter_by(engagement_content_id=content_id) - summary_content: EngagementSummary = query.first() - if not summary_content: - return summary_content_data - query.update(summary_content_data) - db.session.commit() - return summary_content - - @classmethod - def save_engagement_summary_content(cls, summary_content: list) -> None: - """Save summary content.""" - db.session.bulk_save_objects(summary_content) diff --git a/met-api/src/met_api/resources/__init__.py b/met-api/src/met_api/resources/__init__.py index d1c864e2f..f3494bf61 100644 --- a/met-api/src/met_api/resources/__init__.py +++ b/met-api/src/met_api/resources/__init__.py @@ -30,8 +30,6 @@ from .document import API as DOCUMENT_API from .email_verification import API as EMAIL_VERIFICATION_API from .engagement_content import API as ENGAGEMENT_CONTENT_API -from .engagement_custom_content import API as ENGAGEMENT_CUSTOM_CONTENT_API -from .engagement_summary_content import API as ENGAGEMENT_SUMMARY_CONTENT_API from .engagement import API as ENGAGEMENT_API from .engagement_metadata import API as ENGAGEMENT_METADATA_API from .metadata_taxon import API as METADATA_TAXON_API @@ -84,8 +82,6 @@ API.add_namespace(COMMENT_API) API.add_namespace(EMAIL_VERIFICATION_API) API.add_namespace(ENGAGEMENT_CONTENT_API, path='/engagement//content') -API.add_namespace(ENGAGEMENT_CUSTOM_CONTENT_API, path='/content//custom') -API.add_namespace(ENGAGEMENT_SUMMARY_CONTENT_API, path='/content//summary') API.add_namespace(FEEDBACK_API) API.add_namespace(WIDGET_API) API.add_namespace(CONTACT_API) diff --git a/met-api/src/met_api/resources/engagement_content.py b/met-api/src/met_api/resources/engagement_content.py index 8ca5f5a1c..a5f3f42f9 100644 --- a/met-api/src/met_api/resources/engagement_content.py +++ b/met-api/src/met_api/resources/engagement_content.py @@ -36,7 +36,7 @@ @cors_preflight('GET, POST, OPTIONS') @API.route('') class EngagementContent(Resource): - """Resource for managing a engagement content.""" + """Resource for managing an engagement's contents.""" @staticmethod @cross_origin(origins=allowedorigins()) @@ -113,9 +113,8 @@ def patch(engagement_id, engagement_content_id): try: user_id = TokenInfo.get_id() engagement_content_data = request.get_json() - valid_format, errors = schema_utils.validate(engagement_content_data, 'engagement_content_update') - if not valid_format: - return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + if not engagement_content_data or not engagement_content_data.get('title'): + return 'No content provided', HTTPStatus.BAD_REQUEST updated_engagement_content = EngagementContentService().update_engagement_content(engagement_id, engagement_content_id, diff --git a/met-api/src/met_api/resources/engagement_content_translation.py b/met-api/src/met_api/resources/engagement_content_translation.py index 0a7d1e8c2..4aaded417 100644 --- a/met-api/src/met_api/resources/engagement_content_translation.py +++ b/met-api/src/met_api/resources/engagement_content_translation.py @@ -8,12 +8,13 @@ from marshmallow import ValidationError from met_api.auth import jwt as _jwt +from met_api.exceptions.business_exception import BusinessException from met_api.schemas import utils as schema_utils from met_api.schemas.engagement_content_translation import EngagementContentTranslationSchema from met_api.services.engagement_content_translation_service import EngagementContentTranslationService -from met_api.exceptions.business_exception import BusinessException from met_api.utils.util import allowedorigins, cors_preflight + API = Namespace('engagement_content_translation', description='Endpoints for Engagement Content Translation Management') diff --git a/met-api/src/met_api/resources/engagement_custom_content.py b/met-api/src/met_api/resources/engagement_custom_content.py deleted file mode 100644 index 5a33ff44b..000000000 --- a/met-api/src/met_api/resources/engagement_custom_content.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright © 2021 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""API endpoints for managing an engagement custom content resource.""" - -from http import HTTPStatus - -from flask import jsonify, request -from flask_cors import cross_origin -from flask_restx import Namespace, Resource - -from met_api.auth import jwt as _jwt -from met_api.exceptions.business_exception import BusinessException -from met_api.schemas.engagement_custom_content import EngagementCustomSchema -from met_api.services.engagement_custom_content_service import EngagementCustomContentService -from met_api.utils.util import allowedorigins, cors_preflight - - -API = Namespace('custom_content', description='Endpoints for Engagement Custom Content Management') -"""Custom exception messages -""" - - -@cors_preflight('GET, POST, PATCH, OPTIONS') -@API.route('') -class CustomContent(Resource): - """Resource for managing engagement custom content.""" - - @staticmethod - @cross_origin(origins=allowedorigins()) - def get(content_id): - """Get engagement custom content.""" - try: - custom_content = EngagementCustomContentService().get_custom_content(content_id) - return jsonify(EngagementCustomSchema().dump(custom_content, many=True)), HTTPStatus.OK - except BusinessException as err: - return str(err), err.status_code - - @staticmethod - @cross_origin(origins=allowedorigins()) - @_jwt.requires_auth - def post(content_id): - """Create engagement custom content.""" - try: - request_json = request.get_json() - custom_content = EngagementCustomContentService().create_custom_content(content_id, - request_json) - return EngagementCustomSchema().dump(custom_content), HTTPStatus.OK - except BusinessException as err: - return str(err), err.status_code - - @staticmethod - @cross_origin(origins=allowedorigins()) - @_jwt.requires_auth - def patch(content_id): - """Update engagement custom content.""" - request_json = request.get_json() - try: - custom_content = EngagementCustomContentService().update_custom_content(content_id, request_json) - return EngagementCustomSchema().dump(custom_content), HTTPStatus.OK - except BusinessException as err: - return str(err), err.status_code diff --git a/met-api/src/met_api/resources/engagement_summary_content.py b/met-api/src/met_api/resources/engagement_summary_content.py deleted file mode 100644 index d2e504a0d..000000000 --- a/met-api/src/met_api/resources/engagement_summary_content.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright © 2021 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""API endpoints for managing an engagement summary content resource.""" - -from http import HTTPStatus - -from flask import jsonify, request -from flask_cors import cross_origin -from flask_restx import Namespace, Resource - -from met_api.auth import jwt as _jwt -from met_api.exceptions.business_exception import BusinessException -from met_api.schemas.engagement_summary_content import EngagementSummarySchema -from met_api.services.engagement_summary_content_service import EngagementSummaryContentService -from met_api.utils.util import allowedorigins, cors_preflight - - -API = Namespace('summary_content', description='Endpoints for Engagement Summary Content Management') -"""Custom exception messages -""" - - -@cors_preflight('GET, POST, PATCH, OPTIONS') -@API.route('') -class SummaryContent(Resource): - """Resource for managing engagement summary content.""" - - @staticmethod - @cross_origin(origins=allowedorigins()) - def get(content_id): - """Get engagement summary content.""" - try: - summary_content = EngagementSummaryContentService().get_summary_content(content_id) - return jsonify(EngagementSummarySchema().dump(summary_content, many=True)), HTTPStatus.OK - except BusinessException as err: - return str(err), err.status_code - - @staticmethod - @cross_origin(origins=allowedorigins()) - @_jwt.requires_auth - def post(content_id): - """Create engagement summary content.""" - try: - request_json = request.get_json() - summary_content = EngagementSummaryContentService().create_summary_content(content_id, - request_json) - return EngagementSummarySchema().dump(summary_content), HTTPStatus.OK - except BusinessException as err: - return str(err), err.status_code - - @staticmethod - @cross_origin(origins=allowedorigins()) - @_jwt.requires_auth - def patch(content_id): - """Update engagement summary content.""" - request_json = request.get_json() - try: - summary_content = EngagementSummaryContentService().update_summary_content(content_id, request_json) - return EngagementSummarySchema().dump(summary_content), HTTPStatus.OK - except BusinessException as err: - return str(err), err.status_code diff --git a/met-api/src/met_api/schemas/engagement_content.py b/met-api/src/met_api/schemas/engagement_content.py index ef92a0f72..559343c17 100644 --- a/met-api/src/met_api/schemas/engagement_content.py +++ b/met-api/src/met_api/schemas/engagement_content.py @@ -3,8 +3,6 @@ Manages the engagement content. """ from marshmallow import EXCLUDE, Schema, fields -from marshmallow_enum import EnumField -from met_api.constants.engagement_content_type import EngagementContentType class EngagementContentSchema(Schema): @@ -17,8 +15,8 @@ class Meta: # pylint: disable=too-few-public-methods id = fields.Int(data_key='id') title = fields.Str(data_key='title') - icon_name = fields.Str(data_key='icon_name') - content_type = EnumField(EngagementContentType) + text_content = fields.Str(data_key='text_content') + json_content = fields.Str(data_key='json_content') engagement_id = fields.Int(data_key='engagement_id') sort_index = fields.Int(data_key='sort_index') is_internal = fields.Bool(data_key='is_internal') diff --git a/met-api/src/met_api/schemas/engagement_custom_content.py b/met-api/src/met_api/schemas/engagement_custom_content.py deleted file mode 100644 index 96d70d954..000000000 --- a/met-api/src/met_api/schemas/engagement_custom_content.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Engagement custom model class. - -Manages the engagement custom -""" -from marshmallow import EXCLUDE, Schema, fields - - -class EngagementCustomSchema(Schema): - """Schema for engagement custom.""" - - class Meta: # pylint: disable=too-few-public-methods - """Exclude unknown fields in the deserialized output.""" - - unknown = EXCLUDE - - id = fields.Int(data_key='id') - custom_text_content = fields.Str(data_key='custom_text_content') - custom_json_content = fields.Str(data_key='custom_json_content') - engagement_content_id = fields.Int(data_key='engagement_content_id') - engagement_id = fields.Int(data_key='engagement_id') diff --git a/met-api/src/met_api/schemas/engagement_summary_content.py b/met-api/src/met_api/schemas/engagement_summary_content.py deleted file mode 100644 index f76e53b32..000000000 --- a/met-api/src/met_api/schemas/engagement_summary_content.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Engagement summary model class. - -Manages the engagement summary -""" -from marshmallow import EXCLUDE, Schema, fields - - -class EngagementSummarySchema(Schema): - """Schema for engagement summary.""" - - class Meta: # pylint: disable=too-few-public-methods - """Exclude unknown fields in the deserialized output.""" - - unknown = EXCLUDE - - id = fields.Int(data_key='id') - content = fields.Str(data_key='content') - rich_content = fields.Str(data_key='rich_content') - engagement_content_id = fields.Int(data_key='engagement_content_id') - engagement_id = fields.Int(data_key='engagement_id') diff --git a/met-api/src/met_api/schemas/schemas/engagement_content.json b/met-api/src/met_api/schemas/schemas/engagement_content.json index 814620abb..5c01b0d82 100644 --- a/met-api/src/met_api/schemas/schemas/engagement_content.json +++ b/met-api/src/met_api/schemas/schemas/engagement_content.json @@ -8,14 +8,12 @@ "examples": [ { "title": "Summary", - "icon_name": "SummarizeIcon", - "content_type": "Summary", "sort_index": 1, "is_internal": "False", "engagement_id": 1 } ], - "required": ["title", "content_type", "engagement_id"], + "required": ["title", "engagement_id"], "properties": { "title": { "$id": "#/properties/title", @@ -24,13 +22,6 @@ "description": "The title of this content.", "examples": [1] }, - "content_type": { - "$id": "#/properties/content_type", - "type": "string", - "title": "Summary", - "description": "The content type enum.", - "examples": [1] - }, "engagement_id": { "$id": "#/properties/engagement_id", "type": "number", diff --git a/met-api/src/met_api/schemas/schemas/engagement_content_update.json b/met-api/src/met_api/schemas/schemas/engagement_content_update.json deleted file mode 100644 index e6e3325c3..000000000 --- a/met-api/src/met_api/schemas/schemas/engagement_content_update.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "https://met.gov.bc.ca/.well_known/schemas/engagement_content_update", - "type": "object", - "title": "The root schema", - "description": "The root schema comprises the entire JSON document.", - "default": {}, - "examples": [ - { - "title": "Summary" - } - ], - "required": ["title"], - "properties": { - "title": { - "$id": "#/properties/title", - "type": "string", - "title": "Summary", - "description": "The title of this content.", - "examples": [1] - } - } - } - \ No newline at end of file diff --git a/met-api/src/met_api/services/engagement_content_service.py b/met-api/src/met_api/services/engagement_content_service.py index 13371626b..c0af9ff65 100644 --- a/met-api/src/met_api/services/engagement_content_service.py +++ b/met-api/src/met_api/services/engagement_content_service.py @@ -1,13 +1,10 @@ """Service for engagement content management.""" from http import HTTPStatus -from flask import current_app -from met_api.constants.engagement_content_type import EngagementContentType from met_api.constants.membership_type import MembershipType from met_api.exceptions.business_exception import BusinessException from met_api.models.engagement_content import EngagementContent as EngagementContentModel from met_api.schemas.engagement_content import EngagementContentSchema -from met_api.services.engagement_custom_content_service import EngagementCustomContentService from met_api.services import authorization from met_api.utils.roles import Role @@ -27,20 +24,13 @@ def get_content_by_content_id(engagement_content_id): ) content_data['id'] = engagement_content_record.id content_data['title'] = engagement_content_record.title - if engagement_content_record.content_type == EngagementContentType.Custom.name: - custom_content_records = EngagementCustomContentService.get_custom_content(engagement_content_id) - if custom_content_records: - custom_record = custom_content_records[0] - content_data['title'] = custom_record.get('title') - content_data['custom_text_content'] = custom_record.get('custom_text_content') - content_data['custom_json_content'] = custom_record.get('custom_json_content') return content_data @staticmethod def get_contents_by_engagement_id(engagement_id): - """Get contents by engagement id.""" + """Get content by engagement id.""" engagement_content_schema = EngagementContentSchema(many=True) - engagement_content_records = EngagementContentModel.get_contents_by_engagement_id(engagement_id) + engagement_content_records = EngagementContentModel.find_by_engagement_id(engagement_id) engagement_contents = engagement_content_schema.dump(engagement_content_records) return engagement_contents @@ -63,30 +53,13 @@ def create_engagement_content(engagement_content_data, engagement_id): created_content = EngagementContentService._create_content(engagement_id, engagement_content_data) created_content.commit() - if engagement_content_data.get('content_type') == EngagementContentType.Custom.name: - EngagementContentService.create_default_custom_content(engagement_id, created_content.id) - return EngagementContentSchema().dump(created_content) - @staticmethod - def create_default_custom_content(eng_id: int, eng_content_id: int): - """Create default engagement custom content.""" - default_summary_content = { - 'engagement_id': eng_id - } - try: - EngagementCustomContentService.create_custom_content(eng_content_id, default_summary_content) - except Exception as exc: # noqa: B902 - current_app.logger.error('Failed to create default engagement summary content', exc) - raise BusinessException( - error='Failed to create default engagement summary content.', - status_code=HTTPStatus.INTERNAL_SERVER_ERROR) from exc - @staticmethod def _find_higest_sort_index(engagement_id): # find the highest sort order of the engagement content sort_index = 0 - contents = EngagementContentModel.get_contents_by_engagement_id(engagement_id) + contents = EngagementContentModel.find_by_engagement_id(engagement_id) if contents: # Find the largest in the existing engagement contents sort_index = max(content.sort_index for content in contents) @@ -97,8 +70,8 @@ def _create_content(engagement_id, engagement_content_data: dict): engagement_content_model: EngagementContentModel = EngagementContentModel() engagement_content_model.engagement_id = engagement_id engagement_content_model.title = engagement_content_data.get('title') - engagement_content_model.icon_name = engagement_content_data.get('icon_name') - engagement_content_model.content_type = EngagementContentType[engagement_content_data.get('content_type')] + engagement_content_model.text_content = engagement_content_data.get('text_content') + engagement_content_model.json_content = engagement_content_data.get('json_content') engagement_content_model.sort_index = engagement_content_data.get('sort_index') engagement_content_model.is_internal = engagement_content_data.get('is_internal', False) engagement_content_model.flush() @@ -122,12 +95,12 @@ def sort_engagement_content(engagement_id, engagement_contents: list, user_id=No } for index, engagement_content in enumerate(engagement_contents) ] - EngagementContentModel.update_engagement_contents(engagement_content_sort_mappings) + EngagementContentModel.bulk_update_engagement_content(engagement_content_sort_mappings) @staticmethod def _validate_engagement_content_ids(engagement_id, engagement_contents): """Validate if engagement content ids belong to the engagement.""" - eng_contents = EngagementContentModel.get_contents_by_engagement_id(engagement_id) + eng_contents = EngagementContentModel.find_by_engagement_id(engagement_id) content_ids = [content.id for content in eng_contents] input_content_ids = [engagement_content.get('id') for engagement_content in engagement_contents] if len(set(content_ids) - set(input_content_ids)) > 0: diff --git a/met-api/src/met_api/services/engagement_custom_content_service.py b/met-api/src/met_api/services/engagement_custom_content_service.py deleted file mode 100644 index 4f0b3c057..000000000 --- a/met-api/src/met_api/services/engagement_custom_content_service.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Service for engagement custom content management.""" -from met_api.constants.membership_type import MembershipType -from met_api.models.engagement_custom_content import EngagementCustom as EngagementCustomModel -from met_api.services import authorization -from met_api.utils.roles import Role - - -class EngagementCustomContentService: - """Engagement custom content management service.""" - - @staticmethod - def get_custom_content(content_id): - """Get content by engagement custom content id.""" - custom_content = EngagementCustomModel.get_custom_content(content_id) - return custom_content - - @staticmethod - def create_custom_content(content_id, custom_content_details: dict): - """Create engagement custom content.""" - custom_content = dict(custom_content_details) - eng_id = custom_content.get('engagement_id') - authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, - Role.EDIT_ENGAGEMENT.value), engagement_id=eng_id) - - engagement_custom_content = EngagementCustomContentService._create_custom_content_model(content_id, - custom_content) - engagement_custom_content.commit() - return engagement_custom_content - - @staticmethod - def _create_custom_content_model(content_id, custom_content: dict): - custom_content_model: EngagementCustomModel = EngagementCustomModel() - custom_content_model.engagement_content_id = content_id - custom_content_model.engagement_id = custom_content.get('engagement_id') - custom_content_model.custom_text_content = custom_content.get('custom_text_content') - custom_content_model.custom_json_content = custom_content.get('custom_json_content') - custom_content_model.flush() - return custom_content_model - - @staticmethod - def update_custom_content(content_id, request_json): - """Update engagement custom content.""" - custom_content_list: EngagementCustomModel = EngagementCustomModel.get_custom_content(content_id) - if not custom_content_list: - raise KeyError('Engagement custom content ' + content_id + ' does not exist') - - custom_content = custom_content_list[0] - eng_id = custom_content.engagement_id - - authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, - Role.EDIT_ENGAGEMENT.value), engagement_id=eng_id) - - return EngagementCustomModel.update_custom_content(content_id, request_json) diff --git a/met-api/src/met_api/services/engagement_service.py b/met-api/src/met_api/services/engagement_service.py index 4d093b524..e21efb0c8 100644 --- a/met-api/src/met_api/services/engagement_service.py +++ b/met-api/src/met_api/services/engagement_service.py @@ -4,7 +4,6 @@ from flask import current_app -from met_api.constants.engagement_content_type import EngagementContentDefaultValues from met_api.constants.engagement_status import Status from met_api.constants.membership_type import MembershipType from met_api.exceptions.business_exception import BusinessException @@ -19,7 +18,6 @@ from met_api.services.engagement_settings_service import EngagementSettingsService from met_api.services.engagement_slug_service import EngagementSlugService from met_api.services.engagement_content_service import EngagementContentService -from met_api.services.engagement_summary_content_service import EngagementSummaryContentService from met_api.services.object_storage_service import ObjectStorageService from met_api.services.project_service import ProjectService from met_api.utils import email_util, notification @@ -160,7 +158,7 @@ def create_engagement(request_json: dict): eng_model = EngagementService._create_engagement_model(request_json) eng_content = EngagementService.create_default_engagement_content(eng_model.id) - EngagementService.create_default_summary_content(eng_model.id, eng_content['id'], request_json) + EngagementService.create_default_content(eng_model.id, eng_content['id'], request_json) if request_json.get('status_block'): EngagementService._create_eng_status_block(eng_model.id, request_json) @@ -200,9 +198,7 @@ def _create_engagement_model(engagement_data: dict) -> EngagementModel: def create_default_engagement_content(eng_id): """Create default engagement content for the given engagement ID.""" default_engagement_content = { - 'title': EngagementContentDefaultValues.Title.value, - 'icon_name': EngagementContentDefaultValues.Icon.value, - 'content_type': EngagementContentDefaultValues.Type.value, + 'title': "Summary", 'engagement_id': eng_id } try: @@ -216,15 +212,17 @@ def create_default_engagement_content(eng_id): return eng_content @staticmethod - def create_default_summary_content(eng_id: int, eng_content_id: int, content_data: dict): - """Create default summary content for the engagement ID, mandatory for each engagement.""" + def create_default_content(eng_id: int, eng_content_id: int, content_data: dict): + """Create default summary content for the engagement.""" default_summary_content = { + 'title': 'Summary', 'engagement_id': eng_id, 'content': content_data.get('content', None), - 'rich_content': content_data.get('rich_content', None) + 'text_content': content_data.get('content', None), + 'json_content': content_data.get('rich_content', None), } try: - EngagementSummaryContentService.create_summary_content(eng_content_id, default_summary_content) + EngagementContentService.create_engagement_content(default_summary_content, eng_id) except Exception as exc: # noqa: B902 current_app.logger.error('Failed to create default engagement summary content', exc) raise BusinessException( diff --git a/met-api/src/met_api/services/engagement_summary_content_service.py b/met-api/src/met_api/services/engagement_summary_content_service.py deleted file mode 100644 index 2874d3c51..000000000 --- a/met-api/src/met_api/services/engagement_summary_content_service.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Service for engagement summary content management.""" -from met_api.constants.membership_type import MembershipType -from met_api.models.engagement_summary_content import EngagementSummary as EngagementSummaryModel -from met_api.services import authorization -from met_api.utils.roles import Role - - -class EngagementSummaryContentService: - """Engagement summary content management service.""" - - @staticmethod - def get_summary_content(content_id): - """Get content by engagement summary content id.""" - summary_content = EngagementSummaryModel.get_summary_content(content_id) - return summary_content - - @staticmethod - def create_summary_content(content_id, summary_content_details: dict): - """Create engagement summary content.""" - summary_content = dict(summary_content_details) - eng_id = summary_content.get('engagement_id') - authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, - Role.EDIT_ENGAGEMENT.value), engagement_id=eng_id) - - engagement_summary_content = EngagementSummaryContentService._create_summary_content_model(content_id, - summary_content) - engagement_summary_content.commit() - return engagement_summary_content - - @staticmethod - def _create_summary_content_model(content_id, summary_content: dict): - summary_content_model: EngagementSummaryModel = EngagementSummaryModel() - summary_content_model.engagement_content_id = content_id - summary_content_model.engagement_id = summary_content.get('engagement_id') - summary_content_model.content = summary_content.get('content') - summary_content_model.rich_content = summary_content.get('rich_content') - summary_content_model.flush() - return summary_content_model - - @staticmethod - def update_summary_content(content_id, request_json): - """Update engagement summary content.""" - summary_content_list: EngagementSummaryModel = EngagementSummaryModel.get_summary_content(content_id) - if not summary_content_list: - raise KeyError('Engagement summary content ' + str(content_id) + ' does not exist') - - summary_content = summary_content_list[0] - eng_id = summary_content.engagement_id - - authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, - Role.EDIT_ENGAGEMENT.value), engagement_id=eng_id) - - return EngagementSummaryModel.update_summary_content(content_id, request_json) diff --git a/met-api/src/met_api/services/engagement_translation_service.py b/met-api/src/met_api/services/engagement_translation_service.py index 89f87048e..d902c9b68 100644 --- a/met-api/src/met_api/services/engagement_translation_service.py +++ b/met-api/src/met_api/services/engagement_translation_service.py @@ -8,8 +8,8 @@ from met_api.models.engagement import Engagement as EngagementModel from met_api.models.engagement_slug import EngagementSlug as EngagementSlugModel from met_api.models.engagement_status_block import EngagementStatusBlock as EngagementStatusBlockModel -from met_api.models.engagement_summary_content import EngagementSummary as EngagementSummaryModel from met_api.models.engagement_translation import EngagementTranslation as EngagementTranslationModel +from met_api.models.engagement_content import EngagementContent as EngagementContentModel from met_api.models.language import Language as LanguageModel from met_api.schemas.language import LanguageSchema from met_api.schemas.engagement_translation import EngagementTranslationSchema @@ -43,8 +43,7 @@ def create_engagement_translation(translation_data, pre_populate=True): """Create engagement translation.""" try: engagement = EngagementModel.find_by_id(translation_data['engagement_id']) - summary_content = EngagementSummaryModel.get_summary_content_by_engagement_id( - translation_data['engagement_id']) + content = EngagementContentModel.find_by_engagement_id(translation_data['engagement_id'])[0] if not engagement: raise ValueError('Engagement to translate was not found') @@ -59,9 +58,9 @@ def create_engagement_translation(translation_data, pre_populate=True): raise ValueError('Language to translate was not found') if pre_populate: - # prepopulate translation with base language data + # prepopulate translation_date dict with base language data from engagement content EngagementTranslationService._get_default_language_values(engagement, - summary_content, + content, translation_data) created_engagement_translation = EngagementTranslationModel.create_engagement_translation( @@ -131,14 +130,14 @@ def _verify_engagement_translation(engagement_translation_id): return engagement_translation @staticmethod - def _get_default_language_values(engagement, summary_content, translation_data): + def _get_default_language_values(engagement, content, translation_data): """Populate the default values.""" engagement_id = engagement.id translation_data['name'] = engagement.name translation_data['description'] = engagement.description translation_data['rich_description'] = engagement.rich_description - translation_data['content'] = summary_content.content - translation_data['rich_content'] = summary_content.rich_content + translation_data['content'] = content.text_content + translation_data['rich_content'] = content.json_content translation_data['consent_message'] = engagement.consent_message translation_data['sponsor_name'] = engagement.sponsor_name translation_data['cta_message'] = engagement.cta_message diff --git a/met-api/src/met_api/utils/enums.py b/met-api/src/met_api/utils/enums.py index 1b1c3d67d..aff20a197 100644 --- a/met-api/src/met_api/utils/enums.py +++ b/met-api/src/met_api/utils/enums.py @@ -101,11 +101,4 @@ class UserStatus(IntEnum): """User status.""" ACTIVE = 1 - INACTIVE = 2 - - -class ContentTitle(Enum): - """User status.""" - - DEFAULT = 'Summary' - DEFAULT_ICON = 'SummarizeIcon' + INACTIVE = 2 \ No newline at end of file diff --git a/met-api/tests/unit/api/test_engagement_content.py b/met-api/tests/unit/api/test_engagement_content.py index 92d8c53fc..d82315ad6 100644 --- a/met-api/tests/unit/api/test_engagement_content.py +++ b/met-api/tests/unit/api/test_engagement_content.py @@ -24,7 +24,6 @@ import pytest from faker import Faker -from met_api.constants.engagement_content_type import EngagementContentType from met_api.services.engagement_content_service import EngagementContentService from met_api.utils.enums import ContentType from tests.utilities.factory_scenarios import TestEngagementContentInfo @@ -70,7 +69,7 @@ def test_create_engagement_content(client, jwt, session, engagement_content_info @pytest.mark.parametrize('engagement_content_info', [TestEngagementContentInfo.content1]) def test_get_engagement_content(client, jwt, session, engagement_content_info, setup_admin_user_and_claims): # pylint:disable=unused-argument - """Assert that a engagement content can be fetched.""" + """Assert that engagement content can be fetched.""" engagement = factory_engagement_model() engagement_content_info['engagement_id'] = engagement.id user, claims = setup_admin_user_and_claims @@ -117,20 +116,19 @@ def test_create_engagement_content_sort(client, jwt, session, assert rv.status_code == 200 assert len(rv.json) == 2, 'Two Contents Should exist.' engagement_contents = rv.json - summary_content = _find_engagement_content(engagement_contents, EngagementContentType.Summary.name) - assert summary_content.get('sort_index') == 1 - - custom_content = _find_engagement_content(engagement_contents, EngagementContentType.Custom.name) - assert custom_content.get('sort_index') == 2 + summary_content = None + custom_content = None + assert engagement_contents[0].get('title') == 'Summary' + assert engagement_contents[1].get('title') == 'Custom' # Do reorder reorder_dict = [ { - 'id': custom_content.get('id'), + 'id': engagement_contents[1].get('id'), }, { - 'id': summary_content.get('id'), + 'id': engagement_contents[0].get('id'), } ] @@ -142,17 +140,9 @@ def test_create_engagement_content_sort(client, jwt, session, rv = client.get(f'/api/engagement/{engagement.id}/content', headers=headers, content_type=ContentType.JSON.value) engagement_contents = rv.json - summary_content = _find_engagement_content(engagement_contents, EngagementContentType.Summary.name) - assert summary_content.get('sort_index') == 2 - - custom_content = _find_engagement_content(engagement_contents, EngagementContentType.Custom.name) - assert custom_content.get('sort_index') == 1 - - -def _find_engagement_content(engagement_contents, content_type): - search_result = next(x for x in engagement_contents if x.get('content_type') == content_type) - return search_result + assert engagement_contents[1].get('title') == 'Summary' + assert engagement_contents[0].get('title') == 'Custom' def test_create_engagement_content_sort_invalid(client, jwt, session, setup_admin_user_and_claims): # pylint:disable=unused-argument diff --git a/met-api/tests/unit/api/test_engagement_custom_content.py b/met-api/tests/unit/api/test_engagement_custom_content.py deleted file mode 100644 index 16062dcd5..000000000 --- a/met-api/tests/unit/api/test_engagement_custom_content.py +++ /dev/null @@ -1,146 +0,0 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests to verify the engagement custom content API end-point. - -Test-Suite to ensure that the engagement custom content endpoint is working as expected. -""" -import json -from http import HTTPStatus -from unittest.mock import patch - -import pytest -from faker import Faker - -from met_api.exceptions.business_exception import BusinessException -from met_api.services.engagement_custom_content_service import EngagementCustomContentService -from met_api.utils.enums import ContentType -from tests.utilities.factory_scenarios import TestEngagementContentInfo -from tests.utilities.factory_utils import factory_auth_header, factory_engagement_model - - -fake = Faker() - - -@pytest.mark.parametrize('engagement_content_info', [TestEngagementContentInfo.content2]) -def test_default_engagement_custom_content_is_created(client, jwt, session, engagement_content_info, - setup_admin_user_and_claims): # pylint:disable=unused-argument - """Assert that a engagement custom content can be POSTed.""" - engagement = factory_engagement_model() - engagement_content_info['engagement_id'] = engagement.id - user, claims = setup_admin_user_and_claims - headers = factory_auth_header(jwt=jwt, claims=claims) - rv = client.post(f'/api/engagement/{engagement.id}/content', - data=json.dumps(engagement_content_info), - headers=headers, content_type=ContentType.JSON.value) - assert rv.status_code == 200 - response_json = rv.json - created_content_id = response_json.get('id') - - rv = client.get( - f'/api/content/{created_content_id}/custom', - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.OK.value - assert rv.json[0].get('custom_text_content') is None - - -@pytest.mark.parametrize('engagement_content_info', [TestEngagementContentInfo.content2]) -def test_engagement_custom_content(client, jwt, session, engagement_content_info, - setup_admin_user_and_claims): # pylint:disable=unused-argument - """Assert that a engagement custom content can be POSTed.""" - engagement = factory_engagement_model() - engagement_content_info['engagement_id'] = engagement.id - user, claims = setup_admin_user_and_claims - headers = factory_auth_header(jwt=jwt, claims=claims) - rv = client.post(f'/api/engagement/{engagement.id}/content', - data=json.dumps(engagement_content_info), - headers=headers, content_type=ContentType.JSON.value) - assert rv.status_code == 200 - response_json = rv.json - created_content_id = response_json.get('id') - - data = { - 'custom_text_content': 'Content Sample', - 'custom_json_content': '"{\"blocks\":[{\"key\":\"fclgj\",\"text\":\"Rich Content Sample\",\ - \"type\":\"unstyled\",\"depth\":0,\ - \"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}"', - 'engagement_id': engagement.id - } - - rv = client.post( - f'/api/content/{created_content_id}/custom', - data=json.dumps(data), - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.OK.value - - with patch.object(EngagementCustomContentService, 'create_custom_content', - side_effect=BusinessException('Test error', status_code=HTTPStatus.BAD_REQUEST)): - rv = client.post( - f'/api/content/{created_content_id}/custom', - data=json.dumps(data), - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.BAD_REQUEST - - rv = client.get( - f'/api/content/{created_content_id}/custom', - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.OK.value - assert rv.json[1].get('custom_text_content') == data.get('custom_text_content') - - with patch.object(EngagementCustomContentService, 'get_custom_content', - side_effect=BusinessException('Test error', status_code=HTTPStatus.BAD_REQUEST)): - rv = client.get( - f'/api/content/{created_content_id}/custom', - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.BAD_REQUEST - - data_edits = { - 'custom_text_content': fake.text(max_nb_chars=10) - } - - rv = client.patch( - f'/api/content/{created_content_id}/custom', - data=json.dumps(data_edits), - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.OK - - rv = client.get( - f'/api/content/{created_content_id}/custom', - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.OK.value - assert rv.json[0].get('custom_text_content') == data_edits.get('custom_text_content') - - with patch.object(EngagementCustomContentService, 'update_custom_content', - side_effect=BusinessException('Test error', status_code=HTTPStatus.BAD_REQUEST)): - rv = client.patch( - f'/api/content/{created_content_id}/custom', - data=json.dumps(data_edits), - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.BAD_REQUEST diff --git a/met-api/tests/unit/api/test_engagement_summary_content.py b/met-api/tests/unit/api/test_engagement_summary_content.py deleted file mode 100644 index 63095b32c..000000000 --- a/met-api/tests/unit/api/test_engagement_summary_content.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests to verify the engagement summary content API end-point. - -Test-Suite to ensure that the engagement summary content endpoint is working as expected. -""" -import json -from http import HTTPStatus -from unittest.mock import patch - -import pytest -from faker import Faker - -from met_api.exceptions.business_exception import BusinessException -from met_api.services.engagement_summary_content_service import EngagementSummaryContentService -from met_api.utils.enums import ContentType -from tests.utilities.factory_scenarios import TestEngagementContentInfo -from tests.utilities.factory_utils import factory_auth_header, factory_engagement_model - - -fake = Faker() - - -@pytest.mark.parametrize('engagement_content_info', [TestEngagementContentInfo.content1]) -def test_engagement_summary_content(client, jwt, session, engagement_content_info, - setup_admin_user_and_claims): # pylint:disable=unused-argument - """Assert that a engagement summary content can be POSTed.""" - engagement = factory_engagement_model() - engagement_content_info['engagement_id'] = engagement.id - user, claims = setup_admin_user_and_claims - headers = factory_auth_header(jwt=jwt, claims=claims) - rv = client.post(f'/api/engagement/{engagement.id}/content', - data=json.dumps(engagement_content_info), - headers=headers, content_type=ContentType.JSON.value) - assert rv.status_code == 200 - response_json = rv.json - created_content_id = response_json.get('id') - - data = { - 'content': 'Content Sample', - 'rich_content': '"{\"blocks\":[{\"key\":\"fclgj\",\"text\":\"Rich Content Sample\",\ - \"type\":\"unstyled\",\"depth\":0,\ - \"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}"', - 'engagement_id': engagement.id - } - - rv = client.post( - f'/api/content/{created_content_id}/summary', - data=json.dumps(data), - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.OK.value - - with patch.object(EngagementSummaryContentService, 'create_summary_content', - side_effect=BusinessException('Test error', status_code=HTTPStatus.BAD_REQUEST)): - rv = client.post( - f'/api/content/{created_content_id}/summary', - data=json.dumps(data), - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.BAD_REQUEST - - rv = client.get( - f'/api/content/{created_content_id}/summary', - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.OK.value - assert rv.json[0].get('content') == data.get('content') - - with patch.object(EngagementSummaryContentService, 'get_summary_content', - side_effect=BusinessException('Test error', status_code=HTTPStatus.BAD_REQUEST)): - rv = client.get( - f'/api/content/{created_content_id}/summary', - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.BAD_REQUEST - - data_edits = { - 'content': fake.text(max_nb_chars=10) - } - - rv = client.patch( - f'/api/content/{created_content_id}/summary', - data=json.dumps(data_edits), - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.OK - - rv = client.get( - f'/api/content/{created_content_id}/summary', - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.OK.value - assert rv.json[0].get('content') == data_edits.get('content') - - with patch.object(EngagementSummaryContentService, 'update_summary_content', - side_effect=BusinessException('Test error', status_code=HTTPStatus.BAD_REQUEST)): - rv = client.patch( - f'/api/content/{created_content_id}/summary', - data=json.dumps(data_edits), - headers=headers, - content_type=ContentType.JSON.value - ) - assert rv.status_code == HTTPStatus.BAD_REQUEST diff --git a/met-api/tests/utilities/factory_scenarios.py b/met-api/tests/utilities/factory_scenarios.py index 438d3ea81..69776439c 100644 --- a/met-api/tests/utilities/factory_scenarios.py +++ b/met-api/tests/utilities/factory_scenarios.py @@ -24,12 +24,11 @@ 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 Status as EngagementStatus -from met_api.constants.engagement_content_type import EngagementContentType from met_api.constants.engagement_status import SubmissionStatus from met_api.constants.timeline_event_status import TimelineEventStatus from met_api.constants.feedback import CommentType, FeedbackSourceType, FeedbackStatusType, RatingType from met_api.constants.widget import WidgetType -from met_api.utils.enums import ContentTitle, LoginSource, UserStatus +from met_api.utils.enums import LoginSource, UserStatus fake = Faker() @@ -880,16 +879,20 @@ class TestEngagementContentInfo(dict, Enum): """Test scenarios of engagement content.""" content1 = { - 'title': ContentTitle.DEFAULT.value, - 'icon_name': ContentTitle.DEFAULT_ICON.value, - 'content_type': EngagementContentType.Summary.name, + 'title': 'Summary', 'is_internal': False, + 'text_content': fake.text(max_nb_chars=20), + 'json_content': "{\"blocks\":[{\"key\":\"fclgj\",\"text\":\"Rich Content Sample\",\ + \"type\":\"unstyled\",\"depth\":0,\ + \"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}" } content2 = { 'title': 'Custom', - 'icon_name': ContentTitle.DEFAULT_ICON.value, - 'content_type': EngagementContentType.Custom.name, 'is_internal': False, + 'text_content': fake.text(max_nb_chars=20), + 'json_content': "{\"blocks\":[{\"key\":\"fclgj\",\"text\":\"Rich Content Sample\",\ + \"type\":\"unstyled\",\"depth\":0,\ + \"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}" } diff --git a/met-api/tests/utilities/factory_utils.py b/met-api/tests/utilities/factory_utils.py index 61364f417..c21f85989 100644 --- a/met-api/tests/utilities/factory_utils.py +++ b/met-api/tests/utilities/factory_utils.py @@ -857,8 +857,8 @@ def factory_enagement_content_model( engagement_content = EngagementContentModel( title=engagement_content.get('title'), - icon_name=engagement_content.get('icon_name'), - content_type=engagement_content.get('content_type'), + text_content=engagement_content.get('text_content'), + json_content=engagement_content.get('json_content'), engagement_id=engagement_id, is_internal=engagement_content.get('is_internal', False), ) diff --git a/met-web/src/apiManager/endpoints/index.ts b/met-web/src/apiManager/endpoints/index.ts index dac9f9eb0..db80cd18a 100644 --- a/met-web/src/apiManager/endpoints/index.ts +++ b/met-web/src/apiManager/endpoints/index.ts @@ -45,16 +45,6 @@ const Endpoints = { UPDATE: `${AppConfig.apiUrl}/engagement/engagement_id/content/content_id`, DELETE: `${AppConfig.apiUrl}/engagement/engagement_id/content/content_id`, }, - EngagementCustomContent: { - GET: `${AppConfig.apiUrl}/content/content_id/custom`, - CREATE: `${AppConfig.apiUrl}/content/content_id/custom`, - UPDATE: `${AppConfig.apiUrl}/content/content_id/custom`, - }, - EngagementSummaryContent: { - GET: `${AppConfig.apiUrl}/content/content_id/summary`, - CREATE: `${AppConfig.apiUrl}/content/content_id/summary`, - UPDATE: `${AppConfig.apiUrl}/content/content_id/summary`, - }, User: { GET: `${AppConfig.apiUrl}/user/user_id`, CREATE_UPDATE: `${AppConfig.apiUrl}/user/`, diff --git a/met-web/src/components/engagement/form/ActionContext.tsx b/met-web/src/components/engagement/form/ActionContext.tsx index 3d5c5f5e9..a6708c481 100644 --- a/met-web/src/components/engagement/form/ActionContext.tsx +++ b/met-web/src/components/engagement/form/ActionContext.tsx @@ -14,6 +14,7 @@ import { EngagementStatus } from 'constants/engagementStatus'; import { EngagementContent, createDefaultEngagementContent } from 'models/engagementContent'; import { TenantState } from 'reduxSlices/tenantSlice'; import { getEngagementContent } from 'services/engagementContentService'; +import { EngagementLoaderData } from '../public/view'; const CREATE = 'create'; export const ActionContext = createContext({ @@ -71,16 +72,7 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { const [savedBannerImageFileName, setSavedBannerImageFileName] = useState(''); const [contentTabs, setContentTabs] = useState([createDefaultEngagementContent()]); const isCreate = window.location.pathname.includes(CREATE); - const routeLoaderData = useRouteLoaderData('single-engagement') as - | { - engagement: Promise; - content: Promise; - metadata: Promise; - taxa: Promise; - } - | undefined; - - const { engagement, content, metadata, taxa } = routeLoaderData || {}; + const { engagement, content, metadata, taxa } = useRouteLoaderData('single-engagement') as EngagementLoaderData; // Load the engagement from the shared individual engagement loader and watch the engagement variable for any changes. useEffect(() => { @@ -116,7 +108,7 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { setContentTabs(result); }); } - }, [content]); + }, [content, engagementId]); // Load the engagement's metadata and taxa from the shared individual engagement loader and watch the metadata and taxa variables for any changes. useEffect(() => { diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabModal.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabModal.tsx index f26a78ea2..3a6ffde3e 100644 --- a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabModal.tsx +++ b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/ContentTabModal.tsx @@ -1,12 +1,12 @@ import React, { useContext, useEffect, useState } from 'react'; -import { MenuItem, Modal, Grid, Stack, TextField, Select } from '@mui/material'; +import { Modal, Grid, Stack, TextField } from '@mui/material'; import { modalStyle, MetHeader1Old, MetLabel, PrimaryButtonOld } from 'components/common'; import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; import { ActionContext } from '../../ActionContext'; -import { EngagementContentContext } from './EngagementContentContext'; import { EngagementContent } from 'models/engagementContent'; import { postEngagementContent, patchEngagementContent } from 'services/engagementContentService'; +import { EngagementContentContext } from './EngagementContentContext'; interface ContentTabModalProps { open: boolean; @@ -19,36 +19,27 @@ interface ContentTabModalProps { const ContentTabModal = ({ open, updateModal, tabs, setTabs, selectedTabType, tabIndex }: ContentTabModalProps) => { const { savedEngagement } = useContext(ActionContext); - const { isEditMode, setIsSummaryContentsLoading, setIsCustomContentsLoading } = - useContext(EngagementContentContext); + const { isEditMode } = useContext(EngagementContentContext); const dispatch = useAppDispatch(); const [tabTitle, setTabTitle] = useState(''); - const [tabIcon, setTabIcon] = useState(''); useEffect(() => { // Fetch tab details when modal is opened and selectedTabIndex changes if (open && isEditMode && typeof tabIndex === 'number' && tabs[tabIndex]) { setTabTitle(tabs[tabIndex].title); - setTabIcon(tabs[tabIndex].icon_name); } else { // If not in edit mode, initialize tabTitle and tabIcon with empty values setTabTitle(''); - setTabIcon(''); } }, [open, isEditMode, tabIndex, tabs]); const fetchData = async () => { - setIsSummaryContentsLoading(true); - setIsCustomContentsLoading(true); const newtab = isEditMode ? await patchEngagementContent(savedEngagement.id, tabs[tabIndex || 0].id, { title: tabTitle, - icon_name: tabIcon, }) : await postEngagementContent(savedEngagement.id, { title: tabTitle, - icon_name: tabIcon, - content_type: selectedTabType, engagement_id: savedEngagement.id, }); @@ -75,15 +66,12 @@ const ContentTabModal = ({ open, updateModal, tabs, setTabs, selectedTabType, ta } details.`, }), ); - setIsSummaryContentsLoading(false); - setIsCustomContentsLoading(false); handleModalClose(); }; const handleModalClose = () => { updateModal(false); setTabTitle(''); - setTabIcon(''); }; return ( @@ -135,33 +123,6 @@ const ContentTabModal = ({ open, updateModal, tabs, setTabs, selectedTabType, ta - - - - Tab Icon: - - - - - - - - { const { contentTabs, setContentTabs, savedEngagement } = useContext(ActionContext); - const { setIsEditMode, isSummaryContentsLoading } = useContext(EngagementContentContext); + const { setIsEditMode } = useContext(EngagementContentContext); const [tabIndex, setTabIndex] = useState(0); const [isModalOpen, setIsModalOpen] = useState(false); - const [selectedTabType, setSelectedTabType] = useState(''); const [anchorEl, setAnchorEl] = useState(null); - const [customTabAdded, setCustomTabAdded] = useState(false); - const isAllTabTypesPresent = () => { - const tabTypes = Object.values(CONTENT_TYPE); - for (const tabType of tabTypes) { - if (!contentTabs.some((tab) => tab.content_type === tabType)) { - return false; // At least one tab type is missing - } - } - return true; // All tab types are present - }; const handleSetTabIndex = (_event: React.SyntheticEvent, newValue: number) => { setTabIndex(newValue); @@ -53,11 +39,6 @@ export const ContentTabs: React.FC = () => { } await deleteEngagementContent(savedEngagement.id, tab_id); setTabIndex(0); - - // Check if the deleted tab was a custom tab and reset customTabAdded state - if (contentTabs[index].content_type === CONTENT_TYPE.CUSTOM) { - setCustomTabAdded(false); - } }; const handleMenuOpen = (event: React.MouseEvent) => { @@ -68,16 +49,10 @@ export const ContentTabs: React.FC = () => { setAnchorEl(null); }; - const handleMenuClick = (type: string) => { + const handleMenuClick = () => { setIsEditMode(false); - setSelectedTabType(type); handleModalOpen(); handleMenuClose(); - - // Set customTabAdded to true when a custom tab is added - if (type === CONTENT_TYPE.CUSTOM) { - setCustomTabAdded(true); - } }; const handleEditTab = (index: number) => { @@ -87,107 +62,66 @@ export const ContentTabs: React.FC = () => { }; return ( - - - - - - - - - - - - {contentTabs?.map((tab, index) => ( - - {tab.title} - - handleEditTab(index)} aria-label="edit"> - - - - {index !== 0 && ( - - handleDeleteTab(index)} - aria-label="delete" - > - - - - )} - - } - /> - ))} - - - - - - handleMenuClick(CONTENT_TYPE.CUSTOM)} - data-testid="add-new-custom-tab" - > - Add new custom tab - - - - - - {contentTabs && contentTabs.length > 0 ? ( - (() => { - if (contentTabs[tabIndex]) { - switch (contentTabs[tabIndex].content_type) { - case CONTENT_TYPE.SUMMARY: - return ; - case CONTENT_TYPE.CUSTOM: - return ; - default: - return ( -
- Custom tab content for {contentTabs[tabIndex].content_type} -
- ); - } - } else { - return
Invalid tab index
; - } - })() - ) : ( -
No tabs available
- )} -
-
- + + + + + {contentTabs?.map((tab, index) => ( + + {tab.title} + + handleEditTab(index)} aria-label="edit"> + + + + {index !== 0 && ( + + handleDeleteTab(index)} aria-label="delete"> + + + + )} + + } + /> + ))} + + -
-
+ + + handleMenuClick()} data-testid="add-new-custom-tab"> + Add new custom tab + + + + {contentTabs?.map((tab, index) => ( + + + + ))} + + ); }; diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/CustomTabContent.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/CustomTabContent.tsx deleted file mode 100644 index b3bc525ad..000000000 --- a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/CustomTabContent.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useEffect, useState, useContext } from 'react'; -import { Grid } from '@mui/material'; -import { MetDescription, MetLabel } from 'components/common'; -import RichTextEditor from 'components/common/RichTextEditor'; -import { EngagementTabsContext } from '../EngagementTabsContext'; - -const CustomTabContent = () => { - const [initialRichContent, setInitialRichContent] = useState(''); - - const { setCustomTextContent, customJsonContent, setCustomJsonContent } = useContext(EngagementTabsContext); - - const handleContentChange = (rawText: string) => { - setCustomTextContent(rawText); - }; - - const handleRichContentChange = (newState: string) => { - setCustomJsonContent(newState); - }; - - useEffect(() => { - setInitialRichContent(customJsonContent); - }, []); - - return ( - - - Engagement - Page Custom Content - - This is the additional content of the engagement page. - - - - - ); -}; - -export default CustomTabContent; diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/EngagementContentContext.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/EngagementContentContext.tsx index ad5b90fb8..05ba1083a 100644 --- a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/EngagementContentContext.tsx +++ b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementContent/EngagementContentContext.tsx @@ -1,29 +1,13 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; -import { CONTENT_TYPE } from 'models/engagementContent'; import { ActionContext } from '../../ActionContext'; import { EngagementTabsContext } from '../EngagementTabsContext'; -import { useRouteLoaderData } from 'react-router-dom'; -import { EngagementSummaryContent } from 'models/engagementSummaryContent'; -import { EngagementCustomContent } from 'models/engagementCustomContent'; export interface EngagementContentProps { - isSummaryContentsLoading: boolean; - setIsSummaryContentsLoading: React.Dispatch>; - isCustomContentsLoading: boolean; - setIsCustomContentsLoading: React.Dispatch>; isEditMode: boolean; setIsEditMode: React.Dispatch>; } export const EngagementContentContext = createContext({ - isSummaryContentsLoading: false, - setIsSummaryContentsLoading: async () => { - /* empty default method */ - }, - isCustomContentsLoading: false, - setIsCustomContentsLoading: async () => { - /* empty default method */ - }, isEditMode: false, setIsEditMode: async () => { /* empty default method */ @@ -32,70 +16,25 @@ export const EngagementContentContext = createContext({ export const EngagementContextProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => { const { contentTabs, savedEngagement } = useContext(ActionContext); - const { - setRichContent, - engagementFormData, - setEngagementFormData, - setEngagementSummaryContent, - setEngagementCustomContent, - setCustomTextContent, - setCustomJsonContent, - } = useContext(EngagementTabsContext); - const [isSummaryContentsLoading, setIsSummaryContentsLoading] = useState(true); - const [isCustomContentsLoading, setIsCustomContentsLoading] = useState(true); + const { engagementFormData, setEngagementFormData } = useContext(EngagementTabsContext); const [isEditMode, setIsEditMode] = useState(false); - const summaryItem = contentTabs.find((item) => item.content_type === CONTENT_TYPE.SUMMARY); - const customItem = contentTabs.find((item) => item.content_type === CONTENT_TYPE.CUSTOM); - const routeLoaderData = useRouteLoaderData('single-engagement') as - | { - contentSummary: Promise; - customContent: Promise; - } - | undefined; - const { contentSummary, customContent } = routeLoaderData ?? {}; // Load the engagement's summary from the shared individual engagement loader and watch the summary item variable for any changes. useEffect(() => { - if (!savedEngagement.id || !summaryItem) { - setIsSummaryContentsLoading(false); + if (!savedEngagement.id) { return; } - if (savedEngagement && contentSummary) { - contentSummary.then((result: EngagementSummaryContent[]) => { - setEngagementSummaryContent(result[0]); - setRichContent(result[0].rich_content); - setEngagementFormData({ - ...engagementFormData, - content: result[0].content, - }); - setIsSummaryContentsLoading(false); - }); - } - }, [summaryItem, contentSummary, savedEngagement]); - - // Load the engagement's custom content from the shared individual engagement loader and watch the customItem variable for any changes. - useEffect(() => { - if (savedEngagement && customContent) { - customContent.then((result: EngagementCustomContent[]) => { - if (!savedEngagement.id || !customItem) { - setIsCustomContentsLoading(false); - return; - } - setEngagementCustomContent(result); - setCustomTextContent(result[1].custom_text_content); - setCustomJsonContent(result[1].custom_json_content); - setIsCustomContentsLoading(false); + if (savedEngagement && contentTabs) { + setEngagementFormData({ + ...engagementFormData, + content: contentTabs[0]?.json_content, }); } - }, [customItem, customContent, savedEngagement]); + }, [savedEngagement, contentTabs]); return ( { - const { savedEngagement } = useContext(ActionContext); +const TabContent = ({ index }: { index: number }) => { + const { savedEngagement, contentTabs, setContentTabs } = useContext(ActionContext); const [initialRichContent, setInitialRichContent] = useState(''); const [editorDisabled, setEditorDisabled] = useState(false); - const { engagementFormData, setEngagementFormData, richContent, setRichContent } = - useContext(EngagementTabsContext); + const { richContent, setRichContent } = useContext(EngagementTabsContext); const handleContentChange = (rawText: string) => { - setEngagementFormData({ - ...engagementFormData, - content: rawText, + setContentTabs((prevTabs) => { + const newTabs = [...prevTabs]; + newTabs[index].json_content = rawText; + return newTabs; }); }; @@ -42,10 +43,18 @@ const SummaryTabContent = () => { columnSpacing={2} > - - Engagement - Page Content - - This is the main content of the engagement page. + + + Engagement - Page Content + + This is the main content of the engagement page. + + + + Additional Tab {index} - {contentTabs[index].title} + + Additional content for the engagement page. +
{ )}
- - - + + + + +
); }; -export default SummaryTabContent; +export default TabContent; diff --git a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementTabsContext.tsx b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementTabsContext.tsx index 929c1ec98..d3dd5e320 100644 --- a/met-web/src/components/engagement/form/EngagementFormTabs/EngagementTabsContext.tsx +++ b/met-web/src/components/engagement/form/EngagementFormTabs/EngagementTabsContext.tsx @@ -8,8 +8,6 @@ import { getTeamMembers } from 'services/membershipService'; import { openNotification } from 'services/notificationService/notificationSlice'; import { useAppDispatch } from 'hooks'; import { EngagementSettings, createDefaultEngagementSettings } from 'models/engagement'; -import { EngagementSummaryContent } from 'models/engagementSummaryContent'; -import { EngagementCustomContent } from 'models/engagementCustomContent'; import { updatedDiff } from 'deep-object-diff'; import { getSlugByEngagementId } from 'services/engagementSlugService'; import { @@ -17,8 +15,6 @@ import { getEngagementSettings, patchEngagementSettings, } from 'services/engagementSettingService'; -import { PatchSummaryContentRequest, patchSummaryContent } from 'services/engagementSummaryService'; -import { PatchCustomContentRequest, patchCustomContent } from 'services/engagementCustomService'; import { EngagementSlugPatchRequest, patchEngagementSlug } from 'services/engagementSlugService'; import { EngagementForm } from '../types'; import { SubmissionStatus } from 'constants/engagementStatus'; @@ -65,22 +61,6 @@ const initialEngagementSettingsSlugData = { slug: '', }; -const initialEngagementSummaryContent = { - id: 0, - content: '', - rich_content: '', - engagement_id: 0, - engagement_content_id: 0, -}; - -const initialEngagementCustomContent = { - id: 0, - custom_text_content: '', - custom_json_content: '', - engagement_id: 0, - engagement_content_id: 0, -}; - interface EngagementFormError { name: boolean; start_date: boolean; @@ -128,10 +108,6 @@ export interface EngagementTabsContextState { hasBeenOpened: boolean; slug: EngagementSettingsSlugData; setSlug: React.Dispatch>; - engagementSummaryContent: EngagementSummaryContent; - setEngagementSummaryContent: React.Dispatch>; - engagementCustomContent: EngagementCustomContent[]; - setEngagementCustomContent: React.Dispatch>; customTextContent: string; setCustomTextContent: React.Dispatch>; customJsonContent: string; @@ -205,14 +181,6 @@ export const EngagementTabsContext = createContext({ setSlug: () => { /* empty default method */ }, - engagementSummaryContent: initialEngagementSummaryContent, - setEngagementSummaryContent: () => { - /* empty default method */ - }, - engagementCustomContent: [initialEngagementCustomContent], - setEngagementCustomContent: () => { - /* empty default method */ - }, customTextContent: '', setCustomTextContent: () => { /* empty default method */ @@ -249,12 +217,6 @@ export const EngagementTabsContextProvider = ({ children }: { children: React.Re const [richConsentMessage, setRichConsentMessage] = useState(savedEngagement?.consent_message || ''); const [customTextContent, setCustomTextContent] = useState(''); const [customJsonContent, setCustomJsonContent] = useState(''); - const [engagementSummaryContent, setEngagementSummaryContent] = useState( - initialEngagementSummaryContent, - ); - const [engagementCustomContent, setEngagementCustomContent] = useState([ - initialEngagementCustomContent, - ]); const [engagementFormError, setEngagementFormError] = useState(initialFormError); const metadataFormRef = useRef(null); @@ -383,80 +345,6 @@ export const EngagementTabsContextProvider = ({ children }: { children: React.Re } }; - const updateSummaryContent = async () => { - setSaving(true); - try { - const updatedSummaryContent = updatedDiff( - { - content: engagementSummaryContent.content, - rich_content: engagementSummaryContent.rich_content, - }, - { - content: engagementFormData.content, - rich_content: richContent, - }, - ) as PatchSummaryContentRequest; - if (Object.keys(updatedSummaryContent).length === 0) { - setSaving(false); - return; - } - const result = await patchSummaryContent( - engagementSummaryContent.engagement_content_id, - updatedSummaryContent, - ); - setEngagementSummaryContent(result); - setRichContent(result.rich_content); - dispatch(openNotification({ severity: 'success', text: 'Engagement has been saved' })); - setSaving(false); - return; - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: 'Error occurred while trying to update summary content, please refresh the page or try again at a later time', - }), - ); - setSaving(false); - } - }; - - const updateCustomContent = async () => { - setSaving(true); - try { - const updatedCustomContent = updatedDiff( - { - custom_text_content: engagementCustomContent[1].custom_text_content, - custom_json_content: engagementCustomContent[1].custom_json_content, - }, - { - custom_text_content: customTextContent, - custom_json_content: customJsonContent, - }, - ) as PatchCustomContentRequest; - if (Object.keys(updatedCustomContent).length === 0) { - setSaving(false); - return; - } - const result = await patchCustomContent( - engagementCustomContent[1].engagement_content_id, - updatedCustomContent, - ); - setEngagementCustomContent([result]); - setCustomJsonContent(result.custom_json_content); - dispatch(openNotification({ severity: 'success', text: 'Engagement has been saved' })); - setSaving(false); - return; - } catch (error) { - dispatch( - openNotification({ - severity: 'error', - text: 'Error occurred while trying to update custom content, please refresh the page or try again at a later time', - }), - ); - setSaving(false); - } - }; - const [savedSlug, setSavedSlug] = useState(''); const [slug, setSlug] = useState(initialEngagementSettingsSlugData); @@ -578,8 +466,6 @@ export const EngagementTabsContextProvider = ({ children }: { children: React.Re if (!isNewEngagement) { await updateEngagementSettings(sendReport); - await updateSummaryContent(); - await updateCustomContent(); await handleSaveSlug(slug); await handleSaveEngagementMetadata(); } @@ -648,10 +534,6 @@ export const EngagementTabsContextProvider = ({ children }: { children: React.Re hasBeenOpened, slug, setSlug, - engagementSummaryContent, - setEngagementSummaryContent, - engagementCustomContent, - setEngagementCustomContent, customTextContent, setCustomTextContent, customJsonContent, diff --git a/met-web/src/components/engagement/old-view/ActionContext.tsx b/met-web/src/components/engagement/old-view/ActionContext.tsx index ed60b3e9d..2bd5c76cb 100644 --- a/met-web/src/components/engagement/old-view/ActionContext.tsx +++ b/met-web/src/components/engagement/old-view/ActionContext.tsx @@ -9,7 +9,8 @@ import { SubmissionStatus } from 'constants/engagementStatus'; import { verifyEmailVerification } from 'services/emailVerificationService'; import { openNotificationModal } from 'services/notificationModalService/notificationModalSlice'; import { getEngagementIdBySlug } from 'services/engagementSlugService'; -import { EngagementSummaryContent } from 'models/engagementSummaryContent'; +import { EngagementLoaderData } from '../public/view'; +import { EngagementContent } from 'models/engagementContent'; interface EngagementSchedule { id: number; @@ -31,8 +32,7 @@ export interface EngagementViewContext { engagementWidgets: Widget[]; mockStatus: SubmissionStatus; updateMockStatus: (status: SubmissionStatus) => void; - content: string; - richContent: string; + content: EngagementContent[]; } export type EngagementParams = { @@ -56,8 +56,7 @@ export const ActionContext = createContext({ updateMockStatus: (status: SubmissionStatus) => { /* nothing returned */ }, - content: '', - richContent: '', + content: [], }); export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => { @@ -73,14 +72,13 @@ export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Eleme const [engagementWidgets, setEngagementWidgets] = useState([]); const [isEngagementLoading, setEngagementLoading] = useState(true); const [isWidgetsLoading, setIsWidgetsLoading] = useState(true); - const [content, setContent] = useState(''); - const [richContent, setRichContent] = useState(''); + const [content, setContent] = useState([]); - const { engagement } = useRouteLoaderData('single-engagement') as { engagement: Promise }; - const { contentSummary } = useRouteLoaderData('single-engagement') as { - contentSummary: Promise; - }; - const { widgets } = useRouteLoaderData('single-engagement') as { widgets: Promise }; + const { + engagement, + widgets, + content: contentPromise, + } = useRouteLoaderData('single-engagement') as EngagementLoaderData; // Load the engagement from the shared individual engagement loader and watch the engagement variable for any changes. useEffect(() => { @@ -99,27 +97,11 @@ export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Eleme // Load the widgets from the shared individual engagement loader and watch the engagement variable for any changes. useEffect(() => { - if (!engagementId && slug) { - return; - } widgets.then((result) => { setEngagementWidgets(result); setIsWidgetsLoading(false); }); - }, [widgets]); - - // Load the engagement's summary from the shared individual engagement loader and watch the summary variable for any changes. - useEffect(() => { - contentSummary.then((result: EngagementSummaryContent[]) => { - if ((!engagementId && slug) || !result.length) { - return; - } - const selectedEngagement = result[0]; - setContent(selectedEngagement.content); - setRichContent(selectedEngagement.rich_content); - setEngagementLoading(false); - }); - }, [contentSummary]); + }, [widgets, engagementId]); useEffect(() => { verifySubscribeToken(); @@ -129,6 +111,12 @@ export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Eleme setMockStatus(savedEngagement.submission_status); }, [savedEngagement]); + useEffect(() => { + contentPromise.then((result) => { + setContent(result); + }); + }, [contentPromise]); + const verifySubscribeToken = async () => { try { if (!token) { @@ -217,7 +205,6 @@ export const ActionProvider = ({ children }: { children: JSX.Element | JSX.Eleme mockStatus, unpublishEngagement, content, - richContent, }} > {children} diff --git a/met-web/src/components/engagement/old-view/EngagementContent.tsx b/met-web/src/components/engagement/old-view/EngagementContent.tsx index 78fcc45b7..990bb9c83 100644 --- a/met-web/src/components/engagement/old-view/EngagementContent.tsx +++ b/met-web/src/components/engagement/old-view/EngagementContent.tsx @@ -5,16 +5,16 @@ import { ActionContext } from './ActionContext'; import { Skeleton } from '@mui/material'; import { getEditorStateFromRaw } from 'components/common/RichTextEditor/utils'; -export const EngagementContent = () => { - const { richContent, isEngagementLoading } = useContext(ActionContext); +export const EngagementContent = ({ index = 0 }: { index?: number }) => { + const { content, isEngagementLoading } = useContext(ActionContext); - if (isEngagementLoading) { + if (isEngagementLoading || !content.length) { return ; } return ( - + ); }; diff --git a/met-web/src/components/engagement/old-view/ScheduleModal/index.tsx b/met-web/src/components/engagement/old-view/ScheduleModal/index.tsx index a10153360..2f3be53df 100644 --- a/met-web/src/components/engagement/old-view/ScheduleModal/index.tsx +++ b/met-web/src/components/engagement/old-view/ScheduleModal/index.tsx @@ -28,11 +28,11 @@ interface ScheduleModalProps { const ScheduleModal = ({ reschedule, open, updateModal }: ScheduleModalProps) => { const [scheduledDate, setScheduledDate] = useState(dayjs(Date.now())); - const { content, savedEngagement, scheduleEngagement } = useContext(ActionContext); + const { savedEngagement, scheduleEngagement } = useContext(ActionContext); const dispatch = useAppDispatch(); const isEngagementReady = () => { - return content && savedEngagement.description && savedEngagement.banner_url; + return savedEngagement.description && savedEngagement.banner_url; }; const handleChange = (newDate: Dayjs | null) => { diff --git a/met-web/src/components/engagement/public/view/EngagementContentTabs.tsx b/met-web/src/components/engagement/public/view/EngagementContentTabs.tsx index e394acab9..0f2189dd3 100644 --- a/met-web/src/components/engagement/public/view/EngagementContentTabs.tsx +++ b/met-web/src/components/engagement/public/view/EngagementContentTabs.tsx @@ -3,24 +3,19 @@ import { Tab, Skeleton, Box } from '@mui/material'; import { TabContext, TabList, TabPanel } from '@mui/lab'; import { Await, useLoaderData } from 'react-router-dom'; import { EngagementContent } from 'models/engagementContent'; -import { EngagementSummaryContent } from 'models/engagementSummaryContent'; import { getEditorStateFromRaw } from 'components/common/RichTextEditor/utils'; import { Header2 } from 'components/common/Typography'; import { colors } from 'components/common'; import { RichTextArea } from 'components/common/Input/RichTextArea'; +import { EngagementLoaderData } from './EngagementLoader'; export const EngagementContentTabs = () => { - const { content, contentSummary } = useLoaderData() as { - content: Promise; - contentSummary: Promise; - }; + const { content } = useLoaderData() as EngagementLoaderData; const [selectedTab, setSelectedTab] = useState('0'); const handleChange = (event: SyntheticEvent, newValue: string) => { setSelectedTab(newValue); }; - const panelContents = Promise.all([content, contentSummary]); - const tabListRef = useCallback((node: HTMLButtonElement) => { if (!node) return; const scroller = node.getElementsByClassName('MuiTabs-scroller')[0]; @@ -137,25 +132,22 @@ export const EngagementContentTabs = () => { }> - - {([content, contentSummary]: [ - content: EngagementContent[], - contentSummary: EngagementSummaryContent[], - ]) => { - return contentSummary.map((summary, index) => ( - + + {(resolvedContent: EngagementContent[]) => + resolvedContent.map((content, index) => ( + - {content[index].title} + {content.title} - )); - }} + )) + } diff --git a/met-web/src/components/engagement/public/view/EngagementLoader.tsx b/met-web/src/components/engagement/public/view/EngagementLoader.tsx index 792fea0d0..185488e73 100644 --- a/met-web/src/components/engagement/public/view/EngagementLoader.tsx +++ b/met-web/src/components/engagement/public/view/EngagementLoader.tsx @@ -1,17 +1,12 @@ -import { EngagementCustomContent } from 'models/engagementCustomContent'; -import { EngagementSummaryContent } from 'models/engagementSummaryContent'; import { Params, defer } from 'react-router-dom'; import { getEngagementContent } from 'services/engagementContentService'; -import { getCustomContent } from 'services/engagementCustomService'; import { getEngagement } from 'services/engagementService'; import { getEngagementIdBySlug, getSlugByEngagementId } from 'services/engagementSlugService'; -import { getSummaryContent } from 'services/engagementSummaryService'; import { getWidgets } from 'services/widgetService'; import { getEngagementMetadata, getMetadataTaxa } from 'services/engagementMetadataService'; -import { Engagement, EngagementMetadata } from 'models/engagement'; +import { Engagement, EngagementMetadata, MetadataTaxon } from 'models/engagement'; import { Widget } from 'models/widget'; import { EngagementContent } from 'models/engagementContent'; -import { TaxonType } from 'components/metadataManagement/types'; import { getTeamMembers } from 'services/membershipService'; import { EngagementTeamMember } from 'models/engagementTeamMember'; @@ -20,10 +15,8 @@ export type EngagementLoaderData = { slug: Promise; widgets: Promise; content: Promise; - contentSummary: Promise; metadata: Promise; - taxa: Promise; - customContent: Promise; + taxa: Promise; teamMembers: Promise; }; @@ -58,51 +51,13 @@ export const engagementLoader = async ({ params }: { params: Params }) = const taxa = taxaData.then((taxa) => Object.values(taxa)); - const contentSummary = content.then((response) => - Promise.all( - response.map((content) => { - return content.content_type === 'Summary' - ? getSummaryContent(Number(content.id)).then(convertArrayToSingleEntry) - : getCustomContent(content.id).then(castCustomContentToSummaryContent); - }), - ), - ); - - const customContent = content.then((response) => - Promise.all( - response.map((content) => { - if (content.content_type === 'Custom') return getCustomContent(content.id).then((result) => result[0]); - }), - ), - ); - - const convertArrayToSingleEntry = ( - engagementSummaryContent: EngagementSummaryContent[], - ): EngagementSummaryContent => { - return engagementSummaryContent[0]; - }; - - const castCustomContentToSummaryContent = (customContent: EngagementCustomContent[]): EngagementSummaryContent => { - const mappedCustomContent = customContent.map((custom) => ({ - id: custom.id, - content: custom.custom_text_content, - rich_content: custom.custom_json_content, - engagement_id: custom.engagement_id, - engagement_content_id: custom.engagement_content_id, - })); - const finishedContent = mappedCustomContent[0]; - return finishedContent; - }; - return defer({ engagement, slug, widgets, content, - contentSummary, metadata, taxa, - customContent, teamMembers, }); }; diff --git a/met-web/src/models/engagementContent.ts b/met-web/src/models/engagementContent.ts index 14e705198..4287212e2 100644 --- a/met-web/src/models/engagementContent.ts +++ b/met-web/src/models/engagementContent.ts @@ -1,15 +1,8 @@ -export type EngagementContentTypes = 'Summary' | 'Custom'; - -export const CONTENT_TYPE: { [status: string]: EngagementContentTypes } = { - SUMMARY: 'Summary', - CUSTOM: 'Custom', -}; - export interface EngagementContent { id: number; title: string; - icon_name: string; - content_type: string; + text_content: string; + json_content: string; engagement_id: number; sort_index: number; is_internal: boolean; @@ -19,8 +12,8 @@ export const createDefaultEngagementContent = (): EngagementContent => { return { id: 0, title: 'Summary', - icon_name: '', - content_type: CONTENT_TYPE.SUMMARY, + text_content: '', + json_content: '{}', engagement_id: 0, sort_index: 1, is_internal: false, diff --git a/met-web/src/models/engagementCustomContent.ts b/met-web/src/models/engagementCustomContent.ts deleted file mode 100644 index d915f7702..000000000 --- a/met-web/src/models/engagementCustomContent.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface EngagementCustomContent { - id: number; - custom_text_content: string; - custom_json_content: string; - engagement_id: number; - engagement_content_id: number; -} diff --git a/met-web/src/models/engagementSummaryContent.ts b/met-web/src/models/engagementSummaryContent.ts deleted file mode 100644 index 7c58eaa08..000000000 --- a/met-web/src/models/engagementSummaryContent.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface EngagementSummaryContent { - id: number; - content: string; - rich_content: string; - engagement_id: number; - engagement_content_id: number; -} diff --git a/met-web/src/services/engagementContentService/index.ts b/met-web/src/services/engagementContentService/index.ts index d51356fe8..5157abeca 100644 --- a/met-web/src/services/engagementContentService/index.ts +++ b/met-web/src/services/engagementContentService/index.ts @@ -17,11 +17,11 @@ export const getEngagementContent = async (engagementId: number): Promise => { - const url = replaceUrl(Endpoints.EngagementCustomContent.GET, 'content_id', String(contentId)); - if (!contentId || isNaN(Number(contentId))) { - return Promise.reject('Invalid content Id ' + contentId); - } - const response = await http.GetRequest(url); // Notice the change here - if (response.data) { - return response.data; - } - return Promise.reject('Failed to fetch engagement Custom content'); -}; - -interface PostCustomContentRequest { - custom_text_content?: string; - custom_json_content?: string; - engagement_id?: number; - engagement_content_id?: number; -} -export const postCustomContent = async ( - contentId: number, - data: PostCustomContentRequest, -): Promise => { - try { - const url = replaceUrl(Endpoints.EngagementCustomContent.CREATE, 'content_id', String(contentId)); - const response = await http.PostRequest(url, data); - if (response.data) { - return response.data; - } - return Promise.reject('Failed to create engagement Custom content'); - } catch (err) { - return Promise.reject(err); - } -}; - -export interface PatchCustomContentRequest { - custom_text_content?: string; - custom_json_content?: string; -} -export const patchCustomContent = async ( - contentId: number, - data: PatchCustomContentRequest, -): Promise => { - const url = replaceAllInURL({ - URL: Endpoints.EngagementCustomContent.UPDATE, - params: { - content_id: String(contentId), - }, - }); - const response = await http.PatchRequest(url, data); - if (response.data) { - return response.data; - } - return Promise.reject('Failed to update engagement Custom content'); -}; diff --git a/met-web/src/services/engagementSummaryService/index.ts b/met-web/src/services/engagementSummaryService/index.ts deleted file mode 100644 index 7454f107b..000000000 --- a/met-web/src/services/engagementSummaryService/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import http from 'apiManager/httpRequestHandler'; -import { EngagementSummaryContent } from 'models/engagementSummaryContent'; -import Endpoints from 'apiManager/endpoints'; -import { replaceAllInURL, replaceUrl } from 'helper'; - -export const getSummaryContent = async (contentId: number): Promise => { - const url = replaceUrl(Endpoints.EngagementSummaryContent.GET, 'content_id', String(contentId)); - if (!contentId || isNaN(Number(contentId))) { - return Promise.reject('Invalid content Id ' + contentId); - } - const response = await http.GetRequest(url); - if (response.data) { - return response.data; - } - return Promise.reject('Failed to fetch engagement summary content'); -}; - -interface PostSummaryContentRequest { - content?: string; - rich_content?: string; - engagement_id?: number; -} -export const postSummaryContent = async ( - contentId: number, - data: PostSummaryContentRequest, -): Promise => { - try { - const url = replaceUrl(Endpoints.EngagementSummaryContent.CREATE, 'content_id', String(contentId)); - const response = await http.PostRequest(url, data); - if (response.data) { - return response.data; - } - return Promise.reject('Failed to create engagement summary content'); - } catch (err) { - return Promise.reject(err); - } -}; - -export interface PatchSummaryContentRequest { - content?: string; - rich_content?: string; -} -export const patchSummaryContent = async ( - contentId: number, - data: PatchSummaryContentRequest, -): Promise => { - const url = replaceAllInURL({ - URL: Endpoints.EngagementSummaryContent.UPDATE, - params: { - content_id: String(contentId), - }, - }); - const response = await http.PatchRequest(url, data); - if (response.data) { - return response.data; - } - return Promise.reject('Failed to update engagement summary content'); -}; diff --git a/met-web/tests/unit/components/engagement/EngagementFormUserTab.test.tsx b/met-web/tests/unit/components/engagement/EngagementFormUserTab.test.tsx index 7120ec46e..30745e80d 100644 --- a/met-web/tests/unit/components/engagement/EngagementFormUserTab.test.tsx +++ b/met-web/tests/unit/components/engagement/EngagementFormUserTab.test.tsx @@ -92,7 +92,6 @@ jest.mock('react-router-dom', () => ({ engagement: Promise.resolve(draftEngagement), metadata: Promise.resolve([]), widgets: Promise.resolve([]), - contentSummary: Promise.resolve([]), content: Promise.resolve([]), }; } @@ -101,7 +100,6 @@ jest.mock('react-router-dom', () => ({ engagement: Promise.resolve(draftEngagement), metadata: Promise.resolve([]), widgets: Promise.resolve([]), - contentSummary: Promise.resolve([]), content: Promise.resolve([]), }), })); diff --git a/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.Two.test.tsx b/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.Two.test.tsx index 5efe8d896..629cc4ada 100644 --- a/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.Two.test.tsx +++ b/met-web/tests/unit/components/engagement/form/edit/EngagementForm.Edit.Two.test.tsx @@ -367,7 +367,6 @@ describe('Engagement form page tests', () => { await waitFor(() => { expect(getByTestId('add-tab-button')).toBeVisible(); expect(getByText('Tab Title:')).toBeInTheDocument(); - expect(getByText('Tab Icon:')).toBeInTheDocument(); }); }); diff --git a/met-web/tests/unit/components/engagement/old-engagement.test.tsx b/met-web/tests/unit/components/engagement/old-engagement.test.tsx index ff12f5048..efba92468 100644 --- a/met-web/tests/unit/components/engagement/old-engagement.test.tsx +++ b/met-web/tests/unit/components/engagement/old-engagement.test.tsx @@ -12,6 +12,8 @@ import { Widget, WidgetItem, WidgetType } from 'models/widget'; import { createDefaultSurvey, Survey } from 'models/survey'; import { draftEngagement } from '../factory'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; +import { EngagementLoaderData } from 'components/engagement/public/view'; +import { EngagementContent } from 'models/engagementContent'; const survey: Survey = { ...createDefaultSurvey(), @@ -128,18 +130,22 @@ jest.mock('react-router-dom', () => ({ engagement: Promise.resolve(draftEngagement), metadata: Promise.resolve([]), widgets: Promise.resolve([whoIsListeningWidget]), - contentSummary: Promise.resolve([]), - content: Promise.resolve([]), + content: Promise.resolve([ + { + id: 1, + title: 'title', + text_content: 'text content', + json_content: + '{"blocks":[{"key":"fclgj","text":"Rich Content Sample","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}', + engagement_id: 1, + content: 'content', + sort_index: 1, + is_internal: false, + } as EngagementContent, + ]), }; } }, - useLoaderData: () => ({ - engagement: Promise.resolve(draftEngagement), - metadata: Promise.resolve([]), - widgets: Promise.resolve([whoIsListeningWidget]), - contentSummary: Promise.resolve([]), - content: Promise.resolve([]), - }), })); describe('Engagement View page tests', () => { @@ -186,7 +192,7 @@ describe('Engagement View page tests', () => { mockWidgetsRtkUnwrap.mockReturnValueOnce(Promise.resolve([whoIsListeningWidget])); mockContactRtkUnwrap.mockReturnValueOnce(Promise.resolve(mockContact)); const { container } = render(); - + screen.debug(undefined, 10000); await waitFor(() => { expect(container.querySelector('span.MuiSkeleton-root')).toBeNull(); expect(screen.getByText('Who is Listening')).toBeVisible(); diff --git a/met-web/tests/unit/components/factory.ts b/met-web/tests/unit/components/factory.ts index 410506f61..143784f66 100644 --- a/met-web/tests/unit/components/factory.ts +++ b/met-web/tests/unit/components/factory.ts @@ -284,11 +284,11 @@ const engagementSlugData = { const engagementContentData: EngagementContent = { id: 1, title: '', - icon_name: '', - content_type: '', engagement_id: 1, sort_index: 1, is_internal: true, + text_content: 'test', + json_content: '', }; const staffUserState: Partial = { From 33a1fa1284f51c99387b7c5e6f35690dbfa06adf Mon Sep 17 00:00:00 2001 From: Baelx Date: Thu, 22 Aug 2024 14:03:48 -0700 Subject: [PATCH 02/10] DESENG-675 Make widget component "reusable" (#2579) * DESENG-675 Make widget component "reusable" * DESENG-675 Update changelog, contributing, met-web tests * DESENG-675 Merge import statements, add warning style to widget deletion --- CHANGELOG.MD | 4 + CONTRIBUTING.md | 1 + .../c2a384ddfe6a_add_widget_location.py | 24 ++++++ met-api/src/met_api/models/widget.py | 2 + met-api/src/met_api/schemas/widget.py | 1 + .../create/widgets/WidgetPickerButton.tsx | 83 +++++++++++++++++++ .../engagement/admin/create/widgets/index.tsx | 19 +++++ .../Documents/AddFileDrawer.tsx | 2 + .../Documents/CreateFolderForm.tsx | 3 +- .../Documents/UploadFileDrawer.tsx | 2 + .../Events/InPersonEventFormDrawer.tsx | 2 + .../Events/VirtualSessionFormDrawer.tsx | 2 + .../form/EngagementWidgets/Map/Form.tsx | 2 + .../form/EngagementWidgets/Poll/Form.tsx | 2 + .../form/EngagementWidgets/Timeline/Form.tsx | 2 + .../form/EngagementWidgets/Video/Form.tsx | 2 + .../WhoIsListening/WhoIsListeningForm.tsx | 2 +- .../WhoIsListeningOptionCard.tsx | 5 +- .../EngagementWidgets/WidgetCardSwitch.tsx | 11 ++- .../EngagementWidgets/WidgetDrawerContext.tsx | 9 ++ met-web/src/models/widget.tsx | 5 ++ .../widgetService/DocumentService/index.tsx | 2 + .../widgetService/EventService/index.tsx | 2 + .../widgetService/MapService/index.tsx | 2 + .../widgetService/PollService/index.tsx | 2 + .../widgetService/TimelineService/index.tsx | 2 + .../widgetService/VideoService/index.tsx | 2 + .../engagement/old-engagement.test.tsx | 1 + met-web/tests/unit/components/factory.ts | 6 ++ .../widgets/DocumentWidget.test.tsx | 1 + .../widgets/PollWidgetView.test.tsx | 1 + .../widgets/WhoIsListeningWidget.test.tsx | 2 + 32 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 met-api/migrations/versions/c2a384ddfe6a_add_widget_location.py create mode 100644 met-web/src/components/engagement/admin/create/widgets/WidgetPickerButton.tsx create mode 100644 met-web/src/components/engagement/admin/create/widgets/index.tsx diff --git a/CHANGELOG.MD b/CHANGELOG.MD index c3c3042e8..c9f262219 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,7 @@ +## August 21, 2024 + +- **Feature** Reusable widget component [🎟️ DESENG-675](https://citz-gdx.atlassian.net/browse/DESENG-675) + ## August 15, 2024 - **Feature** Add engagement configuration summary [🎟️ DESENG-667](https://citz-gdx.atlassian.net/browse/DESENG-667) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74f18a5eb..d475f89cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,3 +95,4 @@ Examples of when to Request Changes - `StatusIcon`: A simple component that displays a status icon based on the status string passed to it. - `FormStep`: A wrapper around a form component that accepts a completion criteria and displays the user's progress. Accepts a `step` prop that is a number (from 1 to 9) that represents the current step in the form. This will be rendered as an icon with a checkmark if the step is complete, and a number if it's the current step or if it's incomplete. - `SystemMessage`: An informational message that can be displayed to the user. Accepts a `type` prop that can be "error", "warning", "info", or "success", which affects the display of the message. + - `WidgetPicker`: A modular widget picker component that can be placed anywhere in the engagement editing area. In order to align widgets in the backend with the frontend, a "location" prop is required. Add new locations to the `WidgetLocation` enum. diff --git a/met-api/migrations/versions/c2a384ddfe6a_add_widget_location.py b/met-api/migrations/versions/c2a384ddfe6a_add_widget_location.py new file mode 100644 index 000000000..4cc74eb39 --- /dev/null +++ b/met-api/migrations/versions/c2a384ddfe6a_add_widget_location.py @@ -0,0 +1,24 @@ +"""Add locations to widgets + +Revision ID: c2a384ddfe6a +Revises: 901a6724bca2 +Create Date: 2024-08-21 16:04:25.726651 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c2a384ddfe6a' +down_revision = '901a6724bca2' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('widget', sa.Column('location', sa.Integer(), nullable=True)) + + +def downgrade(): + op.drop_column('widget', 'location') diff --git a/met-api/src/met_api/models/widget.py b/met-api/src/met_api/models/widget.py index 9c55a0e63..7acd7edab 100644 --- a/met-api/src/met_api/models/widget.py +++ b/met-api/src/met_api/models/widget.py @@ -29,6 +29,7 @@ class Widget(BaseModel): # pylint: disable=too-few-public-methods title = db.Column(db.String(100), comment='Custom title for the widget.') items = db.relationship('WidgetItem', backref='widget', cascade='all, delete', order_by='WidgetItem.sort_index') sort_index = db.Column(db.Integer, nullable=False, default=1) + location = db.Column(db.Integer, nullable=False) @classmethod def get_widget_by_id(cls, widget_id): @@ -67,6 +68,7 @@ def __create_new_widget_entity(widget): created_by=widget.get('created_by', None), updated_by=widget.get('updated_by', None), title=widget.get('title', None), + location=widget.get('location', None), ) @classmethod diff --git a/met-api/src/met_api/schemas/widget.py b/met-api/src/met_api/schemas/widget.py index c2aec64d7..4c736d784 100644 --- a/met-api/src/met_api/schemas/widget.py +++ b/met-api/src/met_api/schemas/widget.py @@ -23,3 +23,4 @@ class Meta: # pylint: disable=too-few-public-methods updated_date = fields.Str(data_key='updated_date') sort_index = fields.Int(data_key='sort_index') items = fields.List(fields.Nested(WidgetItemSchema)) + location = fields.Int(data_key='location') diff --git a/met-web/src/components/engagement/admin/create/widgets/WidgetPickerButton.tsx b/met-web/src/components/engagement/admin/create/widgets/WidgetPickerButton.tsx new file mode 100644 index 000000000..b1c3e71ac --- /dev/null +++ b/met-web/src/components/engagement/admin/create/widgets/WidgetPickerButton.tsx @@ -0,0 +1,83 @@ +import React, { useContext, useEffect } from 'react'; +import { WidgetDrawerContext } from 'components/engagement/form/EngagementWidgets/WidgetDrawerContext'; +import { Grid, Skeleton } from '@mui/material'; +import { If, Else, Then } from 'react-if'; +import { useAppDispatch } from 'hooks'; +import { colors } from 'styles/Theme'; +import { WidgetCardSwitch } from 'components/engagement/form/EngagementWidgets/WidgetCardSwitch'; +import { openNotificationModal } from 'services/notificationModalService/notificationModalSlice'; +import { WidgetLocation } from 'models/widget'; + +export const WidgetPickerButton = ({ location }: { location: WidgetLocation }) => { + const { widgets, deleteWidget, handleWidgetDrawerOpen, isWidgetsLoading, setWidgetLocation } = + useContext(WidgetDrawerContext); + const dispatch = useAppDispatch(); + + useEffect(() => { + setWidgetLocation(location); + return () => setWidgetLocation(0); + }, []); + + const removeWidget = (widgetId: number) => { + dispatch( + openNotificationModal({ + open: true, + data: { + style: 'warning', + header: 'Remove Widget', + subText: [ + { text: 'You will be removing this widget from the engagement.' }, + { text: 'Do you want to remove this widget?' }, + ], + handleConfirm: () => { + deleteWidget(widgetId); + }, + }, + type: 'confirm', + }), + ); + }; + + return ( + + + + + + + + + + {/* Only ever render the first selected widget. This may change in the future. */} + {widgets.length > 0 ? ( + + ) : ( + + )} + + + + + ); +}; diff --git a/met-web/src/components/engagement/admin/create/widgets/index.tsx b/met-web/src/components/engagement/admin/create/widgets/index.tsx new file mode 100644 index 000000000..b498882b5 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/widgets/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import WidgetDrawer from 'components/engagement/form/EngagementWidgets/WidgetDrawer'; +import { WidgetDrawerProvider } from 'components/engagement/form/EngagementWidgets/WidgetDrawerContext'; +import { WidgetPickerButton } from './WidgetPickerButton'; +import { WidgetLocation } from 'models/widget'; +import { ActionProvider } from 'components/engagement/form/ActionContext'; + +export const WidgetPicker = ({ location }: { location: WidgetLocation }) => { + return ( + + + + + + + ); +}; + +export default WidgetPicker; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Documents/AddFileDrawer.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Documents/AddFileDrawer.tsx index 4e1a17659..553175ab1 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Documents/AddFileDrawer.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Documents/AddFileDrawer.tsx @@ -15,6 +15,7 @@ import ControlledSelect from 'components/common/ControlledInputComponents/Contro import { postDocument, patchDocument, PatchDocumentRequest } from 'services/widgetService/DocumentService'; import { DOCUMENT_TYPE, DocumentItem } from 'models/document'; import { updatedDiff } from 'deep-object-diff'; +import { WidgetLocation } from 'models/widget'; const schema = yup .object({ @@ -91,6 +92,7 @@ const AddFileDrawer = () => { url: data.link, widget_id: widget.id, type: 'file', + location: widget.location in WidgetLocation ? widget.location : null, }); dispatch( openNotification({ diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Documents/CreateFolderForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Documents/CreateFolderForm.tsx index c5b8fcf7f..67d3e367c 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Documents/CreateFolderForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Documents/CreateFolderForm.tsx @@ -7,7 +7,7 @@ import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; import { postDocument } from 'services/widgetService/DocumentService'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; -import { WidgetType } from 'models/widget'; +import { WidgetType, WidgetLocation } from 'models/widget'; import { DOCUMENT_TYPE } from 'models/document'; const CreateFolderForm = () => { @@ -48,6 +48,7 @@ const CreateFolderForm = () => { title: folderName, widget_id: widget.id, type: DOCUMENT_TYPE.FOLDER, + location: widget.location in WidgetLocation ? widget.location : null, }); await loadDocuments(); setCreatingFolder(false); diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Documents/UploadFileDrawer.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Documents/UploadFileDrawer.tsx index 7ef37620c..31cf49531 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Documents/UploadFileDrawer.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Documents/UploadFileDrawer.tsx @@ -24,6 +24,7 @@ import { DOCUMENT_TYPE, DocumentItem } from 'models/document'; import { saveObject } from 'services/objectStorageService'; import FileUpload from 'components/common/FileUpload'; import { If, Then, Else } from 'react-if'; +import { WidgetLocation } from 'models/widget'; const schema = yup .object({ @@ -94,6 +95,7 @@ const UploadFileDrawer = () => { widget_id: widget.id, type: 'file', is_uploaded: true, + location: widget.location in WidgetLocation ? widget.location : null, }); dispatch( openNotification({ diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Events/InPersonEventFormDrawer.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Events/InPersonEventFormDrawer.tsx index 9cace4de2..9a99d10e4 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Events/InPersonEventFormDrawer.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Events/InPersonEventFormDrawer.tsx @@ -18,6 +18,7 @@ import { formEventDates } from './utils'; import dayjs from 'dayjs'; import tz from 'dayjs/plugin/timezone'; import { updatedDiff } from 'deep-object-diff'; +import { WidgetLocation } from 'models/widget'; dayjs.extend(tz); @@ -113,6 +114,7 @@ const InPersonEventFormDrawer = () => { end_date: formatToUTC(dateTo), }, ], + location: widget.location in WidgetLocation ? widget.location : null, }); setEvents((prevWidgetEvents: Event[]) => [...prevWidgetEvents, createdWidgetEvent]); diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Events/VirtualSessionFormDrawer.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Events/VirtualSessionFormDrawer.tsx index 4fb2ea086..9f7a41a0b 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Events/VirtualSessionFormDrawer.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Events/VirtualSessionFormDrawer.tsx @@ -17,6 +17,7 @@ import { formatToUTC, formatDate } from 'components/common/dateHelper'; import { formEventDates } from './utils'; import dayjs from 'dayjs'; import tz from 'dayjs/plugin/timezone'; +import { WidgetLocation } from 'models/widget'; dayjs.extend(tz); @@ -108,6 +109,7 @@ const VirtualSessionFormDrawer = () => { end_date: formatToUTC(dateTo), }, ], + location: widget.location in WidgetLocation ? widget.location : null, }); setEvents((prevWidgetEvents: Event[]) => [...prevWidgetEvents, createdWidgetEvent]); 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 93959318a..eff6221ce 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Map/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Map/Form.tsx @@ -27,6 +27,7 @@ import { faCircleXmark } from '@fortawesome/pro-regular-svg-icons/faCircleXmark' import { When } from 'react-if'; import * as turf from '@turf/turf'; import { WidgetTitle } from '../WidgetTitle'; +import { WidgetLocation } from 'models/widget'; const schema = yup .object({ @@ -98,6 +99,7 @@ const Form = () => { longitude: longitude, latitude: latitude, file: shapefile, + location: widget.location in WidgetLocation ? widget.location : null, }); dispatch(openNotification({ severity: 'success', text: 'A new map was successfully added' })); }; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Poll/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Poll/Form.tsx index b9d32d116..438ec1070 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Poll/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Poll/Form.tsx @@ -16,6 +16,7 @@ import { PollStatus } from 'constants/engagementStatus'; import Alert from '@mui/material/Alert'; import usePollWidgetState from './PollWidget.hook'; import PollAnswerForm from './PollAnswerForm'; +import { WidgetLocation } from 'models/widget'; interface DetailsForm { title: string; @@ -79,6 +80,7 @@ const Form = () => { description: description, answers: answers, status: status, + location: widget.location in WidgetLocation ? widget.location : null, }); dispatch(openNotification({ severity: 'success', text: 'A new Poll was successfully added' })); }; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Timeline/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Timeline/Form.tsx index fd098fca0..5aacb1155 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Timeline/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Timeline/Form.tsx @@ -10,6 +10,7 @@ import { TimelineContext } from './TimelineContext'; import { patchTimeline, postTimeline } from 'services/widgetService/TimelineService'; import { WidgetTitle } from '../WidgetTitle'; import { TimelineEvent } from 'models/timelineWidget'; +import { WidgetLocation } from 'models/widget'; interface DetailsForm { title: string; @@ -81,6 +82,7 @@ const Form = () => { title: title, description: description, events: events, + location: widget.location in WidgetLocation ? widget.location : null, }); dispatch(openNotification({ severity: 'success', text: 'A new timeline was successfully added' })); }; 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 e3df41268..20721b408 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx @@ -13,6 +13,7 @@ import { VideoContext } from './VideoContext'; import { patchVideo, postVideo } from 'services/widgetService/VideoService'; import { updatedDiff } from 'deep-object-diff'; import { WidgetTitle } from '../WidgetTitle'; +import { WidgetLocation } from 'models/widget'; const schema = yup .object({ @@ -61,6 +62,7 @@ const Form = () => { engagement_id: widget.engagement_id, video_url: videoUrl, description: description, + location: widget.location in WidgetLocation ? widget.location : null, }); dispatch(openNotification({ severity: 'success', text: 'A new video was successfully added' })); }; 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 47f0269d3..3847d35b3 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx @@ -19,8 +19,8 @@ const WhoIsListeningForm = () => { 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(() => { const savedContacts = widget.items .map((widget_item) => { 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 99c7fd47e..293652231 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningOptionCard.tsx @@ -6,7 +6,7 @@ import { WidgetTabValues } from '../type'; import { ActionContext } from '../../ActionContext'; import { openNotification } from 'services/notificationService/notificationSlice'; import { useAppDispatch } from 'hooks'; -import { WidgetType } from 'models/widget'; +import { WidgetType, WidgetLocation } from 'models/widget'; import { Else, If, Then } from 'react-if'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faUserGroupSimple } from '@fortawesome/pro-regular-svg-icons/faUserGroupSimple'; @@ -16,7 +16,7 @@ import { optionCardStyle } from '../constants'; const Title = 'Who is Listening'; const WhoIsListeningOptionCard = () => { const { savedEngagement } = useContext(ActionContext); - const { widgets, loadWidgets, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); + const { widgets, loadWidgets, handleWidgetDrawerTabValueChange, widgetLocation } = useContext(WidgetDrawerContext); const dispatch = useAppDispatch(); const [createWidget] = useCreateWidgetMutation(); const [isCreatingWidget, setIsCreatingWidget] = useState(false); @@ -34,6 +34,7 @@ const WhoIsListeningOptionCard = () => { widget_type_id: WidgetType.WhoIsListening, engagement_id: savedEngagement.id, title: Title, + location: widgetLocation in WidgetLocation ? widgetLocation : 0, }); await loadWidgets(); dispatch( diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx index 4a4472a44..5ebb203f3 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx @@ -6,10 +6,11 @@ import { WidgetDrawerContext } from './WidgetDrawerContext'; import { WidgetTabValues } from './type'; interface WidgetCardSwitchProps { + singleSelection?: boolean; widget: Widget; removeWidget: (widgetId: number) => void; } -export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps) => { +export const WidgetCardSwitch = ({ singleSelection = false, widget, removeWidget }: WidgetCardSwitchProps) => { const { handleWidgetDrawerOpen, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); return ( @@ -17,6 +18,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { @@ -30,6 +32,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { @@ -43,6 +46,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { @@ -56,6 +60,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { @@ -69,6 +74,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { @@ -82,6 +88,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { @@ -95,6 +102,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { @@ -108,6 +116,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx index 249b5fe28..2d0d53280 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx @@ -18,6 +18,8 @@ export interface WidgetDrawerContextProps { loadWidgets: () => Promise; deleteWidget: (widgetIndex: number) => void; updateWidgetsSorting: (widgets: Widget[]) => void; + widgetLocation: number; + setWidgetLocation: (widgetLocation: number) => void; } export type EngagementParams = { @@ -45,6 +47,10 @@ export const WidgetDrawerContext = createContext({ updateWidgetsSorting: (widgets: Widget[]) => { /* empty default method */ }, + widgetLocation: 0, + setWidgetLocation: (widgetLocation: number) => { + /* empty default method */ + }, }); export const WidgetDrawerProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => { @@ -56,6 +62,7 @@ export const WidgetDrawerProvider = ({ children }: { children: JSX.Element | JSX const [widgetDrawerTabValue, setWidgetDrawerTabValue] = React.useState(WidgetTabValues.WIDGET_OPTIONS); const [removeWidget] = useDeleteWidgetMutation(); const [sortWidgets] = useSortWidgetsMutation(); + const [widgetLocation, setWidgetLocation] = useState(0); const deleteWidget = async (widgetId: number) => { try { @@ -116,6 +123,8 @@ export const WidgetDrawerProvider = ({ children }: { children: JSX.Element | JSX handleWidgetDrawerTabValueChange, isWidgetsLoading, loadWidgets, + widgetLocation, + setWidgetLocation, }} > {children} diff --git a/met-web/src/models/widget.tsx b/met-web/src/models/widget.tsx index 07043b0e3..a0bbff8b1 100644 --- a/met-web/src/models/widget.tsx +++ b/met-web/src/models/widget.tsx @@ -11,6 +11,7 @@ export interface Widget { engagement_id: number; items: WidgetItem[]; title: string; + location: WidgetLocation; } export enum WidgetType { @@ -24,3 +25,7 @@ export enum WidgetType { Timeline = 9, Poll = 10, } + +export enum WidgetLocation { + engagementAuthoring = 1, +} diff --git a/met-web/src/services/widgetService/DocumentService/index.tsx b/met-web/src/services/widgetService/DocumentService/index.tsx index 75e612772..a8dabf74a 100644 --- a/met-web/src/services/widgetService/DocumentService/index.tsx +++ b/met-web/src/services/widgetService/DocumentService/index.tsx @@ -2,6 +2,7 @@ import http from 'apiManager/httpRequestHandler'; import { DocumentItem, DocumentType } from 'models/document'; import Endpoints from 'apiManager/endpoints'; import { replaceAllInURL, replaceUrl } from 'helper'; +import { WidgetLocation } from 'models/widget'; export const fetchDocuments = async (widget_id: number): Promise => { try { @@ -20,6 +21,7 @@ interface PostDocumentRequest { url?: string; type: DocumentType; is_uploaded?: boolean; + location: WidgetLocation | null; } export const postDocument = async (widget_id: number, data: PostDocumentRequest): Promise => { try { diff --git a/met-web/src/services/widgetService/EventService/index.tsx b/met-web/src/services/widgetService/EventService/index.tsx index 4d9f963f2..6744c805f 100644 --- a/met-web/src/services/widgetService/EventService/index.tsx +++ b/met-web/src/services/widgetService/EventService/index.tsx @@ -2,6 +2,7 @@ import http from 'apiManager/httpRequestHandler'; import Endpoints from 'apiManager/endpoints'; import { replaceUrl, replaceAllInURL } from 'helper'; import { Event, EventTypeLabel } from 'models/event'; +import { WidgetLocation } from 'models/widget'; export const getEvents = async (widget_id: number): Promise => { try { @@ -26,6 +27,7 @@ interface PostEventProps { url?: string; url_label?: string; }[]; + location: WidgetLocation | null; } export const postEvent = async (widget_id: number, data: PostEventProps): Promise => { try { diff --git a/met-web/src/services/widgetService/MapService/index.tsx b/met-web/src/services/widgetService/MapService/index.tsx index afd2d1a1f..de1aa1edb 100644 --- a/met-web/src/services/widgetService/MapService/index.tsx +++ b/met-web/src/services/widgetService/MapService/index.tsx @@ -3,6 +3,7 @@ import { WidgetMap } from 'models/widgetMap'; import Endpoints from 'apiManager/endpoints'; import { replaceUrl } from 'helper'; import { GeoJSON } from 'geojson'; +import { WidgetLocation } from 'models/widget'; export const fetchMaps = async (widget_id: number): Promise => { try { @@ -21,6 +22,7 @@ interface PostMapRequest { latitude: number; marker_label?: string; file?: File; + location: WidgetLocation | null; } export const postMap = async (widget_id: number, data: PostMapRequest): Promise => { diff --git a/met-web/src/services/widgetService/PollService/index.tsx b/met-web/src/services/widgetService/PollService/index.tsx index 2cf5ee0ec..eb8f78abb 100644 --- a/met-web/src/services/widgetService/PollService/index.tsx +++ b/met-web/src/services/widgetService/PollService/index.tsx @@ -2,6 +2,7 @@ import http from 'apiManager/httpRequestHandler'; import Endpoints from 'apiManager/endpoints'; import { replaceAllInURL, replaceUrl } from 'helper'; import { PollWidget, PollAnswer, PollResponse, PollResultResponse } from 'models/pollWidget'; +import { WidgetLocation } from 'models/widget'; interface PostPollRequest { widget_id: number; @@ -10,6 +11,7 @@ interface PostPollRequest { description: string; answers: PollAnswer[]; status: string; + location: WidgetLocation | null; } interface PostPollResponse { diff --git a/met-web/src/services/widgetService/TimelineService/index.tsx b/met-web/src/services/widgetService/TimelineService/index.tsx index e90cd0b84..fcf1fc49b 100644 --- a/met-web/src/services/widgetService/TimelineService/index.tsx +++ b/met-web/src/services/widgetService/TimelineService/index.tsx @@ -2,6 +2,7 @@ import http from 'apiManager/httpRequestHandler'; import Endpoints from 'apiManager/endpoints'; import { replaceAllInURL, replaceUrl } from 'helper'; import { TimelineWidget, TimelineEvent } from 'models/timelineWidget'; +import { WidgetLocation } from 'models/widget'; interface PostTimelineRequest { widget_id: number; @@ -9,6 +10,7 @@ interface PostTimelineRequest { title: string; description: string; events: TimelineEvent[]; + location: WidgetLocation | null; } interface PatchTimelineRequest { diff --git a/met-web/src/services/widgetService/VideoService/index.tsx b/met-web/src/services/widgetService/VideoService/index.tsx index 4398527c8..07bfea460 100644 --- a/met-web/src/services/widgetService/VideoService/index.tsx +++ b/met-web/src/services/widgetService/VideoService/index.tsx @@ -2,6 +2,7 @@ import http from 'apiManager/httpRequestHandler'; import Endpoints from 'apiManager/endpoints'; import { replaceAllInURL, replaceUrl } from 'helper'; import { VideoWidget } from 'models/videoWidget'; +import { WidgetLocation } from 'models/widget'; export const fetchVideoWidgets = async (widget_id: number): Promise => { try { @@ -18,6 +19,7 @@ interface PostVideoRequest { engagement_id: number; video_url: string; description: string; + location: WidgetLocation | null; } export const postVideo = async (widget_id: number, data: PostVideoRequest): Promise => { diff --git a/met-web/tests/unit/components/engagement/old-engagement.test.tsx b/met-web/tests/unit/components/engagement/old-engagement.test.tsx index efba92468..0133886f9 100644 --- a/met-web/tests/unit/components/engagement/old-engagement.test.tsx +++ b/met-web/tests/unit/components/engagement/old-engagement.test.tsx @@ -49,6 +49,7 @@ const whoIsListeningWidget: Widget = { widget_type_id: WidgetType.WhoIsListening, engagement_id: 1, items: [widgetItem], + location: 1, }; const mockLocationData = { state: { open: true }, pathname: '', search: '', hash: '', key: '' }; diff --git a/met-web/tests/unit/components/factory.ts b/met-web/tests/unit/components/factory.ts index 143784f66..99092cd52 100644 --- a/met-web/tests/unit/components/factory.ts +++ b/met-web/tests/unit/components/factory.ts @@ -141,6 +141,7 @@ const eventWidget: Widget = { widget_type_id: WidgetType.Events, engagement_id: 1, items: [eventWidgetItem], + location: 1, }; const mapWidgetItem: WidgetItem = { @@ -156,6 +157,7 @@ const mapWidget: Widget = { widget_type_id: WidgetType.Map, engagement_id: 1, items: [mapWidgetItem], + location: 1, }; const mockMap: WidgetMap = { @@ -182,6 +184,7 @@ const pollWidget: Widget = { widget_type_id: WidgetType.Poll, engagement_id: 1, items: [], + location: 1, }; const videoWidget: Widget = { @@ -190,6 +193,7 @@ const videoWidget: Widget = { widget_type_id: WidgetType.Video, engagement_id: 1, items: [], + location: 1, }; const subscribeWidget: Widget = { @@ -198,6 +202,7 @@ const subscribeWidget: Widget = { widget_type_id: WidgetType.Subscribe, engagement_id: 1, items: [], + location: 1, }; const timeLineWidget: Widget = { @@ -206,6 +211,7 @@ const timeLineWidget: Widget = { widget_type_id: WidgetType.Timeline, engagement_id: 1, items: [], + location: 1, }; const mockPollAnswer1: PollAnswer = { diff --git a/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx b/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx index 3bf6c7ae0..ec85e4e34 100644 --- a/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx @@ -68,6 +68,7 @@ const documentWidget: Widget = { widget_type_id: WidgetType.Document, engagement_id: 1, items: [], + location: 1, }; jest.mock('axios'); diff --git a/met-web/tests/unit/components/widgets/PollWidgetView.test.tsx b/met-web/tests/unit/components/widgets/PollWidgetView.test.tsx index 8f2f838e0..465582003 100644 --- a/met-web/tests/unit/components/widgets/PollWidgetView.test.tsx +++ b/met-web/tests/unit/components/widgets/PollWidgetView.test.tsx @@ -44,6 +44,7 @@ describe('PollWidgetView Component Tests', () => { widget_type_id: 10, engagement_id: 1, items: [], + location: 1, }; const mockPoll = { diff --git a/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx b/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx index 1cdb22e87..7c496b71b 100644 --- a/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx @@ -53,6 +53,7 @@ const whoIsListeningWidget: Widget = { widget_type_id: WidgetType.WhoIsListening, engagement_id: 1, items: [contactWidgetItem], + location: 1, }; const mockEngagementSettings: EngagementSettings = { @@ -226,6 +227,7 @@ describe('Who is Listening widget tests', () => { widget_type_id: WidgetType.WhoIsListening, engagement_id: draftEngagement.id, title: whoIsListeningWidget.title, + location: 0, }); expect(getWidgetsMock).toHaveBeenCalled(); expect(screen.getByText('Add This Contact')).toBeVisible(); From ede7f830e64bb23ec57dbb814fd3713ead90dd9a Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 22 Aug 2024 17:55:03 -0700 Subject: [PATCH 03/10] DESENG-676: Merge DB migration files --- met-api/migrations/versions/42641011576a_.py | 24 +++++++++++++++++++ ... bd493dbd9e0e_merge_engagement_content.py} | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 met-api/migrations/versions/42641011576a_.py rename met-api/migrations/versions/{bd493dbd9e0e_.py => bd493dbd9e0e_merge_engagement_content.py} (97%) diff --git a/met-api/migrations/versions/42641011576a_.py b/met-api/migrations/versions/42641011576a_.py new file mode 100644 index 000000000..8fe6c244d --- /dev/null +++ b/met-api/migrations/versions/42641011576a_.py @@ -0,0 +1,24 @@ +"""Merge revision heads + +Revision ID: 42641011576a +Revises: c2a384ddfe6a, bd493dbd9e0e +Create Date: 2024-08-22 17:53:24.806016 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '42641011576a' +down_revision = ('c2a384ddfe6a', 'bd493dbd9e0e') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/met-api/migrations/versions/bd493dbd9e0e_.py b/met-api/migrations/versions/bd493dbd9e0e_merge_engagement_content.py similarity index 97% rename from met-api/migrations/versions/bd493dbd9e0e_.py rename to met-api/migrations/versions/bd493dbd9e0e_merge_engagement_content.py index f4caada0b..0d9899a14 100644 --- a/met-api/migrations/versions/bd493dbd9e0e_.py +++ b/met-api/migrations/versions/bd493dbd9e0e_merge_engagement_content.py @@ -1,4 +1,4 @@ -"""empty message +"""Merge the engagement_summary_content and engagement_custom_content tables into the engagement_content table Revision ID: bd493dbd9e0e Revises: 901a6724bca2 @@ -109,4 +109,4 @@ def downgrade(): # Re-add the content_type and icon_name columns op.add_column('engagement_content', sa.Column('icon_name', sa.Text(), autoincrement=False, nullable=True)) - op.add_column('engagement_content', sa.Column('content_type', postgresql.ENUM('Summary', 'Custom', name='engagementcontenttype'), autoincrement=False, nullable=False)) + op.add_column('engagement_content', sa.Column('content_type', postgresql.ENUM('Summary', 'Custom', name='engagementcontenttype'), autoincrement=False)) From 37c6dc452979e7e04ea5b37f6d4dcc34580ec5fe Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 22 Aug 2024 17:57:42 -0700 Subject: [PATCH 04/10] DESENG-676: Update changelog --- CHANGELOG.MD | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index c9f262219..1048ecc43 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,11 @@ +## August 22, 2024 + +- **Task** Merge engagement summary content and custom content [🎟️ DESENG-676](https://citz-gdx.atlassian.net/browse/DESENG-676) + - Combine the text and rich content fields from redundant types SummaryContent and CustomContent and store them in + the existing EngagementContent table + - Update the API to handle the new content structure + - Todo: Update the frontend to properly leverage the new content structure and allow editing of the simplified content type + ## August 21, 2024 - **Feature** Reusable widget component [🎟️ DESENG-675](https://citz-gdx.atlassian.net/browse/DESENG-675) From 8fec22355806f0970b43cd9981fe72570da56c01 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 22 Aug 2024 18:01:36 -0700 Subject: [PATCH 05/10] DESENG-676: Sonarcloud - remove redundant boolean checks for promises --- met-web/src/components/engagement/form/ActionContext.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/met-web/src/components/engagement/form/ActionContext.tsx b/met-web/src/components/engagement/form/ActionContext.tsx index a6708c481..2f3cac82f 100644 --- a/met-web/src/components/engagement/form/ActionContext.tsx +++ b/met-web/src/components/engagement/form/ActionContext.tsx @@ -82,7 +82,7 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { if (isCreate && !engagementId) { return; } - if (engagementId && engagement) { + if (engagementId) { engagement.then((result) => { setEngagement(result); }); @@ -100,7 +100,7 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { // Load the engagement's content from the shared individual engagement loader and watch the content variable for any changes. useEffect(() => { - if (engagementId && content) { + if (engagementId) { if (isCreate) { return; } @@ -115,7 +115,7 @@ export const ActionProvider = ({ children }: { children: JSX.Element }) => { if (isCreate) { return; } - if (!isCreate && metadata && taxa) { + if (!isCreate) { metadata?.then((result) => setEngagementMetadata(result)); taxa?.then((result) => setTenantTaxa(Object.values(result))); } From 2b85f7e8db10988c05579be52b1f9073ba933326 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 22 Aug 2024 18:05:04 -0700 Subject: [PATCH 06/10] DESENG-676: Sonarcloud - Remove unused variables --- met-api/tests/unit/api/test_engagement_content.py | 2 -- .../tests/unit/components/engagement/old-engagement.test.tsx | 1 - 2 files changed, 3 deletions(-) diff --git a/met-api/tests/unit/api/test_engagement_content.py b/met-api/tests/unit/api/test_engagement_content.py index d82315ad6..d1e018ab3 100644 --- a/met-api/tests/unit/api/test_engagement_content.py +++ b/met-api/tests/unit/api/test_engagement_content.py @@ -116,8 +116,6 @@ def test_create_engagement_content_sort(client, jwt, session, assert rv.status_code == 200 assert len(rv.json) == 2, 'Two Contents Should exist.' engagement_contents = rv.json - summary_content = None - custom_content = None assert engagement_contents[0].get('title') == 'Summary' assert engagement_contents[1].get('title') == 'Custom' diff --git a/met-web/tests/unit/components/engagement/old-engagement.test.tsx b/met-web/tests/unit/components/engagement/old-engagement.test.tsx index 0133886f9..e10db2cab 100644 --- a/met-web/tests/unit/components/engagement/old-engagement.test.tsx +++ b/met-web/tests/unit/components/engagement/old-engagement.test.tsx @@ -12,7 +12,6 @@ import { Widget, WidgetItem, WidgetType } from 'models/widget'; import { createDefaultSurvey, Survey } from 'models/survey'; import { draftEngagement } from '../factory'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; -import { EngagementLoaderData } from 'components/engagement/public/view'; import { EngagementContent } from 'models/engagementContent'; const survey: Survey = { From 9212c9697895c3570fa0d656a4b263d4571c622d Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 22 Aug 2024 18:08:43 -0700 Subject: [PATCH 07/10] DESENG-676: Lint error - remove unused argument --- met-api/src/met_api/services/engagement_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/met-api/src/met_api/services/engagement_service.py b/met-api/src/met_api/services/engagement_service.py index e21efb0c8..ce2c52445 100644 --- a/met-api/src/met_api/services/engagement_service.py +++ b/met-api/src/met_api/services/engagement_service.py @@ -158,7 +158,7 @@ def create_engagement(request_json: dict): eng_model = EngagementService._create_engagement_model(request_json) eng_content = EngagementService.create_default_engagement_content(eng_model.id) - EngagementService.create_default_content(eng_model.id, eng_content['id'], request_json) + EngagementService.create_default_content(eng_model.id, request_json) if request_json.get('status_block'): EngagementService._create_eng_status_block(eng_model.id, request_json) @@ -212,7 +212,7 @@ def create_default_engagement_content(eng_id): return eng_content @staticmethod - def create_default_content(eng_id: int, eng_content_id: int, content_data: dict): + def create_default_content(eng_id: int, content_data: dict): """Create default summary content for the engagement.""" default_summary_content = { 'title': 'Summary', From 17b480a4036911b29c07762b8f46a1d172f83008 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 22 Aug 2024 18:13:23 -0700 Subject: [PATCH 08/10] DESENG-676: Sonarcloud - remove unused assignment --- met-api/src/met_api/services/engagement_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/met-api/src/met_api/services/engagement_service.py b/met-api/src/met_api/services/engagement_service.py index ce2c52445..a1fe4095f 100644 --- a/met-api/src/met_api/services/engagement_service.py +++ b/met-api/src/met_api/services/engagement_service.py @@ -157,7 +157,7 @@ def create_engagement(request_json: dict): EngagementService.validate_fields(request_json) eng_model = EngagementService._create_engagement_model(request_json) - eng_content = EngagementService.create_default_engagement_content(eng_model.id) + EngagementService.create_default_engagement_content(eng_model.id) EngagementService.create_default_content(eng_model.id, request_json) if request_json.get('status_block'): From 9e7286d2073b727ee97282ccb6381ea5b35cf9c2 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Thu, 22 Aug 2024 18:21:27 -0700 Subject: [PATCH 09/10] DESENG-676: Linting - flake 8 --- met-api/src/met_api/services/engagement_service.py | 2 +- met-api/src/met_api/utils/enums.py | 2 +- met-api/tests/unit/api/test_engagement_content.py | 1 + met-api/tests/utilities/factory_scenarios.py | 12 ++++++------ 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/met-api/src/met_api/services/engagement_service.py b/met-api/src/met_api/services/engagement_service.py index a1fe4095f..ad2a687fa 100644 --- a/met-api/src/met_api/services/engagement_service.py +++ b/met-api/src/met_api/services/engagement_service.py @@ -198,7 +198,7 @@ def _create_engagement_model(engagement_data: dict) -> EngagementModel: def create_default_engagement_content(eng_id): """Create default engagement content for the given engagement ID.""" default_engagement_content = { - 'title': "Summary", + 'title': 'Summary', 'engagement_id': eng_id } try: diff --git a/met-api/src/met_api/utils/enums.py b/met-api/src/met_api/utils/enums.py index aff20a197..382e642df 100644 --- a/met-api/src/met_api/utils/enums.py +++ b/met-api/src/met_api/utils/enums.py @@ -101,4 +101,4 @@ class UserStatus(IntEnum): """User status.""" ACTIVE = 1 - INACTIVE = 2 \ No newline at end of file + INACTIVE = 2 diff --git a/met-api/tests/unit/api/test_engagement_content.py b/met-api/tests/unit/api/test_engagement_content.py index d1e018ab3..cfb6de419 100644 --- a/met-api/tests/unit/api/test_engagement_content.py +++ b/met-api/tests/unit/api/test_engagement_content.py @@ -142,6 +142,7 @@ def test_create_engagement_content_sort(client, jwt, session, assert engagement_contents[1].get('title') == 'Summary' assert engagement_contents[0].get('title') == 'Custom' + def test_create_engagement_content_sort_invalid(client, jwt, session, setup_admin_user_and_claims): # pylint:disable=unused-argument """Assert that a engagement content sort error handling is done.""" diff --git a/met-api/tests/utilities/factory_scenarios.py b/met-api/tests/utilities/factory_scenarios.py index 69776439c..803428fc1 100644 --- a/met-api/tests/utilities/factory_scenarios.py +++ b/met-api/tests/utilities/factory_scenarios.py @@ -882,17 +882,17 @@ class TestEngagementContentInfo(dict, Enum): 'title': 'Summary', 'is_internal': False, 'text_content': fake.text(max_nb_chars=20), - 'json_content': "{\"blocks\":[{\"key\":\"fclgj\",\"text\":\"Rich Content Sample\",\ - \"type\":\"unstyled\",\"depth\":0,\ - \"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}" + 'json_content': '{"blocks":[{"key":"fclgj","text":"Rich Content Sample",\ + "type":"unstyled","depth":0,\ + "inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}' } content2 = { 'title': 'Custom', 'is_internal': False, 'text_content': fake.text(max_nb_chars=20), - 'json_content': "{\"blocks\":[{\"key\":\"fclgj\",\"text\":\"Rich Content Sample\",\ - \"type\":\"unstyled\",\"depth\":0,\ - \"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}" + 'json_content': '{"blocks":[{"key":"fclgj","text":"Rich Content Sample",\ + "type":"unstyled","depth":0,\ + "inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}' } From a3a5461b76ddad2bddc27fc99ce2fb730f2b6991 Mon Sep 17 00:00:00 2001 From: NatSquared Date: Tue, 3 Sep 2024 11:01:09 -0700 Subject: [PATCH 10/10] DESENG-676: Remove extraneous debug statement in test --- met-web/tests/unit/components/engagement/old-engagement.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/met-web/tests/unit/components/engagement/old-engagement.test.tsx b/met-web/tests/unit/components/engagement/old-engagement.test.tsx index e10db2cab..76e10c25a 100644 --- a/met-web/tests/unit/components/engagement/old-engagement.test.tsx +++ b/met-web/tests/unit/components/engagement/old-engagement.test.tsx @@ -192,7 +192,6 @@ describe('Engagement View page tests', () => { mockWidgetsRtkUnwrap.mockReturnValueOnce(Promise.resolve([whoIsListeningWidget])); mockContactRtkUnwrap.mockReturnValueOnce(Promise.resolve(mockContact)); const { container } = render(); - screen.debug(undefined, 10000); await waitFor(() => { expect(container.querySelector('span.MuiSkeleton-root')).toBeNull(); expect(screen.getByText('Who is Listening')).toBeVisible();