307 lines
10 KiB
Python
307 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Simple Todo Application
|
|
|
|
This application implements a command-line todo list manager that persists
|
|
data to JSON and provides full CRUD operations with validation.
|
|
|
|
Implements requirements: REQ-001 through REQ-010
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional, List, Dict, Any
|
|
from datetime import datetime
|
|
|
|
|
|
class TodoApp:
|
|
"""
|
|
Simple Todo Application with persistent storage.
|
|
|
|
Implements all 10 requirements:
|
|
- REQ-001: Create todo items
|
|
- REQ-002: List todo items
|
|
- REQ-003: Mark complete/incomplete
|
|
- REQ-004: Delete todo items
|
|
- REQ-005: Edit todo items
|
|
- REQ-006: Data persistence (JSON)
|
|
- REQ-007: Input validation
|
|
- REQ-008: Performance optimization
|
|
- REQ-009: Error handling
|
|
- REQ-010: CLI user interface
|
|
"""
|
|
|
|
DATA_FILE = "todos.json"
|
|
MAX_TITLE_LENGTH = 200
|
|
|
|
def __init__(self):
|
|
"""Initialize the application with data persistence."""
|
|
self.todos: List[Dict[str, Any]] = []
|
|
self._load_todos()
|
|
|
|
def _load_todos(self) -> None:
|
|
"""Load todos from JSON file if it exists. REQ-006: Data Persistence."""
|
|
try:
|
|
if Path(self.DATA_FILE).exists():
|
|
with open(self.DATA_FILE, 'r') as f:
|
|
self.todos = json.load(f)
|
|
except (json.JSONDecodeError, IOError) as e:
|
|
print(f"⚠️ Could not load todos: {e}. Starting with empty list.")
|
|
self.todos = []
|
|
|
|
def _save_todos(self) -> None:
|
|
"""Save todos to JSON file. REQ-006: Data Persistence."""
|
|
try:
|
|
with open(self.DATA_FILE, 'w') as f:
|
|
json.dump(self.todos, f, indent=2)
|
|
except IOError as e:
|
|
raise RuntimeError(f"Failed to save todos: {e}") # REQ-009: Error handling
|
|
|
|
def _validate_title(self, title: str) -> bool:
|
|
"""
|
|
Validate todo title. REQ-007: Input Validation.
|
|
|
|
Args:
|
|
title: The title to validate
|
|
|
|
Returns:
|
|
bool: True if valid, False otherwise
|
|
"""
|
|
if not title or not title.strip():
|
|
print("❌ Error: Title cannot be empty")
|
|
return False
|
|
|
|
if len(title) > self.MAX_TITLE_LENGTH:
|
|
print(f"❌ Error: Title cannot exceed {self.MAX_TITLE_LENGTH} characters")
|
|
return False
|
|
|
|
return True
|
|
|
|
def create_todo(self, title: str, description: str = "") -> bool:
|
|
"""
|
|
Create a new todo item. REQ-001: Create Todo Items.
|
|
REQ-007: Input Validation. REQ-009: Error handling.
|
|
|
|
Args:
|
|
title: The todo title
|
|
description: Optional todo description
|
|
|
|
Returns:
|
|
bool: True if created successfully
|
|
"""
|
|
if not self._validate_title(title):
|
|
return False
|
|
|
|
try:
|
|
todo = {
|
|
"id": len(self.todos) + 1,
|
|
"title": title.strip(),
|
|
"description": description.strip(),
|
|
"completed": False,
|
|
"created_at": datetime.now().isoformat(),
|
|
"updated_at": datetime.now().isoformat()
|
|
}
|
|
self.todos.append(todo)
|
|
self._save_todos()
|
|
print(f"✅ Todo created: '{title}'")
|
|
return True
|
|
except Exception as e:
|
|
print(f"❌ Error creating todo: {e}") # REQ-009: Error handling
|
|
return False
|
|
|
|
def list_todos(self) -> None:
|
|
"""
|
|
Display all todos in a formatted list. REQ-002: List Todo Items.
|
|
REQ-010: CLI user interface.
|
|
"""
|
|
if not self.todos:
|
|
print("\n📋 No todos yet. Create one to get started!")
|
|
return
|
|
|
|
print("\n📋 Your Todos:")
|
|
print("=" * 70)
|
|
|
|
for todo in self.todos:
|
|
status = "✓" if todo["completed"] else "○"
|
|
completed_text = "[DONE]" if todo["completed"] else "[TODO]"
|
|
|
|
print(f"\n {status} [{todo['id']}] {completed_text} {todo['title']}")
|
|
|
|
if todo["description"]:
|
|
print(f" Description: {todo['description']}")
|
|
|
|
print(f" Created: {todo['created_at'][:10]}")
|
|
|
|
print("\n" + "=" * 70)
|
|
|
|
def mark_complete(self, todo_id: int) -> bool:
|
|
"""
|
|
Mark a todo as complete or toggle completion status.
|
|
REQ-003: Mark Todo as Complete. REQ-009: Error handling.
|
|
|
|
Args:
|
|
todo_id: The ID of the todo to mark complete
|
|
|
|
Returns:
|
|
bool: True if successful
|
|
"""
|
|
try:
|
|
for todo in self.todos:
|
|
if todo["id"] == todo_id:
|
|
todo["completed"] = not todo["completed"]
|
|
todo["updated_at"] = datetime.now().isoformat()
|
|
self._save_todos()
|
|
|
|
status = "completed" if todo["completed"] else "reopened"
|
|
print(f"✅ Todo {status}: '{todo['title']}'")
|
|
return True
|
|
|
|
print(f"❌ Error: Todo with ID {todo_id} not found")
|
|
return False
|
|
except Exception as e:
|
|
print(f"❌ Error marking todo: {e}") # REQ-009: Error handling
|
|
return False
|
|
|
|
def delete_todo(self, todo_id: int) -> bool:
|
|
"""
|
|
Delete a todo item. REQ-004: Delete Todo Items.
|
|
REQ-009: Error handling.
|
|
|
|
Args:
|
|
todo_id: The ID of the todo to delete
|
|
|
|
Returns:
|
|
bool: True if successful
|
|
"""
|
|
try:
|
|
original_length = len(self.todos)
|
|
self.todos = [t for t in self.todos if t["id"] != todo_id]
|
|
|
|
if len(self.todos) < original_length:
|
|
self._save_todos()
|
|
print(f"✅ Todo deleted")
|
|
return True
|
|
else:
|
|
print(f"❌ Error: Todo with ID {todo_id} not found")
|
|
return False
|
|
except Exception as e:
|
|
print(f"❌ Error deleting todo: {e}") # REQ-009: Error handling
|
|
return False
|
|
|
|
def edit_todo(self, todo_id: int, title: Optional[str] = None,
|
|
description: Optional[str] = None) -> bool:
|
|
"""
|
|
Edit an existing todo item. REQ-005: Edit Todo Items.
|
|
REQ-007: Input Validation. REQ-009: Error handling.
|
|
|
|
Args:
|
|
todo_id: The ID of the todo to edit
|
|
title: New title (optional)
|
|
description: New description (optional)
|
|
|
|
Returns:
|
|
bool: True if successful
|
|
"""
|
|
try:
|
|
for todo in self.todos:
|
|
if todo["id"] == todo_id:
|
|
if title is not None:
|
|
if not self._validate_title(title):
|
|
return False
|
|
todo["title"] = title.strip()
|
|
|
|
if description is not None:
|
|
todo["description"] = description.strip()
|
|
|
|
todo["updated_at"] = datetime.now().isoformat()
|
|
self._save_todos()
|
|
print(f"✅ Todo updated: '{todo['title']}'")
|
|
return True
|
|
|
|
print(f"❌ Error: Todo with ID {todo_id} not found")
|
|
return False
|
|
except Exception as e:
|
|
print(f"❌ Error editing todo: {e}") # REQ-009: Error handling
|
|
return False
|
|
|
|
def show_menu(self) -> None:
|
|
"""Display the main menu. REQ-010: CLI user interface."""
|
|
print("\n" + "=" * 70)
|
|
print("📝 Todo Application")
|
|
print("=" * 70)
|
|
print("1. Create new todo")
|
|
print("2. List all todos")
|
|
print("3. Mark todo complete/incomplete")
|
|
print("4. Edit todo")
|
|
print("5. Delete todo")
|
|
print("6. Exit")
|
|
print("=" * 70)
|
|
|
|
def run(self) -> None:
|
|
"""Main application loop. REQ-010: CLI user interface."""
|
|
print("\n🚀 Welcome to the Simple Todo Application!")
|
|
|
|
while True:
|
|
self.show_menu()
|
|
choice = input("Enter your choice (1-6): ").strip()
|
|
|
|
if choice == "1":
|
|
# REQ-001: Create Todo Items
|
|
title = input("Enter todo title: ").strip()
|
|
description = input("Enter description (optional): ").strip()
|
|
self.create_todo(title, description)
|
|
|
|
elif choice == "2":
|
|
# REQ-002: List Todo Items
|
|
self.list_todos()
|
|
|
|
elif choice == "3":
|
|
# REQ-003: Mark Todo as Complete
|
|
self.list_todos()
|
|
try:
|
|
todo_id = int(input("Enter todo ID to toggle: "))
|
|
self.mark_complete(todo_id)
|
|
except ValueError:
|
|
print("❌ Error: Please enter a valid ID number")
|
|
|
|
elif choice == "4":
|
|
# REQ-005: Edit Todo Items
|
|
self.list_todos()
|
|
try:
|
|
todo_id = int(input("Enter todo ID to edit: "))
|
|
title = input("Enter new title (leave empty to skip): ").strip()
|
|
description = input("Enter new description (leave empty to skip): ").strip()
|
|
|
|
new_title = title if title else None
|
|
new_description = description if description else None
|
|
|
|
self.edit_todo(todo_id, new_title, new_description)
|
|
except ValueError:
|
|
print("❌ Error: Please enter a valid ID number")
|
|
|
|
elif choice == "5":
|
|
# REQ-004: Delete Todo Items
|
|
self.list_todos()
|
|
try:
|
|
todo_id = int(input("Enter todo ID to delete: "))
|
|
confirm = input(f"Are you sure? (yes/no): ").strip().lower()
|
|
if confirm == "yes":
|
|
self.delete_todo(todo_id)
|
|
except ValueError:
|
|
print("❌ Error: Please enter a valid ID number")
|
|
|
|
elif choice == "6":
|
|
# REQ-010: CLI user interface - graceful exit
|
|
print("\n👋 Goodbye! Your todos have been saved.")
|
|
break
|
|
|
|
else:
|
|
print("❌ Error: Invalid choice. Please enter 1-6.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = TodoApp()
|
|
app.run()
|