diff --git a/core/templates/games.html b/core/templates/games.html index d01d075..6972c02 100644 --- a/core/templates/games.html +++ b/core/templates/games.html @@ -2,28 +2,33 @@ {% block content %}
- {% for game in games %} -
-
- {{ game.display_name }} -
-
{{ game.display_name }}
-
-
- - -
-
- - +
{{ toc|safe }}
+
+
+ {% for game in games %} +
+
+ {{ game.display_name }} +
+
{{ game.display_name }}
+
+
+ + +
+
+ + +
+
-
+ {% endfor %}
- {% endfor %} +
{% endblock content %} diff --git a/core/templates/index.html b/core/templates/index.html index d3008d0..12c0268 100644 --- a/core/templates/index.html +++ b/core/templates/index.html @@ -3,7 +3,7 @@ {% block content %}
-
{% include "partials/toc.html" %}
+
{{ toc|safe }}
{% include "partials/info_box.html" %} {% include "partials/news.html" %} diff --git a/core/templates/partials/header.html b/core/templates/partials/header.html index 32ce186..cce3236 100644 --- a/core/templates/partials/header.html +++ b/core/templates/partials/header.html @@ -7,15 +7,15 @@ +
  • + Reward campaigns +
  • -
  • - Reward campaigns -
  • diff --git a/core/templates/partials/reward_campaigns_toc.html b/core/templates/partials/reward_campaigns_toc.html deleted file mode 100644 index 102624d..0000000 --- a/core/templates/partials/reward_campaigns_toc.html +++ /dev/null @@ -1,12 +0,0 @@ -
    -
    -
    -
    - {% for campaign in reward_campaigns %} - {{ campaign }} - {% endfor %} -
    -
    -
    -
    diff --git a/core/templates/partials/toc.html b/core/templates/partials/toc.html deleted file mode 100644 index 9f50e40..0000000 --- a/core/templates/partials/toc.html +++ /dev/null @@ -1,19 +0,0 @@ -
    -
    -
    -
    - Information - Site news - {% for org in orgs %} - {{ org }} - {% for game in org.games.all %} - {{ game.display_name }} - {% endfor %} - {% endfor %} -
    -
    -
    -
    diff --git a/core/templates/reward_campaigns.html b/core/templates/reward_campaigns.html index 0de02c9..ac05b56 100644 --- a/core/templates/reward_campaigns.html +++ b/core/templates/reward_campaigns.html @@ -3,7 +3,7 @@ {% block content %}
    -
    {% include "partials/reward_campaigns_toc.html" %}
    +
    {{ toc }}

    Reward Campaigns

    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)