Skip to content

Commit

Permalink
[TO MAIN] DESENG-509 - Language model and API (#2404)
Browse files Browse the repository at this point in the history
* DESENG-509 Creating model and migration for Language table

* DESENG-509: Made code column unique in language model

* DESENG-509: Service and resource files for langauge

* DESENG-509: Removed import

* DESENG-509: Unit test for Language api

* DESENG-509: Unit test update

* DESENG-509: Fix review comments

* Update changelog
  • Loading branch information
ratheesh-aot authored Mar 4, 2024
1 parent d3440c1 commit 122d391
Show file tree
Hide file tree
Showing 15 changed files with 634 additions and 3 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 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.
- Added Unit tests.

## February 27, 2024
- **Bug Fix**Comments cannot be approved while reviewing [DESENG-496](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-496)
- Fixed by adding a missing decorator for transactional methods.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""multi language table migration
Revision ID: e6c320c178fc
Revises: cec8d0371f42
Create Date: 2024-02-29 09:18:13.949848
"""
from alembic import op
import sqlalchemy as sa


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


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('language',
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('name', sa.String(length=50), nullable=False),
sa.Column('code', sa.String(length=2), nullable=False),
sa.Column('right_to_left', sa.Boolean(), nullable=False),
sa.Column('created_by', sa.String(length=50), nullable=True),
sa.Column('updated_by', sa.String(length=50), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('language')
# ### 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 @@ -58,3 +58,4 @@
from .widget_poll import Poll
from .poll_answers import PollAnswer
from .poll_responses import PollResponse
from .language import Language
53 changes: 53 additions & 0 deletions met-api/src/met_api/models/language.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Language model class.
Manages the Language
"""
from __future__ import annotations
from .base_model import BaseModel
from .db import db


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

__tablename__ = 'language'

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(50), nullable=False) # eg. English, French etc
code = db.Column(db.String(2), nullable=False, unique=True) # eg. en, fr etc
right_to_left = db.Column(db.Boolean, nullable=False, default=False)

@staticmethod
def get_languages():
"""Retrieve all languages."""
return Language.query.all()

@staticmethod
def create_language(data):
"""Create a new language."""
language = Language(name=data['name'], code=data['code'],
right_to_left=data.get('right_to_left', False))
db.session.add(language)
db.session.commit()
return language

@staticmethod
def update_language(language_id, data):
"""Update an existing language."""
language = Language.query.get(language_id)
if language:
for key, value in data.items():
setattr(language, key, value)
db.session.commit()
return language
return None

@staticmethod
def delete_language(language_id):
"""Delete a language."""
language = Language.query.get(language_id)
if language:
db.session.delete(language)
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 @@ -55,6 +55,7 @@
from .cac_form import API as CAC_FORM_API
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

__all__ = ('API_BLUEPRINT',)

Expand Down Expand Up @@ -101,3 +102,4 @@
API.add_namespace(CAC_FORM_API, path='/engagements/<int:engagement_id>/cacform')
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')
117 changes: 117 additions & 0 deletions met-api/src/met_api/resources/language.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright © 2021 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
"""API endpoints for managing a Language 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.language import LanguageSchema
from met_api.services.language_service import LanguageService
from met_api.utils.util import allowedorigins, cors_preflight
from met_api.exceptions.business_exception import BusinessException

API = Namespace('languages', description='Endpoints for Language Management')


@cors_preflight('GET, OPTIONS')
@API.route('/<int:language_id>')
class LanguageResource(Resource):
"""Resource for managing languages."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get(language_id):
"""Fetch a language by id."""
try:
language = LanguageService.get_language_by_id(language_id)
return LanguageSchema().dump(language), HTTPStatus.OK
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR

@staticmethod
@_jwt.requires_auth
@cross_origin(origins=allowedorigins())
def patch(language_id):
"""Update saved language partially."""
try:
request_json = request.get_json()
valid_format, errors = schema_utils.validate(
request_json, 'language_update'
)
if not valid_format:
raise BusinessException(
error=schema_utils.serialize(errors),
status_code=HTTPStatus.BAD_REQUEST,
)
language = LanguageService.update_language(
language_id, request_json
)
return LanguageSchema().dump(language), 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(language_id):
"""Delete a language."""
try:
success = LanguageService.delete_language(language_id)
if success:
return 'Successfully deleted language', HTTPStatus.NO_CONTENT
raise ValueError('Language 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, OPTIONS, PATCH, DELETE')
@API.route('/')
class Languages(Resource):
"""Resource for managing multiple languages."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get():
"""Fetch list of languages."""
try:
languages = LanguageService.get_languages()
return (
jsonify(LanguageSchema(many=True).dump(languages)),
HTTPStatus.OK,
)
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR

@staticmethod
@_jwt.requires_auth
@cross_origin(origins=allowedorigins())
def post():
"""Create a new language."""
try:
request_json = request.get_json()
valid_format, errors = schema_utils.validate(
request_json, 'language'
)
if not valid_format:
return {
'message': schema_utils.serialize(errors)
}, HTTPStatus.BAD_REQUEST
result = LanguageService.create_language(request_json)
return LanguageSchema().dump(result), HTTPStatus.CREATED
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR
except ValidationError as err:
return str(err.messages), HTTPStatus.INTERNAL_SERVER_ERROR
except BusinessException as err:
return err.error, err.status_code
17 changes: 17 additions & 0 deletions met-api/src/met_api/schemas/language.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Language schema."""

