From 8f09e100222d34d94f7791a236a4ce1858ef5d77 Mon Sep 17 00:00:00 2001 From: Baelx <16845197+Baelx@users.noreply.github.com> Date: Tue, 4 Jun 2024 15:33:36 -0700 Subject: [PATCH] DESENG-632 Add DB tables & API routes (#2533) * DESENG-632 Add DB tables & API routes * DESENG-623 fix linting * DESENG-623 Move language to migration file, remove delete crud language route * DESENG-623 Remove ability to delete language --------- Co-authored-by: Alex --- met-api/migrations/README | 12 +- ...5b5aed_relax_language_table_constraints.py | 27 + ...f82334_add_languages_and_tenant_mapping.py | 788 ++++++++++++++++++ met-api/src/met_api/models/__init__.py | 1 + met-api/src/met_api/models/language.py | 23 +- .../met_api/models/language_tenant_mapping.py | 44 + met-api/src/met_api/resources/language.py | 85 +- .../schemas/language_tenant_mapping.py | 16 + .../src/met_api/services/language_service.py | 44 +- met-api/tests/unit/api/test_language.py | 18 - met-api/tests/unit/models/test_language.py | 25 - .../unit/services/test_language_service.py | 26 - 12 files changed, 965 insertions(+), 144 deletions(-) create mode 100644 met-api/migrations/versions/22fb6b5b5aed_relax_language_table_constraints.py create mode 100644 met-api/migrations/versions/c656f3f82334_add_languages_and_tenant_mapping.py create mode 100644 met-api/src/met_api/models/language_tenant_mapping.py create mode 100644 met-api/src/met_api/schemas/language_tenant_mapping.py diff --git a/met-api/migrations/README b/met-api/migrations/README index 98e4f9c44..e92555574 100644 --- a/met-api/migrations/README +++ b/met-api/migrations/README @@ -1 +1,11 @@ -Generic single-database configuration. \ No newline at end of file +# Migrations + +Uses alembic, SQL Alchemy, and flask-migrate to perform DB migrations. + +To create a new revision: + +`alembic revision -m "description of revision"` + +## Languages + +The list of languages in `versions/c656f3f82334_add_languages_and_tenant_mapping.py` has been taken from [https://www.w3schools.com/tags/ref_language_codes.asp](https://www.w3schools.com/tags/ref_language_codes.asp) and modified slightly to remove duplicate or multiple language codes per language. \ No newline at end of file diff --git a/met-api/migrations/versions/22fb6b5b5aed_relax_language_table_constraints.py b/met-api/migrations/versions/22fb6b5b5aed_relax_language_table_constraints.py new file mode 100644 index 000000000..1e06bf69e --- /dev/null +++ b/met-api/migrations/versions/22fb6b5b5aed_relax_language_table_constraints.py @@ -0,0 +1,27 @@ +""" +Relax language table constraints. + +Revision ID: 22fb6b5b5aed +Revises: ae232e299180 +Create Date: 2024-05-27 15:56:23.549731 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '22fb6b5b5aed' +down_revision = 'ae232e299180' +branch_labels = None +depends_on = None + +def upgrade(): + op.alter_column('language', 'right_to_left', nullable = True) + op.alter_column('language', 'code', type_ = sa.String(length=20)) + +def downgrade(): + op.alter_column('language', 'right_to_left', nullable = False) + op.alter_column('language', 'code', type_ = sa.String(length=2)) + + + diff --git a/met-api/migrations/versions/c656f3f82334_add_languages_and_tenant_mapping.py b/met-api/migrations/versions/c656f3f82334_add_languages_and_tenant_mapping.py new file mode 100644 index 000000000..301231094 --- /dev/null +++ b/met-api/migrations/versions/c656f3f82334_add_languages_and_tenant_mapping.py @@ -0,0 +1,788 @@ +""" +Add the list of supported languages and create a table to map them to tenants when selected. + +Revision ID: c656f3f82334 +Revises: 22fb6b5b5aed +Create Date: 2024-05-28 15:45:56.151488 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'c656f3f82334' +down_revision = '22fb6b5b5aed' +branch_labels = None +depends_on = None + +language_data = [ + { + "language": "Abkhazian", + "iso_code": "ab" + }, + { + "language": "Afar", + "iso_code": "aa" + }, + { + "language": "Afrikaans", + "iso_code": "af" + }, + { + "language": "Akan", + "iso_code": "ak" + }, + { + "language": "Albanian", + "iso_code": "sq" + }, + { + "language": "Amharic", + "iso_code": "am" + }, + { + "language": "Arabic", + "iso_code": "ar" + }, + { + "language": "Aragonese", + "iso_code": "an" + }, + { + "language": "Armenian", + "iso_code": "hy" + }, + { + "language": "Assamese", + "iso_code": "as" + }, + { + "language": "Avaric", + "iso_code": "av" + }, + { + "language": "Avestan", + "iso_code": "ae" + }, + { + "language": "Aymara", + "iso_code": "ay" + }, + { + "language": "Azerbaijani", + "iso_code": "az" + }, + { + "language": "Bambara", + "iso_code": "bm" + }, + { + "language": "Bashkir", + "iso_code": "ba" + }, + { + "language": "Basque", + "iso_code": "eu" + }, + { + "language": "Belarusian", + "iso_code": "be" + }, + { + "language": "Bengali (Bangla)", + "iso_code": "bn" + }, + { + "language": "Bihari", + "iso_code": "bh" + }, + { + "language": "Bislama", + "iso_code": "bi" + }, + { + "language": "Bosnian", + "iso_code": "bs" + }, + { + "language": "Breton", + "iso_code": "br" + }, + { + "language": "Bulgarian", + "iso_code": "bg" + }, + { + "language": "Burmese", + "iso_code": "my" + }, + { + "language": "Catalan", + "iso_code": "ca" + }, + { + "language": "Chamorro", + "iso_code": "ch" + }, + { + "language": "Chechen", + "iso_code": "ce" + }, + { + "language": "Chichewa, Chewa, Nyanja", + "iso_code": "ny" + }, + { + "language": "Chinese", + "iso_code": "zh" + }, + { + "language": "Chinese (Simplified)", + "iso_code": "zh-Hans" + }, + { + "language": "Chinese (Traditional)", + "iso_code": "zh-Hant" + }, + { + "language": "Chuvash", + "iso_code": "cv" + }, + { + "language": "Cornish", + "iso_code": "kw" + }, + { + "language": "Corsican", + "iso_code": "co" + }, + { + "language": "Cree", + "iso_code": "cr" + }, + { + "language": "Croatian", + "iso_code": "hr" + }, + { + "language": "Czech", + "iso_code": "cs" + }, + { + "language": "Danish", + "iso_code": "da" + }, + { + "language": "Divehi, Dhivehi, Maldivian", + "iso_code": "dv" + }, + { + "language": "Dutch", + "iso_code": "nl" + }, + { + "language": "Dzongkha", + "iso_code": "dz" + }, + { + "language": "English", + "iso_code": "en" + }, + { + "language": "Esperanto", + "iso_code": "eo" + }, + { + "language": "Estonian", + "iso_code": "et" + }, + { + "language": "Ewe", + "iso_code": "ee" + }, + { + "language": "Faroese", + "iso_code": "fo" + }, + { + "language": "Fijian", + "iso_code": "fj" + }, + { + "language": "Finnish", + "iso_code": "fi" + }, + { + "language": "French", + "iso_code": "fr" + }, + { + "language": "Fula, Fulah, Pulaar, Pular", + "iso_code": "ff" + }, + { + "language": "Galician", + "iso_code": "gl" + }, + { + "language": "Gaelic (Scottish)", + "iso_code": "gd" + }, + { + "language": "Georgian", + "iso_code": "ka" + }, + { + "language": "German", + "iso_code": "de" + }, + { + "language": "Greek", + "iso_code": "el" + }, + { + "language": "Guarani", + "iso_code": "gn" + }, + { + "language": "Gujarati", + "iso_code": "gu" + }, + { + "language": "Haitian Creole", + "iso_code": "ht" + }, + { + "language": "Hausa", + "iso_code": "ha" + }, + { + "language": "Hebrew", + "iso_code": "he" + }, + { + "language": "Herero", + "iso_code": "hz" + }, + { + "language": "Hindi", + "iso_code": "hi" + }, + { + "language": "Hiri Motu", + "iso_code": "ho" + }, + { + "language": "Hungarian", + "iso_code": "hu" + }, + { + "language": "Icelandic", + "iso_code": "is" + }, + { + "language": "Ido", + "iso_code": "io" + }, + { + "language": "Igbo", + "iso_code": "ig" + }, + { + "language": "Indonesian", + "iso_code": "id" + }, + { + "language": "Interlingua", + "iso_code": "ia" + }, + { + "language": "Interlingue", + "iso_code": "ie" + }, + { + "language": "Inuktitut", + "iso_code": "iu" + }, + { + "language": "Inupiak", + "iso_code": "ik" + }, + { + "language": "Irish", + "iso_code": "ga" + }, + { + "language": "Italian", + "iso_code": "it" + }, + { + "language": "Japanese", + "iso_code": "ja" + }, + { + "language": "Javanese", + "iso_code": "jv" + }, + { + "language": "Kalaallisut, Greenlandic", + "iso_code": "kl" + }, + { + "language": "Kannada", + "iso_code": "kn" + }, + { + "language": "Kanuri", + "iso_code": "kr" + }, + { + "language": "Kashmiri", + "iso_code": "ks" + }, + { + "language": "Kazakh", + "iso_code": "kk" + }, + { + "language": "Khmer", + "iso_code": "km" + }, + { + "language": "Kikuyu", + "iso_code": "ki" + }, + { + "language": "Kinyarwanda (Rwanda)", + "iso_code": "rw" + }, + { + "language": "Kirundi", + "iso_code": "rn" + }, + { + "language": "Kyrgyz", + "iso_code": "ky" + }, + { + "language": "Komi", + "iso_code": "kv" + }, + { + "language": "Kongo", + "iso_code": "kg" + }, + { + "language": "Korean", + "iso_code": "ko" + }, + { + "language": "Kurdish", + "iso_code": "ku" + }, + { + "language": "Kwanyama", + "iso_code": "kj" + }, + { + "language": "Lao", + "iso_code": "lo" + }, + { + "language": "Latin", + "iso_code": "la" + }, + { + "language": "Latvian (Lettish)", + "iso_code": "lv" + }, + { + "language": "Limburgish ( Limburger)", + "iso_code": "li" + }, + { + "language": "Lingala", + "iso_code": "ln" + }, + { + "language": "Lithuanian", + "iso_code": "lt" + }, + { + "language": "Luga-Katanga", + "iso_code": "lu" + }, + { + "language": "Luganda, Ganda", + "iso_code": "lg" + }, + { + "language": "Luxembourgish", + "iso_code": "lb" + }, + { + "language": "Manx", + "iso_code": "gv" + }, + { + "language": "Macedonian", + "iso_code": "mk" + }, + { + "language": "Malagasy", + "iso_code": "mg" + }, + { + "language": "Malay", + "iso_code": "ms" + }, + { + "language": "Malayalam", + "iso_code": "ml" + }, + { + "language": "Maltese", + "iso_code": "mt" + }, + { + "language": "Maori", + "iso_code": "mi" + }, + { + "language": "Marathi", + "iso_code": "mr" + }, + { + "language": "Marshallese", + "iso_code": "mh" + }, + { + "language": "Moldavian", + "iso_code": "mo" + }, + { + "language": "Mongolian", + "iso_code": "mn" + }, + { + "language": "Nauru", + "iso_code": "na" + }, + { + "language": "Navajo", + "iso_code": "nv" + }, + { + "language": "Ndonga", + "iso_code": "ng" + }, + { + "language": "Northern Ndebele", + "iso_code": "nd" + }, + { + "language": "Nepali", + "iso_code": "ne" + }, + { + "language": "Norwegian", + "iso_code": "no" + }, + { + "language": "Norwegian bokmål", + "iso_code": "nb" + }, + { + "language": "Norwegian nynorsk", + "iso_code": "nn" + }, + { + "language": "Occitan", + "iso_code": "oc" + }, + { + "language": "Ojibwe", + "iso_code": "oj" + }, + { + "language": "Old Church Slavonic, Old Bulgarian", + "iso_code": "cu" + }, + { + "language": "Oriya", + "iso_code": "or" + }, + { + "language": "Oromo (Afaan Oromo)", + "iso_code": "om" + }, + { + "language": "Ossetian", + "iso_code": "os" + }, + { + "language": "Pāli", + "iso_code": "pi" + }, + { + "language": "Pashto, Pushto", + "iso_code": "ps" + }, + { + "language": "Persian (Farsi)", + "iso_code": "fa" + }, + { + "language": "Polish", + "iso_code": "pl" + }, + { + "language": "Portuguese", + "iso_code": "pt" + }, + { + "language": "Punjabi (Eastern)", + "iso_code": "pa" + }, + { + "language": "Quechua", + "iso_code": "qu" + }, + { + "language": "Romansh", + "iso_code": "rm" + }, + { + "language": "Romanian", + "iso_code": "ro" + }, + { + "language": "Russian", + "iso_code": "ru" + }, + { + "language": "Sami", + "iso_code": "se" + }, + { + "language": "Samoan", + "iso_code": "sm" + }, + { + "language": "Sango", + "iso_code": "sg" + }, + { + "language": "Sanskrit", + "iso_code": "sa" + }, + { + "language": "Serbian", + "iso_code": "sr" + }, + { + "language": "Serbo-Croatian", + "iso_code": "sh" + }, + { + "language": "Sesotho", + "iso_code": "st" + }, + { + "language": "Setswana", + "iso_code": "tn" + }, + { + "language": "Shona", + "iso_code": "sn" + }, + { + "language": "Sichuan Yi, Nuosu", + "iso_code": "ii" + }, + { + "language": "Sindhi", + "iso_code": "sd" + }, + { + "language": "Sinhalese", + "iso_code": "si" + }, + { + "language": "Slovak", + "iso_code": "sk" + }, + { + "language": "Slovenian", + "iso_code": "sl" + }, + { + "language": "Somali", + "iso_code": "so" + }, + { + "language": "Southern Ndebele", + "iso_code": "nr" + }, + { + "language": "Spanish", + "iso_code": "es" + }, + { + "language": "Sundanese", + "iso_code": "su" + }, + { + "language": "Swahili (Kiswahili)", + "iso_code": "sw" + }, + { + "language": "Swati", + "iso_code": "ss" + }, + { + "language": "Swedish", + "iso_code": "sv" + }, + { + "language": "Tagalog", + "iso_code": "tl" + }, + { + "language": "Tahitian", + "iso_code": "ty" + }, + { + "language": "Tajik", + "iso_code": "tg" + }, + { + "language": "Tamil", + "iso_code": "ta" + }, + { + "language": "Tatar", + "iso_code": "tt" + }, + { + "language": "Telugu", + "iso_code": "te" + }, + { + "language": "Thai", + "iso_code": "th" + }, + { + "language": "Tibetan", + "iso_code": "bo" + }, + { + "language": "Tigrinya", + "iso_code": "ti" + }, + { + "language": "Tonga", + "iso_code": "to" + }, + { + "language": "Tsonga", + "iso_code": "ts" + }, + { + "language": "Turkish", + "iso_code": "tr" + }, + { + "language": "Turkmen", + "iso_code": "tk" + }, + { + "language": "Twi", + "iso_code": "tw" + }, + { + "language": "Uyghur", + "iso_code": "ug" + }, + { + "language": "Ukrainian", + "iso_code": "uk" + }, + { + "language": "Urdu", + "iso_code": "ur" + }, + { + "language": "Uzbek", + "iso_code": "uz" + }, + { + "language": "Venda", + "iso_code": "ve" + }, + { + "language": "Vietnamese", + "iso_code": "vi" + }, + { + "language": "Volapük", + "iso_code": "vo" + }, + { + "language": "Wallon", + "iso_code": "wa" + }, + { + "language": "Welsh", + "iso_code": "cy" + }, + { + "language": "Wolof", + "iso_code": "wo" + }, + { + "language": "Western Frisian", + "iso_code": "fy" + }, + { + "language": "Xhosa", + "iso_code": "xh" + }, + { + "language": "Yiddish", + "iso_code": "yi" + }, + { + "language": "Yoruba", + "iso_code": "yo" + }, + { + "language": "Zhuang, Chuang", + "iso_code": "za" + }, + { + "language": "Zulu", + "iso_code": "zu" + } +] + +def upgrade(): + for index, value in enumerate(language_data): + op.execute("INSERT INTO language (created_date, updated_date, id, name, code) VALUES ('{0}', '{0}', '{1}', '{2}', '{3}')".format(sa.func.now(), index, value['language'], value['iso_code'])) + op.create_table('language_tenant_mapping', + 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('language_id', sa.Integer(), nullable=True), + sa.Column('tenant_id', sa.Integer(), 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(['tenant_id'], ['tenant.id']), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('language_id', 'tenant_id') + ) + +def downgrade(): + op.execute("DELETE FROM language") + op.drop_table('language_tenant_mapping') \ No newline at end of file diff --git a/met-api/src/met_api/models/__init__.py b/met-api/src/met_api/models/__init__.py index d1fe482f2..ae5a15033 100644 --- a/met-api/src/met_api/models/__init__.py +++ b/met-api/src/met_api/models/__init__.py @@ -59,6 +59,7 @@ from .poll_answers import PollAnswer from .poll_responses import PollResponse from .language import Language +from .language_tenant_mapping import LanguageTenantMapping from .widget_translation import WidgetTranslation from .survey_translation import SurveyTranslation from .event_item_translation import EventItemTranslation diff --git a/met-api/src/met_api/models/language.py b/met-api/src/met_api/models/language.py index 26d8b7894..3bdfeb887 100644 --- a/met-api/src/met_api/models/language.py +++ b/met-api/src/met_api/models/language.py @@ -14,23 +14,14 @@ class Language(BaseModel): 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) + code = db.Column(db.String(20), nullable=False, unique=True) # eg. en, fr etc + right_to_left = db.Column(db.Boolean, nullable=True) @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.""" @@ -41,13 +32,3 @@ def update_language(language_id, data): 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 diff --git a/met-api/src/met_api/models/language_tenant_mapping.py b/met-api/src/met_api/models/language_tenant_mapping.py new file mode 100644 index 000000000..da441f39a --- /dev/null +++ b/met-api/src/met_api/models/language_tenant_mapping.py @@ -0,0 +1,44 @@ +"""Manages which tenants have which languages selected.""" +from __future__ import annotations +from sqlalchemy import UniqueConstraint + +from .base_model import BaseModel +from .language import Language +from .db import db + + +class LanguageTenantMapping(BaseModel): # pylint: disable=too-few-public-methods, too-many-instance-attributes + """Manage language-tenant relationships.""" + + __tablename__ = 'language_tenant_mapping' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + language_id = db.Column(db.Integer, db.ForeignKey('language.id'), nullable=True) + tenant_id = db.Column(db.Integer, db.ForeignKey('tenant.id'), nullable=True) + + # Add a unique constraint on any given combination of tenant_id and language_id + __table_args__ = (UniqueConstraint('language_id', 'tenant_id'),) + + @classmethod + def get_all_by_tenant_id(cls, tenant_id): + """Get all languages selected by a given tenant.""" + language_mappings_query = db.session.query(LanguageTenantMapping.language_id)\ + .filter_by(tenant_id=tenant_id) + return db.session.query(Language).filter(Language.id.in_(language_mappings_query)).all() + + @classmethod + def add_language_to_tenant(cls, language_id, tenant_id): + """Add a language to the langage tenant mapping table.""" + language_tenant_mapping = LanguageTenantMapping(language_id=language_id, tenant_id=tenant_id) + db.session.add(language_tenant_mapping) + db.session.commit() + return language_tenant_mapping + + @classmethod + def remove_language_from_tenant(cls, language_mapping_id): + """Remove a language from the langage tenant mapping table.""" + language_mapping = LanguageTenantMapping.query.get(language_mapping_id) + if language_mapping: + db.session.delete(language_mapping) + db.session.commit() + return True + return False diff --git a/met-api/src/met_api/resources/language.py b/met-api/src/met_api/resources/language.py index 47fe9ec74..ba796be7d 100644 --- a/met-api/src/met_api/resources/language.py +++ b/met-api/src/met_api/resources/language.py @@ -9,6 +9,7 @@ from flask_cors import cross_origin from flask_restx import Namespace, Resource from marshmallow import ValidationError +from sqlalchemy.exc import IntegrityError from met_api.auth import jwt as _jwt from met_api.schemas import utils as schema_utils @@ -59,16 +60,63 @@ def patch(language_id): except ValidationError as err: return str(err.messages), HTTPStatus.BAD_REQUEST + +@cors_preflight('GET, OPTIONS') +@API.route('/tenant/') +class TenantLanguages(Resource): + """Resource for getting existing language-tenant mappings.""" + @staticmethod - @_jwt.requires_auth @cross_origin(origins=allowedorigins()) - def delete(language_id): - """Delete a language.""" + def get(tenant_id): + """Fetch list of languages associated with a given tenant ID.""" + try: + languages = LanguageService.get_languages_by_tenant(tenant_id) + return ( + LanguageSchema(many=True).dump(languages), + HTTPStatus.OK, + ) + except (KeyError, ValueError) as err: + return str(err), HTTPStatus.INTERNAL_SERVER_ERROR + + +@cors_preflight('POST, OPTIONS') +@API.route('//tenant/') +class ManageLanguageMappings(Resource): + """Resource for adding or removing language-tenant relationships.""" + + @staticmethod + def post(language_id, tenant_id): + """Create language-tenant mapping.""" + try: + return LanguageService.map_language_to_tenant(language_id, tenant_id) + 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 + + +@cors_preflight('DELETE, OPTIONS') +@API.route('/mappings/') +class DeleteLanguageMapping(Resource): + """Resourse for removing language-tenant relationships.""" + + @staticmethod + # @_jwt.requires_auth + # @cross_origin(origins=allowedorigins()) + def delete(language_mapping_id): + """Remove a language mapping from a tenant.""" try: - success = LanguageService.delete_language(language_id) + success = LanguageService.remove_language_mapping_from_tenant(language_mapping_id) if success: - return 'Successfully deleted language', HTTPStatus.NO_CONTENT - raise ValueError('Language not found') + return 'Successfully deleted language-tenant mapping', HTTPStatus.NO_CONTENT + raise ValueError('Language-tenant mapping not found') except KeyError as err: return str(err), HTTPStatus.BAD_REQUEST except ValueError as err: @@ -87,31 +135,8 @@ def get(): try: languages = LanguageService.get_languages() return ( - jsonify(LanguageSchema(many=True).dump(languages)), + 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 diff --git a/met-api/src/met_api/schemas/language_tenant_mapping.py b/met-api/src/met_api/schemas/language_tenant_mapping.py new file mode 100644 index 000000000..08377fc98 --- /dev/null +++ b/met-api/src/met_api/schemas/language_tenant_mapping.py @@ -0,0 +1,16 @@ +"""Language-Tenant Mapping schema.""" + +from marshmallow import EXCLUDE, Schema, fields + + +class LanguageTenantMappingSchema(Schema): + """Language-Tenant Mapping schema.""" + + class Meta: + """Exclude unknown fields in the deserialized output.""" + + unknown = EXCLUDE + + id = fields.Int(data_key='id') + language_id = fields.Int(data_key='language_id') + tenant_id = fields.Int(data_key='tenant_id') diff --git a/met-api/src/met_api/services/language_service.py b/met-api/src/met_api/services/language_service.py index 8aefa035f..af3256dbd 100644 --- a/met-api/src/met_api/services/language_service.py +++ b/met-api/src/met_api/services/language_service.py @@ -1,12 +1,11 @@ """Service for Language management.""" -from http import HTTPStatus +from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.exc import IntegrityError - -from met_api.exceptions.business_exception import BusinessException from met_api.models.language import Language +from met_api.models.language_tenant_mapping import LanguageTenantMapping from met_api.schemas.language import LanguageSchema +from met_api.schemas.language_tenant_mapping import LanguageTenantMappingSchema class LanguageService: @@ -24,22 +23,6 @@ def 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.""" @@ -49,6 +32,21 @@ def update_language(language_id, data: dict): return updated_language @staticmethod - def delete_language(language_id): - """Delete language.""" - return Language.delete_language(language_id) + def map_language_to_tenant(language_id: int, tenant_id: int): + """Create an entry in the language tenant mapping table to associated a language with a tenant.""" + language_mapping = LanguageTenantMapping() + try: + language_mapping.add_language_to_tenant(language_id, tenant_id) + except SQLAlchemyError as e: + raise ValueError('Error adding language to tenant.') from e + return LanguageTenantMappingSchema().dump(language_mapping) + + @staticmethod + def remove_language_mapping_from_tenant(language_mapping_id: int): + """Remove the DB entry that maps the language to the tenant.""" + return LanguageTenantMapping.remove_language_from_tenant(language_mapping_id) + + @staticmethod + def get_languages_by_tenant(tenant_id: int): + """Get all languages associated with a given tenant.""" + return LanguageTenantMapping.get_all_by_tenant_id(tenant_id) diff --git a/met-api/tests/unit/api/test_language.py b/met-api/tests/unit/api/test_language.py index d498b5176..7534890ab 100644 --- a/met-api/tests/unit/api/test_language.py +++ b/met-api/tests/unit/api/test_language.py @@ -47,24 +47,6 @@ def test_get_languages(client, jwt, session): assert len(json_data) > 0 -def test_create_language(client, jwt, session, setup_admin_user_and_claims): - """Assert that a new language can be created using the POST API endpoint.""" - _, claims = setup_admin_user_and_claims - headers = factory_auth_header(jwt=jwt, claims=claims) - data = {'name': 'Italian', 'code': 'it', 'right_to_left': False} - - rv = client.post( - '/api/languages/', - data=json.dumps(data), - headers=headers, - content_type=ContentType.JSON.value, - ) - - assert rv.status_code == HTTPStatus.CREATED - json_data = rv.json - assert json_data['name'] == 'Italian' - - def test_update_language(client, jwt, session, setup_admin_user_and_claims): """Assert that a language can be updated using the PATCH API endpoint.""" _, claims = setup_admin_user_and_claims diff --git a/met-api/tests/unit/models/test_language.py b/met-api/tests/unit/models/test_language.py index 54b3e9502..68d258ecd 100644 --- a/met-api/tests/unit/models/test_language.py +++ b/met-api/tests/unit/models/test_language.py @@ -7,17 +7,6 @@ from tests.utilities.factory_utils import factory_language_model -def test_create_language(session): - """Assert that a language can be created.""" - language_data = {'name': 'Spanish', 'code': 'es', 'right_to_left': False} - language = factory_language_model(language_data) - session.add(language) - session.commit() - - assert language.id is not None - assert language.name == 'Spanish' - - def test_get_language_by_id(session): """Assert that a language can be fetched by its ID.""" language = factory_language_model( @@ -44,17 +33,3 @@ def test_update_language(session): updated_language = Language.query.get(language.id) assert updated_language.name == 'Deutsch' - - -def test_delete_language(session): - """Assert that a language can be deleted.""" - language = factory_language_model( - {'name': 'Italian', 'code': 'it', 'right_to_left': False} - ) - session.add(language) - session.commit() - - Language.delete_language(language.id) - deleted_language = Language.query.get(language.id) - - assert deleted_language is None diff --git a/met-api/tests/unit/services/test_language_service.py b/met-api/tests/unit/services/test_language_service.py index b84004b5d..c2b5216e5 100644 --- a/met-api/tests/unit/services/test_language_service.py +++ b/met-api/tests/unit/services/test_language_service.py @@ -34,14 +34,6 @@ def test_get_languages(session): assert len(languages) >= 2 -def test_create_language(session): - """Assert that a language can be created.""" - language_data = {'name': 'Italian', 'code': 'it', 'right_to_left': False} - created_language = LanguageService.create_language(language_data) - - assert created_language.name == 'Italian' - - def test_update_language(session): """Assert that a language can be updated.""" language = factory_language_model( @@ -56,21 +48,3 @@ def test_update_language(session): ) assert updated_language.name == 'Nihongo' - - -def test_delete_language(session): - """Assert that a language can be deleted.""" - language = factory_language_model( - {'name': 'Russian', 'code': 'ru', 'right_to_left': False} - ) - session.add(language) - session.commit() - - LanguageService.delete_language(language.id) - deleted_language = Language.query.get(language.id) - - assert deleted_language is None - - # Testing for invalid id - is_deleted = LanguageService.delete_language(99999) - assert is_deleted is False