Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API changes for dynamic engagement pages #2396

Merged
merged 4 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## February 26, 2024
- **Task**Models for dynamic engagement pages [DESENG-500](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-500)
- Implemented endpoints for dynamic engagement pages, including summary and custom sections.
- Default behavior ensures that each engagement has a dynamic summary page.
- Introduced logic to migrate existing content and rich content for engagements to the summary table.
- **Task**Add font awesome libraries [DESENG-490](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-490)
- Added related libraries to the frontend package.json.
- **Task**Adding missing unit test [DESENG-483](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-483)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""add_tables_for_dynamic_eng_pages

Revision ID: e2625b0d07ab
Revises: 37176ea4708d
Create Date: 2024-02-25 21:49:58.191570

"""
import json
from alembic import op
import sqlalchemy as sa
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'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('engagement_content',
sa.Column('created_date', sa.DateTime(), nullable=False),
sa.Column('updated_date', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('title', sa.String(length=50), nullable=False),
sa.Column('icon_name', sa.Text(), nullable=True),
sa.Column('content_type', sa.Enum('Summary', 'Custom', name='engagementcontenttype'), nullable=False),
sa.Column('engagement_id', sa.Integer(), nullable=True),
sa.Column('sort_index', sa.Integer(), nullable=False),
sa.Column('is_internal', sa.Boolean(), nullable=False),
sa.Column('created_by', sa.String(length=50), nullable=True),
sa.Column('updated_by', sa.String(length=50), nullable=True),
sa.ForeignKeyConstraint(['engagement_id'], ['engagement.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('engagement_custom_content',
sa.Column('created_date', sa.DateTime(), nullable=False),
sa.Column('updated_date', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('custom_text_content', sa.Text(), nullable=True),
sa.Column('custom_json_content', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.Column('engagement_content_id', sa.Integer(), nullable=True),
sa.Column('engagement_id', sa.Integer(), nullable=True),
sa.Column('created_by', sa.String(length=50), nullable=True),
sa.Column('updated_by', sa.String(length=50), nullable=True),
sa.ForeignKeyConstraint(['engagement_content_id'], ['engagement_content.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['engagement_id'], ['engagement.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('engagement_summary_content',
sa.Column('created_date', sa.DateTime(), nullable=False),
sa.Column('updated_date', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('content', sa.Text(), nullable=False),
sa.Column('rich_content', postgresql.JSON(astext_type=sa.Text()), nullable=False),
sa.Column('engagement_content_id', sa.Integer(), nullable=True),
sa.Column('engagement_id', sa.Integer(), nullable=True),
sa.Column('created_by', sa.String(length=50), nullable=True),
sa.Column('updated_by', sa.String(length=50), nullable=True),
sa.ForeignKeyConstraint(['engagement_content_id'], ['engagement_content.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['engagement_id'], ['engagement.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)

# content and rich content data needs to be moved from engagement table to engagement summary table
connection = op.get_bind()
result = connection.execute("""
SELECT id, content, rich_content
FROM engagement
""")
engagements = result.fetchall()

# Iterate through each engagement record and insert into new tables
for index, engagement in enumerate(engagements):
eng_id = engagement['id']

# Insert into 'engagement_content'
connection.execute(
text(
"""
INSERT INTO engagement_content
(created_date, title, icon_name, content_type, engagement_id, sort_index, is_internal)
VALUES
(NOW(), :title, :icon_name, :content_type, :engagement_id, 1, false)
"""
),
{
'title': ContentTitle.DEFAULT.value,
'icon_name': ContentTitle.DEFAULT_ICON.value,
'content_type': EngagementContentType(1).name,
'engagement_id': eng_id,
},
)

engagement_content_id = index + 1 # Adjust the index to start from 1
rich_content = engagement['rich_content']

# Insert into 'engagement_summary'
connection.execute(
text(
"""
INSERT INTO engagement_summary_content
(created_date, content, rich_content, engagement_content_id, engagement_id)
VALUES
(NOW(), :content, :rich_content, :engagement_content_id, :engagement_id)
"""
),
{
'content': engagement['content'],
'rich_content': json.dumps(rich_content),
'engagement_content_id': engagement_content_id,
'engagement_id': eng_id,
},
)

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('engagement_summary_content')
op.drop_table('engagement_custom_content')
op.drop_table('engagement_content')
op.execute('DROP TYPE IF EXISTS engagementcontenttype;')
# ### end Alembic commands ###
25 changes: 25 additions & 0 deletions met-api/src/met_api/constants/engagement_content_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright © 2021 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Constants of engagement content type.

Each value in this corresponds to a specific section or content element.
"""
from enum import IntEnum


class EngagementContentType(IntEnum):
"""Enum of engagement content type."""

Summary = 1
Custom = 2
3 changes: 3 additions & 0 deletions met-api/src/met_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
from .db import db, ma, migrate
from .email_verification import EmailVerification
from .engagement import Engagement
from .engagement_content import EngagementContent
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
Expand Down
69 changes: 69 additions & 0 deletions met-api/src/met_api/models/engagement_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Engagement content model class.

Manages the engagement content. Each record in this table stores the configurations
associated with different sections or content elements within an engagement.
"""

from __future__ import annotations
from datetime import datetime
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


class EngagementContent(BaseModel):
"""Definition of the Engagement content entity."""

__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)
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."""
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."""
db.session.bulk_update_mappings(EngagementContent, update_mappings)
db.session.commit()

@classmethod
def save_engagement_content(cls, content: list) -> None:
"""Update custom content."""
db.session.bulk_save_objects(content)

@classmethod
def remove_engagement_content(cls, engagement_id, engagement_content_id,) -> EngagementContent:
"""Remove engagement content from engagement."""
engagement_content = EngagementContent.query.filter_by(id=engagement_content_id,
engagement_id=engagement_id).delete()
db.session.commit()
return engagement_content

@classmethod
def update_engagement_content(cls, engagement_id, engagement_content_id,
engagement_content_data: dict) -> Optional[EngagementContent]:
"""Update engagement content."""
query = EngagementContent.query.filter_by(id=engagement_content_id, engagement_id=engagement_id)
engagement_content: EngagementContent = query.first()
if not engagement_content:
return None
engagement_content_data['updated_date'] = datetime.utcnow()
query.update(engagement_content_data)
db.session.commit()
return engagement_content
47 changes: 47 additions & 0 deletions met-api/src/met_api/models/engagement_custom_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""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)
47 changes: 47 additions & 0 deletions met-api/src/met_api/models/engagement_summary_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""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 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)
6 changes: 6 additions & 0 deletions met-api/src/met_api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
from .contact import API as CONTACT_API
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
Expand Down Expand Up @@ -75,6 +78,9 @@
API.add_namespace(SUBSCRIPTION_API)
API.add_namespace(COMMENT_API)
API.add_namespace(EMAIL_VERIFICATION_API)
API.add_namespace(ENGAGEMENT_CONTENT_API)
API.add_namespace(ENGAGEMENT_CUSTOM_CONTENT_API, path='/engagement_content/<string:content_id>/custom')
API.add_namespace(ENGAGEMENT_SUMMARY_CONTENT_API, path='/engagement_content/<string:content_id>/summary')
API.add_namespace(FEEDBACK_API)
API.add_namespace(WIDGET_API)
API.add_namespace(CONTACT_API)
Expand Down
Loading
Loading