diff --git a/twitch/models.py b/twitch/models.py index 85bd565..6f7b8bc 100644 --- a/twitch/models.py +++ b/twitch/models.py @@ -70,6 +70,11 @@ class Organization(auto_prefetch.Model): """Return a string representation of the organization.""" return self.name or self.twitch_id + @classmethod + def for_list_view(cls) -> models.QuerySet[Organization]: + """Return organizations with only fields needed by the org list page.""" + return cls.objects.only("twitch_id", "name").order_by("name") + 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 5377555..2debac5 100644 --- a/twitch/tests/test_views.py +++ b/twitch/tests/test_views.py @@ -946,6 +946,16 @@ class TestChannelListView: if connection.vendor == "sqlite": campaigns_uses_index: bool = "USING INDEX" in campaigns_plan.upper() rewards_uses_index: bool = "USING INDEX" in reward_plan.upper() + campaigns_uses_expected_index: bool = ( + "tw_drop_start_end_idx" in campaigns_plan + or "tw_drop_start_end_game_idx" in campaigns_plan + or "tw_drop_start_desc_idx" in campaigns_plan + ) + rewards_uses_expected_index: bool = ( + "tw_reward_starts_ends_idx" in reward_plan + or "tw_reward_ends_starts_idx" in reward_plan + or "tw_reward_starts_desc_idx" in reward_plan + ) elif connection.vendor == "postgresql": campaigns_uses_index = ( "INDEX SCAN" in campaigns_plan.upper() @@ -957,6 +967,16 @@ class TestChannelListView: or "BITMAP INDEX SCAN" in reward_plan.upper() or "INDEX ONLY SCAN" in reward_plan.upper() ) + campaigns_uses_expected_index = ( + "tw_drop_start_end_idx" in campaigns_plan + or "tw_drop_start_end_game_idx" in campaigns_plan + or "tw_drop_start_desc_idx" in campaigns_plan + ) + rewards_uses_expected_index = ( + "tw_reward_starts_ends_idx" in reward_plan + or "tw_reward_ends_starts_idx" in reward_plan + or "tw_reward_starts_desc_idx" in reward_plan + ) else: pytest.skip( f"Unsupported DB vendor for index-plan assertion: {connection.vendor}", @@ -964,6 +984,8 @@ class TestChannelListView: assert campaigns_uses_index, campaigns_plan assert rewards_uses_index, reward_plan + assert campaigns_uses_expected_index, campaigns_plan + assert rewards_uses_expected_index, reward_plan @pytest.mark.django_db def test_dashboard_context_uses_prefetched_data_without_n_plus_one(self) -> None: @@ -2342,6 +2364,30 @@ class TestChannelListView: assert response.status_code == 200 assert "orgs" in response.context + @pytest.mark.django_db + def test_org_list_queryset_only_selects_rendered_fields(self) -> None: + """Organization list queryset should defer non-rendered fields.""" + org: Organization = Organization.objects.create( + twitch_id="org_list_fields", + name="Org List Fields", + ) + + fetched_org: Organization | None = ( + Organization + .for_list_view() + .filter( + twitch_id=org.twitch_id, + ) + .first() + ) + + assert fetched_org is not None + deferred_fields: set[str] = fetched_org.get_deferred_fields() + assert "added_at" in deferred_fields + assert "updated_at" in deferred_fields + assert "twitch_id" not in deferred_fields + assert "name" not in deferred_fields + @pytest.mark.django_db def test_organization_detail_view(self, client: Client, db: None) -> None: """Test organization detail view returns 200 and has organization in context.""" diff --git a/twitch/views.py b/twitch/views.py index 4cc0e86..b324564 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -284,7 +284,7 @@ def org_list_view(request: HttpRequest) -> HttpResponse: Returns: HttpResponse: The rendered organization list page. """ - orgs: QuerySet[Organization] = Organization.objects.all().order_by("name") + orgs: QuerySet[Organization] = Organization.for_list_view() # CollectionPage schema for organizations list collection_schema: dict[str, str] = { @@ -300,10 +300,7 @@ def org_list_view(request: HttpRequest) -> HttpResponse: page_description="List of Twitch organizations.", seo_meta={"schema_data": collection_schema}, ) - context: dict[str, Any] = { - "orgs": orgs, - **seo_context, - } + context: dict[str, Any] = {"orgs": orgs, **seo_context} return render(request, "twitch/org_list.html", context)