doe/be/app/crud/list.py
2025-03-31 00:07:43 +02:00

151 lines
5.9 KiB
Python

# app/crud/list.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload, joinedload
from sqlalchemy import or_, and_, delete as sql_delete # Use alias for delete
from typing import Optional, List as PyList # Use alias for List
from sqlalchemy import func as sql_func, desc # Import func and desc
from app.schemas.list import ListStatus # Import the new schema
from app.models import List as ListModel, UserGroup as UserGroupModel, Item as ItemModel
from app.schemas.list import ListCreate, ListUpdate
async def create_list(db: AsyncSession, list_in: ListCreate, creator_id: int) -> ListModel:
"""Creates a new list record."""
db_list = ListModel(
name=list_in.name,
description=list_in.description,
group_id=list_in.group_id,
created_by_id=creator_id,
is_complete=False # Default on creation
)
db.add(db_list)
await db.commit()
await db.refresh(db_list)
return db_list
async def get_lists_for_user(db: AsyncSession, user_id: int) -> PyList[ListModel]:
"""
Gets all lists accessible by a user:
- Personal lists created by the user (group_id is NULL).
- Lists belonging to groups the user is a member of.
"""
# Get IDs of groups the user is a member of
group_ids_result = await db.execute(
select(UserGroupModel.group_id).where(UserGroupModel.user_id == user_id)
)
user_group_ids = group_ids_result.scalars().all()
# Query for lists
query = select(ListModel).where(
or_(
# Personal lists
and_(ListModel.created_by_id == user_id, ListModel.group_id == None),
# Group lists where user is a member
ListModel.group_id.in_(user_group_ids)
)
).order_by(ListModel.updated_at.desc()) # Order by most recently updated
result = await db.execute(query)
return result.scalars().all()
async def get_list_by_id(db: AsyncSession, list_id: int, load_items: bool = False) -> Optional[ListModel]:
"""Gets a single list by ID, optionally loading its items."""
query = select(ListModel).where(ListModel.id == list_id)
if load_items:
# Eager load items and their creators/completers if needed
query = query.options(
selectinload(ListModel.items)
.options(
joinedload(ItemModel.added_by_user), # Use joinedload for simple FKs
joinedload(ItemModel.completed_by_user)
)
)
result = await db.execute(query)
return result.scalars().first()
async def update_list(db: AsyncSession, list_db: ListModel, list_in: ListUpdate) -> ListModel:
"""Updates an existing list record."""
update_data = list_in.model_dump(exclude_unset=True) # Get only provided fields
for key, value in update_data.items():
setattr(list_db, key, value)
db.add(list_db) # Add to session to track changes
await db.commit()
await db.refresh(list_db)
return list_db
async def delete_list(db: AsyncSession, list_db: ListModel) -> None:
"""Deletes a list record."""
# Items should be deleted automatically due to cascade="all, delete-orphan"
# on List.items relationship and ondelete="CASCADE" on Item.list_id FK
await db.delete(list_db)
await db.commit()
return None # Or return True/False if needed
# --- Helper for Permission Checks ---
async def check_list_permission(db: AsyncSession, list_id: int, user_id: int, require_creator: bool = False) -> Optional[ListModel]:
"""
Fetches a list and verifies user permission.
Args:
db: Database session.
list_id: The ID of the list to check.
user_id: The ID of the user requesting access.
require_creator: If True, only allows the creator access.
Returns:
The ListModel if found and permission granted, otherwise None.
(Raising exceptions might be better handled in the endpoint).
"""
list_db = await get_list_by_id(db, list_id=list_id, load_items=True) # Load items for detail/update/delete context
if not list_db:
return None # List not found
# Check if user is the creator
is_creator = list_db.created_by_id == user_id
if require_creator:
return list_db if is_creator else None
# If not requiring creator, check membership if it's a group list
if is_creator:
return list_db # Creator always has access
if list_db.group_id:
# Check if user is member of the list's group
from app.crud.group import is_user_member # Avoid circular import at top level
is_member = await is_user_member(db, group_id=list_db.group_id, user_id=user_id)
return list_db if is_member else None
else:
# Personal list, not the creator -> no access
return None
async def get_list_status(db: AsyncSession, list_id: int) -> Optional[ListStatus]:
"""
Gets the update timestamps and item count for a list.
Returns None if the list itself doesn't exist.
"""
# Fetch list updated_at time
list_query = select(ListModel.updated_at).where(ListModel.id == list_id)
list_result = await db.execute(list_query)
list_updated_at = list_result.scalar_one_or_none()
if list_updated_at is None:
return None # List not found
# Fetch the latest item update time and count for that list
item_status_query = (
select(
sql_func.max(ItemModel.updated_at).label("latest_item_updated_at"),
sql_func.count(ItemModel.id).label("item_count")
)
.where(ItemModel.list_id == list_id)
)
item_result = await db.execute(item_status_query)
item_status = item_result.first() # Use first() as aggregate always returns one row
return ListStatus(
list_updated_at=list_updated_at,
latest_item_updated_at=item_status.latest_item_updated_at if item_status else None,
item_count=item_status.item_count if item_status else 0
)