Allow operation_names to be a list instead of a singular item
This commit is contained in:
parent
2251475bbe
commit
65a7622582
7 changed files with 104 additions and 42 deletions
|
|
@ -836,10 +836,10 @@ class Command(BaseCommand):
|
||||||
if (
|
if (
|
||||||
response.extensions
|
response.extensions
|
||||||
and response.extensions.operation_name
|
and response.extensions.operation_name
|
||||||
and campaign_obj.operation_name != response.extensions.operation_name
|
and response.extensions.operation_name not in campaign_obj.operation_names
|
||||||
):
|
):
|
||||||
campaign_obj.operation_name = response.extensions.operation_name
|
campaign_obj.operation_names.append(response.extensions.operation_name)
|
||||||
campaign_obj.save(update_fields=["operation_name"])
|
campaign_obj.save(update_fields=["operation_names"])
|
||||||
|
|
||||||
if drop_campaign.time_based_drops:
|
if drop_campaign.time_based_drops:
|
||||||
self._process_time_based_drops(
|
self._process_time_based_drops(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Generated by Django 6.0.1 on 2026-01-17 05:32
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_operation_name_to_list(apps, schema_editor) -> None: # noqa: ANN001, ARG001
|
||||||
|
"""Convert operation_name string values to operation_names list."""
|
||||||
|
DropCampaign = apps.get_model("twitch", "DropCampaign")
|
||||||
|
for campaign in DropCampaign.objects.all():
|
||||||
|
if campaign.operation_name and campaign.operation_name.strip():
|
||||||
|
campaign.operation_names = [campaign.operation_name.strip()]
|
||||||
|
campaign.save(update_fields=["operation_names"])
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_operation_names_to_string(apps, schema_editor) -> None: # noqa: ARG001, ANN001
|
||||||
|
"""Convert operation_names list back to operation_name string (first item only)."""
|
||||||
|
DropCampaign = apps.get_model("twitch", "DropCampaign")
|
||||||
|
for campaign in DropCampaign.objects.all():
|
||||||
|
if campaign.operation_names:
|
||||||
|
campaign.operation_name = campaign.operation_names[0]
|
||||||
|
campaign.save(update_fields=["operation_name"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
"""Rename operation_name field to operation_names and convert to list."""
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("twitch", "0006_add_chat_badges"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name="dropcampaign",
|
||||||
|
name="twitch_drop_operati_8cfeb5_idx",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="dropcampaign",
|
||||||
|
name="operation_names",
|
||||||
|
field=models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
help_text="List of GraphQL operation names used to fetch this campaign data (e.g., ['ViewerDropsDashboard', 'Inventory']).", # noqa: E501
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_operation_name_to_list,
|
||||||
|
reverse_operation_names_to_string,
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="dropcampaign",
|
||||||
|
index=models.Index(fields=["operation_names"], name="twitch_drop_operati_fe3bc8_idx"),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="dropcampaign",
|
||||||
|
name="operation_name",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -346,10 +346,10 @@ class DropCampaign(models.Model):
|
||||||
help_text="Game associated with this campaign.",
|
help_text="Game associated with this campaign.",
|
||||||
)
|
)
|
||||||
|
|
||||||
operation_name = models.TextField(
|
operation_names = models.JSONField(
|
||||||
|
default=list,
|
||||||
blank=True,
|
blank=True,
|
||||||
default="",
|
help_text="List of GraphQL operation names used to fetch this campaign data (e.g., ['ViewerDropsDashboard', 'Inventory']).", # noqa: E501
|
||||||
help_text="The GraphQL operation name used to fetch this campaign data (e.g., 'ViewerDropsDashboard').",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
added_at = models.DateTimeField(
|
added_at = models.DateTimeField(
|
||||||
|
|
@ -371,7 +371,7 @@ class DropCampaign(models.Model):
|
||||||
models.Index(fields=["name"]),
|
models.Index(fields=["name"]),
|
||||||
models.Index(fields=["description"]),
|
models.Index(fields=["description"]),
|
||||||
models.Index(fields=["allow_is_enabled"]),
|
models.Index(fields=["allow_is_enabled"]),
|
||||||
models.Index(fields=["operation_name"]),
|
models.Index(fields=["operation_names"]),
|
||||||
models.Index(fields=["added_at"]),
|
models.Index(fields=["added_at"]),
|
||||||
models.Index(fields=["updated_at"]),
|
models.Index(fields=["updated_at"]),
|
||||||
# Composite indexes for common queries
|
# Composite indexes for common queries
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ class ExtractCampaignsTests(TestCase):
|
||||||
# Check that campaign was created with operation_name
|
# Check that campaign was created with operation_name
|
||||||
campaign: DropCampaign = DropCampaign.objects.get(twitch_id="inventory-campaign-1")
|
campaign: DropCampaign = DropCampaign.objects.get(twitch_id="inventory-campaign-1")
|
||||||
assert campaign.name == "Test Inventory Campaign"
|
assert campaign.name == "Test Inventory Campaign"
|
||||||
assert campaign.operation_name == "Inventory"
|
assert campaign.operation_names == ["Inventory"]
|
||||||
|
|
||||||
def test_handles_inventory_with_null_campaigns(self) -> None:
|
def test_handles_inventory_with_null_campaigns(self) -> None:
|
||||||
"""Ensure Inventory JSON with null dropCampaignsInProgress is handled correctly."""
|
"""Ensure Inventory JSON with null dropCampaignsInProgress is handled correctly."""
|
||||||
|
|
@ -467,24 +467,25 @@ class OperationNameFilteringTests(TestCase):
|
||||||
command.process_responses([viewer_drops_payload], Path("viewer.json"), {})
|
command.process_responses([viewer_drops_payload], Path("viewer.json"), {})
|
||||||
command.process_responses([inventory_payload], Path("inventory.json"), {})
|
command.process_responses([inventory_payload], Path("inventory.json"), {})
|
||||||
|
|
||||||
# Verify we can filter by operation_name
|
# Verify we can filter by operation_names
|
||||||
viewer_campaigns: QuerySet[DropCampaign, DropCampaign] = DropCampaign.objects.filter(
|
# SQLite doesn't support JSON contains, so we filter in Python
|
||||||
operation_name="ViewerDropsDashboard",
|
all_campaigns: QuerySet[DropCampaign, DropCampaign] = DropCampaign.objects.all()
|
||||||
)
|
viewer_campaigns: list[DropCampaign] = [c for c in all_campaigns if "ViewerDropsDashboard" in c.operation_names]
|
||||||
inventory_campaigns: QuerySet[DropCampaign, DropCampaign] = DropCampaign.objects.filter(
|
inventory_campaigns: list[DropCampaign] = [c for c in all_campaigns if "Inventory" in c.operation_names]
|
||||||
operation_name="Inventory",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert viewer_campaigns.count() >= 1
|
assert len(viewer_campaigns) >= 1
|
||||||
assert inventory_campaigns.count() >= 1
|
assert len(inventory_campaigns) >= 1
|
||||||
|
|
||||||
# Verify the correct campaigns are in each queryset
|
# Verify the correct campaigns are in each list
|
||||||
assert viewer_campaigns.filter(twitch_id="viewer-campaign-1").exists()
|
viewer_ids = [c.twitch_id for c in viewer_campaigns]
|
||||||
assert inventory_campaigns.filter(twitch_id="inventory-campaign-1").exists()
|
inventory_ids = [c.twitch_id for c in inventory_campaigns]
|
||||||
|
|
||||||
|
assert "viewer-campaign-1" in viewer_ids
|
||||||
|
assert "inventory-campaign-1" in inventory_ids
|
||||||
|
|
||||||
# Cross-check: Inventory campaign should not be in viewer campaigns
|
# Cross-check: Inventory campaign should not be in viewer campaigns
|
||||||
assert not viewer_campaigns.filter(twitch_id="inventory-campaign-1").exists()
|
assert "inventory-campaign-1" not in viewer_ids
|
||||||
assert not inventory_campaigns.filter(twitch_id="viewer-campaign-1").exists()
|
assert "viewer-campaign-1" not in inventory_ids
|
||||||
|
|
||||||
|
|
||||||
class GameImportTests(TestCase):
|
class GameImportTests(TestCase):
|
||||||
|
|
@ -586,7 +587,7 @@ class ExampleJsonImportTests(TestCase):
|
||||||
assert campaign.allow_channels.count() == 0
|
assert campaign.allow_channels.count() == 0
|
||||||
|
|
||||||
# Operation name provenance
|
# Operation name provenance
|
||||||
assert campaign.operation_name == "DropCampaignDetails"
|
assert campaign.operation_names == ["DropCampaignDetails"]
|
||||||
|
|
||||||
# Related game/org normalization
|
# Related game/org normalization
|
||||||
game: Game = Game.objects.get(twitch_id="2094865572")
|
game: Game = Game.objects.get(twitch_id="2094865572")
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class RSSFeedTestCase(TestCase):
|
||||||
game=self.game,
|
game=self.game,
|
||||||
start_at=timezone.now(),
|
start_at=timezone.now(),
|
||||||
end_at=timezone.now() + timedelta(days=7),
|
end_at=timezone.now() + timedelta(days=7),
|
||||||
operation_name="DropCampaignDetails",
|
operation_names=["DropCampaignDetails"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_organization_feed(self) -> None:
|
def test_organization_feed(self) -> None:
|
||||||
|
|
@ -132,7 +132,7 @@ class RSSFeedTestCase(TestCase):
|
||||||
game=other_game,
|
game=other_game,
|
||||||
start_at=timezone.now(),
|
start_at=timezone.now(),
|
||||||
end_at=timezone.now() + timedelta(days=7),
|
end_at=timezone.now() + timedelta(days=7),
|
||||||
operation_name="DropCampaignDetails",
|
operation_names=["DropCampaignDetails"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get feed for first game
|
# Get feed for first game
|
||||||
|
|
@ -165,7 +165,7 @@ class RSSFeedTestCase(TestCase):
|
||||||
game=other_game,
|
game=other_game,
|
||||||
start_at=timezone.now(),
|
start_at=timezone.now(),
|
||||||
end_at=timezone.now() + timedelta(days=7),
|
end_at=timezone.now() + timedelta(days=7),
|
||||||
operation_name="DropCampaignDetails",
|
operation_names=["DropCampaignDetails"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get feed for first organization
|
# Get feed for first organization
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class TestSearchView:
|
||||||
name="Test Campaign",
|
name="Test Campaign",
|
||||||
description="A test campaign",
|
description="A test campaign",
|
||||||
game=game,
|
game=game,
|
||||||
operation_name="DropCampaignDetails",
|
operation_names=["DropCampaignDetails"],
|
||||||
)
|
)
|
||||||
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
|
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
|
||||||
twitch_id="1011",
|
twitch_id="1011",
|
||||||
|
|
@ -268,7 +268,7 @@ class TestChannelListView:
|
||||||
twitch_id=f"campaign{i}",
|
twitch_id=f"campaign{i}",
|
||||||
name=f"Campaign {i}",
|
name=f"Campaign {i}",
|
||||||
game=game,
|
game=game,
|
||||||
operation_name="DropCampaignDetails",
|
operation_names=["DropCampaignDetails"],
|
||||||
)
|
)
|
||||||
campaign.allow_channels.add(channel)
|
campaign.allow_channels.add(channel)
|
||||||
campaigns.append(campaign)
|
campaigns.append(campaign)
|
||||||
|
|
@ -357,7 +357,7 @@ class TestChannelListView:
|
||||||
twitch_id=f"campaign_ch2_{i}",
|
twitch_id=f"campaign_ch2_{i}",
|
||||||
name=f"Campaign Ch2 {i}",
|
name=f"Campaign Ch2 {i}",
|
||||||
game=game,
|
game=game,
|
||||||
operation_name="DropCampaignDetails",
|
operation_names=["DropCampaignDetails"],
|
||||||
)
|
)
|
||||||
campaign.allow_channels.add(channel2)
|
campaign.allow_channels.add(channel2)
|
||||||
|
|
||||||
|
|
@ -422,7 +422,7 @@ class TestChannelListView:
|
||||||
twitch_id="camp1",
|
twitch_id="camp1",
|
||||||
name="Campaign",
|
name="Campaign",
|
||||||
game=game,
|
game=game,
|
||||||
operation_name="DropCampaignDetails",
|
operation_names=["DropCampaignDetails"],
|
||||||
start_at=now - datetime.timedelta(hours=1),
|
start_at=now - datetime.timedelta(hours=1),
|
||||||
end_at=now + datetime.timedelta(hours=1),
|
end_at=now + datetime.timedelta(hours=1),
|
||||||
)
|
)
|
||||||
|
|
@ -464,7 +464,7 @@ class TestChannelListView:
|
||||||
twitch_id="c1",
|
twitch_id="c1",
|
||||||
name="Campaign",
|
name="Campaign",
|
||||||
game=game,
|
game=game,
|
||||||
operation_name="DropCampaignDetails",
|
operation_names=["DropCampaignDetails"],
|
||||||
)
|
)
|
||||||
url: str = reverse("twitch:campaign_detail", args=[campaign.twitch_id])
|
url: str = reverse("twitch:campaign_detail", args=[campaign.twitch_id])
|
||||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||||
|
|
@ -482,7 +482,7 @@ class TestChannelListView:
|
||||||
twitch_id="c-badge",
|
twitch_id="c-badge",
|
||||||
name="Campaign",
|
name="Campaign",
|
||||||
game=game,
|
game=game,
|
||||||
operation_name="DropCampaignDetails",
|
operation_names=["DropCampaignDetails"],
|
||||||
)
|
)
|
||||||
|
|
||||||
drop = TimeBasedDrop.objects.create(
|
drop = TimeBasedDrop.objects.create(
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
||||||
"start_at",
|
"start_at",
|
||||||
"end_at",
|
"end_at",
|
||||||
"allow_is_enabled",
|
"allow_is_enabled",
|
||||||
"operation_name",
|
"operation_names",
|
||||||
"game",
|
"game",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
|
@ -701,7 +701,7 @@ class GameDetailView(DetailView):
|
||||||
"allow_is_enabled",
|
"allow_is_enabled",
|
||||||
"allow_channels",
|
"allow_channels",
|
||||||
"game",
|
"game",
|
||||||
"operation_name",
|
"operation_names",
|
||||||
"added_at",
|
"added_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
),
|
),
|
||||||
|
|
@ -951,14 +951,16 @@ def debug_view(request: HttpRequest) -> HttpResponse:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Distinct GraphQL operation names used to fetch campaigns with counts
|
# Distinct GraphQL operation names used to fetch campaigns with counts
|
||||||
operation_names_with_counts: list[dict[str, Any]] = list(
|
# Since operation_names is now a JSON list field, we need to flatten and count
|
||||||
DropCampaign.objects
|
operation_names_counter: dict[str, int] = {}
|
||||||
.annotate(trimmed_op=Trim("operation_name"))
|
for campaign in DropCampaign.objects.only("operation_names"):
|
||||||
.filter(~Q(trimmed_op__exact=""))
|
for op_name in campaign.operation_names:
|
||||||
.values("trimmed_op")
|
if op_name and op_name.strip():
|
||||||
.annotate(count=Count("twitch_id"))
|
operation_names_counter[op_name.strip()] = operation_names_counter.get(op_name.strip(), 0) + 1
|
||||||
.order_by("trimmed_op"),
|
|
||||||
)
|
operation_names_with_counts: list[dict[str, Any]] = [
|
||||||
|
{"trimmed_op": op_name, "count": count} for op_name, count in sorted(operation_names_counter.items())
|
||||||
|
]
|
||||||
|
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"now": now,
|
"now": now,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue