diff --git a/app/crud/todo.py b/app/crud/todo.py index d49f205..cfc3e18 100644 --- a/app/crud/todo.py +++ b/app/crud/todo.py @@ -11,10 +11,8 @@ from app.utils.date_utils import ( get_date_range_today, get_date_range_this_week, get_date_range_next_week, - get_date_range_next_days, get_overdue_cutoff, get_due_soon_cutoff, - is_overdue, ) @@ -174,7 +172,7 @@ def get_todos_due_soon( """Get todos due within the next N days (default 7 days).""" current_time = get_timezone_aware_now() future_cutoff = get_due_soon_cutoff(days) - + query = db.query(Todo).filter( and_( Todo.due_date.isnot(None), diff --git a/app/models/todo.py b/app/models/todo.py index fa52c72..05a7cb0 100644 --- a/app/models/todo.py +++ b/app/models/todo.py @@ -56,14 +56,14 @@ class Todo(Base): @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 """ if self.due_date is None: return None - + now = datetime.now(timezone.utc) delta = (self.due_date - now).days - return delta \ No newline at end of file + return delta diff --git a/app/schemas/todo.py b/app/schemas/todo.py index d070513..42dfb17 100644 --- a/app/schemas/todo.py +++ b/app/schemas/todo.py @@ -11,14 +11,16 @@ if TYPE_CHECKING: 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") + 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') + + @field_validator("due_date") @classmethod def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]: if v is not None: @@ -27,25 +29,33 @@ class TodoBase(BaseModel): 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') + 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") + 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") + 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') + 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: @@ -64,7 +74,7 @@ class Todo(TodoBase): updated_at: Optional[datetime] = None category: Optional["Category"] = None project: Optional["Project"] = None - + @computed_field @property def is_overdue(self) -> bool: @@ -72,24 +82,24 @@ class Todo(TodoBase): 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') + + @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: @@ -110,11 +120,13 @@ class TodoListResponse(BaseModel): 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") + 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') + + @field_validator("due_date") @classmethod def validate_due_date(cls, v: Optional[datetime]) -> Optional[datetime]: if v is not None: @@ -123,7 +135,7 @@ class SubtaskCreate(BaseModel): 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') + raise ValueError("Due date must be in the future") return v diff --git a/app/utils/__init__.py b/app/utils/__init__.py index 67b9db6..2829ab7 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -1 +1,5 @@ -# Utils package \ No newline at end of file +"""Utility modules for the todo application.""" + +from . import date_utils + +__all__ = ["date_utils"] diff --git a/app/utils/date_utils.py b/app/utils/date_utils.py index 5b7dd1c..5e07cce 100644 --- a/app/utils/date_utils.py +++ b/app/utils/date_utils.py @@ -22,7 +22,9 @@ def get_date_range_this_week() -> Tuple[datetime, datetime]: now = get_timezone_aware_now() start_of_week = now - timedelta(days=now.weekday()) 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 @@ -31,8 +33,12 @@ def get_date_range_next_week() -> Tuple[datetime, datetime]: now = get_timezone_aware_now() days_until_next_monday = 7 - now.weekday() 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) - end_of_next_week = start_of_next_week + timedelta(days=6, hours=23, minutes=59, seconds=59, microseconds=999999) + start_of_next_week = start_of_next_week.replace( + 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 @@ -69,10 +75,12 @@ def is_due_today(due_date: Optional[datetime]) -> bool: 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.""" if due_date is None or completed: return False cutoff = get_due_soon_cutoff(days) now = get_timezone_aware_now() - return now <= due_date <= cutoff \ No newline at end of file + return now <= due_date <= cutoff