-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[TO MAIN] DESENG-511 - Survey Translation model and API (#2410)
* 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
1 parent
966fc28
commit 74f835a
Showing
14 changed files
with
888 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
met-api/migrations/versions/274a2774607b_survey_translation_migration.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
54
met-api/src/met_api/schemas/schemas/survey_translation.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.