Paginate activity history and aggregate dashboard totals in SQL
This commit is contained in:
+118
@@ -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)
|
||||
Reference in New Issue
Block a user