add retroactive check for yesterday's missed habits on /today
Users often only realize a habit was missed the next day; a same-day-only check made it impossible to record it after the fact.
This commit is contained in:
@@ -51,6 +51,7 @@ def _today_context(db: Session, user_id: int, **extra) -> dict:
|
|||||||
return {
|
return {
|
||||||
"build_items": build_items,
|
"build_items": build_items,
|
||||||
"quit_items": quit_items,
|
"quit_items": quit_items,
|
||||||
|
"yesterday_missed": log_service.get_yesterday_missed_items(db, user_id),
|
||||||
"total_count": len(all_items),
|
"total_count": len(all_items),
|
||||||
"checked_count": sum(1 for item in all_items if item.checked),
|
"checked_count": sum(1 for item in all_items if item.checked),
|
||||||
**extra,
|
**extra,
|
||||||
@@ -89,6 +90,26 @@ def toggle_today_page(request: Request, habit_id: int, db: Session = Depends(get
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/today/{habit_id}/toggle-yesterday")
|
||||||
|
def toggle_yesterday_page(request: Request, habit_id: int, db: Session = Depends(get_db)):
|
||||||
|
current = _current_user_or_redirect(request, db)
|
||||||
|
if isinstance(current, RedirectResponse):
|
||||||
|
return current
|
||||||
|
|
||||||
|
habit = habit_service.get_habit(db, habit_id, current.id)
|
||||||
|
if habit is None:
|
||||||
|
return Response(status_code=404)
|
||||||
|
|
||||||
|
yesterday = date.today() - timedelta(days=1)
|
||||||
|
_, milestone_streak = log_service.toggle_check_and_celebrate(db, habit, yesterday)
|
||||||
|
extra = {"celebrate_habit_name": habit.name, "celebrate_streak": milestone_streak} if milestone_streak else {}
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
|
"partials/today_content.html",
|
||||||
|
{"logged_in": True, "current_user": current, **_today_context(db, current.id, **extra)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/habits")
|
@router.get("/habits")
|
||||||
def habits_page(request: Request, tab: str = "build", db: Session = Depends(get_db)):
|
def habits_page(request: Request, tab: str = "build", db: Session = Depends(get_db)):
|
||||||
current = _current_user_or_redirect(request, db)
|
current = _current_user_or_redirect(request, db)
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ def get_today_items(db: Session, user_id: int, target_date: date) -> tuple[list[
|
|||||||
return build_items, quit_items
|
return build_items, quit_items
|
||||||
|
|
||||||
|
|
||||||
|
def get_yesterday_missed_items(db: Session, user_id: int) -> list[TodayItem]:
|
||||||
|
"""어제 예정되어 있었지만 아직 체크하지 않은 습관 목록 (형성+중단 합쳐서, /today 화면의 소급 체크용)."""
|
||||||
|
yesterday = date.today() - timedelta(days=1)
|
||||||
|
build_items, quit_items = get_today_items(db, user_id, yesterday)
|
||||||
|
return [item for item in build_items + quit_items if not item.checked]
|
||||||
|
|
||||||
|
|
||||||
def toggle_check(db: Session, habit_id: int, log_date: date) -> bool:
|
def toggle_check(db: Session, habit_id: int, log_date: date) -> bool:
|
||||||
"""체크 상태를 반전시키고 토글 후의 체크 여부를 반환한다.
|
"""체크 상태를 반전시키고 토글 후의 체크 여부를 반환한다.
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if yesterday_missed %}
|
||||||
|
<h2>어제 놓친 습관</h2>
|
||||||
|
<div class="card">
|
||||||
|
{% for item in yesterday_missed %}
|
||||||
|
{% include "partials/yesterday_item.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h2>형성 습관</h2>
|
<h2>형성 습관</h2>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
{% for item in build_items %}
|
{% for item in build_items %}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="habit-item">
|
||||||
|
<div class="habit-item-main">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="check-indicator"
|
||||||
|
hx-post="/today/{{ item.habit_id }}/toggle-yesterday"
|
||||||
|
hx-target="#today-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
aria-label="{{ item.name }} 어제 체크"
|
||||||
|
></button>
|
||||||
|
<div>
|
||||||
|
<div class="habit-item-name">{{ item.name }}</div>
|
||||||
|
{% if item.condition_text %}<div class="habit-item-condition">{{ item.condition_text }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -123,6 +123,33 @@ def test_get_today_items_excludes_habits_not_scheduled_today(db_session, test_us
|
|||||||
assert quit_items == []
|
assert quit_items == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---- get_yesterday_missed_items ----
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_yesterday_missed_items_includes_unchecked_scheduled_habit(db_session, test_user):
|
||||||
|
habit = _make_habit(db_session, test_user.id, name="어제 놓친 습관")
|
||||||
|
|
||||||
|
missed = log_service.get_yesterday_missed_items(db_session, test_user.id)
|
||||||
|
assert [item.habit_id for item in missed] == [habit.id]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_yesterday_missed_items_excludes_already_checked(db_session, test_user):
|
||||||
|
habit = _make_habit(db_session, test_user.id)
|
||||||
|
_check(db_session, habit.id, date.today() - timedelta(days=1))
|
||||||
|
|
||||||
|
missed = log_service.get_yesterday_missed_items(db_session, test_user.id)
|
||||||
|
assert missed == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_yesterday_missed_items_excludes_habits_not_scheduled_yesterday(db_session, test_user):
|
||||||
|
yesterday = date.today() - timedelta(days=1)
|
||||||
|
other_day_mask = 1 << ((yesterday.weekday() + 1) % 7) # 어제가 아닌 요일 하나만 선택
|
||||||
|
_make_habit(db_session, test_user.id, weekdays_mask=other_day_mask)
|
||||||
|
|
||||||
|
missed = log_service.get_yesterday_missed_items(db_session, test_user.id)
|
||||||
|
assert missed == []
|
||||||
|
|
||||||
|
|
||||||
# ---- get_habit_stats: completion_rate ----
|
# ---- get_habit_stats: completion_rate ----
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from app.models.habit import ALL_WEEKDAYS_MASK, HabitType
|
from app.models.habit import ALL_WEEKDAYS_MASK, HabitType
|
||||||
|
from app.models.habit_log import HabitLog
|
||||||
from app.schemas.habit import HabitCreate
|
from app.schemas.habit import HabitCreate
|
||||||
from app.services import habit_service
|
from app.services import habit_service
|
||||||
|
|
||||||
@@ -38,3 +41,31 @@ def test_toggle_today_page_for_other_users_habit_returns_404(auth_client, db_ses
|
|||||||
|
|
||||||
response = auth_client.post(f"/today/{others_habit.id}/toggle")
|
response = auth_client.post(f"/today/{others_habit.id}/toggle")
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_today_page_shows_yesterday_missed_habit(auth_client, db_session, test_user):
|
||||||
|
_make_habit(db_session, test_user.id, name="어제 놓친 습관")
|
||||||
|
|
||||||
|
response = auth_client.get("/today")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "어제 놓친 습관" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_toggle_yesterday_page_creates_log_for_yesterday(auth_client, db_session, test_user):
|
||||||
|
habit = _make_habit(db_session, test_user.id, name="어제 습관")
|
||||||
|
|
||||||
|
response = auth_client.post(f"/today/{habit.id}/toggle-yesterday")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
yesterday = date.today() - timedelta(days=1)
|
||||||
|
log = db_session.query(HabitLog).filter_by(habit_id=habit.id, log_date=yesterday).one()
|
||||||
|
assert log is not None
|
||||||
|
# 체크되었으니 더 이상 "어제 놓친 습관" 목록에 나오지 않아야 한다.
|
||||||
|
assert "어제 놓친 습관" not in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_toggle_yesterday_page_for_other_users_habit_returns_404(auth_client, db_session, other_user):
|
||||||
|
others_habit = _make_habit(db_session, other_user.id, name="남의 습관")
|
||||||
|
|
||||||
|
response = auth_client.post(f"/today/{others_habit.id}/toggle-yesterday")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|||||||
Reference in New Issue
Block a user