152 lines
7.0 KiB
Python
152 lines
7.0 KiB
Python
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))
|
|
hr_bands = db.Column(db.JSON)
|
|
activities = db.relationship(
|
|
"Activity", backref="owner", lazy="select", cascade="all, delete-orphan"
|
|
)
|
|
daily_health = db.relationship("DailyHealth", 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)
|
|
health_enabled = db.Column(db.Boolean, nullable=False, default=False, server_default=db.false())
|
|
health_message = db.Column(db.String(200), nullable=False, default="Daily recovery sync is off.",
|
|
server_default="Daily recovery sync is off.")
|
|
|
|
|
|
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)
|
|
session_context = db.Column(db.JSON, nullable=False, default=dict, server_default="{}")
|
|
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 PlannedRoute(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, index=True)
|
|
title = db.Column(db.String(160), nullable=False)
|
|
saved = db.Column(db.Boolean, nullable=False, default=False)
|
|
payload = db.Column(db.JSON, nullable=False)
|
|
created_at = db.Column(
|
|
db.DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC)
|
|
)
|
|
|
|
|
|
class SavedAnalysis(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, index=True)
|
|
name = db.Column(db.String(100), nullable=False)
|
|
collection = db.Column(db.String(80), nullable=False, default="My analyses")
|
|
notes = db.Column(db.Text, nullable=False, default="")
|
|
query = db.Column(db.Text, nullable=False)
|
|
pinned = db.Column(db.Boolean, nullable=False, default=False)
|
|
position = db.Column(db.Integer, nullable=False, default=0)
|
|
width = db.Column(db.String(12), nullable=False, default="half")
|
|
view = db.Column(db.String(12), nullable=False, default="chart")
|
|
created_at = db.Column(
|
|
db.DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC)
|
|
)
|
|
updated_at = db.Column(
|
|
db.DateTime(timezone=True),
|
|
nullable=False,
|
|
default=lambda: datetime.now(UTC),
|
|
onupdate=lambda: datetime.now(UTC),
|
|
)
|
|
|
|
|
|
class DailyHealth(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, index=True)
|
|
day = db.Column(db.Date, nullable=False)
|
|
values = db.Column(db.JSON, nullable=False, default=dict)
|
|
fetched_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
|
default=lambda: datetime.now(UTC))
|
|
__table_args__ = (db.UniqueConstraint("owner_id", "day"),)
|
|
|
|
|
|
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"),
|
|
)
|