Files
Stride/app/models.py
T

153 lines
7.1 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)
hr_zones = 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"),
)