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 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") samples = db.Column(db.JSON, nullable=False, default=list) laps = 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"),) @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"), )