# 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 )