diff --git a/dashboard/api.py b/dashboard/api.py index 0669bed..ad60342 100644 --- a/dashboard/api.py +++ b/dashboard/api.py @@ -789,7 +789,7 @@ class TaskBoardHandler(SimpleHTTPRequestHandler): self.send_json({'error': str(e)}, 500) def handle_habits_get(self): - """Get all habits from habits.json.""" + """Get all habits from habits.json with calculated streaks.""" try: habits_file = KANBAN_DIR / 'habits.json' @@ -816,8 +816,26 @@ class TaskBoardHandler(SimpleHTTPRequestHandler): habits = data.get('habits', []) last_updated = data.get('lastUpdated', datetime.now().isoformat()) + # Get today's date in YYYY-MM-DD format + today = datetime.now().date().isoformat() + + # Enhance each habit with streak and checkedToday + enhanced_habits = [] + for habit in habits: + # Calculate streak using the utility function + completions = habit.get('completions', []) + frequency = habit.get('frequency', 'daily') + streak = calculate_streak(completions, frequency) + + # Check if habit was completed today + checked_today = today in completions + + # Add calculated fields to habit + enhanced_habit = {**habit, 'streak': streak, 'checkedToday': checked_today} + enhanced_habits.append(enhanced_habit) + self.send_json({ - 'habits': habits, + 'habits': enhanced_habits, 'lastUpdated': last_updated }) except Exception as e: diff --git a/dashboard/test_habits_get_enhanced.py b/dashboard/test_habits_get_enhanced.py new file mode 100644 index 0000000..53e412a --- /dev/null +++ b/dashboard/test_habits_get_enhanced.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +""" +Tests for enhanced GET /api/habits endpoint with streak and checkedToday fields. +""" + +import json +import sys +import urllib.request +from pathlib import Path +from datetime import datetime, timedelta + +BASE_URL = 'http://localhost:8088' +KANBAN_DIR = Path(__file__).parent + + +def test_habits_get_includes_streak_field(): + """Test that each habit includes a 'streak' field.""" + # Create test habit with completions + today = datetime.now().date() + yesterday = today - timedelta(days=1) + two_days_ago = today - timedelta(days=2) + + test_data = { + 'habits': [ + { + 'id': 'habit-test1', + 'name': 'Test Habit', + 'frequency': 'daily', + 'createdAt': '2026-02-01T10:00:00Z', + 'completions': [ + two_days_ago.isoformat(), + yesterday.isoformat(), + today.isoformat() + ] + } + ], + 'lastUpdated': datetime.now().isoformat() + } + + habits_file = KANBAN_DIR / 'habits.json' + habits_file.write_text(json.dumps(test_data, indent=2), encoding='utf-8') + + # Test GET + req = urllib.request.Request(f'{BASE_URL}/api/habits') + with urllib.request.urlopen(req) as response: + result = json.loads(response.read().decode('utf-8')) + + assert 'habits' in result, "Response should contain habits array" + assert len(result['habits']) == 1, "Should have one habit" + habit = result['habits'][0] + assert 'streak' in habit, "Habit should include 'streak' field" + assert isinstance(habit['streak'], int), "Streak should be an integer" + assert habit['streak'] == 3, f"Expected streak of 3, got {habit['streak']}" + + print("✓ Each habit includes 'streak' field") + + +def test_habits_get_includes_checked_today_field(): + """Test that each habit includes a 'checkedToday' field.""" + today = datetime.now().date().isoformat() + + test_data = { + 'habits': [ + { + 'id': 'habit-test1', + 'name': 'Checked Today', + 'frequency': 'daily', + 'createdAt': '2026-02-01T10:00:00Z', + 'completions': [today] + }, + { + 'id': 'habit-test2', + 'name': 'Not Checked Today', + 'frequency': 'daily', + 'createdAt': '2026-02-01T10:00:00Z', + 'completions': ['2026-02-01'] + } + ], + 'lastUpdated': datetime.now().isoformat() + } + + habits_file = KANBAN_DIR / 'habits.json' + habits_file.write_text(json.dumps(test_data, indent=2), encoding='utf-8') + + # Test GET + req = urllib.request.Request(f'{BASE_URL}/api/habits') + with urllib.request.urlopen(req) as response: + result = json.loads(response.read().decode('utf-8')) + + assert len(result['habits']) == 2, "Should have two habits" + + habit1 = result['habits'][0] + assert 'checkedToday' in habit1, "Habit should include 'checkedToday' field" + assert isinstance(habit1['checkedToday'], bool), "checkedToday should be boolean" + assert habit1['checkedToday'] is True, "Habit checked today should have checkedToday=True" + + habit2 = result['habits'][1] + assert 'checkedToday' in habit2, "Habit should include 'checkedToday' field" + assert habit2['checkedToday'] is False, "Habit not checked today should have checkedToday=False" + + print("✓ Each habit includes 'checkedToday' boolean field") + + +def test_habits_get_calculates_streak_correctly(): + """Test that streak is calculated using the streak utility function.""" + today = datetime.now().date() + yesterday = today - timedelta(days=1) + two_days_ago = today - timedelta(days=2) + three_days_ago = today - timedelta(days=3) + four_days_ago = today - timedelta(days=4) + + test_data = { + 'habits': [ + { + 'id': 'habit-daily', + 'name': 'Daily Habit', + 'frequency': 'daily', + 'createdAt': '2026-02-01T10:00:00Z', + 'completions': [ + four_days_ago.isoformat(), + three_days_ago.isoformat(), + two_days_ago.isoformat(), + yesterday.isoformat(), + today.isoformat() + ] + }, + { + 'id': 'habit-broken', + 'name': 'Broken Streak', + 'frequency': 'daily', + 'createdAt': '2026-02-01T10:00:00Z', + 'completions': [ + four_days_ago.isoformat(), + three_days_ago.isoformat() + # Missing two_days_ago - streak broken + ] + }, + { + 'id': 'habit-weekly', + 'name': 'Weekly Habit', + 'frequency': 'weekly', + 'createdAt': '2026-02-01T10:00:00Z', + 'completions': [ + today.isoformat(), + (today - timedelta(days=7)).isoformat(), + (today - timedelta(days=14)).isoformat() + ] + } + ], + 'lastUpdated': datetime.now().isoformat() + } + + habits_file = KANBAN_DIR / 'habits.json' + habits_file.write_text(json.dumps(test_data, indent=2), encoding='utf-8') + + # Test GET + req = urllib.request.Request(f'{BASE_URL}/api/habits') + with urllib.request.urlopen(req) as response: + result = json.loads(response.read().decode('utf-8')) + + assert len(result['habits']) == 3, "Should have three habits" + + daily_habit = result['habits'][0] + assert daily_habit['streak'] == 5, f"Expected daily streak of 5, got {daily_habit['streak']}" + + broken_habit = result['habits'][1] + assert broken_habit['streak'] == 0, f"Expected broken streak of 0, got {broken_habit['streak']}" + + weekly_habit = result['habits'][2] + assert weekly_habit['streak'] == 3, f"Expected weekly streak of 3, got {weekly_habit['streak']}" + + print("✓ Streak is calculated correctly using utility function") + + +def test_habits_get_empty_habits_array(): + """Test GET with empty habits array.""" + test_data = { + 'habits': [], + 'lastUpdated': datetime.now().isoformat() + } + + habits_file = KANBAN_DIR / 'habits.json' + habits_file.write_text(json.dumps(test_data, indent=2), encoding='utf-8') + + # Test GET + req = urllib.request.Request(f'{BASE_URL}/api/habits') + with urllib.request.urlopen(req) as response: + result = json.loads(response.read().decode('utf-8')) + + assert result['habits'] == [], "Should return empty array" + assert 'lastUpdated' in result, "Should include lastUpdated" + + print("✓ Empty habits array handled correctly") + + +def test_habits_get_preserves_original_fields(): + """Test that all original habit fields are preserved.""" + today = datetime.now().date().isoformat() + + test_data = { + 'habits': [ + { + 'id': 'habit-test1', + 'name': 'Test Habit', + 'frequency': 'daily', + 'createdAt': '2026-02-01T10:00:00Z', + 'completions': [today] + } + ], + 'lastUpdated': '2026-02-10T10:00:00Z' + } + + habits_file = KANBAN_DIR / 'habits.json' + habits_file.write_text(json.dumps(test_data, indent=2), encoding='utf-8') + + # Test GET + req = urllib.request.Request(f'{BASE_URL}/api/habits') + with urllib.request.urlopen(req) as response: + result = json.loads(response.read().decode('utf-8')) + + habit = result['habits'][0] + assert habit['id'] == 'habit-test1', "Original id should be preserved" + assert habit['name'] == 'Test Habit', "Original name should be preserved" + assert habit['frequency'] == 'daily', "Original frequency should be preserved" + assert habit['createdAt'] == '2026-02-01T10:00:00Z', "Original createdAt should be preserved" + assert habit['completions'] == [today], "Original completions should be preserved" + assert 'streak' in habit, "Should add streak field" + assert 'checkedToday' in habit, "Should add checkedToday field" + + print("✓ All original habit fields are preserved") + + +if __name__ == '__main__': + try: + print("\n=== Testing Enhanced GET /api/habits ===\n") + + test_habits_get_includes_streak_field() + test_habits_get_includes_checked_today_field() + test_habits_get_calculates_streak_correctly() + test_habits_get_empty_habits_array() + test_habits_get_preserves_original_fields() + + print("\n=== All Enhanced GET Tests Passed ✓ ===\n") + sys.exit(0) + except AssertionError as e: + print(f"\n❌ Test failed: {e}\n") + sys.exit(1) + except Exception as e: + print(f"\n❌ Error: {e}\n") + import traceback + traceback.print_exc() + sys.exit(1)