Add dashboard context method
This commit is contained in:
parent
61946f8155
commit
9c951e64ab
3 changed files with 108 additions and 19 deletions
|
|
@ -235,6 +235,11 @@ class Game(auto_prefetch.Model):
|
||||||
"""Alias for box_art_best_url to provide a common interface with benefits."""
|
"""Alias for box_art_best_url to provide a common interface with benefits."""
|
||||||
return self.box_art_best_url
|
return self.box_art_best_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dashboard_box_art_url(self) -> str:
|
||||||
|
"""Return dashboard-safe box art URL without touching deferred image fields."""
|
||||||
|
return normalize_twitch_box_art_url(self.box_art or "")
|
||||||
|
|
||||||
|
|
||||||
# MARK: TwitchGame
|
# MARK: TwitchGame
|
||||||
class TwitchGameData(auto_prefetch.Model):
|
class TwitchGameData(auto_prefetch.Model):
|
||||||
|
|
@ -586,9 +591,6 @@ class DropCampaign(auto_prefetch.Model):
|
||||||
"twitch_id",
|
"twitch_id",
|
||||||
"name",
|
"name",
|
||||||
"image_url",
|
"image_url",
|
||||||
"image_file",
|
|
||||||
"image_width",
|
|
||||||
"image_height",
|
|
||||||
"start_at",
|
"start_at",
|
||||||
"end_at",
|
"end_at",
|
||||||
"allow_is_enabled",
|
"allow_is_enabled",
|
||||||
|
|
@ -598,9 +600,6 @@ class DropCampaign(auto_prefetch.Model):
|
||||||
"game__name",
|
"game__name",
|
||||||
"game__slug",
|
"game__slug",
|
||||||
"game__box_art",
|
"game__box_art",
|
||||||
"game__box_art_file",
|
|
||||||
"game__box_art_width",
|
|
||||||
"game__box_art_height",
|
|
||||||
)
|
)
|
||||||
.select_related("game")
|
.select_related("game")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
|
|
@ -648,7 +647,7 @@ class DropCampaign(auto_prefetch.Model):
|
||||||
game_id,
|
game_id,
|
||||||
{
|
{
|
||||||
"name": game_display_name,
|
"name": game_display_name,
|
||||||
"box_art": game.box_art_best_url,
|
"box_art": game.dashboard_box_art_url,
|
||||||
"owners": list(getattr(game, "owners_for_dashboard", [])),
|
"owners": list(getattr(game, "owners_for_dashboard", [])),
|
||||||
"campaigns": [],
|
"campaigns": [],
|
||||||
},
|
},
|
||||||
|
|
@ -657,7 +656,7 @@ class DropCampaign(auto_prefetch.Model):
|
||||||
game_bucket["campaigns"].append({
|
game_bucket["campaigns"].append({
|
||||||
"campaign": campaign,
|
"campaign": campaign,
|
||||||
"clean_name": campaign.clean_name,
|
"clean_name": campaign.clean_name,
|
||||||
"image_url": campaign.listing_image_url,
|
"image_url": campaign.dashboard_image_url,
|
||||||
"allowed_channels": getattr(campaign, "channels_ordered", []),
|
"allowed_channels": getattr(campaign, "channels_ordered", []),
|
||||||
"game_display_name": game_display_name,
|
"game_display_name": game_display_name,
|
||||||
"game_twitch_directory_url": game.twitch_directory_url,
|
"game_twitch_directory_url": game.twitch_directory_url,
|
||||||
|
|
@ -680,6 +679,24 @@ class DropCampaign(auto_prefetch.Model):
|
||||||
"""
|
"""
|
||||||
return cls.grouped_by_game(cls.active_for_dashboard(now))
|
return cls.grouped_by_game(cls.active_for_dashboard(now))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def dashboard_context(
|
||||||
|
cls,
|
||||||
|
now: datetime.datetime,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return dashboard data assembled by model-layer query helpers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
now: Current timestamp used for active-window filtering.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with grouped drop campaigns and active reward campaigns.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"campaigns_by_game": cls.campaigns_by_game_for_dashboard(now),
|
||||||
|
"active_reward_campaigns": RewardCampaign.active_for_dashboard(now),
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_active(self) -> bool:
|
def is_active(self) -> bool:
|
||||||
"""Check if the campaign is currently active."""
|
"""Check if the campaign is currently active."""
|
||||||
|
|
@ -761,6 +778,11 @@ class DropCampaign(auto_prefetch.Model):
|
||||||
logger.debug("Failed to resolve DropCampaign.image_file url: %s", exc)
|
logger.debug("Failed to resolve DropCampaign.image_file url: %s", exc)
|
||||||
return self.image_url or ""
|
return self.image_url or ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dashboard_image_url(self) -> str:
|
||||||
|
"""Return dashboard-safe campaign image URL without touching deferred image fields."""
|
||||||
|
return self.image_url or ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duration_iso(self) -> str:
|
def duration_iso(self) -> str:
|
||||||
"""Return the campaign duration in ISO 8601 format (e.g., 'P3DT4H30M').
|
"""Return the campaign duration in ISO 8601 format (e.g., 'P3DT4H30M').
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ from twitch.views import _build_seo_context
|
||||||
from twitch.views import _truncate_description
|
from twitch.views import _truncate_description
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
|
|
@ -543,7 +545,7 @@ class TestChannelListView:
|
||||||
assert len(context["campaigns_by_game"][game.twitch_id]["campaigns"]) == 1
|
assert len(context["campaigns_by_game"][game.twitch_id]["campaigns"]) == 1
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_dashboard_queries_use_indexes(self) -> None:
|
def test_dashboard_queries_use_indexes(self, client: Client) -> None:
|
||||||
"""Dashboard source queries should use indexes for active-window filtering."""
|
"""Dashboard source queries should use indexes for active-window filtering."""
|
||||||
now: datetime.datetime = timezone.now()
|
now: datetime.datetime = timezone.now()
|
||||||
|
|
||||||
|
|
@ -627,6 +629,9 @@ class TestChannelListView:
|
||||||
RewardCampaign.active_for_dashboard(now)
|
RewardCampaign.active_for_dashboard(now)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:dashboard"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
campaigns_plan: str = active_campaigns_qs.explain()
|
campaigns_plan: str = active_campaigns_qs.explain()
|
||||||
reward_plan: str = active_reward_campaigns_qs.explain()
|
reward_plan: str = active_reward_campaigns_qs.explain()
|
||||||
|
|
||||||
|
|
@ -650,6 +655,76 @@ class TestChannelListView:
|
||||||
assert campaigns_uses_index, campaigns_plan
|
assert campaigns_uses_index, campaigns_plan
|
||||||
assert rewards_uses_index, reward_plan
|
assert rewards_uses_index, reward_plan
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_dashboard_context_uses_prefetched_data_without_n_plus_one(self) -> None:
|
||||||
|
"""Dashboard context should not trigger extra queries when rendering-used attrs are accessed."""
|
||||||
|
now: datetime.datetime = timezone.now()
|
||||||
|
|
||||||
|
org: Organization = Organization.objects.create(
|
||||||
|
twitch_id="org_dashboard_prefetch",
|
||||||
|
name="Org Dashboard Prefetch",
|
||||||
|
)
|
||||||
|
game: Game = Game.objects.create(
|
||||||
|
twitch_id="game_dashboard_prefetch",
|
||||||
|
name="Game Dashboard Prefetch",
|
||||||
|
display_name="Game Dashboard Prefetch",
|
||||||
|
)
|
||||||
|
game.owners.add(org)
|
||||||
|
|
||||||
|
channel: Channel = Channel.objects.create(
|
||||||
|
twitch_id="channel_dashboard_prefetch",
|
||||||
|
name="channeldashboardprefetch",
|
||||||
|
display_name="Channel Dashboard Prefetch",
|
||||||
|
)
|
||||||
|
|
||||||
|
campaign: DropCampaign = DropCampaign.objects.create(
|
||||||
|
twitch_id="campaign_dashboard_prefetch",
|
||||||
|
name="Campaign Dashboard Prefetch",
|
||||||
|
game=game,
|
||||||
|
operation_names=["DropCampaignDetails"],
|
||||||
|
start_at=now - timedelta(hours=1),
|
||||||
|
end_at=now + timedelta(hours=1),
|
||||||
|
)
|
||||||
|
campaign.allow_channels.add(channel)
|
||||||
|
|
||||||
|
RewardCampaign.objects.create(
|
||||||
|
twitch_id="reward_dashboard_prefetch",
|
||||||
|
name="Reward Dashboard Prefetch",
|
||||||
|
game=game,
|
||||||
|
starts_at=now - timedelta(hours=1),
|
||||||
|
ends_at=now + timedelta(hours=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
dashboard_data: dict[str, Any] = DropCampaign.dashboard_context(now)
|
||||||
|
campaigns_by_game: OrderedDict[str, dict[str, Any]] = dashboard_data[
|
||||||
|
"campaigns_by_game"
|
||||||
|
]
|
||||||
|
reward_campaigns: list[RewardCampaign] = list(
|
||||||
|
dashboard_data["active_reward_campaigns"],
|
||||||
|
)
|
||||||
|
|
||||||
|
with CaptureQueriesContext(connection) as capture:
|
||||||
|
game_bucket: dict[str, Any] = campaigns_by_game[game.twitch_id]
|
||||||
|
_ = game_bucket["name"]
|
||||||
|
_ = game_bucket["box_art"]
|
||||||
|
_ = [owner.name for owner in game_bucket["owners"]]
|
||||||
|
|
||||||
|
campaign_entry: dict[str, Any] = game_bucket["campaigns"][0]
|
||||||
|
campaign_obj: DropCampaign = campaign_entry["campaign"]
|
||||||
|
|
||||||
|
_ = campaign_obj.clean_name
|
||||||
|
_ = campaign_obj.duration_iso
|
||||||
|
_ = campaign_obj.start_at
|
||||||
|
_ = campaign_obj.end_at
|
||||||
|
_ = campaign_entry["image_url"]
|
||||||
|
_ = campaign_entry["game_twitch_directory_url"]
|
||||||
|
_ = [c.display_name for c in campaign_entry["allowed_channels"]]
|
||||||
|
|
||||||
|
_ = [r.is_active for r in reward_campaigns]
|
||||||
|
_ = [r.game.display_name if r.game else None for r in reward_campaigns]
|
||||||
|
|
||||||
|
assert len(capture) == 0
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_dashboard_query_plans_reference_expected_index_names(self) -> None:
|
def test_dashboard_query_plans_reference_expected_index_names(self) -> None:
|
||||||
"""Dashboard active-window plans should mention concrete index names."""
|
"""Dashboard active-window plans should mention concrete index names."""
|
||||||
|
|
|
||||||
|
|
@ -1056,14 +1056,7 @@ def dashboard(request: HttpRequest) -> HttpResponse:
|
||||||
HttpResponse: The rendered dashboard template.
|
HttpResponse: The rendered dashboard template.
|
||||||
"""
|
"""
|
||||||
now: datetime.datetime = timezone.now()
|
now: datetime.datetime = timezone.now()
|
||||||
campaigns_by_game: OrderedDict[str, dict[str, Any]] = (
|
dashboard_data: dict[str, Any] = DropCampaign.dashboard_context(now)
|
||||||
DropCampaign.campaigns_by_game_for_dashboard(now)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get active reward campaigns (Quest rewards)
|
|
||||||
active_reward_campaigns: QuerySet[RewardCampaign] = (
|
|
||||||
RewardCampaign.active_for_dashboard(now)
|
|
||||||
)
|
|
||||||
|
|
||||||
# WebSite schema with SearchAction for sitelinks search box
|
# WebSite schema with SearchAction for sitelinks search box
|
||||||
# TODO(TheLovinator): Should this be on all pages instead of just the dashboard? # noqa: TD003
|
# TODO(TheLovinator): Should this be on all pages instead of just the dashboard? # noqa: TD003
|
||||||
|
|
@ -1096,9 +1089,8 @@ def dashboard(request: HttpRequest) -> HttpResponse:
|
||||||
request,
|
request,
|
||||||
"twitch/dashboard.html",
|
"twitch/dashboard.html",
|
||||||
{
|
{
|
||||||
"campaigns_by_game": campaigns_by_game,
|
|
||||||
"active_reward_campaigns": active_reward_campaigns,
|
|
||||||
"now": now,
|
"now": now,
|
||||||
|
**dashboard_data,
|
||||||
**seo_context,
|
**seo_context,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue