Files
family-hub/backend/app/api/v1/chores.py
jessikitty 3f6240b6bd Fix: Add missing assignment_type field to chore API response
Fixed get_chore_with_assignments() to include assignment_type in dict.
This resolves the ResponseValidationError where assignment_type was
missing from API responses despite being present in the database.
2026-02-04 13:41:14 +11:00

337 lines
10 KiB
Python

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