Update: Sheet 8 & 11 compliance + changing api router due to error unauthorized when access some pages

oh_security
TAMDeveloper13 3 months ago
parent 2a57ac8bec
commit 2615ff9a6e

@ -174,7 +174,7 @@ calculation_router.include_router(
authenticated_api_router.include_router(calculation_router)
api_router.include_router(
authenticated_api_router.include_router(
get_calculation,
prefix="/calculation/time-constraint",
tags=["calculation", "time_constraint"],

@ -17,6 +17,7 @@ from src.overhaul_scope.model import MaintenanceType
from .model import EquipmentWorkscopeGroup
from .schema import ScopeEquipmentJobCreate
from src.soft_delete import apply_not_deleted_filter
# async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]:
# """Returns a document based on the given document id."""
@ -58,6 +59,7 @@ async def get_all(*, common, location_tag: str, scope: Optional[str] = None):
Select(EquipmentWorkscopeGroup)
.where(EquipmentWorkscopeGroup.location_tag == location_tag).filter(EquipmentWorkscopeGroup.workscope_group_id.is_not(None))
)
query = apply_not_deleted_filter(query, EquipmentWorkscopeGroup)
if scope:
query = (

@ -1,4 +1,5 @@
import logging
import asyncio
import time
from src.context import set_request_id, reset_request_id, get_request_id
from contextvars import ContextVar
@ -29,7 +30,7 @@ from src.database.core import async_session, engine, async_collector_session
from src.enums import ResponseStatus
from src.exceptions import handle_exception
from src.logging import configure_logging
from src.middleware import RequestValidationMiddleware
from src.middleware import RequestValidationMiddleware, RequestTimeoutMiddleware
from src.csrf_protect import CSRFProtectMiddleware
from src.rate_limiter import limiter
from fastapi.exceptions import RequestValidationError
@ -40,11 +41,13 @@ log = logging.getLogger(__name__)
# we configure the logging level and format
configure_logging()
REQUEST_TIMEOUT_SECONDS = 20
# we create the ASGI for the app
app = FastAPI(
openapi_url="",
title="LCCA API",
description="Welcome to LCCA's API documentation!",
title="Optimum OH API",
description="Welcome to Optimum OH's API documentation!",
version="0.1.0",
)
@ -58,10 +61,16 @@ app.add_exception_handler(RateLimitExceeded, handle_exception)
app.state.limiter = limiter
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(SlowAPIMiddleware)
app.add_middleware(RequestTimeoutMiddleware, timeout=REQUEST_TIMEOUT_SECONDS)
# credentials: "include",
@app.route('/slow', methods=["GET"])
async def slow_endpoint(request: Request):
await asyncio.sleep(REQUEST_TIMEOUT_SECONDS + 10)
return JSONResponse(content={"message": "This should timeout"}, status_code=200)

@ -424,3 +424,67 @@ class RequestValidationMiddleware(BaseHTTPMiddleware):
request._receive = receive
return await call_next(request)
import threading
import asyncio
import json
import logging
REQUEST_TIMEOUT_SECONDS = 20
log = logging.getLogger("security_logger")
class RequestTimeoutMiddleware:
"""
ASGI middleware that cancels requests exceeding REQUEST_TIMEOUT_SECONDS.
Returns HTTP 408 Request Timeout with a JSON body when threshold is exceeded.
"""
def __init__(self, app, timeout: int = REQUEST_TIMEOUT_SECONDS):
self.app = app
self.timeout = timeout
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
response_started = False
async def send_wrapper(message):
nonlocal response_started
if message["type"] == "http.response.start":
response_started = True
await send(message)
try:
await asyncio.wait_for(
self.app(scope, receive, send_wrapper),
timeout=self.timeout,
)
except asyncio.TimeoutError:
if not response_started:
log.warning(
"Request timeout (%ds): %s %s",
self.timeout,
scope.get("method", ""),
scope.get("path", ""),
)
body = json.dumps({
"status": False,
"message": f"Request timeout. Processing exceeded {self.timeout} seconds.",
"data": None,
}).encode("utf-8")
await send({
"type": "http.response.start",
"status": 408,
"headers": [
[b"content-type", b"application/json; charset=utf-8"],
[b"content-length", str(len(body)).encode()],
],
})
await send({
"type": "http.response.body",
"body": body,
})

@ -49,7 +49,14 @@ class UUIDMixin:
)
class DefaultMixin(TimeStampMixin, UUIDMixin):
class SoftDeleteMixin:
"""Soft delete mixin — adds deleted_at column to mark records as deleted."""
deleted_at = Column(DateTime(timezone=True), nullable=True, default=None)
deleted_at._creation_order = 9999
class DefaultMixin(SoftDeleteMixin, TimeStampMixin, UUIDMixin):
"""Default mixin"""
pass

@ -3,6 +3,7 @@ import datetime
from typing import List, Optional
from uuid import UUID, uuid4
from fastapi import HTTPException, status
from sqlalchemy import Delete, Select, and_, func, select
from sqlalchemy import update as sqlUpdate
from sqlalchemy.dialects.postgresql import insert
@ -30,6 +31,7 @@ from .schema import (OverhaulActivityCreate, OverhaulActivityRead,
import json
from src.database.core import CollectorDbSession
from src.maximo.service import get_cm_cost_summary, get_oh_cost_summary
from src.soft_delete import apply_not_deleted_filter
async def get(
*, db_session: DbSession, assetnum: str, overhaul_session_id: Optional[UUID] = None
@ -40,6 +42,7 @@ async def get(
.where(OverhaulActivity.assetnum == assetnum)
.options(joinedload(OverhaulActivity.equipment))
)
query = apply_not_deleted_filter(query, OverhaulActivity)
if overhaul_session_id:
query = query.filter(OverhaulActivity.overhaul_scope_id == overhaul_session_id)
@ -105,6 +108,7 @@ async def get_all(
(StandardScope.oh_history.has(EquipmentOHHistory.last_oh_type != overhaul.maintenance_type.name))
).distinct()
)
query = apply_not_deleted_filter(query, StandardScope)
if location_tag:
query = query.filter(StandardScope.location_tag == location_tag)
@ -427,6 +431,7 @@ async def get_all_by_session_id(*, db_session: DbSession, overhaul_session_id):
.options(joinedload(OverhaulActivity.equipment).options(joinedload(MasterEquipment.parent).options(joinedload(MasterEquipment.parent))))
.options(selectinload(OverhaulActivity.overhaul_scope))
)
query = apply_not_deleted_filter(query, OverhaulActivity)
results = await db_session.execute(query)
@ -437,14 +442,34 @@ async def get_all_by_session_id(*, db_session: DbSession, overhaul_session_id):
async def update(
*,
db_session: DbSession,
activity: OverhaulActivity,
activity_id: str,
overhaul_session_id: Optional[UUID] = None,
overhaul_activity_in: OverhaulActivityUpdate
):
"""Updates a document."""
data = overhaul_activity_in.model_dump()
"""Fetches the record with a row-level lock (SELECT FOR UPDATE) then applies the update."""
query = (
Select(OverhaulActivity)
.where(OverhaulActivity.assetnum == activity_id)
.options(joinedload(OverhaulActivity.equipment))
.with_for_update()
)
query = apply_not_deleted_filter(query, OverhaulActivity)
update_data = overhaul_activity_in.model_dump(exclude_defaults=True)
if overhaul_session_id:
query = query.filter(OverhaulActivity.overhaul_scope_id == overhaul_session_id)
result = await db_session.execute(query)
activity = result.scalar()
if not activity:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="A data with this id does not exist.",
)
await db_session.refresh(activity)
update_data = overhaul_activity_in.model_dump(exclude_defaults=True)
update_model(activity, update_data)
await db_session.commit()

@ -13,8 +13,9 @@ from src.database.service import search_filter_sort_paginate
from src.workscope_group.model import MasterActivity
from src.workscope_group_maintenance_type.model import WorkscopeOHType
from src.overhaul_scope.model import MaintenanceType
from .model import EquipmentWorkscopeGroup
from src.equipment_workscope_group.model import EquipmentWorkscopeGroup
from .schema import OverhaulJobCreate
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
async def get_all(*, common, location_tag: str, scope: Optional[str] = None):
@ -23,6 +24,7 @@ async def get_all(*, common, location_tag: str, scope: Optional[str] = None):
Select(EquipmentWorkscopeGroup)
.where(EquipmentWorkscopeGroup.location_tag == location_tag)
)
query = apply_not_deleted_filter(query, EquipmentWorkscopeGroup)
if scope:
query = (
@ -84,18 +86,14 @@ async def delete(
AuthorizationError: If user lacks delete permission
"""
try:
# Check if job exists
scope_job = await db_session.get(OverhaulJob, overhaul_job_id)
if not scope_job:
# Soft-delete the record
deleted = await soft_delete_record(db_session, EquipmentWorkscopeGroup, overhaul_job_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="A data with this id does not exist.",
)
# Perform deletion
await db_session.delete(scope_job)
await db_session.commit()
return True
except Exception as e:

@ -10,6 +10,7 @@ from src.database.service import search_filter_sort_paginate
from src.utils import update_model
from src.scope_equipment_job.model import ScopeEquipmentJob
from src.overhaul_activity.model import OverhaulActivity
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
from .model import OverhaulSchedule
from .schema import OverhaulScheduleCreate, OverhaulScheduleUpdate
@ -18,6 +19,7 @@ from .schema import OverhaulScheduleCreate, OverhaulScheduleUpdate
async def get_all(*, common):
"""Returns all documents."""
query = Select(OverhaulSchedule).order_by(OverhaulSchedule.start.desc())
query = apply_not_deleted_filter(query, OverhaulSchedule)
results = await search_filter_sort_paginate(model=query, **common)
return results
@ -38,7 +40,11 @@ async def create(
async def update(*, db_session: DbSession, overhaul_schedule_id: str, overhaul_job_in: OverhaulScheduleUpdate):
"""Updates a document."""
data = overhaul_job_in.model_dump()
overhaul_schedule = await db_session.get(OverhaulSchedule, overhaul_schedule_id)
query = Select(OverhaulSchedule).where(OverhaulSchedule.id == overhaul_schedule_id)
query = apply_not_deleted_filter(query, OverhaulSchedule)
query = query.with_for_update()
result = await db_session.execute(query)
overhaul_schedule = result.scalars().one_or_none()
update_data = overhaul_job_in.model_dump(exclude_defaults=True)
@ -50,7 +56,5 @@ async def update(*, db_session: DbSession, overhaul_schedule_id: str, overhaul_j
async def delete(*, db_session: DbSession, overhaul_schedule_id: str):
"""Deletes a document."""
query = Delete(OverhaulSchedule).where(OverhaulSchedule.id == overhaul_schedule_id)
await db_session.execute(query)
await db_session.commit()
"""Soft-deletes a document."""
await soft_delete_record(db_session=db_session, model=OverhaulSchedule, record_id=overhaul_schedule_id)

@ -58,23 +58,15 @@ async def update_scope(
scope_in: ScopeUpdate,
current_user: CurrentUser,
):
scope = await get(db_session=db_session, overhaul_session_id=scope_id, for_update=True)
if not scope:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="A data with this id does not exist.",
)
return StandardResponse(
data=await update(db_session=db_session, scope=scope, scope_in=scope_in),
data=await update(db_session=db_session, scope_id=scope_id, scope_in=scope_in),
message="Data updated successfully",
)
@router.post("/delete/{scope_id}", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.DELETE, OverhaulScope))])
async def delete_scope(db_session: DbSession, scope_id: str):
scope = await get(db_session=db_session, overhaul_session_id=scope_id, for_update=True)
scope = await get(db_session=db_session, overhaul_session_id=scope_id)
if not scope:
raise HTTPException(

@ -18,20 +18,21 @@ from .schema import ScopeCreate, ScopeUpdate
from .utils import get_material_cost, get_service_cost
from datetime import datetime
from uuid import UUID
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
async def get(
*, db_session: DbSession, overhaul_session_id: UUID, for_update: bool = False
*, db_session: DbSession, overhaul_session_id: UUID
) -> Optional[OverhaulScope]:
"""Returns a document based on the given document id."""
query = Select(OverhaulScope).filter(OverhaulScope.id == overhaul_session_id).options(selectinload(OverhaulScope.maintenance_type))
if for_update:
query = query.with_for_update()
query = apply_not_deleted_filter(query, OverhaulScope)
result = await db_session.execute(query)
return result.scalars().one_or_none()
async def get_prev_oh(*, db_session: DbSession, overhaul_session: OverhaulScope) -> Optional[OverhaulScope]:
"""Returns a document based on the given document id."""
result = Select(OverhaulScope).where(OverhaulScope.end_date < overhaul_session.end_date).order_by(OverhaulScope.end_date.desc()).limit(1)
result = apply_not_deleted_filter(result, OverhaulScope)
result = await db_session.execute(result)
return result.scalars().one_or_none()
@ -39,6 +40,7 @@ async def get_prev_oh(*, db_session: DbSession, overhaul_session: OverhaulScope)
async def get_all(*, common, scope_name: Optional[str] = None):
"""Returns all documents."""
query = Select(OverhaulScope)
query = apply_not_deleted_filter(query, OverhaulScope)
if scope_name:
query = query.filter(OverhaulScope.maintenance_type.name == scope_name)
@ -136,12 +138,27 @@ async def create(*, db_session: DbSession, scope_in: ScopeCreate):
return overhaul_session
async def update(*, db_session: DbSession, scope: OverhaulScope, scope_in: ScopeUpdate):
"""Updates a document."""
data = scope_in.model_dump()
async def update(*, db_session: DbSession, scope_id: str, scope_in: ScopeUpdate):
"""Fetches the record with a row-level lock (SELECT FOR UPDATE) then applies the update."""
query = (
Select(OverhaulScope)
.filter(OverhaulScope.id == scope_id)
.options(selectinload(OverhaulScope.maintenance_type))
.with_for_update()
)
query = apply_not_deleted_filter(query, OverhaulScope)
result = await db_session.execute(query)
scope = result.scalars().one_or_none()
update_data = scope_in.model_dump(exclude_defaults=True)
if not scope:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="A data with this id does not exist.",
)
await db_session.refresh(scope)
update_data = scope_in.model_dump(exclude_defaults=True)
update_model(scope, update_data)
await db_session.commit()
@ -150,10 +167,8 @@ async def update(*, db_session: DbSession, scope: OverhaulScope, scope_in: Scope
async def delete(*, db_session: DbSession, scope_id: str):
"""Deletes a document."""
query = Delete(OverhaulScope).where(OverhaulScope.id == scope_id)
await db_session.execute(query)
await db_session.commit()
"""Soft-deletes a document."""
await soft_delete_record(db_session=db_session, model=OverhaulScope, record_id=scope_id)
async def get_overview_overhaul(*, db_session: DbSession):
@ -168,6 +183,7 @@ async def get_overview_overhaul(*, db_session: DbSession):
OverhaulScope.end_date >= current_date,
)
)
ongoing_query = apply_not_deleted_filter(ongoing_query, OverhaulScope)
ongoing_result = await db_session.execute(ongoing_query.options(selectinload(OverhaulScope.maintenance_type)))
ongoing_overhaul = ongoing_result.scalar_one_or_none()
@ -180,6 +196,7 @@ async def get_overview_overhaul(*, db_session: DbSession):
.order_by(OverhaulScope.start_date.asc())
.limit(1)
)
next_overhaul_query = apply_not_deleted_filter(next_overhaul_query, OverhaulScope)
next_result = await db_session.execute(next_overhaul_query.options(selectinload(OverhaulScope.maintenance_type)))
next_overhaul = next_result.scalar_one_or_none()
@ -208,6 +225,7 @@ async def get_overview_overhaul(*, db_session: DbSession):
(StandardScope.oh_history.has(EquipmentOHHistory.last_oh_type != selected_overhaul.maintenance_type.name))
).distinct()
)
equipments = apply_not_deleted_filter(equipments, StandardScope)
results = await db_session.execute(equipments)
@ -230,5 +248,6 @@ async def get_overview_overhaul(*, db_session: DbSession):
async def get_all_oh_with_history_service(*, db_session: DbSession):
query = Select(OverhaulScope).options(selectinload(OverhaulScope.maintenance_type)).where(OverhaulScope.wo_parent.isnot(None))
query = apply_not_deleted_filter(query, OverhaulScope)
results = await db_session.execute(query)
return results.scalars().all()

@ -0,0 +1,77 @@
"""
Soft delete utilities.
Provides reusable functions to convert hard-delete operations into soft-deletes.
The `soft_delete_record` function works with any model that inherits `SoftDeleteMixin`
(which is included in `DefaultMixin`).
Usage in service files:
from src.soft_delete import soft_delete_record, apply_not_deleted_filter
async def delete(*, db_session, record_id: str):
await soft_delete_record(db_session=db_session, model=MyModel, record_id=record_id)
async def get_all(*, db_session, ...):
query = Select(MyModel)
query = apply_not_deleted_filter(query, MyModel)
...
"""
import logging
from datetime import datetime
from typing import TypeVar, Type
from sqlalchemy import Select, Update
from sqlalchemy.ext.asyncio import AsyncSession
import pytz
from src.config import TIMEZONE
log = logging.getLogger(__name__)
T = TypeVar("T")
def apply_not_deleted_filter(query: Select, model) -> Select:
"""
Appends a WHERE clause to exclude soft-deleted records.
Use this on all "get" queries to hide deleted data.
Example:
query = Select(OverhaulScope)
query = apply_not_deleted_filter(query, OverhaulScope)
"""
if hasattr(model, "deleted_at"):
return query.filter(model.deleted_at.is_(None))
return query
async def soft_delete_record(
*,
db_session: AsyncSession,
model: Type[T],
record_id: str,
id_column: str = "id",
) -> None:
"""
Soft-delete a record by setting its `deleted_at` timestamp.
Instead of DELETE, this performs an UPDATE.
Args:
db_session: The async database session.
model: The SQLAlchemy model class.
record_id: The value of the ID to match.
id_column: The name of the ID column (default: "id").
"""
now = datetime.now(pytz.timezone(TIMEZONE))
id_col = getattr(model, id_column)
stmt = (
Update(model)
.where(id_col == record_id)
.values(deleted_at=now)
)
await db_session.execute(stmt)
await db_session.commit()
log.info(f"Soft-deleted {model.__tablename__} record: {record_id}")

@ -20,6 +20,7 @@ from src.overhaul_scope.service import get as get_scope, get_overview_overhaul
from src.overhaul_scope.service import get_prev_oh
from src.sparepart.model import SparepartRemark
from src.sparepart.schema import ProcurementRecord, ProcurementStatus, SparepartRequirement, SparepartStock
from src.soft_delete import apply_not_deleted_filter
log = logging.getLogger(__name__)
@ -380,7 +381,7 @@ async def get_spareparts_paginated(*, db_session, collector_db_session):
)
sparepart_remark = (await db_session.execute(
select(SparepartRemark)
apply_not_deleted_filter(select(SparepartRemark), SparepartRemark)
)).scalars().all()
sparepart_remark_dict = {item.itemnum: item.remark for item in sparepart_remark}
@ -946,7 +947,7 @@ async def load_sparepart_data_from_db(scope, prev_oh_scope, db_session, app_db_s
sparepart_stocks_query = await db_session.execute(query)
sparepart_remark = (await app_db_session.execute(
select(SparepartRemark)
apply_not_deleted_filter(select(SparepartRemark), SparepartRemark)
)).scalars().all()
sparepart_remark_dict = {item.itemnum: item.remark for item in sparepart_remark}
@ -1398,7 +1399,10 @@ ORDER BY po.item_num, po.pr_issue_date DESC;
async def create_remark(*, db_session, collector_db_session, remark_in):
# Step 1: Check if remark already exists for this itemnum
result = await db_session.execute(
select(SparepartRemark).where(SparepartRemark.itemnum == remark_in.itemnum)
apply_not_deleted_filter(
select(SparepartRemark).where(SparepartRemark.itemnum == remark_in.itemnum),
SparepartRemark
)
)
existing_remark = result.scalar_one_or_none()

@ -13,6 +13,7 @@ from src.database.service import CommonParameters, search_filter_sort_paginate
from src.overhaul_scope.model import OverhaulScope
from src.standard_scope.enum import ScopeEquipmentType
from src.standard_scope.model import EquipmentOHHistory
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
from src.workorder.model import MasterWorkOrder
from src.equipment_workscope_group.model import EquipmentWorkscopeGroup
from src.workscope_group.model import MasterActivity
@ -30,6 +31,7 @@ async def get_by_location_tag(*, db_session: DbSession, location_tag: str):
.filter(StandardScope.location_tag == location_tag)
.options(selectinload(StandardScope.master_equipment))
)
query = apply_not_deleted_filter(query, StandardScope)
result = await db_session.execute(query)
return result.unique().scalars().one_or_none()
@ -40,6 +42,7 @@ async def get_all(*, common, oh_scope: Optional[str] = None):
query = Select(StandardScope).options(
selectinload(StandardScope.master_equipment)
)
query = apply_not_deleted_filter(query, StandardScope)
query = query.order_by(desc(StandardScope.created_at)).options(selectinload(StandardScope.master_equipment))
@ -90,6 +93,7 @@ async def get_by_oh_session_id(*, db_session: DbSession, oh_session_id: UUID):
(StandardScope.oh_history.has(EquipmentOHHistory.last_oh_type != overhaul.maintenance_type.name))
).distinct()
)
query = apply_not_deleted_filter(query, StandardScope)
result = await db_session.execute(query)
return result.scalars().all(), overhaul
@ -147,14 +151,29 @@ async def create(*, db_session: DbSession, scope_equipment_in: ScopeEquipmentCre
async def update(
*,
db_session: DbSession,
scope_equipment: StandardScope,
location_tag: str,
scope_equipment_in: ScopeEquipmentUpdate
):
"""Updates a document."""
data = scope_equipment_in.model_dump()
"""Fetches the record with a row-level lock (SELECT FOR UPDATE) then applies the update."""
query = (
Select(StandardScope)
.filter(StandardScope.location_tag == location_tag)
.options(selectinload(StandardScope.master_equipment))
.with_for_update()
)
query = apply_not_deleted_filter(query, StandardScope)
result = await db_session.execute(query)
scope_equipment = result.unique().scalars().one_or_none()
update_data = scope_equipment_in.model_dump(exclude_defaults=True)
if not scope_equipment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="A data with this id does not exist.",
)
await db_session.refresh(scope_equipment)
update_data = scope_equipment_in.model_dump(exclude_defaults=True)
update_model(scope_equipment, update_data)
await db_session.commit()
@ -163,11 +182,11 @@ async def update(
async def delete(*, db_session: DbSession, assetnum: str):
"""Deletes a document."""
query = Delete(StandardScope).where(StandardScope.assetnum == assetnum)
await db_session.execute(query)
await db_session.commit()
"""Soft-deletes a document."""
result = await get_by_location_tag(db_session=db_session, location_tag=assetnum)
if result:
from src.soft_delete import soft_delete_record
await soft_delete_record(db_session=db_session, model=StandardScope, record_id=str(result.id))
return assetnum
async def get_by_oh_scope(
@ -202,6 +221,7 @@ async def get_all_master_equipment(*, common: CommonParameters, scope_name):
]
query = Select(MasterEquipment).filter(MasterEquipment.location_tag.is_not(None))
query = apply_not_deleted_filter(query, MasterEquipment)
# Only add not_in filter if there are items in equipments_scope
if equipments_scope:
@ -217,6 +237,7 @@ async def get_equipment_level_by_no(*, db_session: DbSession, level: int):
.join(MasterEquipment.equipment_tree)
.where(MasterEquipmentTree.level_no == level)
)
query = apply_not_deleted_filter(query, MasterEquipment)
result = await db_session.execute(query)
return result.scalars().all()

@ -53,17 +53,9 @@ async def get_activity(db_session: DbSession, activity_id: str):
async def update_scope(
db_session: DbSession, activity_in: ActivityMasterCreate, activity_id
):
activity = await get(db_session=db_session, activity_id=activity_id)
if not activity:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="A data with this id does not exist.",
)
return StandardResponse(
data=await update(
db_session=db_session, activity=activity, activity_in=activity_in
db_session=db_session, activity_id=activity_id, activity_in=activity_in
),
message="Data updated successfully",
)

@ -1,5 +1,6 @@
from typing import Optional
from fastapi import HTTPException, status
from sqlalchemy import Delete, Select
from sqlalchemy.orm import joinedload, selectinload
@ -7,6 +8,7 @@ from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.utils import update_model
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
from .model import MasterActivity
from .schema import ActivityMaster, ActivityMasterCreate
@ -14,12 +16,15 @@ from .schema import ActivityMaster, ActivityMasterCreate
async def get(*, db_session: DbSession, activity_id: str) -> Optional[ActivityMaster]:
"""Returns a document based on the given document id."""
result = await db_session.get(MasterActivity, activity_id)
return result
query = Select(MasterActivity).filter(MasterActivity.id == activity_id)
query = apply_not_deleted_filter(query, MasterActivity)
result = await db_session.execute(query)
return result.scalars().one_or_none()
async def get_all(common: CommonParameters):
query = Select(MasterActivity)
query = apply_not_deleted_filter(query, MasterActivity)
results = await search_filter_sort_paginate(model=query, **common)
@ -36,14 +41,25 @@ async def create(*, db_session: DbSession, activty_in: ActivityMasterCreate):
async def update(
*,
db_session: DbSession,
activity: MasterActivity,
activity_id: str,
activity_in: ActivityMasterCreate
):
"""Updates a document."""
data = activity_in.model_dump()
"""Fetches the record with a row-level lock (SELECT FOR UPDATE) then applies the update."""
query = Select(MasterActivity).filter(MasterActivity.id == activity_id)
query = apply_not_deleted_filter(query, MasterActivity)
query = query.with_for_update()
result = await db_session.execute(query)
activity = result.scalars().one_or_none()
update_data = activity_in.model_dump(exclude_defaults=True)
if not activity:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="A data with this id does not exist.",
)
await db_session.refresh(activity)
update_data = activity_in.model_dump(exclude_defaults=True)
update_model(activity, update_data)
await db_session.commit()
@ -52,7 +68,5 @@ async def update(
async def delete(*, db_session: DbSession, activity_id: str):
"""Deletes a document."""
activity = await db_session.get(MasterActivity, activity_id)
await db_session.delete(activity)
await db_session.commit()
"""Soft-deletes a document."""
await soft_delete_record(db_session=db_session, model=MasterActivity, record_id=activity_id)

@ -1,5 +1,6 @@
from typing import Optional
from fastapi import HTTPException, status
from sqlalchemy import Delete, Select
from sqlalchemy.orm import joinedload, selectinload
@ -7,6 +8,7 @@ from src.auth.service import CurrentUser
from src.database.core import DbSession
from src.database.service import CommonParameters, search_filter_sort_paginate
from src.utils import update_model
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
from .model import MasterActivity
from .schema import ActivityMaster, ActivityMasterCreate
@ -14,12 +16,15 @@ from .schema import ActivityMaster, ActivityMasterCreate
async def get(*, db_session: DbSession, activity_id: str) -> Optional[ActivityMaster]:
"""Returns a document based on the given document id."""
result = await db_session.get(MasterActivity, activity_id)
return result
query = Select(MasterActivity).filter(MasterActivity.id == activity_id)
query = apply_not_deleted_filter(query, MasterActivity)
result = await db_session.execute(query)
return result.scalars().one_or_none()
async def get_all(common: CommonParameters):
query = Select(MasterActivity)
query = apply_not_deleted_filter(query, MasterActivity)
results = await search_filter_sort_paginate(model=query, **common)
@ -36,14 +41,25 @@ async def create(*, db_session: DbSession, activty_in: ActivityMasterCreate):
async def update(
*,
db_session: DbSession,
activity: MasterActivity,
activity_id: str,
activity_in: ActivityMasterCreate
):
"""Updates a document."""
data = activity_in.model_dump()
"""Fetches the record with a row-level lock (SELECT FOR UPDATE) then applies the update."""
query = Select(MasterActivity).filter(MasterActivity.id == activity_id)
query = apply_not_deleted_filter(query, MasterActivity)
query = query.with_for_update()
result = await db_session.execute(query)
activity = result.scalars().one_or_none()
update_data = activity_in.model_dump(exclude_defaults=True)
if not activity:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="A data with this id does not exist.",
)
await db_session.refresh(activity)
update_data = activity_in.model_dump(exclude_defaults=True)
update_model(activity, update_data)
await db_session.commit()
@ -52,7 +68,5 @@ async def update(
async def delete(*, db_session: DbSession, activity_id: str):
"""Deletes a document."""
activity = await db_session.get(MasterActivity, activity_id)
await db_session.delete(activity)
await db_session.commit()
"""Soft-deletes a document."""
await soft_delete_record(db_session=db_session, model=MasterActivity, record_id=activity_id)

Loading…
Cancel
Save