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,
|
||||
)
|
||||
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.forms import GameForm
|
||||
from games.models import Game
|
||||
@@ -534,12 +540,35 @@ def _game_history(statuschanges) -> SafeText:
|
||||
|
||||
|
||||
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:
|
||||
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(
|
||||
[("class", "mb-6")],
|
||||
[
|
||||
H1(children=[title], badge=count),
|
||||
header,
|
||||
table if count else empty_message,
|
||||
],
|
||||
)
|
||||
@@ -687,7 +716,13 @@ def _purchases_section(game: Game) -> SafeText:
|
||||
for purchase in purchases
|
||||
]
|
||||
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:
|
||||
@@ -772,7 +807,13 @@ def _sessions_section(game: Game, request: HttpRequest) -> SafeText:
|
||||
elided_page_range=elided_page_range,
|
||||
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:
|
||||
@@ -780,7 +821,11 @@ def _playevents_section(game: Game) -> SafeText:
|
||||
data = create_playevent_tabledata(playevents, exclude_columns=["Game"])
|
||||
table = SimpleTable(columns=data["columns"], rows=data["rows"])
|
||||
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