diff --git a/core/urls.py b/core/urls.py
index 44129e5..7fa7302 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -2,7 +2,7 @@ from __future__ import annotations
from django.urls import URLPattern, URLResolver, path
-from core.views import GameView, RewardCampaignView, index
+from core.views import game_view, index, reward_campaign_view
app_name: str = "core"
@@ -10,12 +10,12 @@ urlpatterns: list[URLPattern | URLResolver] = [
path(route="", view=index, name="index"),
path(
route="games/",
- view=GameView.as_view(),
+ view=game_view,
name="games",
),
path(
route="reward_campaigns/",
- view=RewardCampaignView.as_view(),
+ view=reward_campaign_view,
name="reward_campaigns",
),
]
diff --git a/core/views.py b/core/views.py
index ac236b2..0103e6c 100644
--- a/core/views.py
+++ b/core/views.py
@@ -1,12 +1,13 @@
from __future__ import annotations
import logging
+from dataclasses import dataclass
from typing import TYPE_CHECKING
import hishel
from django.conf import settings
+from django.db.models.manager import BaseManager
from django.template.response import TemplateResponse
-from django.views.generic import ListView
from core.data import WebhookData
from twitch_app.models import Game, RewardCampaign
@@ -58,26 +59,59 @@ def get_webhook_data(webhook: str) -> WebhookData:
)
+@dataclass
+class TOCItem:
+ """Table of contents item."""
+
+ name: str
+ toc_id: str
+
+
+def build_toc(list_of_things: list[TOCItem]) -> str:
+ """Build the table of contents."""
+ html: str = """
+
+
+
+
+ """
+
+ for item in list_of_things:
+ html += (
+ f'
{item.name}'
+ )
+ html += """
"""
+ return html
+
+
def index(request: HttpRequest) -> HttpResponse:
"""Render the index page."""
reward_campaigns: BaseManager[RewardCampaign] = RewardCampaign.objects.all()
- return TemplateResponse(
- request=request,
- template="index.html",
- context={"reward_campaigns": reward_campaigns},
- )
+ toc: str = build_toc([
+ TOCItem(name="Information", toc_id="#info-box"),
+ TOCItem(name="Games", toc_id="#games"),
+ ])
+
+ context: dict[str, BaseManager[RewardCampaign] | str] = {"reward_campaigns": reward_campaigns, "toc": toc}
+ return TemplateResponse(request=request, template="index.html", context=context)
-class GameView(ListView):
- model = Game
- template_name: str = "games.html"
- context_object_name: str = "games"
- paginate_by = 100
+def game_view(request: HttpRequest) -> HttpResponse:
+ """Render the game view page."""
+ games: BaseManager[Game] = Game.objects.all()
+
+ tocs: list[TOCItem] = [
+ TOCItem(name=game.display_name, toc_id=game.slug) for game in games if game.display_name and game.slug
+ ]
+ toc: str = build_toc(tocs)
+
+ context: dict[str, BaseManager[Game] | str] = {"games": games, "toc": toc}
+ return TemplateResponse(request=request, template="games.html", context=context)
-class RewardCampaignView(ListView):
- model = RewardCampaign
- template_name: str = "reward_campaigns.html"
- context_object_name: str = "reward_campaigns"
- paginate_by = 100
+def reward_campaign_view(request: HttpRequest) -> HttpResponse:
+ """Render the reward campaign view page."""
+ reward_campaigns: BaseManager[RewardCampaign] = RewardCampaign.objects.all()
+ context: dict[str, BaseManager[RewardCampaign]] = {"reward_campaigns": reward_campaigns}
+ return TemplateResponse(request=request, template="reward_campaigns.html", context=context)
diff --git a/testboi.py b/testboi.py
new file mode 100644
index 0000000..79ca7b6
--- /dev/null
+++ b/testboi.py
@@ -0,0 +1,380 @@
+from datetime import datetime
+from enum import Enum
+from typing import Any
+from uuid import UUID
+
+
+class ChannelTypename(Enum):
+ CHANNEL = "Channel"
+ GAME = "Game"
+ ORGANIZATION = "Organization"
+
+
+class Channel:
+ id: int
+ display_name: str
+ name: str
+ typename: ChannelTypename
+
+ def __init__(self, id: int, display_name: str, name: str, typename: ChannelTypename) -> None:
+ self.id = id
+ self.display_name = display_name
+ self.name = name
+ self.typename = typename
+
+
+class AllowTypename(Enum):
+ DROP_CAMPAIGN_ACL = "DropCampaignACL"
+
+
+class Allow:
+ channels: list[Channel] | None
+ is_enabled: bool
+ typename: AllowTypename
+
+ def __init__(self, channels: list[Channel] | None, is_enabled: bool, typename: AllowTypename) -> None:
+ self.channels = channels
+ self.is_enabled = is_enabled
+ self.typename = typename
+
+
+class SelfTypename(Enum):
+ DROP_CAMPAIGN_SELF_EDGE = "DropCampaignSelfEdge"
+
+
+class Self:
+ is_account_connected: bool
+ typename: SelfTypename
+
+ def __init__(self, is_account_connected: bool, typename: SelfTypename) -> None:
+ self.is_account_connected = is_account_connected
+ self.typename = typename
+
+
+class Game:
+ id: int
+ slug: str
+ display_name: str
+ typename: ChannelTypename
+
+ def __init__(self, id: int, slug: str, display_name: str, typename: ChannelTypename) -> None:
+ self.id = id
+ self.slug = slug
+ self.display_name = display_name
+ self.typename = typename
+
+
+class PurpleOwner:
+ id: UUID | int
+ name: str | None
+ typename: ChannelTypename
+ display_name: str | None
+ slug: str | None
+
+ def __init__(
+ self,
+ id: UUID | int,
+ name: str | None,
+ typename: ChannelTypename,
+ display_name: str | None,
+ slug: str | None,
+ ) -> None:
+ self.id = id
+ self.name = name
+ self.typename = typename
+ self.display_name = display_name
+ self.slug = slug
+
+
+class Status(Enum):
+ ACTIVE = "ACTIVE"
+ EXPIRED = "EXPIRED"
+
+
+class GameClass:
+ id: UUID | int
+ name: str
+ typename: ChannelTypename
+
+ def __init__(self, id: UUID | int, name: str, typename: ChannelTypename) -> None:
+ self.id = id
+ self.name = name
+ self.typename = typename
+
+
+class BenefitTypename(Enum):
+ DROP_BENEFIT = "DropBenefit"
+
+
+class Benefit:
+ id: str
+ created_at: datetime
+ entitlement_limit: int
+ game: GameClass
+ image_asset_url: str
+ is_ios_available: bool
+ name: str
+ owner_organization: GameClass
+ typename: BenefitTypename
+
+ def __init__(
+ self,
+ id: str,
+ created_at: datetime,
+ entitlement_limit: int,
+ game: GameClass,
+ image_asset_url: str,
+ is_ios_available: bool,
+ name: str,
+ owner_organization: GameClass,
+ typename: BenefitTypename,
+ ) -> None:
+ self.id = id
+ self.created_at = created_at
+ self.entitlement_limit = entitlement_limit
+ self.game = game
+ self.image_asset_url = image_asset_url
+ self.is_ios_available = is_ios_available
+ self.name = name
+ self.owner_organization = owner_organization
+ self.typename = typename
+
+
+class BenefitEdgeTypename(Enum):
+ DROP_BENEFIT_EDGE = "DropBenefitEdge"
+
+
+class BenefitEdge:
+ benefit: Benefit
+ entitlement_limit: int
+ typename: BenefitEdgeTypename
+
+ def __init__(self, benefit: Benefit, entitlement_limit: int, typename: BenefitEdgeTypename) -> None:
+ self.benefit = benefit
+ self.entitlement_limit = entitlement_limit
+ self.typename = typename
+
+
+class TimeBasedDropTypename(Enum):
+ TIME_BASED_DROP = "TimeBasedDrop"
+
+
+class TimeBasedDrop:
+ id: UUID
+ required_subs: int
+ benefit_edges: list[BenefitEdge]
+ end_at: datetime
+ name: str
+ precondition_drops: None
+ required_minutes_watched: int
+ start_at: datetime
+ typename: TimeBasedDropTypename
+
+ def __init__(
+ self,
+ id: UUID,
+ required_subs: int,
+ benefit_edges: list[BenefitEdge],
+ end_at: datetime,
+ name: str,
+ precondition_drops: None,
+ required_minutes_watched: int,
+ start_at: datetime,
+ typename: TimeBasedDropTypename,
+ ) -> None:
+ self.id = id
+ self.required_subs = required_subs
+ self.benefit_edges = benefit_edges
+ self.end_at = end_at
+ self.name = name
+ self.precondition_drops = precondition_drops
+ self.required_minutes_watched = required_minutes_watched
+ self.start_at = start_at
+ self.typename = typename
+
+
+class DropCampaignTypename(Enum):
+ DROP_CAMPAIGN = "DropCampaign"
+
+
+class PurpleDropCampaign:
+ id: UUID
+ drop_campaign_self: Self
+ allow: Allow
+ account_link_url: str
+ description: str
+ details_url: str
+ end_at: datetime
+ event_based_drops: list[Any]
+ game: Game
+ image_url: str
+ name: str
+ owner: PurpleOwner
+ start_at: datetime
+ status: Status
+ time_based_drops: list[TimeBasedDrop]
+ typename: DropCampaignTypename
+
+ def __init__(
+ self,
+ id: UUID,
+ drop_campaign_self: Self,
+ allow: Allow,
+ account_link_url: str,
+ description: str,
+ details_url: str,
+ end_at: datetime,
+ event_based_drops: list[Any],
+ game: Game,
+ image_url: str,
+ name: str,
+ owner: PurpleOwner,
+ start_at: datetime,
+ status: Status,
+ time_based_drops: list[TimeBasedDrop],
+ typename: DropCampaignTypename,
+ ) -> None:
+ self.id = id
+ self.drop_campaign_self = drop_campaign_self
+ self.allow = allow
+ self.account_link_url = account_link_url
+ self.description = description
+ self.details_url = details_url
+ self.end_at = end_at
+ self.event_based_drops = event_based_drops
+ self.game = game
+ self.image_url = image_url
+ self.name = name
+ self.owner = owner
+ self.start_at = start_at
+ self.status = status
+ self.time_based_drops = time_based_drops
+ self.typename = typename
+
+
+class UserTypename(Enum):
+ USER = "User"
+
+
+class PurpleUser:
+ id: int
+ drop_campaign: PurpleDropCampaign
+ typename: UserTypename
+
+ def __init__(self, id: int, drop_campaign: PurpleDropCampaign, typename: UserTypename) -> None:
+ self.id = id
+ self.drop_campaign = drop_campaign
+ self.typename = typename
+
+
+class DropCampaign100_Data:
+ user: PurpleUser
+
+ def __init__(self, user: PurpleUser) -> None:
+ self.user = user
+
+
+class OperationName(Enum):
+ DROP_CAMPAIGN_DETAILS = "DropCampaignDetails"
+
+
+class Extensions:
+ duration_milliseconds: int
+ operation_name: OperationName
+ request_id: str
+
+ def __init__(self, duration_milliseconds: int, operation_name: OperationName, request_id: str) -> None:
+ self.duration_milliseconds = duration_milliseconds
+ self.operation_name = operation_name
+ self.request_id = request_id
+
+
+class DropCampaign99:
+ data: DropCampaign100_Data
+ extensions: Extensions
+
+ def __init__(self, data: DropCampaign100_Data, extensions: Extensions) -> None:
+ self.data = data
+ self.extensions = extensions
+
+
+class FluffyDropCampaign:
+ id: UUID
+ drop_campaign_self: Self
+ allow: Allow
+ account_link_url: str
+ description: str
+ details_url: str
+ end_at: datetime
+ event_based_drops: list[Any]
+ game: Game
+ image_url: str
+ name: str
+ owner: GameClass
+ start_at: datetime
+ status: Status
+ time_based_drops: list[TimeBasedDrop]
+ typename: DropCampaignTypename
+
+ def __init__(
+ self,
+ id: UUID,
+ drop_campaign_self: Self,
+ allow: Allow,
+ account_link_url: str,
+ description: str,
+ details_url: str,
+ end_at: datetime,
+ event_based_drops: list[Any],
+ game: Game,
+ image_url: str,
+ name: str,
+ owner: GameClass,
+ start_at: datetime,
+ status: Status,
+ time_based_drops: list[TimeBasedDrop],
+ typename: DropCampaignTypename,
+ ) -> None:
+ self.id = id
+ self.drop_campaign_self = drop_campaign_self
+ self.allow = allow
+ self.account_link_url = account_link_url
+ self.description = description
+ self.details_url = details_url
+ self.end_at = end_at
+ self.event_based_drops = event_based_drops
+ self.game = game
+ self.image_url = image_url
+ self.name = name
+ self.owner = owner
+ self.start_at = start_at
+ self.status = status
+ self.time_based_drops = time_based_drops
+ self.typename = typename
+
+
+class FluffyUser:
+ id: int
+ drop_campaign: FluffyDropCampaign
+ typename: UserTypename
+
+ def __init__(self, id: int, drop_campaign: FluffyDropCampaign, typename: UserTypename) -> None:
+ self.id = id
+ self.drop_campaign = drop_campaign
+ self.typename = typename
+
+
+class DropCampaign109_Data:
+ user: FluffyUser
+
+ def __init__(self, user: FluffyUser) -> None:
+ self.user = user
+
+
+class DropCampaign149:
+ data: DropCampaign109_Data
+ extensions: Extensions
+
+ def __init__(self, data: DropCampaign109_Data, extensions: Extensions) -> None:
+ self.data = data
+ self.extensions = extensions
diff --git a/testboi2.py b/testboi2.py
new file mode 100644
index 0000000..a8c38ce
--- /dev/null
+++ b/testboi2.py
@@ -0,0 +1,252 @@
+from datetime import datetime
+from enum import Enum
+from uuid import UUID
+
+
+class SelfTypename(Enum):
+ DROP_CAMPAIGN_SELF_EDGE = "DropCampaignSelfEdge"
+
+
+class Self:
+ is_account_connected: bool
+ typename: SelfTypename
+
+ def __init__(self, is_account_connected: bool, typename: SelfTypename) -> None:
+ self.is_account_connected = is_account_connected
+ self.typename = typename
+
+
+class GameTypename(Enum):
+ GAME = "Game"
+
+
+class Game:
+ id: int
+ display_name: str
+ box_art_url: str
+ typename: GameTypename
+
+ def __init__(self, id: int, display_name: str, box_art_url: str, typename: GameTypename) -> None:
+ self.id = id
+ self.display_name = display_name
+ self.box_art_url = box_art_url
+ self.typename = typename
+
+
+class OwnerTypename(Enum):
+ ORGANIZATION = "Organization"
+
+
+class Owner:
+ id: UUID
+ name: str
+ typename: OwnerTypename
+
+ def __init__(self, id: UUID, name: str, typename: OwnerTypename) -> None:
+ self.id = id
+ self.name = name
+ self.typename = typename
+
+
+class Status(Enum):
+ ACTIVE = "ACTIVE"
+ EXPIRED = "EXPIRED"
+
+
+class DropCampaignTypename(Enum):
+ DROP_CAMPAIGN = "DropCampaign"
+
+
+class DropCampaign:
+ id: UUID
+ name: str
+ owner: Owner
+ game: Game
+ status: Status
+ start_at: datetime
+ end_at: datetime
+ details_url: str
+ account_link_url: str
+ drop_campaign_self: Self
+ typename: DropCampaignTypename
+
+ def __init__(
+ self,
+ id: UUID,
+ name: str,
+ owner: Owner,
+ game: Game,
+ status: Status,
+ start_at: datetime,
+ end_at: datetime,
+ details_url: str,
+ account_link_url: str,
+ drop_campaign_self: Self,
+ typename: DropCampaignTypename,
+ ) -> None:
+ self.id = id
+ self.name = name
+ self.owner = owner
+ self.game = game
+ self.status = status
+ self.start_at = start_at
+ self.end_at = end_at
+ self.details_url = details_url
+ self.account_link_url = account_link_url
+ self.drop_campaign_self = drop_campaign_self
+ self.typename = typename
+
+
+class CurrentUser:
+ id: int
+ login: str
+ drop_campaigns: list[DropCampaign]
+ typename: str
+
+ def __init__(self, id: int, login: str, drop_campaigns: list[DropCampaign], typename: str) -> None:
+ self.id = id
+ self.login = login
+ self.drop_campaigns = drop_campaigns
+ self.typename = typename
+
+
+class Image:
+ image1_x_url: str
+ typename: str
+
+ def __init__(self, image1_x_url: str, typename: str) -> None:
+ self.image1_x_url = image1_x_url
+ self.typename = typename
+
+
+class Reward:
+ id: UUID
+ name: str
+ banner_image: Image
+ thumbnail_image: Image
+ earnable_until: datetime
+ redemption_instructions: str
+ redemption_url: str
+ typename: str
+
+ def __init__(
+ self,
+ id: UUID,
+ name: str,
+ banner_image: Image,
+ thumbnail_image: Image,
+ earnable_until: datetime,
+ redemption_instructions: str,
+ redemption_url: str,
+ typename: str,
+ ) -> None:
+ self.id = id
+ self.name = name
+ self.banner_image = banner_image
+ self.thumbnail_image = thumbnail_image
+ self.earnable_until = earnable_until
+ self.redemption_instructions = redemption_instructions
+ self.redemption_url = redemption_url
+ self.typename = typename
+
+
+class UnlockRequirements:
+ subs_goal: int
+ minute_watched_goal: int
+ typename: str
+
+ def __init__(self, subs_goal: int, minute_watched_goal: int, typename: str) -> None:
+ self.subs_goal = subs_goal
+ self.minute_watched_goal = minute_watched_goal
+ self.typename = typename
+
+
+class RewardCampaignsAvailableToUser:
+ id: UUID
+ name: str
+ brand: str
+ starts_at: datetime
+ ends_at: datetime
+ status: str
+ summary: str
+ instructions: str
+ external_url: str
+ reward_value_url_param: str
+ about_url: str
+ is_sitewide: bool
+ game: None
+ unlock_requirements: UnlockRequirements
+ image: Image
+ rewards: list[Reward]
+ typename: str
+
+ def __init__(
+ self,
+ id: UUID,
+ name: str,
+ brand: str,
+ starts_at: datetime,
+ ends_at: datetime,
+ status: str,
+ summary: str,
+ instructions: str,
+ external_url: str,
+ reward_value_url_param: str,
+ about_url: str,
+ is_sitewide: bool,
+ game: None,
+ unlock_requirements: UnlockRequirements,
+ image: Image,
+ rewards: list[Reward],
+ typename: str,
+ ) -> None:
+ self.id = id
+ self.name = name
+ self.brand = brand
+ self.starts_at = starts_at
+ self.ends_at = ends_at
+ self.status = status
+ self.summary = summary
+ self.instructions = instructions
+ self.external_url = external_url
+ self.reward_value_url_param = reward_value_url_param
+ self.about_url = about_url
+ self.is_sitewide = is_sitewide
+ self.game = game
+ self.unlock_requirements = unlock_requirements
+ self.image = image
+ self.rewards = rewards
+ self.typename = typename
+
+
+class Data:
+ current_user: CurrentUser
+ reward_campaigns_available_to_user: list[RewardCampaignsAvailableToUser]
+
+ def __init__(
+ self,
+ current_user: CurrentUser,
+ reward_campaigns_available_to_user: list[RewardCampaignsAvailableToUser],
+ ) -> None:
+ self.current_user = current_user
+ self.reward_campaigns_available_to_user = reward_campaigns_available_to_user
+
+
+class Extensions:
+ duration_milliseconds: int
+ operation_name: str
+ request_id: str
+
+ def __init__(self, duration_milliseconds: int, operation_name: str, request_id: str) -> None:
+ self.duration_milliseconds = duration_milliseconds
+ self.operation_name = operation_name
+ self.request_id = request_id
+
+
+class RewardCampaign11:
+ data: Data
+ extensions: Extensions
+
+ def __init__(self, data: Data, extensions: Extensions) -> None:
+ self.data = data
+ self.extensions = extensions
diff --git a/twitch_app/management/commands/scrape_twitch.py b/twitch_app/management/commands/scrape_twitch.py
index a7a44a2..75a578b 100644
--- a/twitch_app/management/commands/scrape_twitch.py
+++ b/twitch_app/management/commands/scrape_twitch.py
@@ -28,6 +28,7 @@ from twitch_app.models import (
if TYPE_CHECKING:
from playwright.async_api._generated import BrowserContext, Page
+import json
# Where to store the Chrome profile
data_dir = Path(
@@ -66,6 +67,7 @@ async def add_or_get_game(json_data: dict, name: str) -> tuple[Game | None, bool
"slug": json_data.get("slug"),
"display_name": json_data.get("displayName"),
"typename": json_data.get("__typename"),
+ "box_art_url": json_data.get("boxArtURL"), # Only for RewardCampaigns
},
)
@@ -204,6 +206,9 @@ async def add_or_get_drop_campaign(
logger.warning("No drop campaign data found")
return None, False
+ if drop_campaign_data.get("__typename") != "Game":
+ logger.error("__typename is not 'Game' for %s", drop_campaign_data.get("name", "Unknown Drop Campaign"))
+
drop_campaign, _ = await DropCampaign.objects.aupdate_or_create(
id=drop_campaign_data["id"],
defaults={
@@ -546,16 +551,35 @@ class Command(BaseCommand):
continue
if "rewardCampaignsAvailableToUser" in campaign["data"]:
+ # Save to folder named "reward_campaigns"
+ dir_name: Path = Path("reward_campaigns")
+ dir_name.mkdir(parents=True, exist_ok=True)
+ with open(file=Path(dir_name / f"reward_campaign_{num}.json"), mode="w", encoding="utf-8") as f:
+ json.dump(campaign, f, indent=4)
+
await add_reward_campaign(campaign)
if "dropCampaign" in campaign.get("data", {}).get("user", {}):
if not campaign["data"]["user"]["dropCampaign"]:
logger.warning("No drop campaign found")
continue
+
+ # Save to folder named "drop_campaign"
+ dir_name: Path = Path("drop_campaign")
+ dir_name.mkdir(parents=True, exist_ok=True)
+ with open(file=Path(dir_name / f"drop_campaign_{num}.json"), mode="w", encoding="utf-8") as f:
+ json.dump(campaign, f, indent=4)
+
await add_drop_campaign(campaign)
if "dropCampaigns" in campaign.get("data", {}).get("user", {}):
for drop_campaign in campaign["data"]["user"]["dropCampaigns"]:
+ # Save to folder named "drop_campaigns"
+ dir_name: Path = Path("drop_campaigns")
+ dir_name.mkdir(parents=True, exist_ok=True)
+ with open(file=Path(dir_name / f"drop_campaign_{num}.json"), mode="w", encoding="utf-8") as f:
+ json.dump(drop_campaign, f, indent=4)
+
await add_drop_campaign(drop_campaign)
return json_data
diff --git a/twitch_app/migrations/0010_frontendchannel_frontendgame_frontendorg_and_more.py b/twitch_app/migrations/0010_frontendchannel_frontendgame_frontendorg_and_more.py
new file mode 100644
index 0000000..e403de8
--- /dev/null
+++ b/twitch_app/migrations/0010_frontendchannel_frontendgame_frontendorg_and_more.py
@@ -0,0 +1,135 @@
+# Generated by Django 5.1rc1 on 2024-08-02 01:20
+
+import django.db.models.deletion
+import django.db.models.manager
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("twitch_app", "0009_alter_benefit_entitlement_limit_and_more"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="FrontEndChannel",
+ fields=[
+ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ ("name", models.TextField(blank=True, null=True)),
+ ("twitch_url", models.URLField(blank=True, null=True)),
+ ("live", models.BooleanField(default=False)),
+ ],
+ options={
+ "abstract": False,
+ "base_manager_name": "prefetch_manager",
+ },
+ managers=[
+ ("objects", django.db.models.manager.Manager()),
+ ("prefetch_manager", django.db.models.manager.Manager()),
+ ],
+ ),
+ migrations.CreateModel(
+ name="FrontEndGame",
+ fields=[
+ ("twitch_id", models.TextField(primary_key=True, serialize=False)),
+ ("game_url", models.URLField(blank=True, null=True)),
+ ("display_name", models.TextField(blank=True, null=True)),
+ ],
+ options={
+ "abstract": False,
+ "base_manager_name": "prefetch_manager",
+ },
+ managers=[
+ ("objects", django.db.models.manager.Manager()),
+ ("prefetch_manager", django.db.models.manager.Manager()),
+ ],
+ ),
+ migrations.CreateModel(
+ name="FrontEndOrg",
+ fields=[
+ ("id", models.TextField(primary_key=True, serialize=False)),
+ ("name", models.TextField(blank=True, null=True)),
+ ("url", models.TextField(blank=True, null=True)),
+ ],
+ options={
+ "abstract": False,
+ "base_manager_name": "prefetch_manager",
+ },
+ managers=[
+ ("objects", django.db.models.manager.Manager()),
+ ("prefetch_manager", django.db.models.manager.Manager()),
+ ],
+ ),
+ migrations.AddField(
+ model_name="game",
+ name="box_art_url",
+ field=models.URLField(blank=True, null=True),
+ ),
+ migrations.CreateModel(
+ name="FrontEndDropCampaign",
+ fields=[
+ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ ("account_link_url", models.URLField(blank=True, null=True)),
+ ("about_url", models.URLField(blank=True, null=True)),
+ ("ends_at", models.DateTimeField(null=True)),
+ ("starts_at", models.DateTimeField(null=True)),
+ ("channels", models.ManyToManyField(related_name="drop_campaigns", to="twitch_app.frontendchannel")),
+ (
+ "game",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="drop_campaigns",
+ to="twitch_app.frontendgame",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ "base_manager_name": "prefetch_manager",
+ },
+ managers=[
+ ("objects", django.db.models.manager.Manager()),
+ ("prefetch_manager", django.db.models.manager.Manager()),
+ ],
+ ),
+ migrations.CreateModel(
+ name="FrontEndDrop",
+ fields=[
+ ("id", models.TextField(primary_key=True, serialize=False)),
+ ("created_at", models.DateTimeField(null=True)),
+ ("name", models.TextField(blank=True, null=True)),
+ ("image_url", models.URLField(blank=True, null=True)),
+ ("limit", models.PositiveBigIntegerField(null=True)),
+ ("is_ios_available", models.BooleanField(null=True)),
+ ("minutes_watched", models.PositiveBigIntegerField(null=True)),
+ (
+ "drop_campaign",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="drops",
+ to="twitch_app.frontenddropcampaign",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ "base_manager_name": "prefetch_manager",
+ },
+ managers=[
+ ("objects", django.db.models.manager.Manager()),
+ ("prefetch_manager", django.db.models.manager.Manager()),
+ ],
+ ),
+ migrations.AddField(
+ model_name="frontendgame",
+ name="org",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="games",
+ to="twitch_app.frontendorg",
+ ),
+ ),
+ ]
diff --git a/twitch_app/models.py b/twitch_app/models.py
index 9e8b330..92daf42 100644
--- a/twitch_app/models.py
+++ b/twitch_app/models.py
@@ -1,3 +1,5 @@
+import typing
+
import auto_prefetch
from django.db import models
@@ -25,6 +27,7 @@ class Game(auto_prefetch.Model):
id = models.AutoField(primary_key=True)
slug = models.TextField(null=True, blank=True)
display_name = models.TextField(null=True, blank=True)
+ box_art_url = models.URLField(null=True, blank=True)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
@@ -502,6 +505,11 @@ class DropCampaign(auto_prefetch.Model):
typename (str): The type name of the object, typically "DropCampaign".
"""
+ STATUS_CHOICES: typing.ClassVar[list[tuple[str, str]]] = [
+ ("ACTIVE", "Active"),
+ ("EXPIRED", "Expired"),
+ ]
+
id = models.TextField(primary_key=True)
allow = auto_prefetch.ForeignKey(Allow, on_delete=models.CASCADE, related_name="drop_campaigns", null=True)
account_link_url = models.URLField(null=True, blank=True)
@@ -514,9 +522,68 @@ class DropCampaign(auto_prefetch.Model):
name = models.TextField(null=True, blank=True)
owner = auto_prefetch.ForeignKey(Owner, on_delete=models.CASCADE, related_name="drop_campaigns", null=True)
starts_at = models.DateTimeField(null=True)
- status = models.TextField(null=True, blank=True)
+ status = models.TextField(choices=STATUS_CHOICES, null=True, blank=True)
time_based_drops = models.ManyToManyField(TimeBasedDrop, related_name="drop_campaigns")
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return self.name or "Unknown"
+
+
+class FrontEndChannel(auto_prefetch.Model):
+ """This is the channel we will see on the front end."""
+
+ name = models.TextField(null=True, blank=True)
+ twitch_url = models.URLField(null=True, blank=True)
+ live = models.BooleanField(default=False)
+
+
+class FrontEndOrg(auto_prefetch.Model):
+ """Drops are group by organization -> by game -> by drop campaign."""
+
+ id = models.TextField(primary_key=True)
+ name = models.TextField(null=True, blank=True)
+ url = models.TextField(null=True, blank=True)
+
+
+class FrontEndGame(auto_prefetch.Model):
+ """This is the game we will see on the front end."""
+
+ twitch_id = models.TextField(primary_key=True)
+ game_url = models.URLField(null=True, blank=True)
+ display_name = models.TextField(null=True, blank=True)
+
+ org = models.ForeignKey(FrontEndOrg, on_delete=models.CASCADE, related_name="games", null=True)
+
+ def __str__(self) -> str:
+ return self.display_name or "Unknown"
+
+
+class FrontEndDropCampaign(auto_prefetch.Model):
+ """This is the drop campaign we will see on the front end."""
+
+ account_link_url = models.URLField(null=True, blank=True)
+ about_url = models.URLField(null=True, blank=True)
+
+ ends_at = models.DateTimeField(null=True)
+ starts_at = models.DateTimeField(null=True)
+
+ game = models.ForeignKey(FrontEndGame, on_delete=models.CASCADE, related_name="drop_campaigns", null=True)
+
+ channels = models.ManyToManyField(FrontEndChannel, related_name="drop_campaigns")
+
+
+class FrontEndDrop(auto_prefetch.Model):
+ """This is the drop we will see on the front end."""
+
+ id = models.TextField(primary_key=True)
+ created_at = models.DateTimeField(null=True)
+
+ name = models.TextField(null=True, blank=True)
+ image_url = models.URLField(null=True, blank=True)
+
+ drop_campaign = models.ForeignKey(FrontEndDropCampaign, on_delete=models.CASCADE, related_name="drops", null=True)
+
+ limit = models.PositiveBigIntegerField(null=True)
+ is_ios_available = models.BooleanField(null=True)
+ minutes_watched = models.PositiveBigIntegerField(null=True)