feat(filters): programmatic filter links + navbar playtime links (#56)

Add filter_url(), a reverse()-style helper that builds a URL to a filtered
list view from a filter object (target inferred from the filter type).

Add OperatorFilter.where(**lookups), a Django-.filter()-style ergonomic
constructor that resolves each field's criterion class from its annotation
(shared with from_json via _criterion_class_for, removing duplication).

Make SessionFilter.timestamp_start/timestamp_end DateCriterion applied via
the __date lookup, so date ranges over the timestamp columns are expressible.

Wire the navbar 'today' / 'last 7 days' totals as links to the matching
filtered session lists, and align the 'last 7 days' total to the same
calendar-day window so the number matches the list it links to.

Stats-table and game-detail links remain a follow-up (see spec).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RF5L4HtbcykTfY9YUYGds3
This commit is contained in:
2026-06-21 08:53:06 +02:00
parent b9545a780b
commit 3fd02bbcf9
9 changed files with 402 additions and 22 deletions
+28 -6
View File
@@ -188,12 +188,25 @@ def _main_script(mastered: bool) -> str:
def NavbarPlaytime(
today_played: str, last_7_played: str, *, oob: bool = False
today_played: str,
last_7_played: str,
*,
today_url: str | None = None,
last_7_url: str | None = None,
oob: bool = False,
) -> "Node":
"""The navbar 'Today · Last 7 days' totals. Carries a stable id so
htmx endpoints can refresh it out-of-band after a session change."""
htmx endpoints can refresh it out-of-band after a session change.
When ``today_url`` / ``last_7_url`` are given, each total links to the
matching filtered session list."""
from common.components import Safe
def total(text: str, url: str | None) -> str:
if not url:
return text
return f'<a href="{url}" class="hover:underline">{text}</a>'
oob_attr = ' hx-swap-oob="true"' if oob else ""
return Safe(
f'<li id="navbar-playtime"{oob_attr} '
@@ -201,13 +214,20 @@ def NavbarPlaytime(
'<span class="flex uppercase gap-1">Today'
'<span class="dark:text-gray-400">·</span>Last 7 days</span>'
'<span class="flex items-center gap-1">'
f'{today_played}<span class="dark:text-gray-400">·</span>'
f"{last_7_played}</span></li>"
f"{total(today_played, today_url)}"
'<span class="dark:text-gray-400">·</span>'
f"{total(last_7_played, last_7_url)}</span></li>"
)
def Navbar(
*, today_played: str, last_7_played: str, current_year: int, csrf_token: str
*,
today_played: str,
last_7_played: str,
today_url: str | None = None,
last_7_url: str | None = None,
current_year: int,
csrf_token: str,
) -> "Node":
"""Top navigation bar.
@@ -244,7 +264,7 @@ def Navbar(
</svg>
</button>
</li>
{NavbarPlaytime(today_played, last_7_played)}
{NavbarPlaytime(today_played, last_7_played, today_url=today_url, last_7_url=last_7_url)}
<li>
<a href="#" class="block py-2 px-3 text-white bg-blue-700 rounded-sm md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500 dark:bg-blue-600 md:dark:bg-transparent" aria-current="page">Home</a>
</li>
@@ -330,6 +350,8 @@ def Page(
navbar = Navbar(
today_played=counts["today_played"],
last_7_played=counts["last_7_played"],
today_url=counts["today_url"],
last_7_url=counts["last_7_url"],
current_year=year,
csrf_token=get_token(request),
)