
- Add enhanced due date filtering to get_todos function with support for "today", "this_week", "next_week" - Add overdue filtering parameter with timezone-aware comparisons - Add sorting by due_date with null values at end - Add get_overdue_todos function for retrieving overdue todos - Add get_todos_due_soon function for todos due within next N days (default 7) - Add get_todos_by_date_range function for custom date range filtering - Create comprehensive date utilities in app/utils/date_utils.py with timezone support - Add project_id and tag_ids support to TodoCreate and TodoUpdate schemas - Include efficient database indexing for due_date queries - Use SQLAlchemy timezone-aware datetime filtering throughout - Add proper overdue detection logic with timezone handling
144 lines
4.9 KiB
Python
144 lines
4.9 KiB
Python
from datetime import datetime, timezone
|
|
from typing import Optional, TYPE_CHECKING, List
|
|
from pydantic import BaseModel, Field, field_validator, computed_field
|
|
|
|
from app.models.todo import Priority
|
|
|
|
if TYPE_CHECKING:
|
|
from app.schemas.category import Category
|
|
from app.schemas.project import Project
|
|
|
|
|
|
class TodoBase(BaseModel):
|
|
title: str = Field(..., min_length=1, max_length=200, description="Todo title")
|
|
description: Optional[str] = Field(
|
|
None, max_length=500, description="Todo description"
|
|
)
|
|
completed: bool = Field(False, description="Whether the todo is completed")
|
|
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
|
|
category_id: Optional[int] = Field(None, description="Category ID")
|
|
project_id: Optional[int] = Field(None, description="Project ID")
|
|
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
|
|
|
|
@field_validator("due_date")
|
|
@classmethod
|
|
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
|
|
if v is not None:
|
|
# Ensure timezone awareness
|
|
if v.tzinfo is None:
|
|
v = v.replace(tzinfo=timezone.utc)
|
|
# For creation/updates, ensure due date is in the future
|
|
if v < datetime.now(timezone.utc):
|
|
raise ValueError("Due date must be in the future")
|
|
return v
|
|
|
|
|
|
class TodoCreate(TodoBase):
|
|
tag_ids: Optional[List[int]] = Field(
|
|
default=None, description="List of tag IDs to associate with the todo"
|
|
)
|
|
|
|
|
|
class TodoUpdate(BaseModel):
|
|
title: Optional[str] = Field(
|
|
None, min_length=1, max_length=200, description="Todo title"
|
|
)
|
|
description: Optional[str] = Field(
|
|
None, max_length=500, description="Todo description"
|
|
)
|
|
completed: Optional[bool] = Field(None, description="Whether the todo is completed")
|
|
priority: Optional[Priority] = Field(None, description="Priority level")
|
|
category_id: Optional[int] = Field(None, description="Category ID")
|
|
project_id: Optional[int] = Field(None, description="Project ID")
|
|
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
|
|
tag_ids: Optional[List[int]] = Field(
|
|
None, description="List of tag IDs to associate with this todo"
|
|
)
|
|
|
|
@field_validator("due_date")
|
|
@classmethod
|
|
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
|
|
if v is not None:
|
|
# Ensure timezone awareness
|
|
if v.tzinfo is None:
|
|
v = v.replace(tzinfo=timezone.utc)
|
|
# For updates, only validate future date if it's being set to a new value
|
|
# Allow None to clear the due date
|
|
return v
|
|
|
|
|
|
# Forward declaration to handle circular reference
|
|
class Todo(TodoBase):
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
category: Optional["Category"] = None
|
|
project: Optional["Project"] = None
|
|
|
|
@computed_field
|
|
@property
|
|
def is_overdue(self) -> bool:
|
|
"""Check if this todo is overdue."""
|
|
if self.due_date is None or self.completed:
|
|
return False
|
|
return datetime.now(timezone.utc) > self.due_date
|
|
|
|
@computed_field
|
|
@property
|
|
def days_until_due(self) -> Optional[int]:
|
|
"""Calculate the number of days until the due date.
|
|
|
|
Returns:
|
|
int: Number of days until due (negative if overdue)
|
|
None: If no due date is set or todo is completed
|
|
"""
|
|
if self.due_date is None or self.completed:
|
|
return None
|
|
|
|
now = datetime.now(timezone.utc)
|
|
delta = (self.due_date - now).days
|
|
return delta
|
|
|
|
@field_validator("due_date", mode="before")
|
|
@classmethod
|
|
def ensure_timezone_aware(cls, v: Optional[datetime]) -> Optional[datetime]:
|
|
if v is not None and v.tzinfo is None:
|
|
v = v.replace(tzinfo=timezone.utc)
|
|
return v
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class TodoListResponse(BaseModel):
|
|
items: list[Todo]
|
|
total: int
|
|
page: int
|
|
per_page: int
|
|
has_next: bool
|
|
has_prev: bool
|
|
|
|
|
|
class SubtaskCreate(BaseModel):
|
|
title: str = Field(..., min_length=1, max_length=200, description="Subtask title")
|
|
description: Optional[str] = Field(
|
|
None, max_length=500, description="Subtask description"
|
|
)
|
|
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
|
|
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
|
|
|
|
@field_validator("due_date")
|
|
@classmethod
|
|
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
|
|
if v is not None:
|
|
# Ensure timezone awareness
|
|
if v.tzinfo is None:
|
|
v = v.replace(tzinfo=timezone.utc)
|
|
# For creation, ensure due date is in the future
|
|
if v < datetime.now(timezone.utc):
|
|
raise ValueError("Due date must be in the future")
|
|
return v
|
|
|
|
|
|
# Update forward references
|
|
Todo.model_rebuild()
|