From 09b21d4f43062279a63938aed7e93453b9f53b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= <tlovinator@gmail.com> Date: Fri, 2 Aug 2024 05:10:23 +0200 Subject: [PATCH] WIP --- core/templates/games.html | 41 +- core/templates/index.html | 2 +- core/templates/partials/header.html | 6 +- .../partials/reward_campaigns_toc.html | 12 - core/templates/partials/toc.html | 19 - core/templates/reward_campaigns.html | 2 +- core/urls.py | 6 +- core/views.py | 66 ++- testboi.py | 380 ++++++++++++++++++ testboi2.py | 252 ++++++++++++ .../management/commands/scrape_twitch.py | 24 ++ ...annel_frontendgame_frontendorg_and_more.py | 135 +++++++ twitch_app/models.py | 69 +++- 13 files changed, 940 insertions(+), 74 deletions(-) delete mode 100644 core/templates/partials/reward_campaigns_toc.html delete mode 100644 core/templates/partials/toc.html create mode 100644 testboi.py create mode 100644 testboi2.py create mode 100644 twitch_app/migrations/0010_frontendchannel_frontendgame_frontendorg_and_more.py 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 %} <div class="container mt-4"> <div class="row"> - {% for game in games %} - <div class="col-xl-3 col-lg-4 col-md-6 col-sm-12 mb-4"> - <div class="card h-100 shadow-sm"> - <img src="{{ game.image_url }}" - class="card-img-top" - alt="{{ game.display_name }}"> - <div class="card-body d-flex flex-column"> - <h5 class="card-title">{{ game.display_name }}</h5> - <div class="mt-auto"> - <div class="form-check form-switch"> - <input class="form-check-input" type="checkbox" role="switch" id="new"> - <label class="form-check-label" for="new">Notify when new</label> - </div> - <div class="form-check form-switch"> - <input class="form-check-input" type="checkbox" role="switch" id="live"> - <label class="form-check-label" for="live">Notify when farmable</label> + <div class="col-lg-3">{{ toc|safe }}</div> + <div class="col-lg-9"> + <div class="row"> + {% for game in games %} + <div class="col-xl-3 col-lg-4 col-md-6 col-sm-12 mb-4"> + <div class="card h-100 shadow-sm"> + <img src="{{ game.image_url }}" + class="card-img-top" + alt="{{ game.display_name }}"> + <div class="card-body d-flex flex-column"> + <h5 class="card-title">{{ game.display_name }}</h5> + <div class="mt-auto"> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" id="new"> + <label class="form-check-label" for="new">Notify when new</label> + </div> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" id="live"> + <label class="form-check-label" for="live">Notify when farmable</label> + </div> + </div> </div> </div> </div> - </div> + {% endfor %} </div> - {% endfor %} + </div> </div> </div> {% 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 %} <div class="container mt-4"> <div class="row"> - <div class="col-lg-3">{% include "partials/toc.html" %}</div> + <div class="col-lg-3">{{ toc|safe }}</div> <div class="col-lg-9"> {% 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 @@ <li class="nav-item"> <a class="nav-link" href='{% url "core:games" %}'>Games</a> </li> + <li> + <a class="nav-link" href='{% url "core:reward_campaigns" %}'>Reward campaigns</a> + </li> <li class="nav-item"> <a class="nav-link" href=''>API</a> </li> <li class="nav-item d-none d-sm-block"> <a class="nav-link" href="https://github.com/sponsors/TheLovinator1">Donate</a> </li> - <li> - <a class="nav-link" href='{% url "core:reward_campaigns" %}'>Reward campaigns</a> - </li> </ul> </nav> </header> 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 @@ -<div class="position-sticky d-none d-lg-block toc"> - <div class="card"> - <div class="card-body"> - <div id="toc-list" class="list-group"> - {% for campaign in reward_campaigns %} - <a class="list-group-item list-group-item-action plain-text-item" - href="#reward-{{ campaign.id }}">{{ campaign }}</a> - {% endfor %} - </div> - </div> - </div> -</div> 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 @@ -<div class="position-sticky d-none d-lg-block toc"> - <div class="card"> - <div class="card-body"> - <div id="toc-list" class="list-group"> - <a class="list-group-item list-group-item-action plain-text-item" - href="#info-box">Information</a> - <a class="list-group-item list-group-item-action plain-text-item" - href="#game-list">Site news</a> - {% for org in orgs %} - {{ org }} - {% for game in org.games.all %} - <a class="list-group-item list-group-item-action plain-text-item" - href="#game-{{ game.game_id }}">{{ game.display_name }}</a> - {% endfor %} - {% endfor %} - </div> - </div> - </div> -</div> 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 %} <div class="container mt-4"> <div class="row"> - <div class="col-lg-3">{% include "partials/reward_campaigns_toc.html" %}</div> + <div class="col-lg-3">{{ toc }}</div> <div class="col-lg-9"> <h2>Reward Campaigns</h2> <div> 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 = """ + <div class="position-sticky d-none d-lg-block toc"> + <div class="card"> + <div class="card-body"> + <div id="toc-list" class="list-group"> + """ + + for item in list_of_things: + html += ( + f'<a class="list-group-item list-group-item-action plain-text-item" href="#{item.toc_id}">{item.name}</a>' + ) + html += """</div></div></div></div>""" + 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)