Skip to content

Commit

Permalink
[To Main] - DESENG-544 - Multi-language - Created engagement content …
Browse files Browse the repository at this point in the history
…translation tables & API routes (#2459)

* DESENG-544: Engagement Content translation model and service file

* DESENG-544: Engagement Content translation test model and test service file

* DESENG-544: Content translation api lint fixed

* Migration table version updated
  • Loading branch information
ratheesh-aot authored Apr 12, 2024
1 parent 34fd91d commit a0be3d6
Show file tree
Hide file tree
Showing 15 changed files with 858 additions and 24 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## April 11, 2024
- **Task** Multi-language - Create engagement content translation tables & API routes [DESENG-544](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-544)
- Created a new table for engagement content translations
- Created API routes and services for engagement content translations
- Created Unit tests for engagement content translations

## April 10, 2024

- **Task** Remove default taxa from GDX tenant [DESENG-578](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-578)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Enagement Content Translation
Revision ID: 495d2dbe19b8
Revises: e4d15a1af865
Create Date: 2024-04-10 14:20:23.777834
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '495d2dbe19b8'
down_revision = 'e4d15a1af865'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('engagement_content_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('language_id', sa.Integer(), nullable=False),
sa.Column('engagement_content_id', sa.Integer(), nullable=False),
sa.Column('content_title', sa.String(length=50), 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('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(['language_id'], ['language.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('engagement_content_id', 'language_id', name='_engagement_content_language_uc')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('engagement_content_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 @@ -66,3 +66,4 @@
from .timeline_event_translation import TimelineEventTranslation
from .subscribe_item_translation import SubscribeItemTranslation
from .engagement_translation import EngagementTranslation
from .engagement_content_translation import EngagementContentTranslation
101 changes: 101 additions & 0 deletions met-api/src/met_api/models/engagement_content_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Engagement Content translation model class.
Manages the Engagement Content Translations.
"""

from __future__ import annotations
from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.exc import IntegrityError
from .base_model import BaseModel
from .db import db


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

__tablename__ = 'engagement_content_translation'

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
language_id = db.Column(db.Integer, db.ForeignKey('language.id', ondelete='CASCADE'), nullable=False)
engagement_content_id = db.Column(
db.Integer, db.ForeignKey('engagement_content.id', ondelete='CASCADE'), nullable=False
)
content_title = db.Column(db.String(50), unique=False, nullable=False)
custom_text_content = db.Column(db.Text, unique=False, nullable=True)
custom_json_content = db.Column(JSON, unique=False, nullable=True)

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

@classmethod
def get_translations_by_content_and_language(cls, engagement_content_id=None, language_id=None):
"""
Retrieve engagement content translations by content ID and language ID.
:param engagement_content_id: ID of the engagement content.
:param language_id: ID of the language.
:return: List of engagement content translations matching the criteria.
"""
query = cls.query
if engagement_content_id is not None:
query = query.filter(cls.engagement_content_id == engagement_content_id)
if language_id is not None:
query = query.filter(cls.language_id == language_id)

return query.all()

@classmethod
def create_engagement_content_translation(cls, data):
"""
Create a new EngagementContentTranslation record.
:param data: Dictionary containing the fields for EngagementContentTranslation.
:return: EngagementContentTranslation instance.
"""
try:
new_translation = cls(
language_id=data['language_id'],
engagement_content_id=data['engagement_content_id'],
content_title=data['content_title'],
custom_text_content=data.get('custom_text_content'),
custom_json_content=data.get('custom_json_content')
)
db.session.add(new_translation)
db.session.commit()
return new_translation
except IntegrityError as e:
db.session.rollback()
raise e

@classmethod
def update_engagement_content_translation(cls, translation_id, data):
"""
Update an existing EngagementContentTranslation record.
:param translation_id: ID of the EngagementContentTranslation to update.
:param data: Dictionary of fields to update.
:return: Updated EngagementContentTranslation instance.
"""
translation = cls.find_by_id(translation_id)
if translation:
for key, value in data.items():
setattr(translation, key, value)
db.session.commit()
return translation

@classmethod
def delete_engagement_content_translation(cls, translation_id):
"""
Delete an EngagementContentTranslation record.
:param translation_id: ID of the EngagementContentTranslation to delete.
:return: Boolean indicating successful deletion.
"""
translation = cls.find_by_id(translation_id)
if translation:
db.session.delete(translation)
db.session.commit()
return True
return False
9 changes: 3 additions & 6 deletions met-api/src/met_api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,14 @@
from .subscribe_item_translation import API as SUBSCRIBE_ITEM_TRANSLATION_API
from .timeline_event_translation import API as TIMELINE_EVENT_TRANSLATION_API
from .engagement_translation import API as ENGAGEMENT_TRANSLATION_API
from .engagement_content_translation import API as ENGAGEMENT_CONTENT_TRANSLATION_API

__all__ = ('API_BLUEPRINT',)

URL_PREFIX = '/api/'
API_BLUEPRINT = Blueprint('API', __name__, url_prefix=URL_PREFIX)

API = Api(
API_BLUEPRINT,
title='MET API',
version='1.0',
description='The Core API for MET'
)
API = Api(API_BLUEPRINT, title='MET API', version='1.0', description='The Core API for MET')

# HANDLER = ExceptionHandler(API)

Expand Down Expand Up @@ -118,3 +114,4 @@
API.add_namespace(SUBSCRIBE_ITEM_TRANSLATION_API, path='/subscribe/<int:widget_subscribe_id>/translations')
API.add_namespace(TIMELINE_EVENT_TRANSLATION_API, path='/timelines/<int:timeline_id>/translations')
API.add_namespace(ENGAGEMENT_TRANSLATION_API, path='/engagement/<int:engagement_id>/translations')
API.add_namespace(ENGAGEMENT_CONTENT_TRANSLATION_API, path='/engagement_content/<int:content_id>/translations')
120 changes: 120 additions & 0 deletions met-api/src/met_api/resources/engagement_content_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""API endpoints for managing an engagement content 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.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')


@cors_preflight('GET, POST, OPTIONS')
@API.route('/language/<language_id>')
class EngagementContentTranslationsByLanguage(Resource):
"""Resource for managing engagement content translations."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get(content_id, language_id):
"""Fetch content translations based on content_id AND language_id."""
translations = EngagementContentTranslationService().get_translations_by_content_and_language(
content_id, language_id
)
return jsonify(translations), HTTPStatus.OK

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

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

try:
translation = EngagementContentTranslationSchema().load(request_json)
created_translation = EngagementContentTranslationService().create_engagement_content_translation(
translation, pre_populate
)
return jsonify(created_translation), HTTPStatus.CREATED
except (ValidationError, BusinessException) as err:
return {'message': str(err)}, HTTPStatus.BAD_REQUEST


@cors_preflight('GET, PATCH, DELETE, OPTIONS')
@API.route('/<int:translation_id>')
class EditEngagementContentTranslation(Resource):
"""Resource for updating or deleting an engagement content translation."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get(translation_id, **_):
"""Get engagement content translation by id."""
translation = EngagementContentTranslationService().get_engagement_content_translation_by_id(translation_id)
if translation:
return jsonify(translation), HTTPStatus.OK
return {'message': 'Translation not found'}, HTTPStatus.NOT_FOUND

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def patch(translation_id, **_):
"""Update engagement content translation."""
translation_data = request.get_json()
try:
updated_translation = EngagementContentTranslationService().update_engagement_content_translation(
translation_id, translation_data
)
return jsonify(updated_translation), HTTPStatus.OK
except (ValidationError, BusinessException) as err:
return {'message': str(err)}, HTTPStatus.BAD_REQUEST

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def delete(translation_id, **_):
"""Remove engagement content translation."""
try:
EngagementContentTranslationService().delete_engagement_content_translation(translation_id)
return {'message': 'Translation successfully removed'}, HTTPStatus.NO_CONTENT
except BusinessException as err:
return {'message': str(err)}, HTTPStatus.BAD_REQUEST


@cors_preflight('GET, POST, OPTIONS')
@API.route('/')
class EngagementContentTranslations(Resource):
"""Resource for managing engagement content translations."""

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

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

try:
translation = EngagementContentTranslationSchema().load(request_json)
created_translation = EngagementContentTranslationService().create_engagement_content_translation(
translation, pre_populate
)
return jsonify(created_translation), HTTPStatus.CREATED
except (ValidationError, BusinessException) as err:
return {'message': str(err)}, HTTPStatus.BAD_REQUEST
19 changes: 19 additions & 0 deletions met-api/src/met_api/schemas/engagement_content_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Schema for engagement content translation."""

from marshmallow import EXCLUDE, Schema, fields


class EngagementContentTranslationSchema(Schema):
"""Engagement content translation schema."""

class Meta: # pylint: disable=too-few-public-methods
"""Meta class to exclude unknown fields."""

unknown = EXCLUDE

id = fields.Int(data_key='id')
language_id = fields.Int(data_key='language_id', required=True)
engagement_content_id = fields.Int(data_key='engagement_content_id', required=True)
content_title = fields.Str(data_key='content_title', required=True)
custom_text_content = fields.Str(data_key='custom_text_content')
custom_json_content = fields.Str(data_key='custom_json_content')
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://met.gov.bc.ca/.well_known/schemas/engagement_content_translation",
"type": "object",
"title": "Engagement Content Translation Schema",
"description": "Schema for Engagement Content Translation data structure.",
"default": {},
"examples": [
{
"language_id": 1,
"engagement_content_id": 1,
"content_title": "Sample Title",
"custom_text_content": "Sample text content",
"custom_json_content": {},
"pre_populate": true
}
],
"required": ["language_id", "engagement_content_id", "content_title"],
"properties": {
"language_id": {
"$id": "#/properties/language_id",
"type": "number",
"title": "Language ID",
"description": "The ID of the language for the translation."
},
"engagement_content_id": {
"$id": "#/properties/engagement_content_id",
"type": "number",
"title": "Engagement Content ID",
"description": "The ID of the engagement content being translated."
},
"content_title": {
"$id": "#/properties/content_title",
"type": "string",
"title": "Content Title",
"description": "The title of the engagement content."
},
"custom_text_content": {
"$id": "#/properties/custom_text_content",
"type": "string",
"title": "Custom Text Content",
"description": "Custom textual content of the translation.",
"nullable": true
},
"custom_json_content": {
"$id": "#/properties/custom_json_content",
"type": "object",
"title": "Custom JSON Content",
"description": "Custom JSON structured content of the translation.",
"nullable": true
}
}
}
Loading

0 comments on commit a0be3d6

Please sign in to comment.