Merge pull request #81 from KucharczykL/feat/issue-66-game-detail-filter-links
feat(game-detail): link sections to filtered lists (#66)
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
<svg class="dark:text-white w-3" viewBox="5 5 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9 6L15 12L9 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 226 B |
+51
-6
@@ -48,7 +48,13 @@ from common.time import (
|
|||||||
timeformat,
|
timeformat,
|
||||||
)
|
)
|
||||||
from common.utils import build_dynamic_filter, paginate, safe_division, truncate
|
from common.utils import build_dynamic_filter, paginate, safe_division, truncate
|
||||||
from games.filters import parse_game_filter
|
from games.filters import (
|
||||||
|
PlayEventFilter,
|
||||||
|
PurchaseFilter,
|
||||||
|
SessionFilter,
|
||||||
|
filter_url,
|
||||||
|
parse_game_filter,
|
||||||
|
)
|
||||||
from games.sorting import GAME_DEFAULT_SORT, GAME_SORTS, apply_sort, parse_find_filter
|
from games.sorting import GAME_DEFAULT_SORT, GAME_SORTS, apply_sort, parse_find_filter
|
||||||
from games.forms import GameForm
|
from games.forms import GameForm
|
||||||
from games.models import Game
|
from games.models import Game
|
||||||
@@ -534,12 +540,35 @@ def _game_history(statuschanges) -> SafeText:
|
|||||||
|
|
||||||
|
|
||||||
def _game_section(
|
def _game_section(
|
||||||
title: str, count: int, table: SafeText, empty_message: str
|
title: str,
|
||||||
|
count: int,
|
||||||
|
table: SafeText,
|
||||||
|
empty_message: str,
|
||||||
|
view_all_url: str | None = None,
|
||||||
) -> SafeText:
|
) -> SafeText:
|
||||||
|
if view_all_url and count:
|
||||||
|
view_all_link = A(
|
||||||
|
href=view_all_url,
|
||||||
|
children=[
|
||||||
|
StyledButton(
|
||||||
|
icon=True,
|
||||||
|
color="gray",
|
||||||
|
size="xs",
|
||||||
|
title=f"View all {title.lower()} for this game",
|
||||||
|
children=[Icon("arrowright"), "View all"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
header = Div(
|
||||||
|
[("class", "flex items-center justify-between")],
|
||||||
|
[H1(children=[title], badge=count), view_all_link],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
header = H1(children=[title], badge=count)
|
||||||
return Div(
|
return Div(
|
||||||
[("class", "mb-6")],
|
[("class", "mb-6")],
|
||||||
[
|
[
|
||||||
H1(children=[title], badge=count),
|
header,
|
||||||
table if count else empty_message,
|
table if count else empty_message,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -687,7 +716,13 @@ def _purchases_section(game: Game) -> SafeText:
|
|||||||
for purchase in purchases
|
for purchase in purchases
|
||||||
]
|
]
|
||||||
table = SimpleTable(columns=["Name", "Type", "Date", "Price", "Actions"], rows=rows)
|
table = SimpleTable(columns=["Name", "Type", "Date", "Price", "Actions"], rows=rows)
|
||||||
return _game_section("Purchases", purchases.count(), table, "No purchases yet.")
|
return _game_section(
|
||||||
|
"Purchases",
|
||||||
|
purchases.count(),
|
||||||
|
table,
|
||||||
|
"No purchases yet.",
|
||||||
|
view_all_url=filter_url(PurchaseFilter.where(games=[game.id])),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _sessions_section(game: Game, request: HttpRequest) -> SafeText:
|
def _sessions_section(game: Game, request: HttpRequest) -> SafeText:
|
||||||
@@ -772,7 +807,13 @@ def _sessions_section(game: Game, request: HttpRequest) -> SafeText:
|
|||||||
elided_page_range=elided_page_range,
|
elided_page_range=elided_page_range,
|
||||||
request=request,
|
request=request,
|
||||||
)
|
)
|
||||||
return _game_section("Sessions", session_count, table, "No sessions yet.")
|
return _game_section(
|
||||||
|
"Sessions",
|
||||||
|
session_count,
|
||||||
|
table,
|
||||||
|
"No sessions yet.",
|
||||||
|
view_all_url=filter_url(SessionFilter.where(game=[game.id])),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _playevents_section(game: Game) -> SafeText:
|
def _playevents_section(game: Game) -> SafeText:
|
||||||
@@ -780,7 +821,11 @@ def _playevents_section(game: Game) -> SafeText:
|
|||||||
data = create_playevent_tabledata(playevents, exclude_columns=["Game"])
|
data = create_playevent_tabledata(playevents, exclude_columns=["Game"])
|
||||||
table = SimpleTable(columns=data["columns"], rows=data["rows"])
|
table = SimpleTable(columns=data["columns"], rows=data["rows"])
|
||||||
return _game_section(
|
return _game_section(
|
||||||
"Play Events", playevents.count(), table, "No play events yet."
|
"Play Events",
|
||||||
|
playevents.count(),
|
||||||
|
table,
|
||||||
|
"No play events yet.",
|
||||||
|
view_all_url=filter_url(PlayEventFilter.where(game=[game.id])),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Rendering tests: game-detail sections wire "View all" links to filtered lists (#66)."""
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.utils.html import escape
|
||||||
|
|
||||||
|
from games.filters import (
|
||||||
|
PlayEventFilter,
|
||||||
|
PurchaseFilter,
|
||||||
|
SessionFilter,
|
||||||
|
filter_url,
|
||||||
|
)
|
||||||
|
from games.models import Game, Platform, PlayEvent, Purchase, Session
|
||||||
|
from games.views.game import view_game
|
||||||
|
|
||||||
|
|
||||||
|
def _dt(day, hour=12):
|
||||||
|
return datetime(2024, 6, day, hour, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def game(db):
|
||||||
|
platform = Platform.objects.create(name="PC")
|
||||||
|
game = Game.objects.create(
|
||||||
|
name="Test Game", platform=platform, status=Game.Status.PLAYED
|
||||||
|
)
|
||||||
|
Session.objects.create(game=game, timestamp_start=_dt(1), timestamp_end=_dt(1, 13))
|
||||||
|
Purchase.objects.create(date_purchased=_dt(1), type=Purchase.GAME).games.set([game])
|
||||||
|
PlayEvent.objects.create(game=game, ended=_dt(2))
|
||||||
|
return game
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def rendered(game, rf, django_user_model):
|
||||||
|
user = django_user_model.objects.create_user(username="u", password="p")
|
||||||
|
request = rf.get(f"/game/{game.id}/")
|
||||||
|
request.user = user
|
||||||
|
request.session = {}
|
||||||
|
return view_game(request, game.id).content.decode()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sessions_section_links_to_filtered_sessions(game, rendered):
|
||||||
|
href = escape(filter_url(SessionFilter.where(game=[game.id])))
|
||||||
|
assert href in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_purchases_section_links_to_filtered_purchases(game, rendered):
|
||||||
|
href = escape(filter_url(PurchaseFilter.where(games=[game.id])))
|
||||||
|
assert href in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_playevents_section_links_to_filtered_playevents(game, rendered):
|
||||||
|
href = escape(filter_url(PlayEventFilter.where(game=[game.id])))
|
||||||
|
assert href in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_link_filters_scope_to_game(game):
|
||||||
|
"""Each link's filter selects exactly the game's own records, not another
|
||||||
|
game's (parity between the section and the filtered list it links to)."""
|
||||||
|
other = Game.objects.create(name="Other", platform=game.platform)
|
||||||
|
Session.objects.create(game=other, timestamp_start=_dt(3), timestamp_end=_dt(3, 13))
|
||||||
|
Purchase.objects.create(date_purchased=_dt(3), type=Purchase.GAME).games.set(
|
||||||
|
[other]
|
||||||
|
)
|
||||||
|
PlayEvent.objects.create(game=other, ended=_dt(4))
|
||||||
|
|
||||||
|
sessions = Session.objects.filter(SessionFilter.where(game=[game.id]).to_q())
|
||||||
|
assert list(sessions) == list(game.sessions.all())
|
||||||
|
|
||||||
|
purchases = Purchase.objects.filter(PurchaseFilter.where(games=[game.id]).to_q())
|
||||||
|
assert list(purchases) == list(game.purchases.all())
|
||||||
|
|
||||||
|
playevents = PlayEvent.objects.filter(PlayEventFilter.where(game=[game.id]).to_q())
|
||||||
|
assert list(playevents) == list(game.playevents.all())
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_view_all_for_empty_section(db, django_user_model, rf):
|
||||||
|
"""A game with no sessions/purchases/playevents shows no 'View all' link."""
|
||||||
|
platform = Platform.objects.create(name="PC")
|
||||||
|
empty_game = Game.objects.create(name="Empty", platform=platform)
|
||||||
|
user = django_user_model.objects.create_user(username="u2", password="p")
|
||||||
|
request = rf.get(f"/game/{empty_game.id}/")
|
||||||
|
request.user = user
|
||||||
|
request.session = {}
|
||||||
|
html = view_game(request, empty_game.id).content.decode()
|
||||||
|
assert "View all" not in html
|
||||||
Reference in New Issue
Block a user