Initial commit: habit tracker PWA with Google OAuth, push notifications
FastAPI + SQLAlchemy/Alembic + MariaDB backend with Jinja2/htmx/Alpine server-rendered frontend. Multi-user via Google OAuth, daily habit tracking, monthly/weekly history views, Web Push reminders via APScheduler, and PWA support (manifest, service worker, offline caching). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.habit import Habit, HabitStatus, HabitType
|
||||
from app.schemas.habit import HabitCreate, HabitUpdate
|
||||
|
||||
|
||||
def list_habits(
|
||||
db: Session, user_id: int, habit_type: HabitType | None = None, status: HabitStatus | None = None
|
||||
) -> list[Habit]:
|
||||
stmt = select(Habit).where(Habit.user_id == user_id)
|
||||
if habit_type is not None:
|
||||
stmt = stmt.where(Habit.habit_type == habit_type)
|
||||
if status is not None:
|
||||
stmt = stmt.where(Habit.status == status)
|
||||
stmt = stmt.order_by(Habit.sort_order.is_(None), Habit.sort_order, Habit.created_at)
|
||||
return list(db.scalars(stmt))
|
||||
|
||||
|
||||
def list_active_habits_with_reminders(db: Session) -> list[Habit]:
|
||||
"""스케줄러 전용: 유저 스코핑 없이 알림 시각이 설정된 전체 active 습관을 반환한다."""
|
||||
stmt = select(Habit).where(Habit.status == HabitStatus.ACTIVE, Habit.reminder_time.isnot(None))
|
||||
return list(db.scalars(stmt))
|
||||
|
||||
|
||||
def list_active_user_ids(db: Session) -> list[int]:
|
||||
"""스케줄러 전용: 유저 스코핑 없이, active 습관을 하나 이상 가진 유저 id 목록을 반환한다(주간/월간 요약 알림 대상)."""
|
||||
stmt = (
|
||||
select(Habit.user_id)
|
||||
.where(Habit.status == HabitStatus.ACTIVE, Habit.user_id.isnot(None))
|
||||
.distinct()
|
||||
)
|
||||
return list(db.scalars(stmt))
|
||||
|
||||
|
||||
def get_habit(db: Session, habit_id: int, user_id: int) -> Habit | None:
|
||||
return db.scalar(select(Habit).where(Habit.id == habit_id, Habit.user_id == user_id))
|
||||
|
||||
|
||||
def create_habit(db: Session, user_id: int, data: HabitCreate) -> Habit:
|
||||
habit = Habit(
|
||||
user_id=user_id,
|
||||
name=data.name,
|
||||
habit_type=data.habit_type,
|
||||
weekdays_mask=data.weekdays_mask,
|
||||
condition_text=data.condition_text,
|
||||
reminder_time=data.reminder_time,
|
||||
status=HabitStatus.ACTIVE,
|
||||
)
|
||||
db.add(habit)
|
||||
db.commit()
|
||||
db.refresh(habit)
|
||||
return habit
|
||||
|
||||
|
||||
def update_habit(db: Session, habit: Habit, data: HabitUpdate) -> Habit:
|
||||
habit.name = data.name
|
||||
habit.habit_type = data.habit_type
|
||||
habit.weekdays_mask = data.weekdays_mask
|
||||
habit.condition_text = data.condition_text
|
||||
habit.reminder_time = data.reminder_time
|
||||
db.commit()
|
||||
db.refresh(habit)
|
||||
return habit
|
||||
|
||||
|
||||
def delete_habit(db: Session, habit: Habit) -> None:
|
||||
db.delete(habit)
|
||||
db.commit()
|
||||
|
||||
|
||||
def complete_habit(db: Session, habit: Habit) -> Habit:
|
||||
habit.status = HabitStatus.COMPLETED
|
||||
habit.completed_at = datetime.now()
|
||||
db.commit()
|
||||
db.refresh(habit)
|
||||
return habit
|
||||
|
||||
|
||||
def reactivate_habit(db: Session, habit: Habit) -> Habit:
|
||||
habit.status = HabitStatus.ACTIVE
|
||||
habit.completed_at = None
|
||||
db.commit()
|
||||
db.refresh(habit)
|
||||
return habit
|
||||
|
||||
|
||||
def reorder_habits(db: Session, user_id: int, ordered_ids: list[int]) -> None:
|
||||
"""ordered_ids에 나온 순서대로 sort_order를 다시 매긴다. 목록에 없는 id나 다른 유저의 habit은 무시한다."""
|
||||
habits = db.scalars(
|
||||
select(Habit).where(Habit.id.in_(ordered_ids), Habit.user_id == user_id)
|
||||
).all()
|
||||
habit_map = {h.id: h for h in habits}
|
||||
for index, habit_id in enumerate(ordered_ids):
|
||||
habit = habit_map.get(habit_id)
|
||||
if habit is not None:
|
||||
habit.sort_order = index
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user