124 lines
4.2 KiB
Python
124 lines
4.2 KiB
Python
import calendar
|
||
import logging
|
||
from typing import Optional
|
||
|
||
from fastapi import APIRouter, Depends, Request, Form
|
||
from fastapi.responses import HTMLResponse
|
||
from fastapi.templating import Jinja2Templates
|
||
|
||
from app.api.deps import get_training_repository
|
||
from app.core.database.repository import TrainingRepository
|
||
from app.core.metrics import ExerciseProgress, summarize
|
||
from app.core.parsers.obsidian import ObsidianNotesParser, parse_training_data
|
||
from app.core.templates.templates import BaseTemplateResponse
|
||
|
||
web_router = APIRouter()
|
||
templates = Jinja2Templates(directory="app/templates")
|
||
template_renderer = BaseTemplateResponse(templates)
|
||
|
||
|
||
def build_progress_chart(progress: ExerciseProgress) -> dict:
|
||
"""Data for the progress chart: two series over time, x is a UTC timestamp in ms."""
|
||
if progress.weighted:
|
||
series = [
|
||
("e1RM, кг", lambda metrics: metrics.e1rm),
|
||
("Тоннаж, кг", lambda metrics: metrics.volume or None),
|
||
]
|
||
else:
|
||
series = [
|
||
("Лучший подход, повторы", lambda metrics: metrics.best_reps),
|
||
("Всего повторов за тренировку", lambda metrics: metrics.total_reps),
|
||
]
|
||
|
||
chronological = list(reversed(progress.rows))
|
||
return {
|
||
"series": [
|
||
{
|
||
"label": label,
|
||
"points": [
|
||
[calendar.timegm(row.session.date.timetuple()) * 1000, round(value, 1)]
|
||
for row in chronological
|
||
if (value := metric(row.metrics)) is not None
|
||
],
|
||
}
|
||
for label, metric in series
|
||
]
|
||
}
|
||
|
||
|
||
@web_router.get("/", response_class=HTMLResponse)
|
||
async def home(
|
||
request: Request,
|
||
repository: TrainingRepository = Depends(get_training_repository),
|
||
):
|
||
"""Home page: all trainings from the database in a single table"""
|
||
|
||
return template_renderer.render(
|
||
request,
|
||
"index.html",
|
||
context={"trainings": await repository.list()},
|
||
)
|
||
|
||
|
||
@web_router.get("/exercises/", response_class=HTMLResponse)
|
||
async def exercises(
|
||
request: Request,
|
||
name: Optional[str] = None,
|
||
repository: TrainingRepository = Depends(get_training_repository),
|
||
):
|
||
"""Exercise search box and the whole history of the chosen exercise"""
|
||
|
||
query = (name or "").strip()
|
||
exercise_names = await repository.list_exercise_names()
|
||
|
||
# Exact name, but case-insensitive: what was typed may differ in case
|
||
names_by_casefold = {item.name.casefold(): item.name for item in exercise_names}
|
||
selected = names_by_casefold.get(query.casefold())
|
||
history = await repository.exercise_history(selected) if selected else []
|
||
# Nothing matched exactly: offer the names containing the typed text
|
||
suggestions = (
|
||
[item for item in exercise_names if query.casefold() in item.name.casefold()]
|
||
if query and not selected
|
||
else []
|
||
)
|
||
|
||
progress = summarize(history) if history else None
|
||
|
||
return template_renderer.render(
|
||
request,
|
||
"exercises.html",
|
||
context={
|
||
"exercise_names": exercise_names,
|
||
"query": query,
|
||
"selected": selected,
|
||
"suggestions": suggestions,
|
||
"progress": progress,
|
||
"chart": build_progress_chart(progress) if progress else None,
|
||
},
|
||
)
|
||
|
||
|
||
@web_router.get("/obsidian/list/", response_class=HTMLResponse)
|
||
async def obsidian_list(request: Request):
|
||
data = parse_training_data()
|
||
return template_renderer.render(
|
||
request=request, template_name="trainings.html", context={"trainings": data}
|
||
)
|
||
|
||
|
||
@web_router.get("/obsidian/parse", response_class=HTMLResponse)
|
||
async def obsidian_parse_form(request: Request):
|
||
return template_renderer.render(
|
||
request=request, template_name="obsidian/parser.html", context={"trainings": {}}
|
||
)
|
||
|
||
|
||
@web_router.post("/obsidian/parse", response_class=HTMLResponse)
|
||
async def obsidian_parse(request: Request, training_data: str = Form(...)):
|
||
logging.error(training_data)
|
||
op = ObsidianNotesParser()
|
||
result = op.parse(data=training_data)
|
||
logging.error(result)
|
||
return template_renderer.render(
|
||
request=request, template_name="trainings.html", context={"trainings": result}
|
||
)
|