Paginate activity history and aggregate dashboard totals in SQL

This commit is contained in:
Peter Stockings
2026-10-02 14:36:35 +10:00
parent a300ea007d
commit 5830976e31
11 changed files with 386 additions and 65 deletions
+118
View File
@@ -0,0 +1,118 @@
"""Bounded activity lists, SQL totals and small streamed record comparisons."""
from datetime import UTC, datetime, timedelta
from itertools import pairwise
from zoneinfo import ZoneInfo
from flask import abort, request
from sqlalchemy.orm import joinedload
from .extensions import db
from .models import Activity
from .records import EFFORTS_VERSION, record_book
PAGE_SIZE = 25
def activity_page(filters, total=None):
value = request.args.get("page", "1")
if not value.isascii() or not value.isdecimal() or len(value) > 10 or int(value) < 1:
abort(404)
pagination = db.paginate(
db.select(Activity)
.where(*filters)
.options(joinedload(Activity.owner))
.order_by(Activity.started_at.desc(), Activity.id.desc()),
page=int(value),
per_page=PAGE_SIZE,
max_per_page=PAGE_SIZE,
count=total is None,
error_out=True,
)
if total is not None:
pagination.total = total
return pagination
def overview(filters, timezone):
totals = db.session.execute(
db.select(
db.func.count(Activity.id),
db.func.sum(Activity.distance_m),
db.func.sum(Activity.duration_s),
db.func.sum(Activity.elevation_m),
db.func.max(Activity.started_at),
).where(*filters)
).one()
count, distance, duration, elevation, latest = totals
distance, duration, elevation = distance or 0, duration or 0, elevation or 0
zone = ZoneInfo(timezone)
latest_day = (latest or datetime.now(UTC)).replace(tzinfo=UTC).astimezone(zone).date()
monday = latest_day - timedelta(days=latest_day.weekday())
weeks = [monday - timedelta(weeks=n) for n in reversed(range(8))]
# Convert each local week boundary independently, including daylight-saving changes.
boundaries = [
datetime.combine(day, datetime.min.time(), tzinfo=zone).astimezone(UTC)
for day in [*weeks, monday + timedelta(weeks=1)]
]
weekly = db.session.execute(
db.select(
*[
db.func.coalesce(
db.func.sum(
db.case(
(
db.and_(Activity.started_at >= start, Activity.started_at < end),
Activity.distance_m,
),
else_=0,
)
),
0,
)
for start, end in pairwise(boundaries)
]
).where(
*filters, Activity.started_at >= boundaries[0], Activity.started_at < boundaries[-1]
)
).one()
seconds = round(duration * 1000 / distance) if distance else None
return {
"distance": distance / 1000,
"duration": duration,
"count": count,
"elevation": elevation,
"pace": f"{seconds // 60}:{seconds % 60:02d}" if seconds is not None else "—",
"weeks": [day.strftime("%d %b") for day in weeks],
"weekly_km": [round(value / 1000, 2) for value in weekly],
}
def latest_mapped_run(filters):
return db.session.scalar(
db.select(Activity)
.where(*filters, Activity.sport == "running", Activity.has_route.is_(True))
.order_by(Activity.started_at.desc(), Activity.id.desc())
.limit(1)
)
def records_for(owner_id, run_ids):
history = db.session.execute(
db.select(
Activity.id,
Activity.title,
Activity.started_at,
Activity.sport,
Activity.efforts_version,
Activity.best_efforts,
)
.where(
Activity.owner_id == owner_id,
Activity.sport == "running",
Activity.efforts_version == EFFORTS_VERSION,
)
.order_by(Activity.started_at, Activity.id)
.execution_options(yield_per=1000)
)
return record_book(history, run_ids=run_ids, presorted=True)