234 lines
6.6 KiB
Python
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
|