-
Notifications
You must be signed in to change notification settings - Fork 11
Multilingual db update #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9908c72
Add language table and routes as well as tests
lickem22 251fe8c
Add english as default language in migration
lickem22 a66b853
Fix typos
lickem22 c68c35f
Merge branch 'feature-mui-admin' into multilingual/db
lickem22 eb85122
Update core_backend/migrations/versions/1ff08438751b_add_language_tab…
lickem22 a395fa9
Update core_backend/app/languages/models.py
lickem22 ab7f3cf
Update core_backend/app/languages/schemas.py
lickem22 a6c00dc
Fix comments
lickem22 7c7e7ab
Fix typos
lickem22 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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,3 @@ | ||
from .routers import router | ||
|
||
__all__ = ["router"] |
This file contains hidden or 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,153 @@ | ||
from datetime import datetime | ||
from datetime import timezone as tz | ||
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"<Language #{self.language_id}: {self.language_name}>" | ||
|
||
|
||
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.now(tz.utc).replace(tzinfo=None), | ||
updated_datetime_utc=datetime.now(tz.utc).replace(tzinfo=None), | ||
) | ||
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.now(tz.utc).replace(tzinfo=None), | ||
) | ||
|
||
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 |
This file contains hidden or 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,190 @@ | ||
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() | ||
lickem22 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
lickem22 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
lickem22 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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" | ||
) | ||
if old_language.is_default and not language.is_default: | ||
raise HTTPException( | ||
status_code=400, | ||
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() | ||
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 and not old_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: | ||
""" | ||
Retrieve 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, | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.