add habit goal-period difficulty and habit formation research notes
Ground the 21-day habit myth in actual research (Lally et al. 2010) and let users pick a target period (21/66/254 days or unlimited) matching that study's easy/median/hard automaticity timelines when creating a habit, with progress shown on the habits list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.models.habit import ALL_WEEKDAYS_MASK, HabitType
|
||||
from app.models.habit import ALL_WEEKDAYS_MASK, HabitDifficulty, HabitType
|
||||
from app.schemas.habit import HabitCreate
|
||||
|
||||
|
||||
@@ -49,3 +49,18 @@ def test_weekdays_mask_single_day_accepted():
|
||||
def test_weekdays_mask_defaults_to_all_days():
|
||||
habit = HabitCreate(**_base_kwargs())
|
||||
assert habit.weekdays_mask == ALL_WEEKDAYS_MASK
|
||||
|
||||
|
||||
def test_difficulty_defaults_to_medium():
|
||||
habit = HabitCreate(**_base_kwargs())
|
||||
assert habit.difficulty == HabitDifficulty.MEDIUM
|
||||
|
||||
|
||||
def test_difficulty_accepts_explicit_value():
|
||||
habit = HabitCreate(**_base_kwargs(difficulty=HabitDifficulty.UNLIMITED))
|
||||
assert habit.difficulty == HabitDifficulty.UNLIMITED
|
||||
|
||||
|
||||
def test_difficulty_rejects_invalid_value():
|
||||
with pytest.raises(ValidationError):
|
||||
HabitCreate(**_base_kwargs(difficulty="impossible"))
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
from app.models.habit import ALL_WEEKDAYS_MASK
|
||||
from app.template_utils import heatmap_opacity, weekday_label
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.models.habit import ALL_WEEKDAYS_MASK, Habit, HabitDifficulty, HabitType
|
||||
from app.template_utils import difficulty_label, goal_progress, heatmap_opacity, weekday_label
|
||||
|
||||
|
||||
def _habit_created_days_ago(days_ago: int, difficulty: HabitDifficulty) -> Habit:
|
||||
return Habit(
|
||||
name="테스트",
|
||||
habit_type=HabitType.BUILD,
|
||||
difficulty=difficulty,
|
||||
weekdays_mask=ALL_WEEKDAYS_MASK,
|
||||
created_at=datetime.now() - timedelta(days=days_ago),
|
||||
)
|
||||
|
||||
|
||||
def test_weekday_label_all_days_shows_daily():
|
||||
@@ -29,3 +41,43 @@ def test_heatmap_opacity_full_ratio_caps_at_point_nine():
|
||||
|
||||
def test_heatmap_opacity_partial_ratio():
|
||||
assert heatmap_opacity(checked_count=1, scheduled_count=2) == round(0.12 + 0.5 * 0.78, 2)
|
||||
|
||||
|
||||
def test_difficulty_label_maps_to_target_days():
|
||||
assert difficulty_label(HabitDifficulty.EASY) == "하 (21일)"
|
||||
assert difficulty_label(HabitDifficulty.MEDIUM) == "중 (66일)"
|
||||
assert difficulty_label(HabitDifficulty.HARD) == "상 (254일)"
|
||||
assert difficulty_label(HabitDifficulty.UNLIMITED) == "기간 무제한"
|
||||
|
||||
|
||||
def test_goal_progress_none_for_unlimited():
|
||||
habit = _habit_created_days_ago(10, HabitDifficulty.UNLIMITED)
|
||||
assert goal_progress(habit) is None
|
||||
|
||||
|
||||
def test_goal_progress_day_one_on_creation_day():
|
||||
habit = _habit_created_days_ago(0, HabitDifficulty.EASY)
|
||||
progress = goal_progress(habit)
|
||||
assert progress == {"elapsed": 1, "target": 21, "remaining": 20, "reached": False}
|
||||
|
||||
|
||||
def test_goal_progress_mid_period():
|
||||
habit = _habit_created_days_ago(10, HabitDifficulty.MEDIUM)
|
||||
progress = goal_progress(habit)
|
||||
assert progress == {"elapsed": 11, "target": 66, "remaining": 55, "reached": False}
|
||||
|
||||
|
||||
def test_goal_progress_reached_caps_elapsed_at_target():
|
||||
habit = _habit_created_days_ago(100, HabitDifficulty.EASY)
|
||||
progress = goal_progress(habit)
|
||||
assert progress == {"elapsed": 21, "target": 21, "remaining": 0, "reached": True}
|
||||
|
||||
|
||||
def test_target_days_and_goal_target_date_on_model():
|
||||
habit = _habit_created_days_ago(0, HabitDifficulty.EASY)
|
||||
assert habit.target_days == 21
|
||||
assert habit.goal_target_date == habit.created_at.date() + timedelta(days=20)
|
||||
|
||||
unlimited = _habit_created_days_ago(0, HabitDifficulty.UNLIMITED)
|
||||
assert unlimited.target_days is None
|
||||
assert unlimited.goal_target_date is None
|
||||
|
||||
Reference in New Issue
Block a user