from marshmallow import EXCLUDE, Schema, fields


class LanguageSchema(Schema):
"""Language schema."""

class Meta:
"""Exclude unknown fields in the deserialized output."""

unknown = EXCLUDE

id = fields.Int(data_key='id')
name = fields.Str(data_key='name', required=True)
code = fields.Str(data_key='code', required=True)
right_to_left = fields.Bool(data_key='right_to_left')
40 changes: 40 additions & 0 deletions met-api/src/met_api/schemas/schemas/language.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://met.gov.bc.ca/.well_known/schemas/language",
"type": "object",
"title": "The Language Schema",
"description": "Schema for Language POST request validation.",
"default": {},
"examples": [
{
"name": "Spanish",
"code": "es",
"right_to_left": false
}
],
"required": ["name", "code", "right_to_left"],
"properties": {
"name": {
"$id": "#/properties/name",
"type": "string",
"title": "Language Name",
"description": "The name of the language.",
"examples": ["Spanish"]
},
"code": {
"$id": "#/properties/code",
"type": "string",
"title": "Language Code",
"description": "The two-letter code of the language.",
"examples": ["es"]
},
"right_to_left": {
"$id": "#/properties/right_to_left",
"type": "boolean",
"title": "Right to Left",
"description": "Indicates if the language is written from right to left.",
"examples": [false]
}
}
}

40 changes: 40 additions & 0 deletions met-api/src/met_api/schemas/schemas/language_update.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://met.gov.bc.ca/.well_known/schemas/language_update",
"type": "object",
"title": "The Language Schema",
"description": "Schema for Language PATCH request validation.",
"default": {},
"examples": [
{
"name": "Spanish",
"code": "es",
"right_to_left": false
}
],
"required": [],
"properties": {
"name": {
"$id": "#/properties/name",
"type": "string",
"title": "Language Name",
"description": "The name of the language.",
"examples": ["Spanish"]
},
"code": {
"$id": "#/properties/code",
"type": "string",
"title": "Language Code",
"description": "The two-letter code of the language.",
"examples": ["es"]
},
"right_to_left": {
"$id": "#/properties/right_to_left",
"type": "boolean",
"title": "Right to Left",
"description": "Indicates if the language is written from right to left.",
"examples": [false]
}
}
}

54 changes: 54 additions & 0 deletions met-api/src/met_api/services/language_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Service for Language management."""

from http import HTTPStatus

from sqlalchemy.exc import IntegrityError

from met_api.exceptions.business_exception import BusinessException
from met_api.models.language import Language
from met_api.schemas.language import LanguageSchema


class LanguageService:
"""Language management service."""

@staticmethod
def get_language_by_id(language_id):
"""Get language by id."""
language_record = Language.find_by_id(language_id)
return LanguageSchema().dump(language_record)

@staticmethod
def get_languages():
"""Get languages."""
languages_records = Language.get_languages()
return LanguageSchema(many=True).dump(languages_records)

@staticmethod
def create_language(language_data):
"""Create language."""
try:
return Language.create_language(language_data)
except IntegrityError as e:
# Catching language code already exists error
detail = (
str(e.orig).split('DETAIL: ')[1]
if 'DETAIL: ' in str(e.orig)
else 'Duplicate entry.'
)
raise BusinessException(
str(detail), HTTPStatus.INTERNAL_SERVER_ERROR
) from e

@staticmethod
def update_language(language_id, data: dict):
"""Update language partially."""
updated_language = Language.update_language(language_id, data)
if not updated_language:
raise ValueError('Language to update was not found')
return updated_language

@staticmethod
def delete_language(language_id):
"""Delete language."""
return Language.delete_language(language_id)
Loading

0 comments on commit 122d391

Please sign in to comment.