mitlist/be/app/api/v1/endpoints/items.py
mohamad 66daa19cd5
Some checks failed
Deploy to Production, build images and push to Gitea Registry / build_and_push (pull_request) Failing after 1m17s
feat: Implement WebSocket enhancements and introduce new components for settlements
This commit includes several key updates and new features:

- Enhanced WebSocket functionality across various components, improving real-time communication and user experience.
- Introduced new components for managing settlements, including `SettlementCard.vue`, `SettlementForm.vue`, and `SuggestedSettlementsCard.vue`, to streamline financial interactions.
- Updated existing components and services to support the new settlement features, ensuring consistency and improved performance.
- Added advanced performance optimizations to enhance loading times and responsiveness throughout the application.

These changes aim to provide a more robust and user-friendly experience in managing financial settlements and real-time interactions.
2025-06-30 01:07:10 +02:00

283 lines
10 KiB
Python

import logging
from typing import List as PyList, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Response, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_transactional_session
from app.models import User as UserModel
from app.models import Item as ItemModel
from app.schemas.item import ItemCreate, ItemUpdate, ItemPublic
from app.crud import item as crud_item
from app.crud import list as crud_list
from app.core.exceptions import ItemNotFoundError, ListPermissionError, ConflictError
from app.auth import current_active_user
from app.core.redis import broadcast_event
logger = logging.getLogger(__name__)
router = APIRouter()
async def get_item_and_verify_access(
item_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user)
) -> ItemModel:
"""Dependency to get an item and verify the user has access to its list."""
item_db = await crud_item.get_item_by_id(db, item_id=item_id)
if not item_db:
raise ItemNotFoundError(item_id)
try:
await crud_list.check_list_permission(db=db, list_id=item_db.list_id, user_id=current_user.id)
except ListPermissionError as e:
raise ListPermissionError(item_db.list_id, "access this item's list")
return item_db
@router.post(
"/lists/{list_id}/items",
response_model=ItemPublic,
status_code=status.HTTP_201_CREATED,
summary="Add Item to List",
tags=["Items"]
)
async def create_list_item(
list_id: int,
item_in: ItemCreate,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Adds a new item to a specific list. User must have access to the list."""
user_email = current_user.email
logger.info(f"User {user_email} adding item to list {list_id}: {item_in.name}")
try:
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
except ListPermissionError as e:
raise ListPermissionError(list_id, "add items to this list")
created_item = await crud_item.create_item(
db=db, item_in=item_in, list_id=list_id, user_id=current_user.id
)
logger.info(f"Item '{created_item.name}' (ID: {created_item.id}) added to list {list_id} by user {user_email}.")
return created_item
@router.get(
"/lists/{list_id}/items",
response_model=PyList[ItemPublic],
summary="List Items in List",
tags=["Items"]
)
async def read_list_items(
list_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Retrieves all items for a specific list if the user has access."""
user_email = current_user.email
logger.info(f"User {user_email} listing items for list {list_id}")
try:
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
except ListPermissionError as e:
raise ListPermissionError(list_id, "view items in this list")
items = await crud_item.get_items_by_list_id(db=db, list_id=list_id)
return items
@router.put(
"/lists/{list_id}/items/{item_id}",
response_model=ItemPublic,
summary="Update Item",
tags=["Items"],
responses={
status.HTTP_409_CONFLICT: {"description": "Conflict: Item has been modified by someone else"}
}
)
async def update_item(
list_id: int,
item_id: int,
item_in: ItemUpdate,
item_db: ItemModel = Depends(get_item_and_verify_access),
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""
Updates an item's details (name, quantity, is_complete, price).
User must have access to the list the item belongs to.
The client MUST provide the current `version` of the item in the `item_in` payload.
If the version does not match, a 409 Conflict is returned.
Sets/unsets `completed_by_id` based on `is_complete` flag.
"""
user_email = current_user.email
logger.info(f"User {user_email} attempting to update item ID: {item_id} with version {item_in.version}")
try:
updated_item = await crud_item.update_item(
db=db, item_db=item_db, item_in=item_in, user_id=current_user.id
)
logger.info(f"Item {item_id} updated successfully by user {user_email} to version {updated_item.version}.")
return updated_item
except ConflictError as e:
logger.warning(f"Conflict updating item {item_id} for user {user_email}: {str(e)}")
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
except Exception as e:
logger.error(f"Error updating item {item_id} for user {user_email}: {str(e)}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred while updating the item.")
@router.delete(
"/lists/{list_id}/items/{item_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete Item",
tags=["Items"],
responses={
status.HTTP_409_CONFLICT: {"description": "Conflict: Item has been modified, cannot delete specified version"}
}
)
async def delete_item(
list_id: int,
item_id: int,
expected_version: Optional[int] = Query(None, description="The expected version of the item to delete for optimistic locking."),
item_db: ItemModel = Depends(get_item_and_verify_access),
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""
Deletes an item. User must have access to the list the item belongs to.
If `expected_version` is provided and does not match the item's current version,
a 409 Conflict is returned.
"""
user_email = current_user.email
if expected_version is not None and item_db.version != expected_version:
logger.warning(
f"Conflict deleting item {item_id} for user {user_email}. "
f"Expected version {expected_version}, actual version {item_db.version}."
)
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Item has been modified. Expected version {expected_version}, but current version is {item_db.version}. Please refresh."
)
await crud_item.delete_item(db=db, item_db=item_db)
logger.info(f"Item {item_id} (version {item_db.version}) deleted successfully by user {user_email}.")
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/items/{item_id}/claim",
response_model=ItemPublic,
summary="Claim an Item",
tags=["Items"],
responses={
status.HTTP_409_CONFLICT: {"description": "Item is already claimed or completed"},
status.HTTP_403_FORBIDDEN: {"description": "User does not have permission to claim this item"}
}
)
async def claim_item(
item_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Marks an item as claimed by the current user."""
item_db = await get_item_and_verify_access(item_id, db, current_user)
if item_db.list.group_id is None:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Items on personal lists cannot be claimed.")
try:
updated_item = await crud_item.claim_item(db, item=item_db, user_id=current_user.id)
# Broadcast the event
event = {
"type": "item_claimed",
"payload": {
"list_id": updated_item.list_id,
"item_id": updated_item.id,
"claimed_by": {
"id": current_user.id,
"name": current_user.name
},
"claimed_at": updated_item.claimed_at.isoformat(),
"version": updated_item.version
}
}
await broadcast_event(f"list_{updated_item.list_id}", event)
return updated_item
except ConflictError as e:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
@router.delete(
"/items/{item_id}/claim",
response_model=ItemPublic,
summary="Unclaim an Item",
tags=["Items"],
responses={
status.HTTP_403_FORBIDDEN: {"description": "User cannot unclaim an item they did not claim"}
}
)
async def unclaim_item(
item_id: int,
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Removes the current user's claim from an item."""
item_db = await get_item_and_verify_access(item_id, db, current_user)
if item_db.claimed_by_user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You can only unclaim items that you have claimed.")
updated_item = await crud_item.unclaim_item(db, item=item_db)
# Broadcast the event
event = {
"type": "item_unclaimed",
"payload": {
"list_id": updated_item.list_id,
"item_id": updated_item.id,
"version": updated_item.version
}
}
await broadcast_event(f"list_{updated_item.list_id}", event)
return updated_item
@router.put(
"/lists/{list_id}/items/reorder",
status_code=status.HTTP_204_NO_CONTENT,
summary="Reorder Items in List",
tags=["Items"]
)
async def reorder_list_items(
list_id: int,
ordered_ids: PyList[int],
db: AsyncSession = Depends(get_transactional_session),
current_user: UserModel = Depends(current_active_user),
):
"""Reorders items in a list based on the provided ordered list of item IDs."""
user_email = current_user.email
logger.info(f"User {user_email} reordering items in list {list_id}: {ordered_ids}")
try:
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
except ListPermissionError as e:
raise ListPermissionError(list_id, "reorder items in this list")
await crud_item.reorder_items(db=db, list_id=list_id, ordered_ids=ordered_ids)
# Broadcast the reorder event
event = {
"type": "item_reordered",
"payload": {
"list_id": list_id,
"ordered_ids": ordered_ids
}
}
await broadcast_event(f"list_{list_id}", event)
logger.info(f"Items in list {list_id} reordered successfully by user {user_email}.")
return Response(status_code=status.HTTP_204_NO_CONTENT)