This commit is contained in:
parent
17ef09465d
commit
4663a827e4
12 changed files with 434 additions and 405 deletions
|
|
@ -599,6 +599,14 @@ class DropCampaign(auto_prefetch.Model):
|
|||
"""Return the campaign image URL for RSS enclosures."""
|
||||
return self.image_best_url
|
||||
|
||||
@property
|
||||
def sorted_benefits(self) -> list[DropBenefit]:
|
||||
"""Return a sorted list of benefits for the campaign."""
|
||||
benefits: list[DropBenefit] = []
|
||||
for drop in self.time_based_drops.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||
benefits.extend(drop.benefits.all()) # pyright: ignore[reportAttributeAccessIssue]
|
||||
return sorted(benefits, key=lambda benefit: benefit.name)
|
||||
|
||||
|
||||
# MARK: DropBenefit
|
||||
class DropBenefit(auto_prefetch.Model):
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ class TestChannelListView:
|
|||
)
|
||||
game.owners.add(org1, org2)
|
||||
|
||||
campaign: DropCampaign = DropCampaign.objects.create(
|
||||
_campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="camp1",
|
||||
name="Campaign",
|
||||
game=game,
|
||||
|
|
@ -519,14 +519,11 @@ class TestChannelListView:
|
|||
if isinstance(context, list):
|
||||
context = context[-1]
|
||||
|
||||
# campaigns_by_game should include one deduplicated campaign entry for the game.
|
||||
assert "campaigns_by_game" in context
|
||||
assert game.twitch_id in context["campaigns_by_game"]
|
||||
assert len(context["campaigns_by_game"][game.twitch_id]["campaigns"]) == 1
|
||||
|
||||
# Template renders each campaign with a stable id, so we can assert it appears once.
|
||||
html = response.content.decode("utf-8")
|
||||
assert html.count(f"campaign-article-{campaign.twitch_id}") == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_debug_view(self, client: Client) -> None:
|
||||
"""Test debug view returns 200 and has games_without_owner in context."""
|
||||
|
|
|
|||
|
|
@ -290,23 +290,29 @@ def search_view(request: HttpRequest) -> HttpResponse:
|
|||
results["games"] = Game.objects.filter(
|
||||
Q(name__istartswith=query) | Q(display_name__istartswith=query),
|
||||
)
|
||||
|
||||
results["campaigns"] = DropCampaign.objects.filter(
|
||||
Q(name__istartswith=query) | Q(description__icontains=query),
|
||||
).select_related("game")
|
||||
|
||||
results["drops"] = TimeBasedDrop.objects.filter(
|
||||
name__istartswith=query,
|
||||
).select_related("campaign")
|
||||
|
||||
results["benefits"] = DropBenefit.objects.filter(
|
||||
name__istartswith=query,
|
||||
).prefetch_related("drops__campaign")
|
||||
|
||||
results["reward_campaigns"] = RewardCampaign.objects.filter(
|
||||
Q(name__istartswith=query)
|
||||
| Q(brand__istartswith=query)
|
||||
| Q(summary__icontains=query),
|
||||
).select_related("game")
|
||||
|
||||
results["badge_sets"] = ChatBadgeSet.objects.filter(
|
||||
set_id__istartswith=query,
|
||||
)
|
||||
|
||||
results["badges"] = ChatBadge.objects.filter(
|
||||
Q(title__istartswith=query) | Q(description__icontains=query),
|
||||
).select_related("badge_set")
|
||||
|
|
@ -317,20 +323,25 @@ def search_view(request: HttpRequest) -> HttpResponse:
|
|||
results["games"] = Game.objects.filter(
|
||||
Q(name__icontains=query) | Q(display_name__icontains=query),
|
||||
)
|
||||
|
||||
results["campaigns"] = DropCampaign.objects.filter(
|
||||
Q(name__icontains=query) | Q(description__icontains=query),
|
||||
).select_related("game")
|
||||
|
||||
results["drops"] = TimeBasedDrop.objects.filter(
|
||||
name__icontains=query,
|
||||
).select_related("campaign")
|
||||
|
||||
results["benefits"] = DropBenefit.objects.filter(
|
||||
name__icontains=query,
|
||||
).prefetch_related("drops__campaign")
|
||||
|
||||
results["reward_campaigns"] = RewardCampaign.objects.filter(
|
||||
Q(name__icontains=query)
|
||||
| Q(brand__icontains=query)
|
||||
| Q(summary__icontains=query),
|
||||
).select_related("game")
|
||||
|
||||
results["badge_sets"] = ChatBadgeSet.objects.filter(set_id__icontains=query)
|
||||
results["badges"] = ChatBadge.objects.filter(
|
||||
Q(title__icontains=query) | Q(description__icontains=query),
|
||||
|
|
@ -1127,42 +1138,13 @@ class GameDetailView(DetailView):
|
|||
either end date or status.
|
||||
"""
|
||||
context: dict[str, Any] = super().get_context_data(**kwargs)
|
||||
game: Game = self.get_object() # pyright: ignore[reportAssignmentType]
|
||||
game: Game = self.object # pyright: ignore[reportAssignmentType]
|
||||
|
||||
now: datetime.datetime = timezone.now()
|
||||
# For each drop, find awarded badge (distribution_type BADGE)
|
||||
drop_awarded_badges: dict[str, ChatBadge] = {}
|
||||
drops: QuerySet[TimeBasedDrop, TimeBasedDrop] = TimeBasedDrop.objects.filter(
|
||||
campaign__game=game,
|
||||
).prefetch_related("benefits")
|
||||
|
||||
# Materialize drops so we can iterate multiple times without extra DB hits
|
||||
drops_list: list[TimeBasedDrop] = list(drops)
|
||||
|
||||
# Collect all benefit names that award badges
|
||||
benefit_badge_titles: set[str] = set()
|
||||
for drop in drops_list:
|
||||
for benefit in drop.benefits.all():
|
||||
if benefit.distribution_type == "BADGE" and benefit.name:
|
||||
benefit_badge_titles.add(benefit.name)
|
||||
|
||||
# Bulk-load all matching ChatBadge instances to avoid N+1 queries
|
||||
badges_by_title: dict[str, ChatBadge] = {
|
||||
badge.title: badge
|
||||
for badge in ChatBadge.objects.filter(title__in=benefit_badge_titles)
|
||||
}
|
||||
|
||||
for drop in drops_list:
|
||||
for benefit in drop.benefits.all():
|
||||
if benefit.distribution_type == "BADGE":
|
||||
badge: ChatBadge | None = badges_by_title.get(benefit.name)
|
||||
if badge:
|
||||
drop_awarded_badges[drop.twitch_id] = badge
|
||||
|
||||
all_campaigns: QuerySet[DropCampaign] = (
|
||||
DropCampaign.objects
|
||||
.filter(game=game)
|
||||
.prefetch_related("game__owners")
|
||||
.select_related("game")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"time_based_drops",
|
||||
|
|
@ -1177,9 +1159,34 @@ class GameDetailView(DetailView):
|
|||
.order_by("-end_at")
|
||||
)
|
||||
|
||||
campaigns_list: list[DropCampaign] = list(all_campaigns)
|
||||
|
||||
# For each drop, find awarded badge (distribution_type BADGE)
|
||||
drop_awarded_badges: dict[str, ChatBadge] = {}
|
||||
benefit_badge_titles: set[str] = set()
|
||||
for campaign in campaigns_list:
|
||||
for drop in campaign.time_based_drops.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||
for benefit in drop.benefits.all():
|
||||
if benefit.distribution_type == "BADGE" and benefit.name:
|
||||
benefit_badge_titles.add(benefit.name)
|
||||
|
||||
# Bulk-load all matching ChatBadge instances to avoid N+1 queries
|
||||
badges_by_title: dict[str, ChatBadge] = {
|
||||
badge.title: badge
|
||||
for badge in ChatBadge.objects.filter(title__in=benefit_badge_titles)
|
||||
}
|
||||
|
||||
for campaign in campaigns_list:
|
||||
for drop in campaign.time_based_drops.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||
for benefit in drop.benefits.all():
|
||||
if benefit.distribution_type == "BADGE":
|
||||
badge: ChatBadge | None = badges_by_title.get(benefit.name)
|
||||
if badge:
|
||||
drop_awarded_badges[drop.twitch_id] = badge
|
||||
|
||||
active_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.start_at is not None
|
||||
and campaign.start_at <= now
|
||||
and campaign.end_at is not None
|
||||
|
|
@ -1195,7 +1202,7 @@ class GameDetailView(DetailView):
|
|||
|
||||
upcoming_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.start_at is not None and campaign.start_at > now
|
||||
]
|
||||
|
||||
|
|
@ -1209,7 +1216,7 @@ class GameDetailView(DetailView):
|
|||
|
||||
expired_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.end_at is not None and campaign.end_at < now
|
||||
]
|
||||
|
||||
|
|
@ -1229,10 +1236,10 @@ class GameDetailView(DetailView):
|
|||
)
|
||||
game_data: list[dict[str, Any]] = json.loads(serialized_game)
|
||||
|
||||
if all_campaigns.exists():
|
||||
if campaigns_list:
|
||||
serialized_campaigns = serialize(
|
||||
"json",
|
||||
all_campaigns,
|
||||
campaigns_list,
|
||||
fields=(
|
||||
"twitch_id",
|
||||
"name",
|
||||
|
|
@ -2028,13 +2035,13 @@ class ChannelDetailView(DetailView):
|
|||
dict: Context data with active, upcoming, and expired campaigns.
|
||||
"""
|
||||
context: dict[str, Any] = super().get_context_data(**kwargs)
|
||||
channel: Channel = self.get_object() # pyright: ignore[reportAssignmentType]
|
||||
channel: Channel = self.object # pyright: ignore[reportAssignmentType]
|
||||
|
||||
now: datetime.datetime = timezone.now()
|
||||
all_campaigns: QuerySet[DropCampaign] = (
|
||||
DropCampaign.objects
|
||||
.filter(allow_channels=channel)
|
||||
.prefetch_related("game__owners")
|
||||
.select_related("game")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"time_based_drops",
|
||||
|
|
@ -2049,9 +2056,11 @@ class ChannelDetailView(DetailView):
|
|||
.order_by("-start_at")
|
||||
)
|
||||
|
||||
campaigns_list: list[DropCampaign] = list(all_campaigns)
|
||||
|
||||
active_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.start_at is not None
|
||||
and campaign.start_at <= now
|
||||
and campaign.end_at is not None
|
||||
|
|
@ -2067,7 +2076,7 @@ class ChannelDetailView(DetailView):
|
|||
|
||||
upcoming_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.start_at is not None and campaign.start_at > now
|
||||
]
|
||||
upcoming_campaigns.sort(
|
||||
|
|
@ -2080,7 +2089,7 @@ class ChannelDetailView(DetailView):
|
|||
|
||||
expired_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.end_at is not None and campaign.end_at < now
|
||||
]
|
||||
|
||||
|
|
@ -2091,10 +2100,10 @@ class ChannelDetailView(DetailView):
|
|||
)
|
||||
channel_data: list[dict[str, Any]] = json.loads(serialized_channel)
|
||||
|
||||
if all_campaigns.exists():
|
||||
if campaigns_list:
|
||||
serialized_campaigns: str = serialize(
|
||||
"json",
|
||||
all_campaigns,
|
||||
campaigns_list,
|
||||
fields=(
|
||||
"twitch_id",
|
||||
"name",
|
||||
|
|
@ -2112,7 +2121,7 @@ class ChannelDetailView(DetailView):
|
|||
channel_data[0]["fields"]["campaigns"] = campaigns_data
|
||||
|
||||
name: str = channel.display_name or channel.name or channel.twitch_id
|
||||
total_campaigns: int = len(all_campaigns)
|
||||
total_campaigns: int = len(campaigns_list)
|
||||
description: str = f"{name} participates in {total_campaigns} drop campaign"
|
||||
if total_campaigns > 1:
|
||||
description += "s"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue