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(" |