121 lines
4.1 KiB
Python
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)
|