7b34ce1a98
- 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.
142 lines
5.5 KiB
Python
142 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
CheckConstraint,
|
|
DateTime,
|
|
ForeignKey,
|
|
Integer,
|
|
Numeric,
|
|
String,
|
|
Text,
|
|
)
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
def uuid_pk() -> Mapped[uuid.UUID]:
|
|
return mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
|
|
|
|
def now() -> datetime:
|
|
return datetime.now(UTC)
|
|
|
|
|
|
class TimestampMixin:
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now, onupdate=now)
|
|
|
|
|
|
class Equipment(Base, TimestampMixin):
|
|
__tablename__ = "logic_equipment"
|
|
|
|
id: Mapped[uuid.UUID] = uuid_pk()
|
|
owner_user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), index=True)
|
|
name: Mapped[str] = mapped_column(String(160), index=True)
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
image_s3_url: Mapped[str | None] = mapped_column(Text)
|
|
image_s3_key: Mapped[str | None] = mapped_column(Text)
|
|
is_builtin: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
|
|
|
workout_items: Mapped[list[WorkoutItem]] = relationship(back_populates="equipment")
|
|
|
|
|
|
class Exercise(Base, TimestampMixin):
|
|
__tablename__ = "logic_exercises"
|
|
|
|
id: Mapped[uuid.UUID] = uuid_pk()
|
|
owner_user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), index=True)
|
|
equipment_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("logic_equipment.id"))
|
|
name: Mapped[str] = mapped_column(String(160), index=True)
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
image_s3_url: Mapped[str | None] = mapped_column(Text)
|
|
image_s3_key: Mapped[str | None] = mapped_column(Text)
|
|
is_builtin: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
|
default_calories_per_minute: Mapped[float | None] = mapped_column(Numeric(8, 2))
|
|
|
|
equipment: Mapped[Equipment | None] = relationship()
|
|
workout_items: Mapped[list[WorkoutItem]] = relationship(back_populates="exercise")
|
|
|
|
|
|
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(
|
|
back_populates="workout", cascade="all, delete-orphan", order_by="WorkoutItem.order_index"
|
|
)
|
|
|
|
|
|
class WorkoutItem(Base):
|
|
__tablename__ = "logic_workout_items"
|
|
__table_args__ = (
|
|
CheckConstraint(
|
|
"(exercise_id IS NOT NULL AND equipment_id IS NULL) OR "
|
|
"(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)
|
|
planned_working_weight: Mapped[float | None] = mapped_column(Numeric(8, 2))
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
|
|
|
|
workout: Mapped[Workout] = relationship(back_populates="items")
|
|
exercise: Mapped[Exercise | None] = relationship(back_populates="workout_items")
|
|
equipment: Mapped[Equipment | None] = relationship(back_populates="workout_items")
|
|
sets: Mapped[list[WorkoutSet]] = relationship(
|
|
back_populates="workout_item", cascade="all, delete-orphan", order_by="WorkoutSet.set_index"
|
|
)
|
|
|
|
|
|
class WorkoutSet(Base):
|
|
__tablename__ = "logic_workout_sets"
|
|
|
|
id: Mapped[uuid.UUID] = uuid_pk()
|
|
workout_item_id: Mapped[uuid.UUID] = mapped_column(
|
|
ForeignKey("logic_workout_items.id", ondelete="CASCADE")
|
|
)
|
|
set_index: Mapped[int] = mapped_column(Integer)
|
|
weight: Mapped[float] = mapped_column(Numeric(8, 2), default=0)
|
|
reps: Mapped[int] = mapped_column(Integer, default=0)
|
|
duration_seconds: Mapped[int | None] = mapped_column(Integer)
|
|
calories: Mapped[float | None] = mapped_column(Numeric(8, 2))
|
|
completed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
|
|
|
|
workout_item: Mapped[WorkoutItem] = relationship(back_populates="sets")
|