from datetime import UTC, datetime from flask_login import UserMixin from werkzeug.security import check_password_hash, generate_password_hash from .extensions import db class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(40), unique=True, nullable=False) email = db.Column(db.String(254), unique=True, nullable=False) display_name = db.Column(db.String(80), nullable=False) password_hash = db.Column(db.String(256), nullable=False) timezone = db.Column(db.String(64), nullable=False, default="Australia/Sydney") # Assigned only by local setup / operator CLI. Never accepted from registration input. local_account_key = db.Column(db.String(64), unique=True) created_at = db.Column(db.DateTime(timezone=True), default=lambda: datetime.now(UTC)) activities = db.relationship( "Activity", backref="owner", lazy="select", cascade="all, delete-orphan" ) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class GarminConnection(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), unique=True, nullable=False) account_key = db.Column(db.String(24), unique=True, nullable=False) tokens = db.Column(db.Text, nullable=False) pending = db.Column(db.Boolean, nullable=False, default=True) scheduled = db.Column(db.Boolean, nullable=False, default=True) next_sync = db.Column(db.DateTime(timezone=True), nullable=False) last_success = db.Column(db.DateTime(timezone=True)) message = db.Column(db.String(200), nullable=False, default="Sync queued.") failures = db.Column(db.Integer, nullable=False, default=0) class Activity(db.Model): id = db.Column(db.Integer, primary_key=True) owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, index=True) source_key = db.Column(db.String(180), nullable=False) title = db.Column(db.String(160), nullable=False) sport = db.Column(db.String(40), nullable=False, default="running") started_at = db.Column(db.DateTime(timezone=True), nullable=False, index=True) distance_m = db.Column(db.Float, nullable=False, default=0) duration_s = db.Column(db.Float, nullable=False, default=0) elevation_m = db.Column(db.Float, nullable=False, default=0) avg_hr = db.Column(db.Float) metrics = db.Column(db.JSON, nullable=False, default=dict, server_default="{}") visibility = db.Column(db.String(12), nullable=False, default="private") share_token = db.Column(db.String(64), unique=True) best_efforts = db.Column(db.JSON, nullable=False, default=list, server_default="[]") efforts_version = db.Column(db.Integer, nullable=False, default=0, server_default="0") has_route = db.Column(db.Boolean, nullable=False, default=False, server_default=db.false()) analysis_version = db.Column(db.Integer, nullable=False, default=0, server_default="0") analysis_summary = db.deferred( db.Column(db.JSON, nullable=False, default=dict, server_default="{}") ) display_data = db.deferred( db.Column(db.JSON, nullable=False, default=dict, server_default="{}") ) route_geometry = db.deferred( db.Column(db.JSON, nullable=False, default=list, server_default="[]") ) samples = db.deferred(db.Column(db.JSON, nullable=False, default=list)) laps = db.deferred(db.Column(db.JSON, nullable=False, default=list)) created_at = db.Column(db.DateTime(timezone=True), default=lambda: datetime.now(UTC)) __table_args__ = ( db.UniqueConstraint("owner_id", "source_key"), db.Index("ix_activity_owner_started_id", "owner_id", "started_at", "id"), db.Index( "ix_activity_owner_visibility_started_id", "owner_id", "visibility", "started_at", "id" ), ) @property def distance_km(self): return self.distance_m / 1000 @property def pace(self): if self.distance_m <= 0: return "—" seconds = round(self.duration_s / self.distance_km) return f"{seconds // 60}:{seconds % 60:02d}" class Friendship(db.Model): id = db.Column(db.Integer, primary_key=True) low_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) high_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) requested_by = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) status = db.Column(db.String(12), nullable=False, default="pending") __table_args__ = ( db.UniqueConstraint("low_id", "high_id"), db.CheckConstraint("low_id < high_id"), )