From 9908c7248ed57e851d97bea784e416d19f2c1bbc Mon Sep 17 00:00:00 2001 From: Carlos Samey Date: Mon, 18 Mar 2024 18:50:58 +0300 Subject: [PATCH 1/8] Add language table and routes as well as tests --- core_backend/Makefile | 4 +- core_backend/app/__init__.py | 3 +- core_backend/app/languages/__init__.py | 3 + core_backend/app/languages/models.py | 153 +++++++++++++++ core_backend/app/languages/routers.py | 182 ++++++++++++++++++ core_backend/app/languages/schemas.py | 37 ++++ .../1ff08438751b_add_language_table.py | 51 +++++ core_backend/tests/api/conftest.py | 30 +++ .../tests/api/test_manage_languages.py | 143 ++++++++++++++ 9 files changed, 603 insertions(+), 3 deletions(-) create mode 100644 core_backend/app/languages/__init__.py create mode 100644 core_backend/app/languages/models.py create mode 100644 core_backend/app/languages/routers.py create mode 100644 core_backend/app/languages/schemas.py create mode 100644 core_backend/migrations/versions/1ff08438751b_add_language_table.py create mode 100644 core_backend/tests/api/test_manage_languages.py diff --git a/core_backend/Makefile b/core_backend/Makefile index 40f9e8cf3..1643fec1a 100644 --- a/core_backend/Makefile +++ b/core_backend/Makefile @@ -35,9 +35,9 @@ run-tests: python -m pytest -rPQ -m "not rails" # Test DBs -setup-test-containers: setup-test-db setup-alignscore-container +setup-test-containers: setup-test-db #setup-alignscore-container -teardown-test-containers: stop-test-db stop-alignscore-container +teardown-test-containers: stop-test-db #stop-alignscore-container setup-test-db: -@docker stop testdb diff --git a/core_backend/app/__init__.py b/core_backend/app/__init__.py index 0d43129d8..cd67b9385 100644 --- a/core_backend/app/__init__.py +++ b/core_backend/app/__init__.py @@ -8,7 +8,7 @@ multiprocess, ) -from . import admin, auth, contents, question_answer, whatsapp_qa +from . import admin, auth, contents, languages, question_answer, whatsapp_qa from .config import ( DOMAIN, ) @@ -33,6 +33,7 @@ def create_app() -> FastAPI: app.include_router(contents.router) app.include_router(auth.router) app.include_router(whatsapp_qa.router) + app.include_router(languages.router) origins = [ f"http://{DOMAIN}", diff --git a/core_backend/app/languages/__init__.py b/core_backend/app/languages/__init__.py new file mode 100644 index 000000000..bfd53e012 --- /dev/null +++ b/core_backend/app/languages/__init__.py @@ -0,0 +1,3 @@ +from .routers import router + +__all__ = ["router"] diff --git a/core_backend/app/languages/models.py b/core_backend/app/languages/models.py new file mode 100644 index 000000000..bbdc31cdf --- /dev/null +++ b/core_backend/app/languages/models.py @@ -0,0 +1,153 @@ +from datetime import datetime +from typing import List, Optional + +from sqlalchemy import ( + Boolean, + DateTime, + Integer, + String, + delete, + select, +) +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.schema import Index + +from ..models import Base +from .schemas import LanguageBase + + +class LanguageDB(Base): + """ + SQL Alchemy data model for language + """ + + __tablename__ = "languages" + + language_id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False) + + language_name: Mapped[str] = mapped_column(String, nullable=False) + is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + created_datetime_utc: Mapped[datetime] = mapped_column(DateTime, nullable=False) + updated_datetime_utc: Mapped[datetime] = mapped_column(DateTime, nullable=False) + + __table_args__ = ( + Index( + "ix_languages_is_default_true", + "is_default", + unique=True, + postgresql_where=(is_default.is_(True)), + ), + ) + + def __repr__(self) -> str: + """Pretty Print""" + return f"" + + +async def save_language_to_db( + language: LanguageBase, + asession: AsyncSession, +) -> LanguageDB: + """ + Saves a new language in the database + """ + + language_db = LanguageDB( + language_name=language.language_name, + is_default=language.is_default, + created_datetime_utc=datetime.utcnow(), + updated_datetime_utc=datetime.utcnow(), + ) + asession.add(language_db) + + await asession.commit() + await asession.refresh(language_db) + + return language_db + + +async def update_language_in_db( + language_id: int, + language: LanguageBase, + asession: AsyncSession, +) -> LanguageDB: + """ + Updates a language in the database + """ + + language_db = LanguageDB( + language_id=language_id, + is_default=language.is_default, + language_name=language.language_name, + updated_datetime_utc=datetime.utcnow(), + ) + + language_db = await asession.merge(language_db) + await asession.commit() + return language_db + + +async def delete_language_from_db( + language_id: int, + asession: AsyncSession, +) -> None: + """ + Deletes a content from the database + """ + stmt = delete(LanguageDB).where(LanguageDB.language_id == language_id) + await asession.execute(stmt) + await asession.commit() + + +async def get_language_from_db( + language_id: int, + asession: AsyncSession, +) -> Optional[LanguageDB]: + """ + Retrieves a content from the database + """ + stmt = select(LanguageDB).where(LanguageDB.language_id == language_id) + language_row = (await asession.execute(stmt)).scalar_one_or_none() + return language_row + + +async def get_default_language_from_db( + asession: AsyncSession, +) -> Optional[LanguageDB]: + """ + Retrieves a content from the database + """ + truth_bool = True + stmt = select(LanguageDB).where(LanguageDB.is_default == truth_bool) + language_row = (await asession.execute(stmt)).scalar_one_or_none() + return language_row + + +async def get_list_of_languages_from_db( + asession: AsyncSession, offset: int = 0, limit: Optional[int] = None +) -> List[LanguageDB]: + """ + Retrieves all content from the database + """ + stmt = select(LanguageDB) + if offset > 0: + stmt = stmt.offset(offset) + if limit is not None: + stmt = stmt.limit(limit) + + language_rows = (await asession.execute(stmt)).all() + + return [c[0] for c in language_rows] if language_rows else [] + + +async def is_language_name_unique(language_name: str, asession: AsyncSession) -> bool: + """ + Check if the language name is unique + """ + stmt = select(LanguageDB).where(LanguageDB.language_name == language_name) + language_row = (await asession.execute(stmt)).scalar_one_or_none() + if language_row: + return False + else: + return True diff --git a/core_backend/app/languages/routers.py b/core_backend/app/languages/routers.py new file mode 100644 index 000000000..92657b63b --- /dev/null +++ b/core_backend/app/languages/routers.py @@ -0,0 +1,182 @@ +from typing import Annotated, List + +from fastapi import APIRouter, Depends +from fastapi.exceptions import HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from ..auth.dependencies import get_current_fullaccess_user, get_current_readonly_user +from ..auth.schemas import AuthenticatedUser +from ..database import get_async_session +from ..utils import setup_logger +from .models import ( + LanguageDB, + delete_language_from_db, + get_default_language_from_db, + get_language_from_db, + get_list_of_languages_from_db, + is_language_name_unique, + save_language_to_db, + update_language_in_db, +) +from .schemas import LanguageBase, LanguageRetrieve + +router = APIRouter(prefix="/languages") +logger = setup_logger() + + +@router.post("/", response_model=LanguageRetrieve) +async def create_language( + language: LanguageBase, + full_access_user: Annotated[ + AuthenticatedUser, Depends(get_current_fullaccess_user) + ], + asession: AsyncSession = Depends(get_async_session), +) -> LanguageRetrieve | None: + """ + Create language endpoint. Upsert language to PG database + """ + language.language_name = language.language_name.upper() + if not (await is_language_name_unique(language.language_name, asession)): + raise HTTPException(status_code=400, detail="Language name already exists") + + if language.is_default is True: + await unset_default_language(asession) + + language_db = await save_language_to_db(language, asession) + + return _convert_language_record_to_schema(language_db) + + +@router.put("/{language_id}", response_model=LanguageRetrieve) +async def edit_language( + language_id: int, + language: LanguageBase, + full_access_user: Annotated[ + AuthenticatedUser, Depends(get_current_fullaccess_user) + ], + asession: AsyncSession = Depends(get_async_session), +) -> LanguageRetrieve | None: + """ + Edit language endpoint. + """ + old_language = await get_language_from_db( + language_id, + asession, + ) + + if not old_language: + raise HTTPException( + status_code=404, detail=f"Language id `{language_id}` not found" + ) + language.language_name = language.language_name.upper() + if (old_language.language_name != language.language_name) and ( + not (await is_language_name_unique(language.language_name, asession)) + ): + raise HTTPException(status_code=400, detail="Language name already exists") + + if language.is_default: + await unset_default_language(asession) + + updated_language = await update_language_in_db( + language_id, + language, + asession, + ) + + return _convert_language_record_to_schema(updated_language) + + +@router.get("/", response_model=list[LanguageRetrieve]) +async def retrieve_languages( + readonly_user: Annotated[AuthenticatedUser, Depends(get_current_readonly_user)], + asession: AsyncSession = Depends(get_async_session), +) -> List[LanguageRetrieve]: + """ + Retrieve languages endpoint + """ + languages = await get_list_of_languages_from_db(asession) + return [_convert_language_record_to_schema(language) for language in languages] + + +@router.get("/default", response_model=LanguageRetrieve) +async def retrieve_default_language( + readonly_user: Annotated[AuthenticatedUser, Depends(get_current_readonly_user)], + asession: AsyncSession = Depends(get_async_session), +) -> LanguageRetrieve: + """ + Retrieve default language endpoint + """ + language = await get_default_language_from_db(asession) + if not language: + raise HTTPException(status_code=404, detail="Default language not found") + return _convert_language_record_to_schema(language) + + +@router.get("/{language_id}", response_model=LanguageRetrieve) +async def retrieve_language_by_id( + language_id: int, + readonly_user: Annotated[AuthenticatedUser, Depends(get_current_readonly_user)], + asession: AsyncSession = Depends(get_async_session), +) -> LanguageRetrieve: + """ + Rretrieve language by id endpoint + """ + language = await get_language_from_db(language_id, asession) + if not language: + raise HTTPException( + status_code=404, detail=f"Language id `{language_id}` not found" + ) + return _convert_language_record_to_schema(language) + + +@router.delete("/{language_id}") +async def delete_language( + language_id: int, + full_access_user: Annotated[ + AuthenticatedUser, Depends(get_current_fullaccess_user) + ], + asession: AsyncSession = Depends(get_async_session), +) -> None: + """ + Delete language endpoint + """ + language = await get_language_from_db( + language_id, + asession, + ) + + if not language: + raise HTTPException( + status_code=404, detail=f"Language id `{language_id}` not found" + ) + + if language.is_default: + raise HTTPException( + status_code=400, detail="Default language cannot be deleted" + ) + await delete_language_from_db(language_id, asession) + + +async def unset_default_language(asession: AsyncSession) -> None: + """ + Unset default language + """ + default_language = await get_default_language_from_db(asession) + + if default_language: + default_language.is_default = False + default_language_schema = _convert_language_record_to_schema(default_language) + await update_language_in_db( + default_language.language_id, default_language_schema, asession + ) + return None + + +def _convert_language_record_to_schema(language_db: LanguageDB) -> LanguageRetrieve: + return LanguageRetrieve( + language_id=language_db.language_id, + language_name=language_db.language_name, + is_default=language_db.is_default, + created_datetime_utc=language_db.created_datetime_utc, + updated_datetime_utc=language_db.updated_datetime_utc, + ) diff --git a/core_backend/app/languages/schemas.py b/core_backend/app/languages/schemas.py new file mode 100644 index 000000000..7018f7ef3 --- /dev/null +++ b/core_backend/app/languages/schemas.py @@ -0,0 +1,37 @@ +import re +from datetime import datetime + +from pydantic import ( + BaseModel, + ConfigDict, + validator, +) + + +class LanguageBase(BaseModel): + """ + Pydantic model for language + """ + + language_name: str + is_default: bool + model_config = ConfigDict(from_attributes=True) + + @validator("language_name") + def language_name_must_be_valid(cls, value: str) -> str: + # Regex pattern allows letters, spaces, hyphens, and apostrophes + if not re.match(r"^[A-Za-zÀ-ÖØ-öø-ÿ\-' ]+$", value): + raise ValueError("Invalid characters in language name.") + return value + + +class LanguageRetrieve(LanguageBase): + """ + Pydantic model for language retrieval + """ + + language_id: int + created_datetime_utc: datetime + updated_datetime_utc: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/core_backend/migrations/versions/1ff08438751b_add_language_table.py b/core_backend/migrations/versions/1ff08438751b_add_language_table.py new file mode 100644 index 000000000..8c973d56c --- /dev/null +++ b/core_backend/migrations/versions/1ff08438751b_add_language_table.py @@ -0,0 +1,51 @@ +"""add language table + +Revision ID: 1ff08438751b +Revises: f269c75dbf69 +Create Date: 2024-03-18 18:38:29.579259 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "1ff08438751b" +down_revision: Union[str, None] = "f269c75dbf69" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "languages", + sa.Column("language_id", sa.Integer(), nullable=False), + sa.Column("language_name", sa.String(), nullable=False), + sa.Column("is_default", sa.Boolean(), nullable=False), + sa.Column("created_datetime_utc", sa.DateTime(), nullable=False), + sa.Column("updated_datetime_utc", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("language_id"), + ) + op.create_index( + "ix_languages_is_default_true", + "languages", + ["is_default"], + unique=True, + postgresql_where=sa.text("is_default IS true"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index( + "ix_languages_is_default_true", + table_name="languages", + postgresql_where=sa.text("is_default IS true"), + ) + op.drop_table("languages") + # ### end Alembic commands ### diff --git a/core_backend/tests/api/conftest.py b/core_backend/tests/api/conftest.py index 66eb9d9f8..0be5ba65a 100644 --- a/core_backend/tests/api/conftest.py +++ b/core_backend/tests/api/conftest.py @@ -73,6 +73,36 @@ def faq_contents(client: TestClient, db_session: pytest.FixtureRequest) -> None: db_session.commit() +@pytest.fixture(scope="module") +def existing_language_id( + client: TestClient, + fullaccess_token: str, +) -> Generator[tuple[int, int], None, None]: + response_1 = client.post( + "/languages", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + json={"language_name": "ENGLISH", "is_default": True}, + ) + + response_2 = client.post( + "/languages", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + json={"language_name": "HINDI", "is_default": False}, + ) + + language_id_1 = response_1.json()["language_id"] + language_id_2 = response_2.json()["language_id"] + yield (language_id_1, language_id_2) + client.delete( + f"/languages/{language_id_1}/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + ) + client.delete( + f"/languages/{language_id_2}/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + ) + + @pytest.fixture(scope="session") def client(patch_llm_call: pytest.FixtureRequest) -> Generator[TestClient, None, None]: app = create_app() diff --git a/core_backend/tests/api/test_manage_languages.py b/core_backend/tests/api/test_manage_languages.py new file mode 100644 index 000000000..e455a0fb1 --- /dev/null +++ b/core_backend/tests/api/test_manage_languages.py @@ -0,0 +1,143 @@ +import pytest +from fastapi.testclient import TestClient + + +class TestManageLanguage: + def test_create_and_delete_language( + self, + client: TestClient, + fullaccess_token: str, + ) -> None: + language_name = "test-language" + response = client.post( + "/languages/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + json={"language_name": language_name, "is_default": False}, + ) + assert response.status_code == 200 + json_response = response.json() + assert json_response["language_name"] == language_name.upper() + response = client.delete( + f"/languages/{json_response['language_id']}/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + ) + assert response.status_code == 200 + + def test_edit_language( + self, + client: TestClient, + existing_language_id: tuple[int, int], + fullaccess_token: str, + readonly_token: str, + ) -> None: + new_language = "XHOSA" + + response = client.put( + f"/languages/{existing_language_id[0]}/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + json={"language_name": new_language, "is_default": True}, + ) + assert response.status_code == 200 + + response = client.get( + f"/languages/{existing_language_id[0]}", + headers={"Authorization": f"Bearer {readonly_token}"}, + ) + + assert response.status_code == 200 + assert response.json()["language_name"] == new_language + + @pytest.mark.parametrize( + "language_name, expected_status", + [ + ("test-language-new", 200), + ("test_language", 422), + ("TESTLANGUAGE", 200), + ("#language", 422), + ], + ) + def test_language_name_validation( + self, + client: TestClient, + fullaccess_token: str, + language_name: str, + expected_status: int, + ) -> None: + response = client.post( + "/languages/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + json={"language_name": language_name, "is_default": False}, + ) + assert response.status_code == expected_status + + def test_language_name_unique( + self, + client: TestClient, + existing_language_id: tuple[int, int], + fullaccess_token: str, + ) -> None: + language_name = "HINDI" + response = client.post( + "/languages/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + json={"language_name": language_name, "is_default": False}, + ) + assert response.status_code == 400 + + def test_delete_default_language( + self, + existing_language_id: tuple, + client: TestClient, + fullaccess_token: str, + readonly_token: str, + ) -> None: + response = client.get( + "/languages/default", + headers={"Authorization": f"Bearer {readonly_token}"}, + ) + assert response.status_code == 200 + default_id = response.json()["language_id"] + response = client.delete( + f"/languages/{default_id}/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + ) + assert response.status_code == 400 + + @pytest.mark.parametrize( + "language_name", + [ + ("FIRST-DEFAULT"), + ("SECOND-DEFAULT"), + ], + ) + def test_always_one_default_language( + self, + client: TestClient, + existing_language_id: tuple, + language_name: str, + fullaccess_token: str, + readonly_token: str, + ) -> None: + response = client.post( + "/languages/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + json={"language_name": language_name, "is_default": True}, + ) + new_default_id = response.json()["language_id"] + assert response.status_code == 200 + assert response.json()["is_default"] is True + + response = client.get( + "/languages/default", + headers={"Authorization": f"Bearer {readonly_token}"}, + ) + assert response.status_code == 200 + assert response.json()["language_id"] == new_default_id + + response = client.put( + f"/languages/{existing_language_id[0]}/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + json={"language_name": "ENGLISH", "is_default": True}, + ) + assert response.status_code == 200 + assert response.json()["is_default"] is True From 251fe8cdd3fb259dcc701e160bc5a872ef7f50ee Mon Sep 17 00:00:00 2001 From: Carlos Samey Date: Tue, 19 Mar 2024 16:52:16 +0300 Subject: [PATCH 2/8] Add english as default language in migration --- core_backend/Makefile | 4 ++-- .../versions/1ff08438751b_add_language_table.py | 8 ++++++++ core_backend/tests/api/conftest.py | 2 +- core_backend/tests/api/test_manage_languages.py | 4 ++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core_backend/Makefile b/core_backend/Makefile index 1643fec1a..40f9e8cf3 100644 --- a/core_backend/Makefile +++ b/core_backend/Makefile @@ -35,9 +35,9 @@ run-tests: python -m pytest -rPQ -m "not rails" # Test DBs -setup-test-containers: setup-test-db #setup-alignscore-container +setup-test-containers: setup-test-db setup-alignscore-container -teardown-test-containers: stop-test-db #stop-alignscore-container +teardown-test-containers: stop-test-db stop-alignscore-container setup-test-db: -@docker stop testdb diff --git a/core_backend/migrations/versions/1ff08438751b_add_language_table.py b/core_backend/migrations/versions/1ff08438751b_add_language_table.py index 8c973d56c..9bd8ced30 100644 --- a/core_backend/migrations/versions/1ff08438751b_add_language_table.py +++ b/core_backend/migrations/versions/1ff08438751b_add_language_table.py @@ -6,6 +6,7 @@ """ +from datetime import datetime from typing import Sequence, Union from alembic import op @@ -37,6 +38,13 @@ def upgrade() -> None: unique=True, postgresql_where=sa.text("is_default IS true"), ) + # add default language + date = str(datetime.now()) + op.execute( + f"""INSERT INTO languages + (language_name, is_default, created_datetime_utc, updated_datetime_utc) + VALUES ('ENGLISH', true, '{date}', '{date}')""" + ) # ### end Alembic commands ### diff --git a/core_backend/tests/api/conftest.py b/core_backend/tests/api/conftest.py index 0be5ba65a..c0f892364 100644 --- a/core_backend/tests/api/conftest.py +++ b/core_backend/tests/api/conftest.py @@ -81,7 +81,7 @@ def existing_language_id( response_1 = client.post( "/languages", headers={"Authorization": f"Bearer {fullaccess_token}"}, - json={"language_name": "ENGLISH", "is_default": True}, + json={"language_name": "XHOSA", "is_default": True}, ) response_2 = client.post( diff --git a/core_backend/tests/api/test_manage_languages.py b/core_backend/tests/api/test_manage_languages.py index e455a0fb1..606c087c9 100644 --- a/core_backend/tests/api/test_manage_languages.py +++ b/core_backend/tests/api/test_manage_languages.py @@ -30,7 +30,7 @@ def test_edit_language( fullaccess_token: str, readonly_token: str, ) -> None: - new_language = "XHOSA" + new_language = "ZULU" response = client.put( f"/languages/{existing_language_id[0]}/", @@ -137,7 +137,7 @@ def test_always_one_default_language( response = client.put( f"/languages/{existing_language_id[0]}/", headers={"Authorization": f"Bearer {fullaccess_token}"}, - json={"language_name": "ENGLISH", "is_default": True}, + json={"language_name": "XHOSA", "is_default": True}, ) assert response.status_code == 200 assert response.json()["is_default"] is True From a66b8531b1932c44d40e81d97b6ee0c12bf5aa54 Mon Sep 17 00:00:00 2001 From: Carlos Samey Date: Tue, 19 Mar 2024 18:27:24 +0300 Subject: [PATCH 3/8] Fix typos --- core_backend/app/languages/routers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core_backend/app/languages/routers.py b/core_backend/app/languages/routers.py index 92657b63b..89892fc89 100644 --- a/core_backend/app/languages/routers.py +++ b/core_backend/app/languages/routers.py @@ -119,7 +119,7 @@ async def retrieve_language_by_id( asession: AsyncSession = Depends(get_async_session), ) -> LanguageRetrieve: """ - Rretrieve language by id endpoint + Retrieve language by id endpoint """ language = await get_language_from_db(language_id, asession) if not language: From eb85122d5fa1c44bd45aae98f76085479503d96a Mon Sep 17 00:00:00 2001 From: lickem22 <44327443+lickem22@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:24:52 +0300 Subject: [PATCH 4/8] Update core_backend/migrations/versions/1ff08438751b_add_language_table.py Co-authored-by: Amir Emami <41763233+amiraliemami@users.noreply.github.com> --- .../migrations/versions/1ff08438751b_add_language_table.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core_backend/migrations/versions/1ff08438751b_add_language_table.py b/core_backend/migrations/versions/1ff08438751b_add_language_table.py index 9bd8ced30..b24c984be 100644 --- a/core_backend/migrations/versions/1ff08438751b_add_language_table.py +++ b/core_backend/migrations/versions/1ff08438751b_add_language_table.py @@ -9,9 +9,8 @@ from datetime import datetime from typing import Sequence, Union -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. revision: str = "1ff08438751b" From a395fa9d0be17798ad9ef80c2562d03556ee20ca Mon Sep 17 00:00:00 2001 From: lickem22 <44327443+lickem22@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:25:03 +0300 Subject: [PATCH 5/8] Update core_backend/app/languages/models.py Co-authored-by: Amir Emami <41763233+amiraliemami@users.noreply.github.com> --- core_backend/app/languages/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core_backend/app/languages/models.py b/core_backend/app/languages/models.py index bbdc31cdf..cc355216e 100644 --- a/core_backend/app/languages/models.py +++ b/core_backend/app/languages/models.py @@ -25,7 +25,6 @@ class LanguageDB(Base): __tablename__ = "languages" language_id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False) - language_name: Mapped[str] = mapped_column(String, nullable=False) is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) created_datetime_utc: Mapped[datetime] = mapped_column(DateTime, nullable=False) From ab7f3cfcbe258c9befd5e245d38baf60791e97db Mon Sep 17 00:00:00 2001 From: lickem22 <44327443+lickem22@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:31:59 +0300 Subject: [PATCH 6/8] Update core_backend/app/languages/schemas.py Co-authored-by: Amir Emami <41763233+amiraliemami@users.noreply.github.com> --- core_backend/app/languages/schemas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core_backend/app/languages/schemas.py b/core_backend/app/languages/schemas.py index 7018f7ef3..ef5e85431 100644 --- a/core_backend/app/languages/schemas.py +++ b/core_backend/app/languages/schemas.py @@ -33,5 +33,4 @@ class LanguageRetrieve(LanguageBase): language_id: int created_datetime_utc: datetime updated_datetime_utc: datetime - model_config = ConfigDict(from_attributes=True) From a6c00dc28dda74aceedd2b8c8d2939ad924d392c Mon Sep 17 00:00:00 2001 From: Carlos Samey Date: Thu, 21 Mar 2024 16:48:20 +0300 Subject: [PATCH 7/8] Fix comments --- core_backend/Makefile | 4 ++-- core_backend/app/languages/models.py | 8 ++++---- core_backend/app/languages/routers.py | 9 ++++++++- core_backend/tests/api/test_manage_languages.py | 13 +++++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/core_backend/Makefile b/core_backend/Makefile index 40f9e8cf3..1643fec1a 100644 --- a/core_backend/Makefile +++ b/core_backend/Makefile @@ -35,9 +35,9 @@ run-tests: python -m pytest -rPQ -m "not rails" # Test DBs -setup-test-containers: setup-test-db setup-alignscore-container +setup-test-containers: setup-test-db #setup-alignscore-container -teardown-test-containers: stop-test-db stop-alignscore-container +teardown-test-containers: stop-test-db #stop-alignscore-container setup-test-db: -@docker stop testdb diff --git a/core_backend/app/languages/models.py b/core_backend/app/languages/models.py index cc355216e..10b02e091 100644 --- a/core_backend/app/languages/models.py +++ b/core_backend/app/languages/models.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone as tz from typing import List, Optional from sqlalchemy import ( @@ -55,8 +55,8 @@ async def save_language_to_db( language_db = LanguageDB( language_name=language.language_name, is_default=language.is_default, - created_datetime_utc=datetime.utcnow(), - updated_datetime_utc=datetime.utcnow(), + created_datetime_utc=datetime.now(tz.utc).replace(tzinfo=None), + updated_datetime_utc=datetime.now(tz.utc).replace(tzinfo=None), ) asession.add(language_db) @@ -79,7 +79,7 @@ async def update_language_in_db( language_id=language_id, is_default=language.is_default, language_name=language.language_name, - updated_datetime_utc=datetime.utcnow(), + updated_datetime_utc=datetime.now(tz.utc).replace(tzinfo=None), ) language_db = await asession.merge(language_db) diff --git a/core_backend/app/languages/routers.py b/core_backend/app/languages/routers.py index 89892fc89..9b3c48071 100644 --- a/core_backend/app/languages/routers.py +++ b/core_backend/app/languages/routers.py @@ -68,13 +68,20 @@ async def edit_language( raise HTTPException( status_code=404, detail=f"Language id `{language_id}` not found" ) + if old_language.is_default and not language.is_default: + raise HTTPException( + status_code=400, + detail=f"Default language cannot be unset." + f"Please either create a default language or set an existing language as default.", + ) + language.language_name = language.language_name.upper() if (old_language.language_name != language.language_name) and ( not (await is_language_name_unique(language.language_name, asession)) ): raise HTTPException(status_code=400, detail="Language name already exists") - if language.is_default: + if language.is_default and not old_language.is_default: await unset_default_language(asession) updated_language = await update_language_in_db( diff --git a/core_backend/tests/api/test_manage_languages.py b/core_backend/tests/api/test_manage_languages.py index 606c087c9..f4ff73ca1 100644 --- a/core_backend/tests/api/test_manage_languages.py +++ b/core_backend/tests/api/test_manage_languages.py @@ -141,3 +141,16 @@ def test_always_one_default_language( ) assert response.status_code == 200 assert response.json()["is_default"] is True + + def test_default_language_cannot_be_unset( + self, + client: TestClient, + existing_language_id: tuple, + fullaccess_token: str, + ) -> None: + response = client.put( + f"/languages/{existing_language_id[0]}/", + headers={"Authorization": f"Bearer {fullaccess_token}"}, + json={"language_name": "XHOSA", "is_default": False}, + ) + assert response.status_code == 400 From 7c7e7ab73cad57e3b52bda10ed682368c9ae1d58 Mon Sep 17 00:00:00 2001 From: Carlos Samey Date: Fri, 22 Mar 2024 10:49:18 +0300 Subject: [PATCH 8/8] Fix typos --- core_backend/app/languages/models.py | 3 ++- core_backend/app/languages/routers.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core_backend/app/languages/models.py b/core_backend/app/languages/models.py index 10b02e091..312f382f3 100644 --- a/core_backend/app/languages/models.py +++ b/core_backend/app/languages/models.py @@ -1,4 +1,5 @@ -from datetime import datetime, timezone as tz +from datetime import datetime +from datetime import timezone as tz from typing import List, Optional from sqlalchemy import ( diff --git a/core_backend/app/languages/routers.py b/core_backend/app/languages/routers.py index 9b3c48071..132f5c515 100644 --- a/core_backend/app/languages/routers.py +++ b/core_backend/app/languages/routers.py @@ -71,8 +71,9 @@ async def edit_language( if old_language.is_default and not language.is_default: raise HTTPException( status_code=400, - detail=f"Default language cannot be unset." - f"Please either create a default language or set an existing language as default.", + detail="Default language cannot be unset." + "Please either create a default language" + "or set an existing language as default.", ) language.language_name = language.language_name.upper()