Files
2026-06-06 12:36:25 +00:00

234 lines
6.6 KiB
Python

"""Todo item data model and manager module.
This module defines the TodoItem dataclass and the TodoManager class
that provides CRUD operations for todo items, backed by a Storage instance.
"""
import datetime
from dataclasses import dataclass, field, asdict
from typing import List, Optional, Dict, Any
from storage import Storage
@dataclass
class TodoItem:
"""Represents a single todo item.
Attributes:
id: Unique identifier for the item.
title: Short title of the task.
description: Optional detailed description.
due_date: Optional due date string (e.g., '2025-01-01').
completed: Whether the task is completed.
created_at: ISO-formatted creation timestamp.
"""
id: int
title: str
description: str = ""
due_date: str = ""
completed: bool = False
created_at: str = field(default_factory=lambda: datetime.datetime.now().isoformat())
def to_dict(self) -> Dict[str, Any]:
"""Convert the TodoItem to a dictionary for JSON serialization.
Returns:
A dictionary with all fields.
"""
return asdict(self)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "TodoItem":
"""Create a TodoItem from a dictionary (e.g., loaded from JSON).
Args:
data: Dictionary containing todo item fields.
Returns:
A new TodoItem instance.
"""
return cls(
id=data["id"],
title=data["title"],
description=data.get("description", ""),
due_date=data.get("due_date", ""),
completed=data.get("completed", False),
created_at=data.get("created_at", ""),
)
class TodoManager:
"""Manages a collection of TodoItems with persistent storage.
Attributes:
items: List of current TodoItem objects.
storage: Storage instance for data persistence.
next_id: The next available ID for new items.
"""
def __init__(self, storage: Storage) -> None:
"""Initialize the manager with a Storage instance and load existing data.
Args:
storage: A Storage object for reading/writing JSON data.
"""
self.storage = storage
self.items: List[TodoItem] = []
self.next_id: int = 1
self._load()
def add(self, title: str, description: str = "", due_date: str = "") -> TodoItem:
"""Add a new todo item.
Args:
title: The title of the task.
description: Optional description.
due_date: Optional due date string.
Returns:
The newly created TodoItem.
"""
item = TodoItem(
id=self.next_id,
title=title,
description=description,
due_date=due_date,
)
self.items.append(item)
self.next_id += 1
self._save()
return item
def list_all(self) -> List[TodoItem]:
"""Return all todo items.
Returns:
A list of all TodoItem objects.
"""
return self.items
def list_by_status(self, completed: bool) -> List[TodoItem]:
"""Return todo items filtered by completion status.
Args:
completed: True for completed items, False for pending.
Returns:
A list of matching TodoItem objects.
"""
return [item for item in self.items if item.completed == completed]
def mark_done(self, item_id: int) -> bool:
"""Mark a todo item as completed.
Args:
item_id: The ID of the item to mark.
Returns:
True if the item was found and updated, False otherwise.
"""
item = self._find_by_id(item_id)
if item is None:
return False
item.completed = True
self._save()
return True
def mark_undone(self, item_id: int) -> bool:
"""Mark a todo item as not completed.
Args:
item_id: The ID of the item to mark.
Returns:
True if the item was found and updated, False otherwise.
"""
item = self._find_by_id(item_id)
if item is None:
return False
item.completed = False
self._save()
return True
def delete(self, item_id: int) -> bool:
"""Delete a todo item by its ID.
Args:
item_id: The ID of the item to delete.
Returns:
True if the item was found and removed, False otherwise.
"""
item = self._find_by_id(item_id)
if item is None:
return False
self.items.remove(item)
self._save()
return True
def edit(
self,
item_id: int,
title: Optional[str] = None,
description: Optional[str] = None,
due_date: Optional[str] = None,
completed: Optional[bool] = None,
) -> bool:
"""Edit fields of an existing todo item.
Only fields that are not None will be updated.
Args:
item_id: The ID of the item to edit.
title: New title (or None to keep unchanged).
description: New description (or None to keep unchanged).
due_date: New due date (or None to keep unchanged).
completed: New completion status (or None to keep unchanged).
Returns:
True if the item was found and updated, False otherwise.
"""
item = self._find_by_id(item_id)
if item is None:
return False
if title is not None:
item.title = title
if description is not None:
item.description = description
if due_date is not None:
item.due_date = due_date
if completed is not None:
item.completed = completed
self._save()
return True
def _load(self) -> None:
"""Load todo items from storage and initialize next_id."""
raw_data = self.storage.load()
self.items = [TodoItem.from_dict(d) for d in raw_data]
if self.items:
max_id = max(item.id for item in self.items)
self.next_id = max_id + 1
else:
self.next_id = 1
def _save(self) -> None:
"""Save current todo items to storage."""
data = [item.to_dict() for item in self.items]
self.storage.save(data)
def _find_by_id(self, item_id: int) -> Optional[TodoItem]:
"""Find a todo item by its ID.
Args:
item_id: The ID to search for.
Returns:
The TodoItem if found, None otherwise.
"""
for item in self.items:
if item.id == item_id:
return item
return None