diff --git a/twitch/models.py b/twitch/models.py index 6f7b8bc..4b93c99 100644 --- a/twitch/models.py +++ b/twitch/models.py @@ -75,6 +75,27 @@ class Organization(auto_prefetch.Model): """Return organizations with only fields needed by the org list page.""" return cls.objects.only("twitch_id", "name").order_by("name") + @classmethod + def for_detail_view(cls) -> models.QuerySet[Organization]: + """Return organizations with only fields and relations needed by detail page.""" + return cls.objects.only( + "twitch_id", + "name", + "added_at", + "updated_at", + ).prefetch_related( + models.Prefetch( + "games", + queryset=Game.objects.only( + "twitch_id", + "name", + "display_name", + "slug", + ).order_by("display_name"), + to_attr="games_for_detail", + ), + ) + def feed_description(self: Organization) -> str: """Return a description of the organization for RSS feeds.""" name: str = self.name or "Unknown Organization" diff --git a/twitch/tests/test_views.py b/twitch/tests/test_views.py index 2debac5..62cf1d8 100644 --- a/twitch/tests/test_views.py +++ b/twitch/tests/test_views.py @@ -2397,6 +2397,73 @@ class TestChannelListView: assert response.status_code == 200 assert "organization" in response.context + @pytest.mark.django_db + def test_organization_detail_queryset_only_selects_rendered_fields(self) -> None: + """Organization detail queryset should defer non-rendered organization fields.""" + org: Organization = Organization.objects.create( + twitch_id="org_detail_fields", + name="Org Detail Fields", + ) + Game.objects.create( + twitch_id="org_detail_game_fields", + name="Org Detail Game Fields", + display_name="Org Detail Game Fields", + ).owners.add(org) + + fetched_org: Organization | None = ( + Organization.for_detail_view().filter(twitch_id=org.twitch_id).first() + ) + + assert fetched_org is not None + deferred_fields: set[str] = fetched_org.get_deferred_fields() + assert "twitch_id" not in deferred_fields + assert "name" not in deferred_fields + assert "added_at" not in deferred_fields + assert "updated_at" not in deferred_fields + + @pytest.mark.django_db + def test_organization_detail_prefetched_games_do_not_trigger_extra_queries( + self, + ) -> None: + """Organization detail should prefetch games used by the template.""" + org: Organization = Organization.objects.create( + twitch_id="org_detail_prefetch", + name="Org Detail Prefetch", + ) + game: Game = Game.objects.create( + twitch_id="org_detail_prefetch_game", + name="Org Detail Prefetch Game", + display_name="Org Detail Prefetch Game", + slug="org-detail-prefetch-game", + ) + game.owners.add(org) + + fetched_org: Organization = Organization.for_detail_view().get( + twitch_id=org.twitch_id, + ) + + games_for_detail: list[Game] = list( + getattr(fetched_org, "games_for_detail", []), + ) + assert len(games_for_detail) == 1 + + with CaptureQueriesContext(connection) as queries: + game_row: Game = games_for_detail[0] + _ = game_row.twitch_id + _ = game_row.display_name + _ = game_row.name + _ = str(game_row) + + select_queries: list[str] = [ + query_info["sql"] + for query_info in queries.captured_queries + if query_info["sql"].lstrip().upper().startswith("SELECT") + ] + assert not select_queries, ( + "Organization detail prefetched games triggered unexpected SELECT queries. " + f"Queries: {select_queries}" + ) + @pytest.mark.django_db def test_channel_detail_view(self, client: Client, db: None) -> None: """Test channel detail view returns 200 and has channel in context.""" diff --git a/twitch/views.py b/twitch/views.py index b324564..0e2badf 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -316,21 +316,18 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon Returns: HttpResponse: The rendered organization detail page. - Raises: - Http404: If the organization is not found. """ - try: - organization: Organization = Organization.objects.get(twitch_id=twitch_id) - except Organization.DoesNotExist as exc: - msg = "No organization found matching the query" - raise Http404(msg) from exc + organization: Organization = get_object_or_404( + Organization.for_detail_view(), + twitch_id=twitch_id, + ) - games: QuerySet[Game] = organization.games.all() # pyright: ignore[reportAttributeAccessIssue] + games: list[Game] = list(getattr(organization, "games_for_detail", [])) org_name: str = organization.name or organization.twitch_id - games_count: int = games.count() - s: Literal["", "s"] = "" if games_count == 1 else "s" - org_description: str = f"{org_name} has {games_count} game{s}." + games_count: int = len(games) + noun: str = "game" if games_count == 1 else "games" + org_description: str = f"{org_name} has {games_count} {noun}." url: str = build_absolute_uri( reverse("twitch:organization_detail", args=[organization.twitch_id]),