Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/app/common/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(self, message: str | None = None, status_code: int | None = None) -

class ValidationError(AppError):
code = "VALIDATION_ERROR"
default_status = status.HTTP_422_UNPROCESSABLE_ENTITY
default_status = status.HTTP_422_UNPROCESSABLE_CONTENT


class NotFoundError(AppError):
Expand Down
3 changes: 3 additions & 0 deletions backend/app/modules/recipes/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ async def flush(self) -> None:

async def commit(self) -> None:
await self._session.commit()

async def refresh(self, recipe: Recipe) -> None:
await self._session.refresh(recipe)
4 changes: 4 additions & 0 deletions backend/app/modules/recipes/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,8 @@ async def update(self, recipe_id: str, payload: RecipePayload) -> dict:
recipe.parameters = dict(payload.parameters)
recipe.updated_by = payload.updated_by
await self._repo.commit()
# ``updated_at`` is a server-side onupdate column the DB regenerated on this
# UPDATE, so it is expired post-commit; refresh before serializing to avoid a
# lazy load on the async session (MissingGreenlet).
await self._repo.refresh(recipe)
return recipe_dict(recipe)
7 changes: 6 additions & 1 deletion backend/app/modules/reports/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,12 @@ async def create_report(
OrderStatus.WAITING_REPORT_RETURN.value,
)
await self._repo.commit()
return report_dict(rpt)
# Re-fetch via the eager-loading repo path so ``attachments``/``versions`` are
# loaded before serialization (the Report model requires repositories to
# eager-load; serializing a post-commit instance would lazy-load on the async
# session and raise MissingGreenlet when no attachment was appended).
created = await self._repo.get_report(rid)
return report_dict(created if created is not None else rpt)

async def list_templates(self) -> list[dict]:
return [template_dict(t) for t in await self._repo.list_templates()]
Expand Down
33 changes: 24 additions & 9 deletions backend/app/repos/order_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __init__(self, db: AsyncSession) -> None:

async def create_order(self, payload: OrderCreate, current_user: CurrentUser) -> Order:
require_role(current_user, {"plant_user"})
await self._validate_order_master_data(payload.department_id, payload.items)
labs_by_key = await self._validate_order_master_data(payload.department_id, payload.items)

applicant_id = user_id(current_user)
now = utc_now()
Expand All @@ -72,7 +72,7 @@ async def create_order(self, payload: OrderCreate, current_user: CurrentUser) ->
created_at=now,
updated_at=now,
)
order.items = [self._make_item(item, now) for item in payload.items]
order.items = [self._make_item(item, now, labs_by_key) for item in payload.items]
self.db.add(order)
await self.db.flush()

Expand Down Expand Up @@ -183,7 +183,7 @@ async def update_order(
for item in order.items
]

await self._validate_order_master_data(next_department_id, next_items)
labs_by_key = await self._validate_order_master_data(next_department_id, next_items)

if payload.department_id is not None:
order.department_id = payload.department_id
Expand Down Expand Up @@ -211,7 +211,7 @@ async def update_order(
raise bad_request("Only returned order items can be edited individually")

# validate mapping for new lab/experiment
await self._validate_order_master_data(
patch_labs_by_key = await self._validate_order_master_data(
order.department_id,
[
OrderItemCreate(
Expand All @@ -228,7 +228,7 @@ async def update_order(

target.sample_id = item_patch.sample_id
target.sample_name = item_patch.sample_name
target.lab_id = item_patch.lab_id
target.lab_id = self._resolve_lab_id(item_patch.lab_id, patch_labs_by_key)
target.experiment_id = item_patch.experiment_id
target.target_group = item_patch.target_group
target.target = item_patch.target
Expand All @@ -247,7 +247,7 @@ async def update_order(
else:
order.items.clear()
await self.db.flush()
order.items = [self._make_item(item, now) for item in payload.items]
order.items = [self._make_item(item, now, labs_by_key) for item in payload.items]
order.total_items = len(order.items)

order.updated_at = now
Expand Down Expand Up @@ -989,11 +989,24 @@ async def _get_order_model(self, order_id: int) -> OrderModel:
raise not_found("Order not found")
return order

def _make_item(self, payload: OrderItemCreate, now: datetime) -> OrderItemModel:
@staticmethod
def _resolve_lab_id(lab_key: str, labs_by_key: dict[str, Lab]) -> str:
"""Normalize a submitted lab key (Lab.code or UUID string) to the canonical
``Lab.id`` UUID string that every order-item reader expects (approve gate,
dashboard scoping, lab-name lookups all compare against ``Lab.id``)."""
lab = labs_by_key.get(lab_key)
return str(lab.id) if lab is not None else lab_key

def _make_item(
self,
payload: OrderItemCreate,
now: datetime,
labs_by_key: dict[str, Lab],
) -> OrderItemModel:
return OrderItemModel(
sample_id=payload.sample_id,
sample_name=payload.sample_name,
lab_id=payload.lab_id,
lab_id=self._resolve_lab_id(payload.lab_id, labs_by_key),
experiment_id=payload.experiment_id,
target_group=payload.target_group,
target=payload.target,
Expand All @@ -1007,7 +1020,7 @@ async def _validate_order_master_data(
self,
department_id: str,
items: Sequence[OrderItemMasterData],
) -> None:
) -> dict[str, Lab]:
department_exists = (
await self.db.execute(
select(Department.id).where(
Expand Down Expand Up @@ -1063,6 +1076,8 @@ async def _validate_order_master_data(
f"Experiment {item.experiment_id} does not belong to lab {item.lab_id}"
)

return labs_by_key

@staticmethod
def _truncate_text(value: str | None, max_length: int) -> str | None:
if value is None:
Expand Down
2 changes: 2 additions & 0 deletions backend/app/routes/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ async def list_orders_by_applicant(
@router.get("/{order_id}")
async def get_order(
order_id: int,
current_user: CurrentUser = Depends(get_current_user),
service: OrderService = Depends(get_order_service),
) -> ApiResponse:
order = await service.get_order(order_id)
Expand Down Expand Up @@ -132,6 +133,7 @@ async def handle_order_action(
@router.get("/{order_id}/history")
async def get_order_history(
order_id: int,
current_user: CurrentUser = Depends(get_current_user),
service: OrderService = Depends(get_order_service),
) -> ApiResponse:
return ApiResponse(
Expand Down
Empty file.
Loading
Loading