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:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user