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

[TO MAIN] DESENG-514 - Adding widget translation model #2411

Merged
merged 5 commits into from
Mar 8, 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
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
Loading