-
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-509 - Language model and API (#2404)
* 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
1 parent
d3440c1
commit 122d391
Showing
15 changed files
with
634 additions
and
3 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
39 changes: 39 additions & 0 deletions
39
met-api/migrations/versions/e6c320c178fc_multi_langauge_table_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,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 ### |
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,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 |
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,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 |
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,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') |
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,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] | ||
} | ||
} | ||
} | ||
|
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,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] | ||
} | ||
} | ||
} | ||
|
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 @@ | ||
"""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) |
Oops, something went wrong.