Skip to content

Commit

Permalink
[TO MAIN] DESENG-514 - Adding widget translation model (#2411)
Browse files Browse the repository at this point in the history
* Adding widget translation model (#2407)
  • Loading branch information
VineetBala-AOT authored Mar 8, 2024
1 parent 74f835a commit 8366029
Show file tree
Hide file tree
Showing 13 changed files with 640 additions and 3 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## 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.
- Added Widget translation API.
- Added Unit tests.
- **Task**Multi-language - Create survey translation table & API routes [DESENG-511](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-511)
- Added Survey Translation model.
- Added Survey Translation API.
Expand All @@ -9,8 +13,6 @@
- The existing "Save" button in the floating bar has been split into two distinct actions: "Save and Continue" and "Save and Exit".
- Tabs are greyed out, and widgets are disabled until the engagement is successfully saved. A helpful tool-tip has been added to inform users that the engagement needs to be saved before accessing certain features.
- Independent save buttons previously present in tabs, such as "Additional Details", "Settings" have been removed. Now, the floating save bar is universally employed when editing an engagement.

## March 04, 2024
- **Task**Multi-language - Create language table & API [DESENG-509](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-509)
- Added Language model.
- Added Language API.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""create_widget_translation_table
Revision ID: 35124d2e41cb
Revises: 274a2774607b
Create Date: 2024-03-05 16:43:50.911576
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

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


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('widget_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('widget_id', sa.Integer(), nullable=False),
sa.Column('language_id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=100), nullable=True, comment='Custom title for the widget.'),
sa.Column('map_marker_label', sa.String(length=30), nullable=True),
sa.Column('map_file_name', sa.Text(), nullable=True),
sa.Column('poll_title', sa.String(length=255), nullable=True),
sa.Column('poll_description', sa.String(length=2048), nullable=True),
sa.Column('video_url', sa.String(length=255), nullable=True),
sa.Column('video_description', 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(['language_id'], ['language.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['widget_id'], ['widget.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('widget_id', 'language_id', name='unique_widget_language')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('widget_translation')
# ### end Alembic commands ###
1 change: 1 addition & 0 deletions met-api/src/met_api/constants/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ class WidgetType(IntEnum):
Map = 6
Video = 7
Timeline = 9
Poll = 10
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 @@ -59,4 +59,5 @@
from .poll_answers import PollAnswer
from .poll_responses import PollResponse
from .language import Language
from .widget_translation import WidgetTranslation
from .survey_translation import SurveyTranslation
84 changes: 84 additions & 0 deletions met-api/src/met_api/models/widget_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Widget translation model class.
Manages the widget language translation
"""
from __future__ import annotations
from typing import Optional

from sqlalchemy.sql.schema import ForeignKey

from .base_model import BaseModel
from .db import db


class WidgetTranslation(BaseModel): # pylint: disable=too-few-public-methods
"""Definition of the Widget translation entity."""

__tablename__ = 'widget_translation'
__table_args__ = (
db.UniqueConstraint('widget_id', 'language_id', name='unique_widget_language'),
)

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
widget_id = db.Column(db.Integer, ForeignKey('widget.id', ondelete='CASCADE'), nullable=False)
language_id = db.Column(db.Integer, ForeignKey('language.id', ondelete='CASCADE'), nullable=False)
title = db.Column(db.String(100), comment='Custom title for the widget.')
map_marker_label = db.Column(db.String(30))
map_file_name = db.Column(db.Text())
poll_title = db.Column(db.String(255))
poll_description = db.Column(db.String(2048))
video_url = db.Column(db.String(255))
video_description = db.Column(db.Text())

@classmethod
def get_translation_by_widget_id_and_language_id(cls, widget_id=None, language_id=None):
"""Get translation by widget_id and language_id, or by either one."""
query = WidgetTranslation.query
if widget_id is not None:
query = query.filter_by(widget_id=widget_id)
if language_id is not None:
query = query.filter_by(language_id=language_id)

widget_translation_records = query.all()
return widget_translation_records

@classmethod
def create_widget_translation(cls, translation) -> WidgetTranslation:
"""Create widget translation."""
new_widget_translation = cls.__create_new_widget_translation_entity(translation)
db.session.add(new_widget_translation)
db.session.commit()
return new_widget_translation

@staticmethod
def __create_new_widget_translation_entity(translation):
"""Create new widget translation entity."""
return WidgetTranslation(
widget_id=translation.get('widget_id'),
language_id=translation.get('language_id'),
title=translation.get('title', None),
map_marker_label=translation.get('map_marker_label', None),
map_file_name=translation.get('map_file_name', None),
poll_title=translation.get('poll_title', None),
poll_description=translation.get('poll_description', None),
video_url=translation.get('video_url', None),
video_description=translation.get('video_description', None),
)

@classmethod
def remove_widget_translation(cls, widget_translation_id) -> WidgetTranslation:
"""Remove widget translation from widget."""
widget_translation = WidgetTranslation.query.filter_by(id=widget_translation_id).delete()
db.session.commit()
return widget_translation

@classmethod
def update_widget_translation(cls, widget_translation_id, translation: dict) -> Optional[WidgetTranslation]:
"""Update widget translation."""
query = WidgetTranslation.query.filter_by(id=widget_translation_id)
widget_translation: WidgetTranslation = query.first()
if not widget_translation:
return None
query.update(translation)
db.session.commit()
return widget_translation
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 @@ -56,6 +56,7 @@
from .widget_timeline import API as WIDGET_TIMELINE_API
from .widget_poll import API as WIDGET_POLL_API
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

__all__ = ('API_BLUEPRINT',)
Expand Down Expand Up @@ -104,4 +105,5 @@
API.add_namespace(WIDGET_TIMELINE_API, path='/widgets/<int:widget_id>/timelines')
API.add_namespace(WIDGET_POLL_API, path='/widgets/<int:widget_id>/polls')
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')
111 changes: 111 additions & 0 deletions met-api/src/met_api/resources/widget_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# 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 widget 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.widget_translation import WidgetTranslationSchema
from met_api.services.widget_translation_service import WidgetTranslationService
from met_api.utils.util import allowedorigins, cors_preflight


API = Namespace('widget_translation', description='Endpoints for Widget translation Management')


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

@staticmethod
@cross_origin(origins=allowedorigins())
def get(widget_id, language_id):
"""Fetch a list of widgets by widget_id and language_id."""
try:
widgets = WidgetTranslationService().get_translation_by_widget_id_and_language_id(
widget_id, language_id)
return jsonify(widgets), HTTPStatus.OK
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR


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

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

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

translation = WidgetTranslationSchema().load(request_json)
created_widget_translation = WidgetTranslationService().create_widget_translation(translation,
pre_populate)
return created_widget_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


@cors_preflight('GET, DELETE, PATCH')
@API.route('/<int:widget_translation_id>')
class EditWidgetTranslation(Resource):
"""Resource for updating or deleting a widget translation."""

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def delete(widget_id, widget_translation_id):
"""Remove widget translation for a widget."""
try:
WidgetTranslationService().delete_widget_translation(widget_id, widget_translation_id)
return 'Widget 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(widget_id, widget_translation_id):
"""Update widget translation."""
try:
translation_data = request.get_json()
updated_widget = WidgetTranslationService().update_widget_translation(widget_id,
widget_translation_id,
translation_data)
return updated_widget, HTTPStatus.OK
except ValueError as err:
return str(err), HTTPStatus.NOT_FOUND
except ValidationError as err:
return str(err.messages), HTTPStatus.BAD_REQUEST
31 changes: 31 additions & 0 deletions met-api/src/met_api/schemas/schemas/widget_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/widget_translation",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"default": {},
"examples": [
{
"widget_id": 1,
"language_id": 1
}
],
"required": ["widget_id", "language_id"],
"properties": {
"widget_id": {
"$id": "#/properties/widget_id",
"type": "number",
"title": "Widget id",
"description": "The widget 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]
}
}
}
23 changes: 23 additions & 0 deletions met-api/src/met_api/schemas/widget_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Widget translation schema class."""

from marshmallow import EXCLUDE, Schema, fields


class WidgetTranslationSchema(Schema):
"""Widget 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')
widget_id = fields.Int(data_key='widget_id', required=True)
language_id = fields.Int(data_key='language_id', required=True)
title = fields.Str(data_key='title')
map_marker_label = fields.Str(data_key='map_marker_label')
map_file_name = fields.Str(data_key='map_file_name')
poll_title = fields.Str(data_key='poll_title')
poll_description = fields.Str(data_key='poll_description')
video_url = fields.Str(data_key='video_url')
video_description = fields.Str(data_key='video_description')
Loading

0 comments on commit 8366029

Please sign in to comment.