diff --git a/task_manager.py b/task_manager.py index 5b97f2f..2ed0653 100644 --- a/task_manager.py +++ b/task_manager.py @@ -3,6 +3,23 @@ import re import datetime +from enum import Enum + + +class TaskStatus(Enum): + TODO = "待办" + IN_PROGRESS = "进行中" + COMPLETED = "已完成" + CANCELLED = "已取消" + + +STATUS_TRANSITIONS = { + TaskStatus.TODO: [TaskStatus.IN_PROGRESS, TaskStatus.CANCELLED], + TaskStatus.IN_PROGRESS: [TaskStatus.TODO, TaskStatus.COMPLETED, TaskStatus.CANCELLED], + TaskStatus.COMPLETED: [TaskStatus.IN_PROGRESS], + TaskStatus.CANCELLED: [TaskStatus.TODO], +} + class TaskManager: def __init__(self, filename="data.txt"): @@ -14,6 +31,77 @@ def create_backup(self): shutil.copy(self.filename, backup_file) print(f"📁 Backup created as: {backup_file}") + def _validate_status(self, status): + try: + return TaskStatus(status) + except ValueError: + return None + + def _validate_progress(self, progress): + try: + p = int(progress) + if 0 <= p <= 100: + return p + return None + except ValueError: + return None + + def _can_transition_to(self, current_status, new_status): + if current_status in STATUS_TRANSITIONS: + return new_status in STATUS_TRANSITIONS[current_status] + return False + + def _get_available_transitions(self, current_status): + if current_status in STATUS_TRANSITIONS: + return STATUS_TRANSITIONS[current_status] + return [] + + def _parse_tasks(self, lines): + tasks = [] + current_task = {} + for line in lines: + line = line.strip() + if line == "==========================": + if current_task: + tasks.append(current_task) + current_task = {} + elif ":" in line: + key, value = line.split(":", 1) + key = key.strip() + value = value.strip() + current_task[key] = value + if current_task: + tasks.append(current_task) + return tasks + + def _task_to_lines(self, task): + lines = [] + lines.append(f"Title:{task.get('Title', '')}\n") + lines.append(f"Description:{task.get('Description', '')}\n") + lines.append(f"Deadline:{task.get('Deadline', '')}\n") + lines.append(f"Priority:{task.get('Priority', '')}\n") + lines.append(f"Category:{task.get('Category', '')}\n") + lines.append(f"Status:{task.get('Status', '待办')}\n") + lines.append(f"Progress:{task.get('Progress', '0')}\n") + lines.append("==========================\n") + return lines + + def _input_status(self, prompt="Enter status (待办/进行中/已完成/已取消): "): + while True: + status_input = input(prompt).strip() + status = self._validate_status(status_input) + if status: + return status.value + print("❌ Invalid status. Choose: 待办, 进行中, 已完成, 已取消") + + def _input_progress(self, prompt="Enter progress (0-100): "): + while True: + progress_input = input(prompt).strip() + progress = self._validate_progress(progress_input) + if progress is not None: + return str(progress) + print("❌ Invalid progress. Enter a number between 0 and 100.") + def add_task(self): print("==== Welcome to Task Manager ====") title = input("Enter title: ").strip() @@ -32,6 +120,8 @@ def add_task(self): else: print("❌ Invalid priority. Choose High, Medium, or Low.") category = input("Enter category: ").strip() + status = self._input_status() + progress = self._input_progress() try: with open(self.filename, "a", encoding="utf-8") as f: @@ -40,6 +130,8 @@ def add_task(self): f.write(f"Deadline:{deadline}\n") f.write(f"Priority:{priority}\n") f.write(f"Category:{category}\n") + f.write(f"Status:{status}\n") + f.write(f"Progress:{progress}\n") f.write("==========================\n") except Exception as e: print(f"Error saving task: {e}") @@ -59,7 +151,6 @@ def search_task(self, field): for line in lines: if line.strip() == "==========================": - # Check current block before resetting if any(re.match(pattern, l.strip(), re.IGNORECASE) for l in block): print("\n".join(block)) print("========================") @@ -78,6 +169,79 @@ def search_task(self, field): if not found: print(f"No contact found with that {field.lower()}.") + def filter_by_status(self): + try: + with open(self.filename, "r", encoding="utf-8") as f: + lines = f.readlines() + except FileNotFoundError: + print("⚠️ No file found") + return + + status_value = self._input_status("Enter status to filter (待办/进行中/已完成/已取消): ") + tasks = self._parse_tasks(lines) + filtered_tasks = [t for t in tasks if t.get("Status", "待办") == status_value] + + if not filtered_tasks: + print(f"No tasks found with status: {status_value}") + return + + print(f"\n📋 Tasks with status: {status_value}") + for idx, task in enumerate(filtered_tasks, 1): + print(f"\nTask {idx}:") + print(f"Title: {task.get('Title', '')}") + print(f"Description: {task.get('Description', '')}") + print(f"Deadline: {task.get('Deadline', '')}") + print(f"Priority: {task.get('Priority', '')}") + print(f"Category: {task.get('Category', '')}") + print(f"Status: {task.get('Status', '待办')}") + print(f"Progress: {task.get('Progress', '0')}%") + + def status_statistics(self): + try: + with open(self.filename, "r", encoding="utf-8") as f: + lines = f.readlines() + except FileNotFoundError: + print("⚠️ No file found") + return + + tasks = self._parse_tasks(lines) + if not tasks: + print("No tasks found.") + return + + stats = { + "待办": 0, + "进行中": 0, + "已完成": 0, + "已取消": 0, + } + + total_progress = 0 + completed_progress = 0 + + for task in tasks: + status = task.get("Status", "待办") + if status in stats: + stats[status] += 1 + + progress = int(task.get("Progress", 0)) + total_progress += progress + if status == "已完成": + completed_progress += 100 + + total = len(tasks) + avg_progress = total_progress / total if total > 0 else 0 + + print("\n📊 Task Status Statistics") + print("=" * 40) + print(f"Total Tasks: {total}") + print() + for status, count in stats.items(): + percentage = (count / total * 100) if total > 0 else 0 + print(f"{status}: {count} ({percentage:.1f}%)") + print("=" * 40) + print(f"Average Progress: {avg_progress:.1f}%") + print(f"Completion Rate: {(stats['已完成'] / total * 100) if total > 0 else 0:.1f}%") def delete_task(self): try: @@ -100,7 +264,6 @@ def delete_task(self): if confirm == "yes": self.create_backup() found_any = True - # skip else: new_lines.extend(block + ["==========================\n"]) else: @@ -154,11 +317,51 @@ def update_task(self): print("❌ Invalid priority.") new_category = input("Enter category: ") + current_task = self._parse_tasks(block + ["==========================\n"])[0] if block else {} + current_status_str = current_task.get("Status", "待办") + current_status = self._validate_status(current_status_str) + + print(f"\nCurrent Status: {current_status_str}") + if current_status: + available_transitions = self._get_available_transitions(current_status) + if available_transitions: + print(f"Available transitions: {[s.value for s in available_transitions]}") + + new_status = self._input_status("Enter new status (待办/进行中/已完成/已取消): ") + + if current_status: + new_status_enum = self._validate_status(new_status) + if new_status_enum and not self._can_transition_to(current_status, new_status_enum): + print(f"⚠️ Warning: Transition from {current_status_str} to {new_status} is not recommended.") + confirm_transition = input("Do you want to proceed anyway? (yes/no): ").strip().lower() + if confirm_transition != "yes": + new_status = current_status_str + print(f"Status kept as: {current_status_str}") + + new_progress = self._input_progress("Enter new progress (0-100): ") + + new_progress_int = int(new_progress) + if new_status == "已完成" and new_progress_int < 100: + print("⚠️ Task marked as completed but progress is less than 100%.") + auto_complete = input("Set progress to 100% automatically? (yes/no): ").strip().lower() + if auto_complete == "yes": + new_progress = "100" + print("Progress set to 100%.") + + if new_progress_int == 100 and new_status != "已完成": + print("⚠️ Progress is 100% but task is not marked as completed.") + auto_complete = input("Mark as completed? (yes/no): ").strip().lower() + if auto_complete == "yes": + new_status = "已完成" + print("Task marked as completed.") + new_lines.append(f"Title:{new_title}\n") new_lines.append(f"Description:{new_description}\n") new_lines.append(f"Deadline:{new_deadline}\n") new_lines.append(f"Priority:{new_priority}\n") new_lines.append(f"Category:{new_category}\n") + new_lines.append(f"Status:{new_status}\n") + new_lines.append(f"Progress:{new_progress}\n") new_lines.append("==========================\n") else: new_lines.extend(block + ["==========================\n"]) @@ -227,15 +430,17 @@ def print_all_tasks(self): print("4. Update Task ") print("5. List All Titles") print("6. Print All Tasks") - print("7. Exit") - choice = input("Enter your choice (1-7): ").strip() + print("7. Filter by Status") + print("8. Status Statistics") + print("9. Exit") + choice = input("Enter your choice (1-9): ").strip() if choice == "1": obj.add_task() elif choice == "2": - print("Make sure write (Title//Category) otherwise it does not work") + print("Make sure write (Title//Category//Status) otherwise it does not work") print() - field = input("Search by (Title//Category): ").strip().capitalize() + field = input("Search by (Title//Category//Status): ").strip().capitalize() obj.search_task(field) elif choice == "3": obj.delete_task() @@ -246,8 +451,11 @@ def print_all_tasks(self): elif choice == "6": obj.print_all_tasks() elif choice == "7": + obj.filter_by_status() + elif choice == "8": + obj.status_statistics() + elif choice == "9": print("👋 Exiting Task Manager. Goodbye!") break else: print("❌ Invalid input. Try again.") - diff --git a/tests/test_sample.py b/tests/test_sample.py index e2adf39..1be9d54 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -2,46 +2,103 @@ import os import sys -# Make sure Python can find your task_manager.py file sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from task_manager import Task_Manager +from task_manager import TaskManager, TaskStatus, STATUS_TRANSITIONS + class TestTaskManager(unittest.TestCase): def setUp(self): - # Use a separate test file so main data.txt is not affected self.test_file = "test_data.txt" - self.tm = Task_Manager(self.test_file) + self.tm = TaskManager(self.test_file) def tearDown(self): - # Clean up after test: delete test file if it exists if os.path.exists(self.test_file): os.remove(self.test_file) + backup_file = self.test_file.replace(".txt", "_backup.txt") + if os.path.exists(backup_file): + os.remove(backup_file) def test_add_task_function_exists(self): self.assertTrue(hasattr(self.tm, "add_task")) - def test_add_task_file_created(self): - # We'll simulate task writing by mocking input() - import builtins - input_values = [ - "Test Task", # title - "This is a test task", # description - "2025-12-31", # deadline - "High", # priority - "Testing" # category + def test_validate_status_valid(self): + self.assertEqual(self.tm._validate_status("待办"), TaskStatus.TODO) + self.assertEqual(self.tm._validate_status("进行中"), TaskStatus.IN_PROGRESS) + self.assertEqual(self.tm._validate_status("已完成"), TaskStatus.COMPLETED) + self.assertEqual(self.tm._validate_status("已取消"), TaskStatus.CANCELLED) + + def test_validate_status_invalid(self): + self.assertIsNone(self.tm._validate_status("无效状态")) + self.assertIsNone(self.tm._validate_status("")) + + def test_validate_progress_valid(self): + self.assertEqual(self.tm._validate_progress("0"), 0) + self.assertEqual(self.tm._validate_progress("50"), 50) + self.assertEqual(self.tm._validate_progress("100"), 100) + + def test_validate_progress_invalid(self): + self.assertIsNone(self.tm._validate_progress("-1")) + self.assertIsNone(self.tm._validate_progress("101")) + self.assertIsNone(self.tm._validate_progress("abc")) + + def test_status_transitions_todo(self): + self.assertIn(TaskStatus.IN_PROGRESS, STATUS_TRANSITIONS[TaskStatus.TODO]) + self.assertIn(TaskStatus.CANCELLED, STATUS_TRANSITIONS[TaskStatus.TODO]) + self.assertNotIn(TaskStatus.COMPLETED, STATUS_TRANSITIONS[TaskStatus.TODO]) + + def test_status_transitions_in_progress(self): + self.assertIn(TaskStatus.TODO, STATUS_TRANSITIONS[TaskStatus.IN_PROGRESS]) + self.assertIn(TaskStatus.COMPLETED, STATUS_TRANSITIONS[TaskStatus.IN_PROGRESS]) + self.assertIn(TaskStatus.CANCELLED, STATUS_TRANSITIONS[TaskStatus.IN_PROGRESS]) + + def test_status_transitions_completed(self): + self.assertIn(TaskStatus.IN_PROGRESS, STATUS_TRANSITIONS[TaskStatus.COMPLETED]) + self.assertNotIn(TaskStatus.TODO, STATUS_TRANSITIONS[TaskStatus.COMPLETED]) + + def test_status_transitions_cancelled(self): + self.assertIn(TaskStatus.TODO, STATUS_TRANSITIONS[TaskStatus.CANCELLED]) + self.assertNotIn(TaskStatus.IN_PROGRESS, STATUS_TRANSITIONS[TaskStatus.CANCELLED]) + + def test_can_transition_to(self): + self.assertTrue(self.tm._can_transition_to(TaskStatus.TODO, TaskStatus.IN_PROGRESS)) + self.assertFalse(self.tm._can_transition_to(TaskStatus.TODO, TaskStatus.COMPLETED)) + self.assertTrue(self.tm._can_transition_to(TaskStatus.IN_PROGRESS, TaskStatus.COMPLETED)) + + def test_parse_tasks(self): + lines = [ + "Title:Test Task\n", + "Description:Test Description\n", + "Deadline:2025-12-31\n", + "Priority:High\n", + "Category:Test\n", + "Status:待办\n", + "Progress:50\n", + "==========================\n" ] - def mock_input(prompt): - return input_values.pop(0) - - original_input = builtins.input - builtins.input = mock_input - try: - self.tm.add_task() - self.assertTrue(os.path.exists(self.test_file)) - finally: - builtins.input = original_input + tasks = self.tm._parse_tasks(lines) + self.assertEqual(len(tasks), 1) + self.assertEqual(tasks[0]["Title"], "Test Task") + self.assertEqual(tasks[0]["Status"], "待办") + self.assertEqual(tasks[0]["Progress"], "50") + + def test_parse_tasks_multiple(self): + lines = [ + "Title:Task 1\n", + "Status:待办\n", + "Progress:0\n", + "==========================\n", + "Title:Task 2\n", + "Status:进行中\n", + "Progress:50\n", + "==========================\n" + ] + tasks = self.tm._parse_tasks(lines) + self.assertEqual(len(tasks), 2) + self.assertEqual(tasks[0]["Title"], "Task 1") + self.assertEqual(tasks[1]["Status"], "进行中") + if __name__ == '__main__': unittest.main()