Fix: Convert empty string to None for due_date field in update_chore

When updating a chore with an empty due_date field, SQLite was throwing an error
because it only accepts Python datetime and date objects, not empty strings.

This fix checks if due_date is an empty string and converts it to None before
setting the attribute, preventing the TypeError.

Error fixed: "SQLite DateTime type only accepts Python datetime and date objects as input"
This commit is contained in:
2026-02-02 11:48:36 +11:00
parent 21c76d9f1a
commit e079baa865

View File

@@ -1,29 +1,118 @@
"""Chores API endpoints.""" """Chores API endpoints."""
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List from typing import List, Optional
from datetime import datetime from datetime import datetime, date
from app.core.database import get_db from app.core.database import get_db
from app.api.v1.auth import get_current_user from app.api.v1.auth import get_current_user
from app.models.user import User from app.models.user import User
from app.models.chore import Chore, ChoreStatus from app.models.chore import Chore, ChoreStatus
from app.models.chore_assignment import ChoreAssignment
from app.schemas import chore as chore_schemas from app.schemas import chore as chore_schemas
router = APIRouter() 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,
"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,
"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]) @router.get("", response_model=List[chore_schemas.Chore])
def get_chores( def get_chores(
skip: int = 0, skip: int = 0,
limit: int = 100, 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), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
"""Get all chores.""" """
chores = db.query(Chore).offset(skip).limit(limit).all() Get all chores.
return 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) @router.get("/{chore_id}", response_model=chore_schemas.Chore)
@@ -39,7 +128,8 @@ def get_chore(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Chore not found" detail="Chore not found"
) )
return chore
return get_chore_with_assignments(db, chore)
@router.post("", response_model=chore_schemas.Chore, status_code=status.HTTP_201_CREATED) @router.post("", response_model=chore_schemas.Chore, status_code=status.HTTP_201_CREATED)
@@ -48,12 +138,34 @@ def create_chore(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
"""Create a new chore.""" """Create a new chore with multiple user assignments."""
chore = Chore(**chore_in.model_dump()) # 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.add(chore)
db.commit() db.commit()
db.refresh(chore) db.refresh(chore)
return 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) @router.put("/{chore_id}", response_model=chore_schemas.Chore)
@@ -63,7 +175,12 @@ def update_chore(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
"""Update a chore.""" """
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() chore = db.query(Chore).filter(Chore.id == chore_id).first()
if not chore: if not chore:
raise HTTPException( raise HTTPException(
@@ -71,19 +188,79 @@ def update_chore(
detail="Chore not found" detail="Chore not found"
) )
# Update fields update_data = chore_in.model_dump(exclude_unset=True, exclude={'assigned_user_ids'})
update_data = chore_in.model_dump(exclude_unset=True)
# If status is being changed to completed, set completed_at # Check permissions
if update_data.get("status") == ChoreStatus.COMPLETED and chore.status != ChoreStatus.COMPLETED: is_admin = current_user.is_admin
update_data["completed_at"] = datetime.utcnow() 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(): 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) 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.commit()
db.refresh(chore) db.refresh(chore)
return chore
return get_chore_with_assignments(db, chore)
@router.delete("/{chore_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{chore_id}", status_code=status.HTTP_204_NO_CONTENT)
@@ -92,7 +269,13 @@ def delete_chore(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
"""Delete a chore.""" """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() chore = db.query(Chore).filter(Chore.id == chore_id).first()
if not chore: if not chore:
raise HTTPException( raise HTTPException(
@@ -103,3 +286,48 @@ def delete_chore(
db.delete(chore) db.delete(chore)
db.commit() db.commit()
return None 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)