266 lines
10 KiB
Python
266 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.auth import current_active_user
|
|
from app.models import User as UserModel
|
|
from app.schemas.list import ListCreate, ListUpdate, ListPublic, ListDetail
|
|
from app.crud import list as crud_list
|
|
from app.crud import group as crud_group
|
|
from app.schemas.list import ListStatus, ListStatusWithId
|
|
from app.schemas.expense import ExpensePublic
|
|
from app.core.exceptions import (
|
|
GroupMembershipError,
|
|
ConflictError,
|
|
DatabaseIntegrityError
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
@router.post(
|
|
"",
|
|
response_model=ListPublic,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create New List",
|
|
tags=["Lists"],
|
|
responses={
|
|
status.HTTP_409_CONFLICT: {
|
|
"description": "Conflict: A list with this name already exists in the specified group",
|
|
"model": ListPublic
|
|
}
|
|
}
|
|
)
|
|
async def create_list(
|
|
list_in: ListCreate,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""
|
|
Creates a new shopping list.
|
|
- If `group_id` is provided, the user must be a member of that group.
|
|
- If `group_id` is null, it's a personal list.
|
|
- If a list with the same name already exists in the group, returns 409 with the existing list.
|
|
"""
|
|
logger.info(f"User {current_user.email} creating list: {list_in.name}")
|
|
group_id = list_in.group_id
|
|
|
|
if group_id:
|
|
is_member = await crud_group.is_user_member(db, group_id=group_id, user_id=current_user.id)
|
|
if not is_member:
|
|
logger.warning(f"User {current_user.email} attempted to create list in group {group_id} but is not a member.")
|
|
raise GroupMembershipError(group_id, "create lists")
|
|
|
|
try:
|
|
created_list = await crud_list.create_list(db=db, list_in=list_in, creator_id=current_user.id)
|
|
logger.info(f"List '{created_list.name}' (ID: {created_list.id}) created successfully for user {current_user.email}.")
|
|
return created_list
|
|
except DatabaseIntegrityError as e:
|
|
if "unique constraint" in str(e).lower():
|
|
existing_list = await crud_list.get_list_by_name_and_group(
|
|
db=db,
|
|
name=list_in.name,
|
|
group_id=group_id,
|
|
user_id=current_user.id
|
|
)
|
|
if existing_list:
|
|
logger.info(f"List '{list_in.name}' already exists in group {group_id}. Returning existing list.")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=f"A list named '{list_in.name}' already exists in this group.",
|
|
headers={"X-Existing-List": str(existing_list.id)}
|
|
)
|
|
raise
|
|
|
|
|
|
@router.get(
|
|
"",
|
|
response_model=PyList[ListDetail],
|
|
summary="List Accessible Lists",
|
|
tags=["Lists"]
|
|
)
|
|
async def read_lists(
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""
|
|
Retrieves lists accessible to the current user:
|
|
- Personal lists created by the user.
|
|
- Lists belonging to groups the user is a member of.
|
|
"""
|
|
logger.info(f"Fetching lists accessible to user: {current_user.email}")
|
|
lists = await crud_list.get_lists_for_user(db=db, user_id=current_user.id)
|
|
return lists
|
|
|
|
|
|
@router.get(
|
|
"/statuses",
|
|
response_model=PyList[ListStatusWithId],
|
|
summary="Get Status for Multiple Lists",
|
|
tags=["Lists"]
|
|
)
|
|
async def read_lists_statuses(
|
|
ids: PyList[int] = Query(...),
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""
|
|
Retrieves the status for a list of lists.
|
|
- `updated_at`: The timestamp of the last update to the list itself.
|
|
- `item_count`: The total number of items in the list.
|
|
The user must have permission to view each list requested.
|
|
Lists that the user does not have permission for will be omitted from the response.
|
|
"""
|
|
logger.info(f"User {current_user.email} requesting statuses for list IDs: {ids}")
|
|
|
|
statuses = await crud_list.get_lists_statuses_by_ids(db=db, list_ids=ids, user_id=current_user.id)
|
|
|
|
return [
|
|
ListStatusWithId(
|
|
id=s.id,
|
|
updated_at=s.updated_at,
|
|
item_count=s.item_count,
|
|
latest_item_updated_at=s.latest_item_updated_at
|
|
) for s in statuses
|
|
]
|
|
|
|
|
|
@router.get(
|
|
"/{list_id}",
|
|
response_model=ListDetail,
|
|
summary="Get List Details",
|
|
tags=["Lists"]
|
|
)
|
|
async def read_list(
|
|
list_id: int,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""
|
|
Retrieves details for a specific list, including its items,
|
|
if the user has permission (creator or group member).
|
|
"""
|
|
logger.info(f"User {current_user.email} requesting details for list ID: {list_id}")
|
|
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
|
|
return list_db
|
|
|
|
|
|
@router.put(
|
|
"/{list_id}",
|
|
response_model=ListPublic,
|
|
summary="Update List",
|
|
tags=["Lists"],
|
|
responses={
|
|
status.HTTP_409_CONFLICT: {"description": "Conflict: List has been modified by someone else"}
|
|
}
|
|
)
|
|
async def update_list(
|
|
list_id: int,
|
|
list_in: ListUpdate,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""
|
|
Updates a list's details (name, description, is_complete).
|
|
Requires user to be the creator or a member of the list's group.
|
|
The client MUST provide the current `version` of the list in the `list_in` payload.
|
|
If the version does not match, a 409 Conflict is returned.
|
|
"""
|
|
logger.info(f"User {current_user.email} attempting to update list ID: {list_id} with version {list_in.version}")
|
|
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
|
|
|
|
try:
|
|
updated_list = await crud_list.update_list(db=db, list_db=list_db, list_in=list_in)
|
|
logger.info(f"List {list_id} updated successfully by user {current_user.email} to version {updated_list.version}.")
|
|
return updated_list
|
|
except ConflictError as e:
|
|
logger.warning(f"Conflict updating list {list_id} for user {current_user.email}: {str(e)}")
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error updating list {list_id} for user {current_user.email}: {str(e)}")
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected error occurred while updating the list.")
|
|
|
|
|
|
@router.delete(
|
|
"/{list_id}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Delete List",
|
|
tags=["Lists"],
|
|
responses={
|
|
status.HTTP_409_CONFLICT: {"description": "Conflict: List has been modified, cannot delete specified version"}
|
|
}
|
|
)
|
|
async def delete_list(
|
|
list_id: int,
|
|
expected_version: Optional[int] = Query(None, description="The expected version of the list to delete for optimistic locking."),
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""
|
|
Deletes a list. Requires user to be the creator of the list.
|
|
If `expected_version` is provided and does not match the list's current version,
|
|
a 409 Conflict is returned.
|
|
"""
|
|
logger.info(f"User {current_user.email} attempting to delete list ID: {list_id}, expected version: {expected_version}")
|
|
list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id, require_creator=True)
|
|
|
|
if expected_version is not None and list_db.version != expected_version:
|
|
logger.warning(
|
|
f"Conflict deleting list {list_id} for user {current_user.email}. "
|
|
f"Expected version {expected_version}, actual version {list_db.version}."
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=f"List has been modified. Expected version {expected_version}, but current version is {list_db.version}. Please refresh."
|
|
)
|
|
|
|
await crud_list.delete_list(db=db, list_db=list_db)
|
|
logger.info(f"List {list_id} (version: {list_db.version}) deleted successfully by user {current_user.email}.")
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
@router.get(
|
|
"/{list_id}/status",
|
|
response_model=ListStatus,
|
|
summary="Get List Status",
|
|
tags=["Lists"]
|
|
)
|
|
async def read_list_status(
|
|
list_id: int,
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""
|
|
Retrieves the update timestamp and item count for a specific list
|
|
if the user has permission (creator or group member).
|
|
"""
|
|
logger.info(f"User {current_user.email} requesting status for list ID: {list_id}")
|
|
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
|
|
return await crud_list.get_list_status(db=db, list_id=list_id)
|
|
|
|
@router.get(
|
|
"/{list_id}/expenses",
|
|
response_model=PyList[ExpensePublic],
|
|
summary="Get Expenses for List",
|
|
tags=["Lists", "Expenses"]
|
|
)
|
|
async def read_list_expenses(
|
|
list_id: int,
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=200),
|
|
db: AsyncSession = Depends(get_transactional_session),
|
|
current_user: UserModel = Depends(current_active_user),
|
|
):
|
|
"""
|
|
Retrieves expenses associated with a specific list
|
|
if the user has permission (creator or group member).
|
|
"""
|
|
from app.crud import expense as crud_expense
|
|
|
|
logger.info(f"User {current_user.email} requesting expenses for list ID: {list_id}")
|
|
|
|
await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
|
|
|
|
expenses = await crud_expense.get_expenses_for_list(db, list_id=list_id, skip=skip, limit=limit)
|
|
return expenses |