Skip to content

Commit

Permalink
[TO MAIN] DESENG-511 - Survey Translation model and API (#2410)
Browse files Browse the repository at this point in the history
* DESENG-511: Survey translation model (#2406)

* DESENG-511: Adding API for survey translation

* DESENG-511 : Added Unit test Survey translation

* DESENG-511: Fixed the review comment

* DESENG-511: Fixing lint

* Updaed Changelog

* DESENG-511: Fixing Review comments
  • Loading branch information
ratheesh-aot authored Mar 8, 2024
1 parent 966fc28 commit 74f835a
Show file tree
Hide file tree
Showing 14 changed files with 888 additions and 48 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## March 06, 2024
- **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.
- Added Survey Translation tests.

## March 04, 2024
- **Task**Engagement "save" enhancements [DESENG-507](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-507)
- The existing "Save" button in the floating bar has been split into two distinct actions: "Save and Continue" and "Save and Exit".
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""survey translation migration
Revision ID: 274a2774607b
Revises: e6c320c178fc
Create Date: 2024-03-05 13:41:19.539004
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

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


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('survey_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('survey_id', sa.Integer(), nullable=False),
sa.Column('language_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=50), nullable=True),
sa.Column('form_json', postgresql.JSONB(astext_type=sa.Text()), server_default='{}', 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'], ),
sa.ForeignKeyConstraint(['survey_id'], ['survey.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('survey_id', 'language_id', name='_survey_language_uc')
)
op.create_index(op.f('ix_survey_translation_name'), 'survey_translation', ['name'], unique=False)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_survey_translation_name'), table_name='survey_translation')
op.drop_table('survey_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 @@ -59,3 +59,4 @@
from .poll_answers import PollAnswer
from .poll_responses import PollResponse
from .language import Language
from .survey_translation import SurveyTranslation
95 changes: 95 additions & 0 deletions met-api/src/met_api/models/survey_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""SurveyTranslation model class.
Manages the Survey Translations.
"""

from __future__ import annotations

from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects import postgresql

from .base_model import BaseModel
from .db import db


class SurveyTranslation(BaseModel):
"""Definition of the SurveyTranslation entity."""

__tablename__ = 'survey_translation'

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
survey_id = db.Column(
db.Integer,
db.ForeignKey('survey.id', ondelete='CASCADE'),
nullable=False,
)
language_id = db.Column(
db.Integer, db.ForeignKey('language.id'), nullable=False
)
name = db.Column(
db.String(50), index=True, nullable=True
) # pre-populate it with the base Survey content is optional so can be nullable
form_json = db.Column(
postgresql.JSONB(astext_type=db.Text()),
nullable=True,
server_default='{}',
) # pre-populate it with the base Survey content is optional so can be nullable

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

@staticmethod
def get_survey_translation_by_survey_and_language(
survey_id=None, language_id=None
):
"""Get survey translation by survey_id and language_id, or by either one."""
query = SurveyTranslation.query
if survey_id is not None:
query = query.filter_by(survey_id=survey_id)
if language_id is not None:
query = query.filter_by(language_id=language_id)

survey_translation_records = query.all()
return survey_translation_records

@staticmethod
def create_survey_translation(data):
"""Create a new survey translation."""
survey_translation = SurveyTranslation(
survey_id=data['survey_id'],
language_id=data['language_id'],
name=data.get(
'name'
), # Returns `None` if 'name' is not in `data` as its optional
form_json=data.get(
'form_json'
), # Returns `None` if 'form_json' is not in `data` as its optional
)
survey_translation.save()
return survey_translation

@staticmethod
def update_survey_translation(survey_translation_id, data):
"""Update an existing survey translation."""
survey_translation = SurveyTranslation.query.get(survey_translation_id)
if survey_translation:
for key, value in data.items():
setattr(survey_translation, key, value)
db.session.commit()
return survey_translation
return None

@staticmethod
def delete_survey_translation(survey_translation_id):
"""Delete a survey translation."""
survey_translation = SurveyTranslation.query.get(survey_translation_id)
if survey_translation:
db.session.delete(survey_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 @@ -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 .survey_translation import API as SURVEY_TRANSLATION_API

__all__ = ('API_BLUEPRINT',)

Expand Down Expand Up @@ -103,3 +104,4 @@
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(SURVEY_TRANSLATION_API, path='/surveys/<int:survey_id>/translations')
149 changes: 149 additions & 0 deletions met-api/src/met_api/resources/survey_translation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Copyright © 2024 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
"""API endpoints for managing a SurveyTranslation resource."""

from http import HTTPStatus

from flask import 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.survey_translation_schema import SurveyTranslationSchema
from met_api.services.survey_translation_service import SurveyTranslationService
from met_api.utils.util import allowedorigins, cors_preflight


API = Namespace(
'survey_translations',
description='Endpoints for SurveyTranslation Management',
)


@cors_preflight('GET, POST, PATCH, DELETE, OPTIONS')
@API.route('/<int:survey_translation_id>')
class SurveyTranslationResource(Resource):
"""Resource for managing survey translations."""

@staticmethod
@cross_origin(origins=allowedorigins())
# pylint: disable=unused-argument
def get(survey_id, survey_translation_id):
"""Fetch a survey translation by id."""
try:
survey_translation = (
SurveyTranslationService.get_survey_translation_by_id(
survey_translation_id
)
)
return (
SurveyTranslationSchema().dump(survey_translation),
HTTPStatus.OK,
)
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR

@staticmethod
@_jwt.requires_auth
@cross_origin(origins=allowedorigins())
def patch(survey_id, survey_translation_id):
"""Update saved survey translation partially."""
try:
request_json = request.get_json()
survey_translation = (
SurveyTranslationService.update_survey_translation(
survey_id, survey_translation_id, request_json
)
)
return (
SurveyTranslationSchema().dump(survey_translation),
HTTPStatus.OK,
)
except ValueError as err:
return str(err), HTTPStatus.NOT_FOUND
except ValidationError as err:
return str(err.messages), HTTPStatus.BAD_REQUEST

@staticmethod
@_jwt.requires_auth
@cross_origin(origins=allowedorigins())
def delete(survey_id, survey_translation_id):
"""Delete a survey translation."""
try:
success = SurveyTranslationService.delete_survey_translation(
survey_id, survey_translation_id
)
if success:
return (
'Successfully deleted survey translation',
HTTPStatus.NO_CONTENT,
)
raise ValueError('Survey translation not found')
except KeyError as err:
return str(err), HTTPStatus.BAD_REQUEST
except ValueError as err:
return str(err), HTTPStatus.NOT_FOUND


@cors_preflight('GET, POST, PATCH, DELETE, OPTIONS')
@API.route('/language/<int:language_id>')
class SurveyTranslationResourceByLanguage(Resource):
"""Resource for managing survey using language_id."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get(survey_id, language_id):
"""Fetch a survey translation by language_id."""
try:
survey_translation = SurveyTranslationService.get_translation_by_survey_and_language(
survey_id, language_id
)
return (
SurveyTranslationSchema().dump(survey_translation, many=True),
HTTPStatus.OK,
)
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR


@cors_preflight('POST, OPTIONS')
@API.route('/')
class SurveyTranslations(Resource):
"""Resource for managing multiple survey translations."""

@staticmethod
@_jwt.requires_auth
@cross_origin(origins=allowedorigins())
def post(survey_id):
"""Create a new survey translation."""
try:
request_json = request.get_json()
request_json['survey_id'] = survey_id
valid_format, errors = schema_utils.validate(
request_json, 'survey_translation'
)
if not valid_format:
return {
'message': schema_utils.serialize(errors)
}, HTTPStatus.BAD_REQUEST
pre_populate = request_json.get('pre_populate', True)

survey_translation = (
SurveyTranslationService.create_survey_translation(
request_json, pre_populate
)
)
return (
SurveyTranslationSchema().dump(survey_translation),
HTTPStatus.CREATED,
)
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, err.status_code
54 changes: 54 additions & 0 deletions met-api/src/met_api/schemas/schemas/survey_translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://met.gov.bc.ca/.well_known/schemas/survey_translation",
"type": "object",
"title": "The SurveyTranslation Schema",
"description": "Schema for SurveyTranslation POST request validation.",
"default": {},
"examples": [
{
"survey_id": 1,
"language_id": 2,
"name": "Survey Name in Spanish",
"form_json": {},
"pre_populate" : false
}
],
"required": ["survey_id", "language_id"],
"properties": {
"survey_id": {
"$id": "#/properties/survey_id",
"type": "integer",
"title": "Survey ID",
"description": "The ID of the survey."
},
"language_id": {
"$id": "#/properties/language_id",
"type": "integer",
"title": "Language ID",
"description": "The ID of the language in which the survey is translated."
},
"name": {
"$id": "#/properties/name",
"type": "string",
"title": "Survey Name",
"description": "The name of the survey in the translated language.",
"maxLength": 50,
"examples": ["Survey Name in Spanish"]
},
"form_json": {
"$id": "#/properties/form_json",
"type": "object",
"title": "Form JSON",
"description": "The JSON representation of the survey form.",
"default": {}
},
"pre_populate": {
"$id": "#/properties/pre_populate",
"type": "boolean",
"title": "Prepopulate",
"description": "Indicates whether the survey translation should be prepopulated with survey data. Default true.",
"examples": [false]
}
}
}
22 changes: 22 additions & 0 deletions met-api/src/met_api/schemas/survey_translation_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""SurveyTranslation schema."""

from marshmallow import fields
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema

from met_api.models.survey_translation import SurveyTranslation


class SurveyTranslationSchema(SQLAlchemyAutoSchema):
"""Schema for SurveyTranslation serialization and deserialization."""

class Meta:
"""SurveyTranslationSchema metadata."""

model = SurveyTranslation
load_instance = True # Optional: deserialize to model instances

id = fields.Int(dump_only=True)
survey_id = fields.Int(required=True)
language_id = fields.Int(required=True)
name = fields.Str(required=False, allow_none=True)
form_json = fields.Raw(required=False, allow_none=True)
Loading

0 comments on commit 74f835a

Please sign in to comment.