feat: Implement active workout flow with status management

- Added `status`, `total_sets`, and `total_volume` fields to the Workout model.
- Introduced `source_kind`, `title_snapshot`, and `image_s3_url_snapshot` fields to the WorkoutItem model.
- Created endpoints for managing active workouts, including finishing and discarding workouts.
- Updated workout creation to ensure only one active workout exists per user.
- Implemented batch addition of workout sets and updates to workout set details.
- Enhanced database schema with Alembic migrations to support new fields and constraints.
- Added validation to ensure at least one field is provided for workout set updates.
- Updated calorie estimation logic to reflect new workout set structure.
This commit is contained in:
Artem Kashaev
2026-05-29 10:09:56 +05:00
parent d7b0c7754f
commit 7b34ce1a98
30 changed files with 2081 additions and 846 deletions
+16
View File
@@ -67,12 +67,21 @@ class Exercise(Base, TimestampMixin):
class Workout(Base, TimestampMixin):
__tablename__ = "logic_workouts"
__table_args__ = (
CheckConstraint(
"status IN ('active', 'finished', 'discarded')",
name="ck_workout_status",
),
)
id: Mapped[uuid.UUID] = uuid_pk()
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
status: Mapped[str] = mapped_column(String(20), default="active", index=True)
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
notes: Mapped[str | None] = mapped_column(Text)
total_sets: Mapped[int] = mapped_column(Integer, default=0)
total_volume: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
estimated_calories: Mapped[float] = mapped_column(Numeric(10, 2), default=0)
items: Mapped[list[WorkoutItem]] = relationship(
@@ -88,12 +97,19 @@ class WorkoutItem(Base):
"(exercise_id IS NULL AND equipment_id IS NOT NULL)",
name="ck_workout_item_exactly_one_entity",
),
CheckConstraint(
"source_kind IN ('exercise', 'equipment')",
name="ck_workout_item_source_kind",
),
)
id: Mapped[uuid.UUID] = uuid_pk()
workout_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("logic_workouts.id", ondelete="CASCADE")
)
source_kind: Mapped[str] = mapped_column(String(20))
title_snapshot: Mapped[str] = mapped_column(String(160))
image_s3_url_snapshot: Mapped[str | None] = mapped_column(Text)
exercise_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("logic_exercises.id"))
equipment_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("logic_equipment.id"))
order_index: Mapped[int] = mapped_column(Integer, default=0)