Skip to content

Commit

Permalink
Added Engagement translation model (#2412) (#2415)
Browse files Browse the repository at this point in the history
  • Loading branch information
VineetBala-AOT authored Mar 13, 2024
1 parent 8366029 commit c27ddcd
Show file tree
Hide file tree
Showing 12 changed files with 751 additions and 4 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## March 08, 2024
- **Task**Multi-language - Create engagement translation table & API routes [DESENG-510](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-510)
- Added Engagement translation model.
- Added Engagement translation API.
- Added Unit tests.

## March 06, 2024
- **Task**Multi-language - Create simple widget translation tables & API routes [DESENG-514](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-514)
- Added Widget translation model.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""create_engagement_translation_table
Revision ID: c4f7189494ed
Revises: 35124d2e41cb
Create Date: 2024-03-07 16:38:26.958748
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = 'c4f7189494ed'
down_revision = '35124d2e41cb'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('engagement_translation',
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('engagement_id', sa.Integer(), nullable=False),
sa.Column('language_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=50), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('rich_description', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.Column('content', sa.Text(), nullable=True),
sa.Column('rich_content', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.Column('consent_message', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.Column('slug', sa.String(length=200), nullable=True),
sa.Column('upcoming_status_block_text', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.Column('open_status_block_text', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.Column('closed_status_block_text', postgresql.JSON(astext_type=sa.Text()), 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_id'], ['engagement.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['language_id'], ['language.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('engagement_id', 'language_id', name='_engagement_language_uc')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('engagement_translation')
# ### end Alembic commands ###
1 change: 1 addition & 0 deletions met-api/src/met_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@
from .language import Language
from .widget_translation import WidgetTranslation
from .survey_translation import SurveyTranslation
from .engagement_translation import EngagementTranslation
102 changes: 102 additions & 0 deletions met-api/src/met_api/models/engagement_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Engagement translation model class.
Manages the Engagement Translations.
"""

from __future__ import annotations
from typing import Optional

from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects.postgresql import JSON

from .base_model import BaseModel
from .db import db


class EngagementTranslation(BaseModel):
"""Definition of the Engagement Translation entity."""

__tablename__ = 'engagement_translation'

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
engagement_id = db.Column(db.Integer, db.ForeignKey('engagement.id', ondelete='CASCADE'), nullable=False)
language_id = db.Column(db.Integer, db.ForeignKey('language.id', ondelete='CASCADE'), nullable=False)
name = db.Column(db.String(50))
description = db.Column(db.Text())
rich_description = db.Column(JSON, unique=False, nullable=True)
content = db.Column(db.Text())
rich_content = db.Column(JSON, unique=False, nullable=True)
consent_message = db.Column(JSON, unique=False, nullable=True)
slug = db.Column(db.String(200))
upcoming_status_block_text = db.Column(JSON, unique=False, nullable=True)
open_status_block_text = db.Column(JSON, unique=False, nullable=True)
closed_status_block_text = db.Column(JSON, unique=False, nullable=True)

# Add a unique constraint on engagement_id and language_id
# A engagement has only one version in a particular language
__table_args__ = (
UniqueConstraint(
'engagement_id', 'language_id', name='_engagement_language_uc'
),
)

@staticmethod
def get_engagement_translation_by_engagement_and_language(
engagement_id=None, language_id=None
):
"""Get engagement translation by engagement_id and language_id, or by either one."""
query = EngagementTranslation.query
if engagement_id is not None:
query = query.filter_by(engagement_id=engagement_id)
if language_id is not None:
query = query.filter_by(language_id=language_id)

engagement_translation_records = query.all()
return engagement_translation_records

@classmethod
def create_engagement_translation(cls, data):
"""Create a new engagement translation."""
engagement_translation = cls.__create_new_engagement_translation_entity(data)
db.session.add(engagement_translation)
db.session.commit()
return engagement_translation

@staticmethod
def __create_new_engagement_translation_entity(data):
"""Create new engagement translation entity."""
return EngagementTranslation(
engagement_id=data.get('engagement_id'),
language_id=data.get('language_id'),
name=data.get('name', None),
description=data.get('description', None),
rich_description=data.get('rich_description', None),
content=data.get('content', None),
rich_content=data.get('rich_content', None),
consent_message=data.get('consent_message', None),
slug=data.get('slug', None),
upcoming_status_block_text=data.get('upcoming_status_block_text', None),
open_status_block_text=data.get('open_status_block_text', None),
closed_status_block_text=data.get('closed_status_block_text', None),
)

@staticmethod
def update_engagement_translation(engagement_translation_id, data: dict) -> Optional[EngagementTranslation]:
"""Update an existing engagement translation."""
query = EngagementTranslation.query.filter_by(id=engagement_translation_id)
engagement_translation: EngagementTranslation = query.first()
if not engagement_translation:
return None
query.update(data)
db.session.commit()
return engagement_translation

@staticmethod
def delete_engagement_translation(engagement_translation_id):
"""Delete a engagement translation."""
engagement_translation = EngagementTranslation.query.get(engagement_translation_id)
if engagement_translation:
db.session.delete(engagement_translation)
db.session.commit()
return True
return False
2 changes: 2 additions & 0 deletions met-api/src/met_api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from .language import API as LANGUAGE_API
from .widget_translation import API as WIDGET_TRANSLATION_API
from .survey_translation import API as SURVEY_TRANSLATION_API
from .engagement_translation import API as ENGAGEMENT_TRANSLATION_API

__all__ = ('API_BLUEPRINT',)

Expand Down Expand Up @@ -107,3 +108,4 @@
API.add_namespace(LANGUAGE_API, path='/languages')
API.add_namespace(WIDGET_TRANSLATION_API, path='/widget/<int:widget_id>/translations')
API.add_namespace(SURVEY_TRANSLATION_API, path='/surveys/<int:survey_id>/translations')
API.add_namespace(ENGAGEMENT_TRANSLATION_API, path='/engagement/<int:engagement_id>/translations')
132 changes: 132 additions & 0 deletions met-api/src/met_api/resources/engagement_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# 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 translation resource."""

from http import HTTPStatus

from flask import jsonify, request
from flask_cors import cross_origin
from flask_restx import Namespace, Resource
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_translation import EngagementTranslationSchema
from met_api.services.engagement_translation_service import EngagementTranslationService
from met_api.utils.util import allowedorigins, cors_preflight


API = Namespace('engagement_translation', description='Endpoints for Engagement translation Management')


@cors_preflight('GET, OPTIONS')
@API.route('/language/<int:language_id>')
class EngagementTranslationResourceByLanguage(Resource):
"""Resource for managing a engagement translation."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get(engagement_id, language_id):
"""Fetch a engagement by widget_id and language_id."""
try:
engagement = EngagementTranslationService().get_translation_by_engagement_and_language(
engagement_id, language_id)
return jsonify(engagement), HTTPStatus.OK
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR


@cors_preflight('POST, OPTIONS')
@API.route('/')
class EngagementTranslations(Resource):
"""Resource for creating a engagement translation."""

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def post(engagement_id):
"""Add new engagement translation."""
try:
request_json = request.get_json()
request_json['engagement_id'] = engagement_id
valid_format, errors = schema_utils.validate(request_json, 'engagement_translation')
if not valid_format:
return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST

pre_populate = request_json.get('pre_populate', True)

engagement_translation = EngagementTranslationSchema().load(request_json)
created_engagement_translation = EngagementTranslationService().create_engagement_translation(
engagement_translation, pre_populate)
return created_engagement_translation, HTTPStatus.OK
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR
except ValidationError as err:
return str(err.messages), HTTPStatus.BAD_REQUEST
except BusinessException as err:
return err.error, HTTPStatus.CONFLICT


@cors_preflight('GET, DELETE, PATCH, OPTIONS')
@API.route('/<int:engagement_translation_id>')
class EngagementTranslation(Resource):
"""Resource for managing engagement translations."""

@staticmethod
@cross_origin(origins=allowedorigins())
# pylint: disable=unused-argument
def get(engagement_id, engagement_translation_id):
"""Fetch a engagement translation by id."""
try:
engagement_translation = (
EngagementTranslationService.get_engagement_translation_by_id(
engagement_translation_id
)
)
return (
EngagementTranslationSchema().dump(engagement_translation),
HTTPStatus.OK,
)
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def delete(engagement_id, engagement_translation_id):
"""Remove engagement translation."""
try:
EngagementTranslationService().delete_engagement_translation(engagement_id,
engagement_translation_id)
return 'Engagement translation successfully removed', HTTPStatus.OK
except KeyError as err:
return str(err), HTTPStatus.BAD_REQUEST
except ValueError as err:
return str(err), HTTPStatus.NOT_FOUND

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def patch(engagement_id, engagement_translation_id):
"""Update engagement translation."""
try:
translation_data = request.get_json()
updated_engagement = EngagementTranslationService().update_engagement_translation(
engagement_id, engagement_translation_id, translation_data)
return updated_engagement, HTTPStatus.OK
except ValueError as err:
return str(err), HTTPStatus.NOT_FOUND
except ValidationError as err:
return str(err.messages), HTTPStatus.BAD_REQUEST
26 changes: 26 additions & 0 deletions met-api/src/met_api/schemas/engagement_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Engagement translation schema class."""

from marshmallow import EXCLUDE, Schema, fields


class EngagementTranslationSchema(Schema):
"""Engagement translation schema."""

class Meta: # pylint: disable=too-few-public-methods
"""Exclude unknown fields in the deserialized output."""

unknown = EXCLUDE

id = fields.Int(data_key='id')
engagement_id = fields.Int(data_key='engagement_id', required=True)
language_id = fields.Int(data_key='language_id', required=True)
name = fields.Str(data_key='name')
description = fields.Str(data_key='description')
rich_description = fields.Str(data_key='rich_description')
content = fields.Str(data_key='content')
rich_content = fields.Str(data_key='rich_content')
consent_message = fields.Str(data_key='consent_message')
slug = fields.Str(data_key='slug')
upcoming_status_block_text = fields.Str(data_key='upcoming_status_block_text')
open_status_block_text = fields.Str(data_key='open_status_block_text')
closed_status_block_text = fields.Str(data_key='closed_status_block_text')
31 changes: 31 additions & 0 deletions met-api/src/met_api/schemas/schemas/engagement_translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://met.gov.bc.ca/.well_known/schemas/engagement_translation",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"default": {},
"examples": [
{
"engagement_id": 1,
"language_id": 1
}
],
"required": ["engagement_id", "language_id"],
"properties": {
"engagement_id": {
"$id": "#/properties/engagement_id",
"type": "number",
"title": "engagement id",
"description": "The engagement to which this translation belongs.",
"examples": [1]
},
"language_id": {
"$id": "#/properties/language_id",
"type": "number",
"title": "Language id",
"description": "The language to which this translation belongs.",
"examples": [1]
}
}
}
Loading

0 comments on commit c27ddcd

Please sign in to comment.