Files

121 lines
4.1 KiB
Python

"""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, shared_only=False):
visibility_filters = [Activity.visibility == "friends"] if shared_only else []
history = db.session.execute(
db.select(
Activity.id,
Activity.title,
Activity.started_at,
Activity.sport,
Activity.efforts_version,
Activity.best_efforts,
)
.where(
*visibility_filters,
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)