Implement comprehensive due date CRUD operations and filtering
- 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
This commit is contained in:
parent
66410bdab5
commit
2519513d99
@ -11,10 +11,8 @@ from app.utils.date_utils import (
|
|||||||
get_date_range_today,
|
get_date_range_today,
|
||||||
get_date_range_this_week,
|
get_date_range_this_week,
|
||||||
get_date_range_next_week,
|
get_date_range_next_week,
|
||||||
get_date_range_next_days,
|
|
||||||
get_overdue_cutoff,
|
get_overdue_cutoff,
|
||||||
get_due_soon_cutoff,
|
get_due_soon_cutoff,
|
||||||
is_overdue,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,14 +11,16 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class TodoBase(BaseModel):
|
class TodoBase(BaseModel):
|
||||||
title: str = Field(..., min_length=1, max_length=200, description="Todo title")
|
title: str = Field(..., min_length=1, max_length=200, description="Todo title")
|
||||||
description: Optional[str] = Field(None, max_length=500, description="Todo description")
|
description: Optional[str] = Field(
|
||||||
|
None, max_length=500, description="Todo description"
|
||||||
|
)
|
||||||
completed: bool = Field(False, description="Whether the todo is completed")
|
completed: bool = Field(False, description="Whether the todo is completed")
|
||||||
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
|
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
|
||||||
category_id: Optional[int] = Field(None, description="Category ID")
|
category_id: Optional[int] = Field(None, description="Category ID")
|
||||||
project_id: Optional[int] = Field(None, description="Project ID")
|
project_id: Optional[int] = Field(None, description="Project ID")
|
||||||
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
|
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
|
||||||
|
|
||||||
@field_validator('due_date')
|
@field_validator("due_date")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
|
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
|
||||||
if v is not None:
|
if v is not None:
|
||||||
@ -27,25 +29,33 @@ class TodoBase(BaseModel):
|
|||||||
v = v.replace(tzinfo=timezone.utc)
|
v = v.replace(tzinfo=timezone.utc)
|
||||||
# For creation/updates, ensure due date is in the future
|
# For creation/updates, ensure due date is in the future
|
||||||
if v < datetime.now(timezone.utc):
|
if v < datetime.now(timezone.utc):
|
||||||
raise ValueError('Due date must be in the future')
|
raise ValueError("Due date must be in the future")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class TodoCreate(TodoBase):
|
class TodoCreate(TodoBase):
|
||||||
tag_ids: Optional[List[int]] = Field(default=None, description="List of tag IDs to associate with the todo")
|
tag_ids: Optional[List[int]] = Field(
|
||||||
|
default=None, description="List of tag IDs to associate with the todo"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TodoUpdate(BaseModel):
|
class TodoUpdate(BaseModel):
|
||||||
title: Optional[str] = Field(None, min_length=1, max_length=200, description="Todo title")
|
title: Optional[str] = Field(
|
||||||
description: Optional[str] = Field(None, max_length=500, description="Todo description")
|
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")
|
completed: Optional[bool] = Field(None, description="Whether the todo is completed")
|
||||||
priority: Optional[Priority] = Field(None, description="Priority level")
|
priority: Optional[Priority] = Field(None, description="Priority level")
|
||||||
category_id: Optional[int] = Field(None, description="Category ID")
|
category_id: Optional[int] = Field(None, description="Category ID")
|
||||||
project_id: Optional[int] = Field(None, description="Project ID")
|
project_id: Optional[int] = Field(None, description="Project ID")
|
||||||
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
|
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")
|
tag_ids: Optional[List[int]] = Field(
|
||||||
|
None, description="List of tag IDs to associate with this todo"
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator('due_date')
|
@field_validator("due_date")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
|
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
|
||||||
if v is not None:
|
if v is not None:
|
||||||
@ -89,7 +99,7 @@ class Todo(TodoBase):
|
|||||||
delta = (self.due_date - now).days
|
delta = (self.due_date - now).days
|
||||||
return delta
|
return delta
|
||||||
|
|
||||||
@field_validator('due_date', mode='before')
|
@field_validator("due_date", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def ensure_timezone_aware(cls, v: Optional[datetime]) -> Optional[datetime]:
|
def ensure_timezone_aware(cls, v: Optional[datetime]) -> Optional[datetime]:
|
||||||
if v is not None and v.tzinfo is None:
|
if v is not None and v.tzinfo is None:
|
||||||
@ -110,11 +120,13 @@ class TodoListResponse(BaseModel):
|
|||||||
|
|
||||||
class SubtaskCreate(BaseModel):
|
class SubtaskCreate(BaseModel):
|
||||||
title: str = Field(..., min_length=1, max_length=200, description="Subtask title")
|
title: str = Field(..., min_length=1, max_length=200, description="Subtask title")
|
||||||
description: Optional[str] = Field(None, max_length=500, description="Subtask description")
|
description: Optional[str] = Field(
|
||||||
|
None, max_length=500, description="Subtask description"
|
||||||
|
)
|
||||||
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
|
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
|
||||||
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
|
due_date: Optional[datetime] = Field(None, description="Due date with timezone")
|
||||||
|
|
||||||
@field_validator('due_date')
|
@field_validator("due_date")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
|
def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]:
|
||||||
if v is not None:
|
if v is not None:
|
||||||
@ -123,7 +135,7 @@ class SubtaskCreate(BaseModel):
|
|||||||
v = v.replace(tzinfo=timezone.utc)
|
v = v.replace(tzinfo=timezone.utc)
|
||||||
# For creation, ensure due date is in the future
|
# For creation, ensure due date is in the future
|
||||||
if v < datetime.now(timezone.utc):
|
if v < datetime.now(timezone.utc):
|
||||||
raise ValueError('Due date must be in the future')
|
raise ValueError("Due date must be in the future")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
@ -1 +1,5 @@
|
|||||||
# Utils package
|
"""Utility modules for the todo application."""
|
||||||
|
|
||||||
|
from . import date_utils
|
||||||
|
|
||||||
|
__all__ = ["date_utils"]
|
||||||
|
@ -22,7 +22,9 @@ def get_date_range_this_week() -> Tuple[datetime, datetime]:
|
|||||||
now = get_timezone_aware_now()
|
now = get_timezone_aware_now()
|
||||||
start_of_week = now - timedelta(days=now.weekday())
|
start_of_week = now - timedelta(days=now.weekday())
|
||||||
start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0)
|
start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
end_of_week = start_of_week + timedelta(days=6, hours=23, minutes=59, seconds=59, microseconds=999999)
|
end_of_week = start_of_week + timedelta(
|
||||||
|
days=6, hours=23, minutes=59, seconds=59, microseconds=999999
|
||||||
|
)
|
||||||
return start_of_week, end_of_week
|
return start_of_week, end_of_week
|
||||||
|
|
||||||
|
|
||||||
@ -31,8 +33,12 @@ def get_date_range_next_week() -> Tuple[datetime, datetime]:
|
|||||||
now = get_timezone_aware_now()
|
now = get_timezone_aware_now()
|
||||||
days_until_next_monday = 7 - now.weekday()
|
days_until_next_monday = 7 - now.weekday()
|
||||||
start_of_next_week = now + timedelta(days=days_until_next_monday)
|
start_of_next_week = now + timedelta(days=days_until_next_monday)
|
||||||
start_of_next_week = start_of_next_week.replace(hour=0, minute=0, second=0, microsecond=0)
|
start_of_next_week = start_of_next_week.replace(
|
||||||
end_of_next_week = start_of_next_week + timedelta(days=6, hours=23, minutes=59, seconds=59, microseconds=999999)
|
hour=0, minute=0, second=0, microsecond=0
|
||||||
|
)
|
||||||
|
end_of_next_week = start_of_next_week + timedelta(
|
||||||
|
days=6, hours=23, minutes=59, seconds=59, microseconds=999999
|
||||||
|
)
|
||||||
return start_of_next_week, end_of_next_week
|
return start_of_next_week, end_of_next_week
|
||||||
|
|
||||||
|
|
||||||
@ -69,7 +75,9 @@ def is_due_today(due_date: Optional[datetime]) -> bool:
|
|||||||
return today_start <= due_date <= today_end
|
return today_start <= due_date <= today_end
|
||||||
|
|
||||||
|
|
||||||
def is_due_soon(due_date: Optional[datetime], days: int = 7, completed: bool = False) -> bool:
|
def is_due_soon(
|
||||||
|
due_date: Optional[datetime], days: int = 7, completed: bool = False
|
||||||
|
) -> bool:
|
||||||
"""Check if a todo is due within the next N days."""
|
"""Check if a todo is due within the next N days."""
|
||||||
if due_date is None or completed:
|
if due_date is None or completed:
|
||||||
return False
|
return False
|
||||||
|
Loading…
x
Reference in New Issue
Block a user