feat: 6.0 - Backend API - GET /api/habits with streaks
This commit is contained in:
@@ -789,7 +789,7 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
|||||||
self.send_json({'error': str(e)}, 500)
|
self.send_json({'error': str(e)}, 500)
|
||||||
|
|
||||||
def handle_habits_get(self):
|
def handle_habits_get(self):
|
||||||
"""Get all habits from habits.json."""
|
"""Get all habits from habits.json with calculated streaks."""
|
||||||
try:
|
try:
|
||||||
habits_file = KANBAN_DIR / 'habits.json'
|
habits_file = KANBAN_DIR / 'habits.json'
|
||||||
|
|
||||||
@@ -816,8 +816,26 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
|||||||
habits = data.get('habits', [])
|
habits = data.get('habits', [])
|
||||||
last_updated = data.get('lastUpdated', datetime.now().isoformat())
|
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({
|
self.send_json({
|
||||||
'habits': habits,
|
'habits': enhanced_habits,
|
||||||
'lastUpdated': last_updated
|
'lastUpdated': last_updated
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
252
dashboard/test_habits_get_enhanced.py
Normal file
252
dashboard/test_habits_get_enhanced.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user