Feature: Habit Tracker with Streak Calculation #1

Closed
Marius wants to merge 26 commits from feature/habit-tracker into master
9 changed files with 1711 additions and 0 deletions
Showing only changes of commit c84135d67c - Show all commits

View File

@@ -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:

View 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)