Files
shinalok 800f42eab1 add habit leveling/XP system, explicit fail marking for missed habits, and scrollable habit tabs
- Habits and the account now accumulate XP and levels from check streaks, with
  streak/level-up celebration banners and push notifications.
- The "yesterday missed" banner on /today now lets a habit be explicitly marked
  failed (not just checked done), backed by a new HabitLog.status column so
  completion-rate/streak calculations never count a failed day as done.
- The habit management tabs scroll horizontally on narrow screens instead of
  wrapping to two lines, with a pure-CSS edge shadow indicating more content.
2026-07-20 18:18:56 +09:00

32 lines
1.2 KiB
Python

import enum
from datetime import date, datetime
from sqlalchemy import Date, Enum, ForeignKey, Integer, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from app.database import Base
from app.models.habit import _by_value
class HabitLogStatus(str, enum.Enum):
DONE = "done"
FAILED = "failed" # /today의 "어제 놓친 습관" 배너에서 사용자가 명시적으로 실패를 확정한 경우
class HabitLog(Base):
__tablename__ = "habit_log"
__table_args__ = (UniqueConstraint("habit_id", "log_date", name="uq_habit_log_habit_date"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
habit_id: Mapped[int] = mapped_column(ForeignKey("habit.id", ondelete="CASCADE"), nullable=False)
log_date: Mapped[date] = mapped_column(Date, nullable=False)
status: Mapped[HabitLogStatus] = mapped_column(
Enum(HabitLogStatus, native_enum=False, length=20, values_callable=_by_value),
nullable=False,
default=HabitLogStatus.DONE,
)
checked_at: Mapped[datetime] = mapped_column(server_default=func.now(), nullable=False)
habit: Mapped["Habit"] = relationship(back_populates="logs")