Fix owners not getting imported correctly
This commit is contained in:
parent
026bc57f77
commit
df2941cdbc
7 changed files with 138 additions and 9 deletions
|
|
@ -20,7 +20,7 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = ["pytest", "pytest-django", "djlint"]
|
dev = ["pytest", "pytest-django", "djlint", "django-stubs"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
DJANGO_SETTINGS_MODULE = "config.settings"
|
DJANGO_SETTINGS_MODULE = "config.settings"
|
||||||
|
|
|
||||||
|
|
@ -432,25 +432,34 @@ class Command(BaseCommand):
|
||||||
def _get_or_create_game(
|
def _get_or_create_game(
|
||||||
self,
|
self,
|
||||||
game_data: GameSchema,
|
game_data: GameSchema,
|
||||||
org_obj: Organization,
|
campaign_org_obj: Organization,
|
||||||
) -> Game:
|
) -> Game:
|
||||||
"""Get or create a game from cache or database.
|
"""Get or create a game from cache or database, using correct owner organization.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
game_data: Game data from Pydantic model.
|
game_data: Game data from Pydantic model.
|
||||||
org_obj: Organization that owns this game.
|
campaign_org_obj: Organization that owns the campaign (fallback).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Game instance.
|
Game instance.
|
||||||
"""
|
"""
|
||||||
|
# Determine correct owner organization for the game
|
||||||
|
owner_org_obj = campaign_org_obj
|
||||||
|
if hasattr(game_data, "owner_organization") and game_data.owner_organization:
|
||||||
|
owner_org_data = game_data.owner_organization
|
||||||
|
if isinstance(owner_org_data, dict):
|
||||||
|
# Convert dict to OrganizationSchema
|
||||||
|
owner_org_data = OrganizationSchema.model_validate(owner_org_data)
|
||||||
|
owner_org_obj = self._get_or_create_organization(owner_org_data)
|
||||||
|
|
||||||
if game_data.twitch_id in self.game_cache:
|
if game_data.twitch_id in self.game_cache:
|
||||||
game_obj: Game = self.game_cache[game_data.twitch_id]
|
game_obj: Game = self.game_cache[game_data.twitch_id]
|
||||||
|
|
||||||
update_fields: list[str] = []
|
update_fields: list[str] = []
|
||||||
|
|
||||||
# Ensure owner is correct without triggering a read
|
# Ensure owner is correct without triggering a read
|
||||||
if game_obj.owner_id != org_obj.pk: # type: ignore[attr-defined]
|
if game_obj.owner_id != owner_org_obj.pk: # type: ignore[attr-defined]
|
||||||
game_obj.owner = org_obj
|
game_obj.owner = owner_org_obj
|
||||||
update_fields.append("owner")
|
update_fields.append("owner")
|
||||||
|
|
||||||
# Persist normalized display name when provided
|
# Persist normalized display name when provided
|
||||||
|
|
@ -485,7 +494,7 @@ class Command(BaseCommand):
|
||||||
"name": game_data.name or "",
|
"name": game_data.name or "",
|
||||||
"slug": game_data.slug or "",
|
"slug": game_data.slug or "",
|
||||||
"box_art": game_data.box_art_url or "",
|
"box_art": game_data.box_art_url or "",
|
||||||
"owner": org_obj,
|
"owner": owner_org_obj,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if created:
|
if created:
|
||||||
|
|
@ -631,7 +640,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
game_obj: Game = self._get_or_create_game(
|
game_obj: Game = self._get_or_create_game(
|
||||||
game_data=drop_campaign.game,
|
game_data=drop_campaign.game,
|
||||||
org_obj=org_obj,
|
campaign_org_obj=org_obj,
|
||||||
)
|
)
|
||||||
|
|
||||||
start_at_dt: datetime | None = parse_date(drop_campaign.start_at)
|
start_at_dt: datetime | None = parse_date(drop_campaign.start_at)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class GameSchema(BaseModel):
|
||||||
slug: str | None = None # Present in Inventory format
|
slug: str | None = None # Present in Inventory format
|
||||||
name: str | None = None # Present in Inventory format (alternative to displayName)
|
name: str | None = None # Present in Inventory format (alternative to displayName)
|
||||||
type_name: Literal["Game"] = Field(alias="__typename") # Present in both formats
|
type_name: Literal["Game"] = Field(alias="__typename") # Present in both formats
|
||||||
|
owner_organization: dict | None = Field(default=None, alias="ownerOrganization")
|
||||||
|
|
||||||
model_config = {
|
model_config = {
|
||||||
"extra": "forbid",
|
"extra": "forbid",
|
||||||
|
|
@ -58,7 +59,6 @@ class GameSchema(BaseModel):
|
||||||
"""
|
"""
|
||||||
if isinstance(data, dict) and "displayName" not in data and "name" in data:
|
if isinstance(data, dict) and "displayName" not in data and "name" in data:
|
||||||
data["displayName"] = data["name"]
|
data["displayName"] = data["name"]
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,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",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_organization_feed(self) -> None:
|
def test_organization_feed(self) -> None:
|
||||||
|
|
@ -94,6 +95,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",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get feed for first game
|
# Get feed for first game
|
||||||
|
|
@ -126,6 +128,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",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get feed for first organization
|
# Get feed for first organization
|
||||||
|
|
|
||||||
75
twitch/tests/test_game_owner_organization.py
Normal file
75
twitch/tests/test_game_owner_organization.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from twitch.management.commands.better_import_drops import Command
|
||||||
|
from twitch.models import Game
|
||||||
|
from twitch.models import Organization
|
||||||
|
|
||||||
|
|
||||||
|
class GameOwnerOrganizationTests(TestCase):
|
||||||
|
"""Tests for correct precedence of game owner organization during import."""
|
||||||
|
|
||||||
|
def test_game_owner_organization_precedence(self) -> None:
|
||||||
|
"""If both owner and ownerOrganization are present, game owner should be ownerOrganization."""
|
||||||
|
command = Command()
|
||||||
|
command.pre_fill_cache()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"id": "17658559",
|
||||||
|
"dropCampaign": {
|
||||||
|
"id": "test-campaign-1",
|
||||||
|
"name": "Rustmas 2025",
|
||||||
|
"description": "Test campaign desc",
|
||||||
|
"startAt": "2025-12-08T18:00:00Z",
|
||||||
|
"endAt": "2026-01-01T07:59:59.999Z",
|
||||||
|
"accountLinkURL": "https://www.twitch.tv/",
|
||||||
|
"detailsURL": "https://help.twitch.tv/s/article/twitch-chat-badges-guide",
|
||||||
|
"imageURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/495ebb6b-8134-4e51-b9d0-1f4a221b4f8d.png",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"self": {"isAccountConnected": True, "__typename": "DropCampaignSelfEdge"},
|
||||||
|
"game": {
|
||||||
|
"id": "263490",
|
||||||
|
"slug": "rust",
|
||||||
|
"displayName": "Rust",
|
||||||
|
"__typename": "Game",
|
||||||
|
"ownerOrganization": {
|
||||||
|
"id": "d32de13d-937e-4196-8198-1a7f875f295a",
|
||||||
|
"name": "Twitch Gaming",
|
||||||
|
"__typename": "Organization",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"owner": {"id": "other-org-id", "name": "Other Org", "__typename": "Organization"},
|
||||||
|
"timeBasedDrops": [],
|
||||||
|
"eventBasedDrops": [],
|
||||||
|
"allow": {"channels": None, "isEnabled": False, "__typename": "DropCampaignACL"},
|
||||||
|
"__typename": "DropCampaign",
|
||||||
|
},
|
||||||
|
"__typename": "User",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"extensions": {"operationName": "DropCampaignDetails"},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run import logic
|
||||||
|
success, broken_dir = command.process_responses(
|
||||||
|
responses=[payload],
|
||||||
|
file_path=Path("test_owner_org.json"),
|
||||||
|
options={},
|
||||||
|
)
|
||||||
|
assert success is True
|
||||||
|
assert broken_dir is None
|
||||||
|
|
||||||
|
# Check game owner is Twitch Gaming, not Other Org
|
||||||
|
game: Game = Game.objects.get(twitch_id="263490")
|
||||||
|
org: Organization = Organization.objects.get(twitch_id="d32de13d-937e-4196-8198-1a7f875f295a")
|
||||||
|
assert game.owner == org
|
||||||
|
assert game.owner
|
||||||
|
assert game.owner.name == "Twitch Gaming"
|
||||||
|
|
||||||
|
# Check both organizations exist
|
||||||
|
Organization.objects.get(twitch_id="other-org-id")
|
||||||
|
|
@ -42,6 +42,7 @@ class TestSearchView:
|
||||||
name="Test Campaign",
|
name="Test Campaign",
|
||||||
description="A test campaign",
|
description="A test campaign",
|
||||||
game=game,
|
game=game,
|
||||||
|
operation_name="DropCampaignDetails",
|
||||||
)
|
)
|
||||||
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
|
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
|
||||||
twitch_id="1011",
|
twitch_id="1011",
|
||||||
|
|
@ -262,6 +263,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",
|
||||||
)
|
)
|
||||||
campaign.allow_channels.add(channel)
|
campaign.allow_channels.add(channel)
|
||||||
campaigns.append(campaign)
|
campaigns.append(campaign)
|
||||||
|
|
@ -350,6 +352,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",
|
||||||
)
|
)
|
||||||
campaign.allow_channels.add(channel2)
|
campaign.allow_channels.add(channel2)
|
||||||
|
|
||||||
|
|
|
||||||
39
uv.lock
generated
39
uv.lock
generated
|
|
@ -118,6 +118,34 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/72/685c978af45ad08257e2c69687a873eda6b6531c79b6e6091794c41c5ff6/django_debug_toolbar-6.1.0-py3-none-any.whl", hash = "sha256:e214dea4494087e7cebdcea84223819c5eb97f9de3110a3665ad673f0ba98413", size = 269069, upload-time = "2025-10-30T19:50:37.71Z" },
|
{ url = "https://files.pythonhosted.org/packages/6d/72/685c978af45ad08257e2c69687a873eda6b6531c79b6e6091794c41c5ff6/django_debug_toolbar-6.1.0-py3-none-any.whl", hash = "sha256:e214dea4494087e7cebdcea84223819c5eb97f9de3110a3665ad673f0ba98413", size = 269069, upload-time = "2025-10-30T19:50:37.71Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-stubs"
|
||||||
|
version = "5.2.8"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "django-stubs-ext" },
|
||||||
|
{ name = "types-pyyaml" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6c/75/97626224fd8f1787bb6f7f06944efcfddd5da7764bf741cf7f59d102f4a0/django_stubs-5.2.8.tar.gz", hash = "sha256:9bba597c9a8ed8c025cae4696803d5c8be1cf55bfc7648a084cbf864187e2f8b", size = 257709, upload-time = "2025-12-01T08:13:09.569Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/3f/7c9543ad5ade5ce1d33d187a3abd82164570314ebee72c6206ab5c044ebf/django_stubs-5.2.8-py3-none-any.whl", hash = "sha256:a3c63119fd7062ac63d58869698d07c9e5ec0561295c4e700317c54e8d26716c", size = 508136, upload-time = "2025-12-01T08:13:07.963Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-stubs-ext"
|
||||||
|
version = "5.2.8"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/14/a2/d67f4a5200ff7626b104eddceaf529761cba4ed318a73ffdb0677551be73/django_stubs_ext-5.2.8.tar.gz", hash = "sha256:b39938c46d7a547cd84e4a6378dbe51a3dd64d70300459087229e5fee27e5c6b", size = 6487, upload-time = "2025-12-01T08:12:37.486Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/2d/cb0151b780c3730cf0f2c0fcb1b065a5e88f877cf7a9217483c375353af1/django_stubs_ext-5.2.8-py3-none-any.whl", hash = "sha256:1dd5470c9675591362c78a157a3cf8aec45d0e7a7f0cf32f227a1363e54e0652", size = 9949, upload-time = "2025-12-01T08:12:36.397Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djlint"
|
name = "djlint"
|
||||||
version = "1.36.4"
|
version = "1.36.4"
|
||||||
|
|
@ -515,6 +543,7 @@ dependencies = [
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "django-stubs" },
|
||||||
{ name = "djlint" },
|
{ name = "djlint" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-django" },
|
{ name = "pytest-django" },
|
||||||
|
|
@ -538,11 +567,21 @@ requires-dist = [
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "django-stubs", specifier = ">=5.2.8" },
|
||||||
{ name = "djlint" },
|
{ name = "djlint" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-django" },
|
{ name = "pytest-django" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-pyyaml"
|
||||||
|
version = "6.0.12.20250915"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue