From ba1849e80eeffb42ba98fba753135365fb2bec50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Sat, 20 Jun 2026 21:17:21 +0200 Subject: [PATCH] refactor(session): extract canonical session_row_data builder Co-Authored-By: Claude Opus 4.8 --- games/views/session.py | 147 ++++++++++++++++++++++---------------- tests/test_session_row.py | 37 ++++++++++ 2 files changed, 122 insertions(+), 62 deletions(-) create mode 100644 tests/test_session_row.py diff --git a/games/views/session.py b/games/views/session.py index c93157f..d3359e2 100644 --- a/games/views/session.py +++ b/games/views/session.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, TypedDict from django.contrib.auth.decorators import login_required from django.db.models import Q @@ -26,6 +26,7 @@ from common.components import ( SessionDeviceSelector, SessionTimestampButtons, StyledButton, + TableRow, paginated_table_content, ) from common.components.primitives import Span, Td, Tr @@ -40,6 +41,87 @@ from games.forms import SessionForm from games.models import Device, Game, Session +class SessionRowData(TypedDict): + row_id: str + hx_trigger: str + hx_get: str + hx_select: str + hx_swap: str + cell_data: list[Node] + + +def session_row_data( + session: Session, device_list, csrf_token: str +) -> SessionRowData: + """Canonical session-list row. Single source of truth shared by + list_sessions and the htmx finish/reset fragments.""" + row_selector = f"#session-row-{session.pk}" + end_url = reverse("games:list_sessions_end_session", args=[session.pk]) + reset_url = reverse( + "games:list_sessions_reset_session_start", args=[session.pk] + ) + actions = ButtonGroup( + [ + { + "href": end_url, + "hx_get": end_url, + "hx_target": row_selector, + "hx_swap": "outerHTML", + "slot": Icon("end"), + "title": "Finish session now", + "color": "green", + } + if session.timestamp_end is None + else {}, + { + "href": reset_url, + "hx_get": reset_url, + "hx_target": row_selector, + "hx_swap": "outerHTML", + "hx_confirm": "Reset this session's start time to now?", + "slot": Icon("reset"), + "title": "Reset start to now", + "color": "gray", + } + if session.timestamp_end is None + else {}, + { + "href": reverse("games:edit_session", args=[session.pk]), + "slot": Icon("edit"), + "title": "Edit", + }, + { + "href": reverse("games:delete_session", args=[session.pk]), + "slot": Icon("delete"), + "title": "Delete", + "color": "red", + }, + ] + ) + return SessionRowData( + row_id=f"session-row-{session.pk}", + hx_trigger="device-changed from:body", + hx_get="", + hx_select=row_selector, + hx_swap="outerHTML", + cell_data=[ + NameWithIcon(session=session), + f"{local_strftime(session.timestamp_start)}" + f"{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}", + session.duration_formatted_with_mark(), + SessionDeviceSelector(session, device_list, csrf_token), + session.created_at.strftime(dateformat), + actions, + ], + ) + + +def session_row(session: Session, device_list, csrf_token: str) -> Node: + """The single-session node, rendered through the same TableRow + path the list table uses.""" + return TableRow(session_row_data(session, device_list, csrf_token)) + + @login_required def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse: sessions = Session.objects.order_by("-timestamp_start", "created_at") @@ -69,6 +151,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse except Session.DoesNotExist: last_session = None sessions, page_obj, elided_page_range = paginate(request, sessions) + csrf_token = get_token(request) data = { "header_action": Div( @@ -120,67 +203,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse "Actions", ], "rows": [ - { - "row_id": f"session-row-{session.pk}", - "hx_trigger": "device-changed from:body", - "hx_get": "", - "hx_select": f"#session-row-{session.pk}", - "hx_swap": "outerHTML", - "cell_data": [ - NameWithIcon(session=session), - f"{local_strftime(session.timestamp_start)}{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}", - session.duration_formatted_with_mark(), - SessionDeviceSelector(session, device_list, get_token(request)), - session.created_at.strftime(dateformat), - ButtonGroup( - [ - { - "href": reverse( - "games:list_sessions_end_session", args=[session.pk] - ), - "slot": Icon("end"), - "title": "Finish session now", - "color": "green", - } - if session.timestamp_end is None - else {}, - { - "href": reverse( - "games:list_sessions_reset_session_start", - args=[session.pk], - ), - "hx_get": reverse( - "games:list_sessions_reset_session_start", - args=[session.pk], - ), - "hx_confirm": ( - "Reset this session's start time to now?" - ), - "slot": Icon("reset"), - "title": "Reset start to now", - "color": "gray", - } - if session.timestamp_end is None - else {}, - { - "href": reverse( - "games:edit_session", args=[session.pk] - ), - "slot": Icon("edit"), - "title": "Edit", - }, - { - "href": reverse( - "games:delete_session", args=[session.pk] - ), - "slot": Icon("delete"), - "title": "Delete", - "color": "red", - }, - ] - ), - ], - } + session_row_data(session, device_list, csrf_token) for session in sessions ], } diff --git a/tests/test_session_row.py b/tests/test_session_row.py new file mode 100644 index 0000000..bb69b3a --- /dev/null +++ b/tests/test_session_row.py @@ -0,0 +1,37 @@ +import pytest +from django.utils import timezone + +from games.models import Device, Game, Platform, Session +from games.views.session import session_row, session_row_data + + +@pytest.fixture +def running_session(db): + platform = Platform.objects.create(name="PC") + game = Game.objects.create(name="Celeste", platform=platform) + device = Device.objects.create(name="Desktop") + return Session.objects.create( + game=game, device=device, timestamp_start=timezone.now() + ) + + +def test_session_row_data_shape(running_session): + device_list = Device.objects.order_by("name") + data = session_row_data(running_session, device_list, "tok") + assert data["row_id"] == f"session-row-{running_session.pk}" + assert len(data["cell_data"]) == 6 + assert data["hx_select"] == f"#session-row-{running_session.pk}" + + +def test_session_row_renders_id_and_six_cells(running_session): + device_list = Device.objects.order_by("name") + html = str(session_row(running_session, device_list, "tok")) + assert f'id="session-row-{running_session.pk}"' in html + assert html.count("