feat: add task and habit reordering
ci / docker (push) Successful in 3m33s

This commit is contained in:
2026-09-08 06:51:31 +08:00
parent a02b6f660c
commit 988d131d9b
13 changed files with 329 additions and 22 deletions
+36 -5
View File
@@ -525,6 +525,16 @@ class HabitCreate(BaseModel):
return self
class HabitReorder(BaseModel):
habit_ids: list[UUID] = Field(min_length=1)
@model_validator(mode="after")
def unique_ids(self):
if len(self.habit_ids) != len(set(self.habit_ids)):
raise ValueError("habit_ids must be unique")
return self
class HabitLogInput(BaseModel):
day: date
value: float = Field(gt=0)
@@ -541,7 +551,7 @@ class PauseInput(BaseModel):
def habit_dict(h):
return {"id": h.id, "name": h.name, "kind": h.kind, "target": h.target, "max_value": h.max_value, "schedule_type": h.schedule_type, "weekdays": [int(x) for x in h.weekdays.split(",")] if h.weekdays else None, "month_days": [int(x) for x in h.month_days.split(",")] if h.month_days else None, "interval_days": h.interval_days, "start_date": h.start_date, "archived_at": h.archived_at}
return {"id": h.id, "name": h.name, "kind": h.kind, "target": h.target, "max_value": h.max_value, "schedule_type": h.schedule_type, "weekdays": [int(x) for x in h.weekdays.split(",")] if h.weekdays else None, "month_days": [int(x) for x in h.month_days.split(",")] if h.month_days else None, "interval_days": h.interval_days, "start_date": h.start_date, "archived_at": h.archived_at, "position": h.position}
async def owned_habit(db, user_id, habit_id):
@@ -552,14 +562,34 @@ async def owned_habit(db, user_id, habit_id):
@router.post("/habits", status_code=201)
async def create_habit(payload: HabitCreate, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
row = Habit(user_id=user.id, **payload.model_dump(exclude={"weekdays", "month_days"}), weekdays=",".join(map(str, payload.weekdays)) if payload.weekdays else None, month_days=",".join(map(str, payload.month_days)) if payload.month_days else None)
max_position = await db.scalar(select(func.max(Habit.position)).where(Habit.user_id == user.id))
row = Habit(user_id=user.id, position=(max_position if max_position is not None else -1) + 1, **payload.model_dump(exclude={"weekdays", "month_days"}), weekdays=",".join(map(str, payload.weekdays)) if payload.weekdays else None, month_days=",".join(map(str, payload.month_days)) if payload.month_days else None)
db.add(row); await db.commit(); await db.refresh(row); return habit_dict(row)
@router.get("/habits")
async def list_habits(archived: bool = False, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
condition = Habit.archived_at.is_not(None) if archived else Habit.archived_at.is_(None)
return [habit_dict(h) for h in (await db.scalars(select(Habit).where(Habit.user_id == user.id, condition).order_by(Habit.created_at))).all()]
return [habit_dict(h) for h in (await db.scalars(select(Habit).where(Habit.user_id == user.id, condition).order_by(Habit.position, Habit.created_at))).all()]
@router.put("/habits/reorder", status_code=204)
async def reorder_habits(payload: HabitReorder, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
rows = list((await db.scalars(select(Habit).where(
Habit.id.in_(payload.habit_ids), Habit.user_id == user.id, Habit.archived_at.is_(None)
))).all())
if len(rows) != len(payload.habit_ids):
raise HTTPException(404, "习惯不存在")
scope_rows = list((await db.scalars(select(Habit).where(
Habit.user_id == user.id, Habit.archived_at.is_(None)
).order_by(Habit.position, Habit.created_at))).all())
requested = set(payload.habit_ids)
ordered_rows = iter([next(row for row in rows if row.id == habit_id) for habit_id in payload.habit_ids])
merged = [next(ordered_rows) if row.id in requested else row for row in scope_rows]
for position, row in enumerate(merged):
row.position = position
await db.commit()
return Response(status_code=204)
@router.patch("/habits/{habit_id}")
@@ -623,7 +653,7 @@ def scheduled(h, day):
@router.get("/habits/grid")
async def habits_grid(week: date, user: User = Depends(current_user), db: AsyncSession = Depends(get_db)):
start = week - timedelta(days=week.weekday()); days = [start + timedelta(days=i) for i in range(7)]
habits = list((await db.scalars(select(Habit).where(Habit.user_id == user.id, Habit.archived_at.is_(None)).order_by(Habit.created_at))).all())
habits = list((await db.scalars(select(Habit).where(Habit.user_id == user.id, Habit.archived_at.is_(None)).order_by(Habit.position, Habit.created_at))).all())
if not habits:
return {"days": days, "habits": []}
habit_ids = [habit.id for habit in habits]
@@ -755,7 +785,7 @@ async def export_json(user: User = Depends(current_user), db: AsyncSession = Dep
"folders": [serialize(x, ["id", "name", "position", "deleted_at"]) for x in folders],
"lists": [serialize(x, ["id", "folder_id", "name", "is_inbox", "position", "deleted_at"]) for x in lists],
"tasks": [serialize(x, ["id", "list_id", "parent_id", "title", "description", "priority", "completed", "due_at", "external_id", "deleted_at"]) for x in tasks],
"habits": [serialize(x, ["id", "name", "kind", "target", "max_value", "schedule_type", "weekdays", "month_days", "interval_days", "start_date", "archived_at"]) for x in habits],
"habits": [serialize(x, ["id", "name", "kind", "target", "max_value", "schedule_type", "weekdays", "month_days", "interval_days", "start_date", "archived_at", "position"]) for x in habits],
"countdowns": [serialize(x, ["id", "title", "event_date", "calendar_mode", "lunar_month", "lunar_day", "ignore_year", "kind", "repeat_rule", "icon", "pinned", "archived_at", "created_at", "updated_at"]) for x in countdowns],
}
@@ -835,6 +865,7 @@ async def restore_json(payload: dict, mode: str = Query("merge", pattern="^(merg
interval_days=raw.get("interval_days"),
start_date=date.fromisoformat(raw["start_date"]),
archived_at=datetime.fromisoformat(raw["archived_at"]) if raw.get("archived_at") else None,
position=raw.get("position", 0),
)
db.add(row)
existing_countdown_ids = set((await db.scalars(select(Countdown.id).where(Countdown.user_id == user.id))).all())