"""Chores API endpoints.""" from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session from typing import List, Optional from datetime import datetime, date from app.core.database import get_db from app.api.v1.auth import get_current_user from app.models.user import User from app.models.chore import Chore, ChoreStatus from app.models.chore_assignment import ChoreAssignment from app.schemas import chore as chore_schemas router = APIRouter() def is_birthday_today(user: User) -> bool: """Check if today is the user's birthday.""" if not user.birthday: return False today = date.today() return user.birthday.month == today.month and user.birthday.day == today.day def get_chore_with_assignments(db: Session, chore: Chore) -> dict: """Convert chore to dict with assignment details.""" # Get all assignments for this chore assignments = db.query(ChoreAssignment).filter( ChoreAssignment.chore_id == chore.id ).all() # Build assigned users list with completion status assigned_users = [] for assignment in assignments: user = db.query(User).filter(User.id == assignment.user_id).first() if user: assigned_users.append({ "id": user.id, "username": user.username, "full_name": user.full_name, "avatar_url": user.avatar_url, "birthday": user.birthday, "completed_at": assignment.completed_at }) # Convert chore to dict chore_dict = { "id": chore.id, "title": chore.title, "description": chore.description, "room": chore.room, "frequency": chore.frequency, "points": chore.points, "image_url": chore.image_url, "assignment_type": chore.assignment_type, "status": chore.status, "due_date": chore.due_date, "completed_at": chore.completed_at, "created_at": chore.created_at, "updated_at": chore.updated_at, "assigned_users": assigned_users, "assigned_user_id": chore.assigned_user_id # Legacy compatibility } return chore_dict @router.get("", response_model=List[chore_schemas.Chore]) def get_chores( skip: int = 0, limit: int = 100, user_id: Optional[int] = Query(None, description="Filter by assigned user ID"), exclude_birthdays: bool = Query(False, description="Exclude chores for users with birthdays today"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Get all chores. - **user_id**: Filter chores assigned to specific user - **exclude_birthdays**: Skip chores for users with birthdays today """ query = db.query(Chore) # Apply user filter if specified if user_id: # Find chores assigned to this user through the assignments table assignment_ids = db.query(ChoreAssignment.chore_id).filter( ChoreAssignment.user_id == user_id ).all() chore_ids = [aid[0] for aid in assignment_ids] query = query.filter(Chore.id.in_(chore_ids)) chores = query.offset(skip).limit(limit).all() # Build response with assignments result = [] for chore in chores: chore_data = get_chore_with_assignments(db, chore) # Filter out if birthday exclusion is enabled if exclude_birthdays: # Skip if any assigned user has birthday today skip_chore = False for assigned_user in chore_data["assigned_users"]: user = db.query(User).filter(User.id == assigned_user["id"]).first() if user and is_birthday_today(user): skip_chore = True break if skip_chore: continue result.append(chore_data) return result @router.get("/{chore_id}", response_model=chore_schemas.Chore) def get_chore( chore_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get a specific chore by ID.""" chore = db.query(Chore).filter(Chore.id == chore_id).first() if not chore: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Chore not found" ) return get_chore_with_assignments(db, chore) @router.post("", response_model=chore_schemas.Chore, status_code=status.HTTP_201_CREATED) def create_chore( chore_in: chore_schemas.ChoreCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Create a new chore with multiple user assignments.""" # Extract user IDs before creating chore assigned_user_ids = chore_in.assigned_user_ids or [] # Create chore without assigned_user_ids (not a DB column) chore_data = chore_in.model_dump(exclude={'assigned_user_ids'}) chore = Chore(**chore_data) db.add(chore) db.commit() db.refresh(chore) # Create assignments for each user for user_id in assigned_user_ids: # Verify user exists user = db.query(User).filter(User.id == user_id).first() if not user: continue assignment = ChoreAssignment( chore_id=chore.id, user_id=user_id ) db.add(assignment) db.commit() return get_chore_with_assignments(db, chore) @router.put("/{chore_id}", response_model=chore_schemas.Chore) def update_chore( chore_id: int, chore_in: chore_schemas.ChoreUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Update a chore (admin only or assigned user for status updates). Admin users can update all fields. Non-admin users can only update status for chores assigned to them. """ chore = db.query(Chore).filter(Chore.id == chore_id).first() if not chore: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Chore not found" ) update_data = chore_in.model_dump(exclude_unset=True, exclude={'assigned_user_ids'}) # Check permissions is_admin = current_user.is_admin is_assigned = db.query(ChoreAssignment).filter( ChoreAssignment.chore_id == chore_id, ChoreAssignment.user_id == current_user.id ).first() is not None if not is_admin and not is_assigned: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to update this chore" ) # Non-admins can only update status if not is_admin: allowed_fields = {'status'} update_data = {k: v for k, v in update_data.items() if k in allowed_fields} # Handle status change to completed if update_data.get("status") == ChoreStatus.COMPLETED: # Mark assignment as completed for current user assignment = db.query(ChoreAssignment).filter( ChoreAssignment.chore_id == chore_id, ChoreAssignment.user_id == current_user.id ).first() if assignment: assignment.completed_at = datetime.utcnow() # If all assignments are completed, mark chore as completed all_assignments = db.query(ChoreAssignment).filter( ChoreAssignment.chore_id == chore_id ).all() if all(a.completed_at is not None for a in all_assignments): chore.completed_at = datetime.utcnow() chore.status = ChoreStatus.COMPLETED # Update chore fields (admins only) if is_admin: for field, value in update_data.items(): # Convert empty string to None for datetime fields if field == 'due_date' and value == '': value = None setattr(chore, field, value) # Handle user assignments update if 'assigned_user_ids' in chore_in.model_dump(exclude_unset=True): assigned_user_ids = chore_in.assigned_user_ids or [] # Remove old assignments db.query(ChoreAssignment).filter( ChoreAssignment.chore_id == chore_id ).delete() # Add new assignments for user_id in assigned_user_ids: user = db.query(User).filter(User.id == user_id).first() if not user: continue assignment = ChoreAssignment( chore_id=chore.id, user_id=user_id ) db.add(assignment) db.commit() db.refresh(chore) return get_chore_with_assignments(db, chore) @router.delete("/{chore_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_chore( chore_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Delete a chore (admin only).""" if not current_user.is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Only admins can delete chores" ) chore = db.query(Chore).filter(Chore.id == chore_id).first() if not chore: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Chore not found" ) db.delete(chore) db.commit() return None @router.post("/{chore_id}/assign", response_model=chore_schemas.Chore) def assign_users_to_chore( chore_id: int, user_ids: List[int], db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Assign multiple users to a chore (admin only). Replaces existing assignments. """ if not current_user.is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Only admins can assign chores" ) chore = db.query(Chore).filter(Chore.id == chore_id).first() if not chore: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Chore not found" ) # Remove existing assignments db.query(ChoreAssignment).filter(ChoreAssignment.chore_id == chore_id).delete() # Add new assignments for user_id in user_ids: user = db.query(User).filter(User.id == user_id).first() if not user: continue assignment = ChoreAssignment( chore_id=chore.id, user_id=user_id ) db.add(assignment) db.commit() db.refresh(chore) return get_chore_with_assignments(db, chore)