From a410fac8e82f832953b7bdb0daec0a13fc20a920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Mon, 12 Aug 2024 06:47:08 +0200 Subject: [PATCH] Rewrite models and Twitch scraper --- .vscode/settings.json | 2 + core/admin.py | 12 + core/management/commands/scrape_twitch.py | 701 ++++++------------ core/migrations/0001_initial.py | 498 ++++--------- core/models.py | 643 ---------------- core/models/__init__.py | 0 core/models/twitch.py | 152 ++++ core/settings.py | 6 + core/templates/index.html | 6 +- core/templates/partials/game_card.html | 59 +- .../partials/reward_campaign_card.html | 4 +- core/testboi.py | 57 -- core/urls.py | 2 + core/views.py | 13 +- 14 files changed, 578 insertions(+), 1577 deletions(-) create mode 100644 core/admin.py delete mode 100644 core/models.py create mode 100644 core/models/__init__.py create mode 100644 core/models/twitch.py delete mode 100644 core/testboi.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 8f4e84b..c24bd27 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,8 +5,10 @@ "asgiref", "Behaviour", "cacd", + "dungeonborne", "forloop", "logdir", + "lvthalo", "memlock", "networkidle", "PGID", diff --git a/core/admin.py b/core/admin.py new file mode 100644 index 0000000..605bd55 --- /dev/null +++ b/core/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin + +from .models.twitch import Benefit, Channel, DropCampaign, Game, Owner, Reward, RewardCampaign, TimeBasedDrop + +admin.site.register(Game) +admin.site.register(Owner) +admin.site.register(RewardCampaign) +admin.site.register(DropCampaign) +admin.site.register(Channel) +admin.site.register(TimeBasedDrop) +admin.site.register(Benefit) +admin.site.register(Reward) diff --git a/core/management/commands/scrape_twitch.py b/core/management/commands/scrape_twitch.py index d3ed386..06269db 100644 --- a/core/management/commands/scrape_twitch.py +++ b/core/management/commands/scrape_twitch.py @@ -2,30 +2,15 @@ import asyncio import json import logging import typing -from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING -from asgiref.sync import sync_to_async from django.core.management.base import BaseCommand from platformdirs import user_data_dir from playwright.async_api import Playwright, async_playwright from playwright.async_api._generated import Response -from core.models import ( - Allow, - Benefit, - BenefitEdge, - Channel, - DropCampaign, - Game, - Image, - Owner, - Reward, - RewardCampaign, - TimeBasedDrop, - UnlockRequirements, -) +from core.models.twitch import Benefit, Channel, DropCampaign, Game, Owner, Reward, RewardCampaign, TimeBasedDrop if TYPE_CHECKING: from playwright.async_api._generated import BrowserContext, Page @@ -50,444 +35,6 @@ def get_data_dir() -> Path: ) -async def add_or_get_game(json_data: dict | None) -> tuple[Game | None, bool]: - """Add or get Game from JSON data. - - Args: - json_data (dict): JSON data to add to the database. - - Returns: - tuple[Game | None, bool]: Game instance and whether it was created. - """ - if not json_data: - logger.warning("Couldn't find game data, probably a reward campaign?") - return None, False - - game, created = await Game.objects.aupdate_or_create( - id=json_data["id"], - defaults={ - "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 - }, - ) - - if created: - logger.info("Found new game: %s", game.display_name or "Unknown Game") - - return game, created - - -async def add_or_get_owner(json_data: dict | None) -> tuple[Owner | None, bool]: - """Add or get Owner from JSON data. - - Args: - json_data (dict): JSON data to add to the database. - - Returns: - Owner: Owner instance. - """ - if not json_data: - logger.warning("No owner data provided") - return None, False - - owner, created = await Owner.objects.aupdate_or_create( - id=json_data["id"], - defaults={ - "display_name": json_data.get("name"), - "typename": json_data.get("__typename"), - }, - ) - - return owner, created - - -async def add_or_get_allow(json_data: dict | None) -> tuple[Allow | None, bool]: - """Add or get Allow from JSON data. - - Args: - json_data (dict): JSON data to add to the database. - - Returns: - Allow: Allow instance. - """ - if not json_data: - logger.warning("No allow data provided") - return None, False - - allow, created = await Allow.objects.aupdate_or_create( - is_enabled=json_data.get("isEnabled"), - typename=json_data.get("__typename"), - ) - - return allow, created - - -async def add_or_get_time_based_drops( - time_based_drops_data: list[dict] | None, - owner: Owner | None, - game: Game | None, -) -> list[TimeBasedDrop]: - """Handle TimeBasedDrops from JSON data. - - Args: - time_based_drops_data (list[dict]): Time based drops data from JSON. - owner (Owner): Owner instance. - game (Game): Game instance. - - - Returns: - list[TimeBasedDrop]: TimeBasedDrop instances. - """ - time_based_drops: list[TimeBasedDrop] = [] - - if not time_based_drops_data: - logger.warning("No time based drops data provided") - return time_based_drops - - for time_based_drop_data in time_based_drops_data: - time_based_drop, _ = await TimeBasedDrop.objects.aupdate_or_create( - id=time_based_drop_data["id"], - defaults={ - "created_at": time_based_drop_data.get("createdAt"), - "entitlement_limit": time_based_drop_data.get("entitlementLimit"), - "image_asset_url": time_based_drop_data.get("imageAssetURL"), - "is_ios_available": time_based_drop_data.get("isIosAvailable"), - "name": time_based_drop_data.get("name"), - "owner_organization": owner, - "game": game, - "typename": time_based_drop_data.get("__typename"), - }, - ) - - benefit_edges_data: list[dict] = time_based_drop_data.get("benefitEdges", []) - for benefit_edge_data in benefit_edges_data: - benefit_data: dict = benefit_edge_data.get("benefit", {}) - benefit, _ = await Benefit.objects.aupdate_or_create( - id=benefit_data["id"], - defaults={ - "created_at": benefit_data.get("createdAt"), - "entitlement_limit": benefit_data.get("entitlementLimit"), - "image_asset_url": benefit_data.get("imageAssetURL"), - "is_ios_available": benefit_data.get("isIosAvailable"), - "name": benefit_data.get("name"), - "owner_organization": owner, - "game": game, - "typename": benefit_data.get("__typename"), - }, - ) - - await BenefitEdge.objects.aupdate_or_create( - benefit=benefit, - defaults={ - "entitlement_limit": benefit_edge_data.get("entitlementLimit"), - "typename": benefit_edge_data.get("__typename"), - }, - ) - - time_based_drops.append(time_based_drop) - - return time_based_drops - - -async def add_or_get_drop_campaign( - drop_campaign_data: dict | None, - game: Game | None, - owner: Owner | None, -) -> tuple[DropCampaign | None, bool]: - """Handle DropCampaign from JSON data. - - Args: - drop_campaign_data (dict): Drop campaign data from JSON. - game (Game): Game instance. - owner (Owner): Owner instance. - - - Returns: - tuple[DropCampaign, bool]: DropCampaign instance and whether it was created. - """ - if not drop_campaign_data: - logger.warning("No drop campaign data provided") - 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={ - # "allow": allow, # We add this later - "account_link_url": drop_campaign_data.get("accountLinkURL"), - "description": drop_campaign_data.get("description"), - "details_url": drop_campaign_data.get("detailsURL"), - "ends_at": drop_campaign_data.get("endAt"), - # event_based_drops = ???? # TODO(TheLovinator): Find out what this is # noqa: TD003 - "game": game, - "image_url": drop_campaign_data.get("imageURL"), - "name": drop_campaign_data.get("name"), - "owner": owner, - "starts_at": drop_campaign_data.get("startAt"), - "status": drop_campaign_data.get("status"), - # "time_based_drops": time_based_drops, # We add this later - "typename": drop_campaign_data.get("__typename"), - }, - ) - - return drop_campaign, True - - -async def add_or_get_channel(json_data: dict | None) -> tuple[Channel | None, bool]: - """Add or get Channel from JSON data. - - Args: - json_data (dict): JSON data to add to the database. - - - Returns: - tuple[Channel | None, bool]: Channel instance and whether it was created. - """ - if not json_data: - logger.warning("No channel data provided") - return None, False - - channel, created = await Channel.objects.aupdate_or_create( - id=json_data["id"], - defaults={ - "display_name": json_data.get("displayName"), - "name": json_data.get("name"), - "typename": json_data.get("__typename"), - }, - ) - - return channel, created - - -async def add_drop_campaign(json_data: dict | None) -> None: - """Add data from JSON to the database. - - Args: - json_data (dict): JSON data to add to the database. - """ - if not json_data: - logger.warning("No JSON data provided") - return - - # Get the data from the JSON - user_data: dict = json_data.get("data", {}).get("user", {}) - drop_campaign_data: dict = user_data.get("dropCampaign", {}) - - # Add or get Game - game_data: dict = drop_campaign_data.get("game", {}) - game, _ = await add_or_get_game(json_data=game_data) - - # Add or get Owner - owner_data: dict = drop_campaign_data.get("owner", {}) - owner, _ = await add_or_get_owner(json_data=owner_data) - - # Add or get Allow - allow_data: dict = drop_campaign_data.get("allow", {}) - allow, _ = await add_or_get_allow(json_data=allow_data) - - # Add channels to Allow - if allow: - channel_data: list[dict] = allow_data.get("channels", []) - - if channel_data: - for json_channel in channel_data: - channel, _ = await add_or_get_channel(json_channel) - if channel: - await allow.channels.aadd(channel) - - # Add or get TimeBasedDrops - time_based_drops_data = drop_campaign_data.get("timeBasedDrops", []) - time_based_drops: list[TimeBasedDrop] = await add_or_get_time_based_drops(time_based_drops_data, owner, game) - - # Add or get DropCampaign - drop_campaign, _ = await add_or_get_drop_campaign( - drop_campaign_data=drop_campaign_data, - game=game, - owner=owner, - ) - if drop_campaign: - drop_campaign.allow = allow - await drop_campaign.time_based_drops.aset(time_based_drops) - await drop_campaign.asave() - - logger.info("Added Drop Campaign: %s", drop_campaign.name or "Unknown Drop Campaign") - - -async def add_or_get_image(json_data: dict | None) -> tuple[Image | None, bool]: - """Add or get Image from JSON data. - - Args: - json_data (dict): JSON data to add to the database. - - Returns: - tuple[Image | None, bool]: Image instance and whether it was created. - """ - # TODO(TheLovinator): We should download the image and store it locally # noqa: TD003 - if not json_data: - logger.warning("Image data is missing") - return None, False - - if not json_data.get("image1xURL"): - logger.warning("Image URL is missing") - return None, False - - image, created = await Image.objects.aupdate_or_create( - image1_x_url=json_data.get("image1xURL"), - defaults={ - "typename": json_data.get("__typename"), - }, - ) - - return image, created - - -async def add_or_get_rewards(json_data: dict | None) -> list[Reward]: - """Add or get Rewards from JSON data. - - Args: - json_data (dict): JSON data to add to the database. - - Returns: - list[Reward]: Reward instances - """ - rewards: list[Reward] = [] - - if not json_data: - logger.warning("No rewards found") - return [] - - if "rewards" not in json_data: - logger.warning("No rewards found") - return [] - - rewards_json: list[dict] = json_data.get("rewards", []) - for reward_data in rewards_json: - # Add or get bannerImage - banner_image_data: dict = reward_data.get("bannerImage", {}) - if banner_image_data: - banner_image, _ = await sync_to_async(Image.objects.get_or_create)( - image1_x_url=banner_image_data["image1xURL"], - defaults={"typename": banner_image_data["__typename"]}, - ) - - # Add or get thumbnailImage - thumbnail_image_data = reward_data.get("thumbnailImage", {}) - if thumbnail_image_data: - thumbnail_image, _ = await sync_to_async(Image.objects.get_or_create)( - image1_x_url=thumbnail_image_data["image1xURL"], - defaults={"typename": thumbnail_image_data["__typename"]}, - ) - - # Convert earnableUntil to a datetime object - earnable_until: str | None = reward_data.get("earnableUntil") - earnable_until_date: datetime | None = None - if earnable_until: - earnable_until_date = datetime.fromisoformat(earnable_until.replace("Z", "+00:00")) - - reward, _ = await sync_to_async(Reward.objects.get_or_create)( - id=reward_data["id"], - defaults={ - "name": reward_data.get("name"), - "banner_image": banner_image, - "thumbnail_image": thumbnail_image, - "earnable_until": earnable_until_date, - "redemption_instructions": reward_data.get("redemptionInstructions"), - "redemption_url": reward_data.get("redemptionURL"), - "typename": reward_data.get("__typename"), - }, - ) - rewards.append(reward) - - return rewards - - -async def add_or_get_unlock_requirements(json_data: dict | None) -> tuple[UnlockRequirements | None, bool]: - """Add or get UnlockRequirements from JSON data. - - Args: - json_data (dict): JSON data to add to the database. - - - - Returns: - tuple[UnlockRequirements | None, bool]: UnlockRequirements instance and whether it was created. - """ - if not json_data: - logger.warning("No unlock requirements data provided") - return None, False - - unlock_requirements, created = await UnlockRequirements.objects.aget_or_create( - subs_goal=json_data["subsGoal"], - defaults={ - "minute_watched_goal": json_data["minuteWatchedGoal"], - "typename": json_data["__typename"], - }, - ) - - return unlock_requirements, created - - -async def add_reward_campaign(json_data: dict | None) -> None: - """Add data from JSON to the database. - - Args: - json_data (dict): JSON data to add to the database. - """ - if not json_data: - logger.warning("No JSON data provided") - return - - campaign_data: list[dict] = json_data["data"]["rewardCampaignsAvailableToUser"] - for campaign in campaign_data: - # Add or get Game - game_data: dict = campaign.get("game", {}) - game, _ = await add_or_get_game(json_data=game_data) - - # Add or get Image - image_data: dict = campaign.get("image", {}) - image, _ = await add_or_get_image(json_data=image_data) - - # Add or get Rewards - rewards: list[Reward] = await add_or_get_rewards(campaign) - - # Add or get Unlock Requirements - unlock_requirements_data: dict = campaign["unlockRequirements"] - unlock_requirements, _ = await add_or_get_unlock_requirements(unlock_requirements_data) - - # Create Reward Campaign - reward_campaign, _ = await RewardCampaign.objects.aget_or_create( - id=campaign["id"], - defaults={ - "name": campaign.get("name"), - "brand": campaign.get("brand"), - "starts_at": campaign.get("startsAt"), - "ends_at": campaign.get("endsAt"), - "status": campaign.get("status"), - "summary": campaign.get("summary"), - "instructions": campaign.get("instructions"), - "external_url": campaign.get("externalURL"), - "reward_value_url_param": campaign.get("rewardValueURLParam"), - "about_url": campaign.get("aboutURL"), - "is_sitewide": campaign.get("isSitewide"), - "game": game, - "unlock_requirements": unlock_requirements, - "image": image, - # "rewards": rewards, # We add this later - "typename": campaign.get("__typename"), - }, - ) - - # Add Rewards to the Campaign - for reward in rewards: - await reward_campaign.rewards.aadd(reward) - - await reward_campaign.asave() - - def get_profile_dir() -> Path: """Get the profile directory for the browser. @@ -517,23 +64,253 @@ def save_json(campaign: dict, dir_name: str) -> None: json.dump(campaign, f, indent=4) -async def process_json_data(num: int, campaign: dict | None, json_data: list[dict] | None) -> None: +async def add_reward_campaign(campaign: dict) -> None: + """Add a reward campaign to the database. + + Args: + campaign (dict): The reward campaign to add. + """ + logger.info("Adding reward campaign to database") + for reward_campaign in campaign["data"]["rewardCampaignsAvailableToUser"]: + our_reward_campaign, created = await RewardCampaign.objects.aget_or_create( + id=reward_campaign["id"], + defaults={ + "name": reward_campaign["name"], + "brand": reward_campaign["brand"], + "starts_at": reward_campaign["startsAt"], + "ends_at": reward_campaign["endsAt"], + "status": reward_campaign["status"], + "summary": reward_campaign["summary"], + "instructions": reward_campaign["instructions"], + "reward_value_url_param": reward_campaign["rewardValueURLParam"], + "external_url": reward_campaign["externalURL"], + "about_url": reward_campaign["aboutURL"], + "is_site_wide": reward_campaign["isSitewide"], + "sub_goal": reward_campaign["unlockRequirements"]["subsGoal"], + "minute_watched_goal": reward_campaign["unlockRequirements"]["minuteWatchedGoal"], + "image_url": reward_campaign["image"]["image1xURL"], + # "game" # To be implemented + }, + ) + if created: + logger.info("Added reward campaign %s", our_reward_campaign.id) + else: + logger.info("Updated reward campaign %s", our_reward_campaign.id) + + if reward_campaign["game"]: + # TODO(TheLovinator): Add game to reward campaign # noqa: TD003 + # TODO(TheLovinator): Send JSON to Discord # noqa: TD003 + logger.error("Not implemented: Add game to reward campaign, JSON: %s", reward_campaign["game"]) + + # Add rewards + for reward in reward_campaign["rewards"]: + our_reward, created = await Reward.objects.aget_or_create( + id=reward["id"], + defaults={ + "name": reward["name"], + "banner_image_url": reward["bannerImage"]["image1xURL"], + "thumbnail_image_url": reward["thumbnailImage"]["image1xURL"], + "earnable_until": reward["earnableUntil"], + "redemption_instructions": reward["redemptionInstructions"], + "redemption_url": reward["redemptionURL"], + "campaign": our_reward_campaign, + }, + ) + if created: + logger.info("Added reward %s", our_reward.id) + else: + logger.info("Updated reward %s", our_reward.id) + + +async def add_or_update_game(game_json: dict, owner: Owner | None) -> Game | None: + """Add or update a game in the database. + + Args: + game_json (dict): The game to add or update. + owner (Owner): The owner of the game. + + Returns: + Game: The game that was added or updated. + """ + if game_json: + game_url: str | None = ( + f"https://www.twitch.tv/directory/game/{game_json["slug"]}" if game_json["slug"] else None + ) + our_game, created = await Game.objects.aget_or_create( + twitch_id=game_json["id"], + defaults={ + "slug": game_json["slug"], + "name": game_json["displayName"], + "game_url": game_url, + "org": owner, + # TODO(TheLovinator): Add box_art_url to game # noqa: TD003 + }, + ) + if created: + logger.info("Added game %s", our_game.twitch_id) + else: + logger.info("Updated game %s", our_game.twitch_id) + return our_game + return None + + +async def add_or_update_owner(owner_json: dict) -> Owner | None: + """Add or update an owner in the database. + + Args: + owner_json (dict): The owner to add or update. + + Returns: + Owner: The owner that was added or updated. + """ + if owner_json: + our_owner, created = await Owner.objects.aget_or_create( + id=owner_json["id"], + defaults={"name": owner_json["name"]}, + ) + if created: + logger.info("Added owner %s", our_owner.id) + else: + logger.info("Updated owner %s", our_owner.id) + return our_owner + return None + + +async def add_or_update_channels(channels_json: list[dict]) -> list[Channel] | None: + """Add or update channels in the database. + + Args: + channels_json (list[dict]): The channels to add or update. + + Returns: + list[Channel]: The channels that were added or updated. + """ + if not channels_json: + return None + + channels: list[Channel] = [] + for channel_json in channels_json: + twitch_url: str | None = f"https://www.twitch.tv/{channel_json["name"]}" if channel_json["name"] else None + our_channel, created = await Channel.objects.aget_or_create( + twitch_id=channel_json["id"], + defaults={ + "name": channel_json["name"], + "display_name": channel_json["displayName"], + "twitch_url": twitch_url, + "live": False, # Toggle this later + }, + ) + if created: + logger.info("Added channel %s", our_channel.twitch_id) + else: + logger.info("Updated channel %s", our_channel.twitch_id) + channels.append(our_channel) + + return channels + + +async def add_benefit(benefit: dict, time_based_drop: TimeBasedDrop) -> None: + """Add a benefit to the database. + + Args: + benefit (dict): The benefit to add. + time_based_drop (TimeBasedDrop): The time-based drop the benefit belongs to. + """ + our_benefit, created = await Benefit.objects.aget_or_create( + id=benefit["id"], + defaults={ + "twitch_created_at": benefit["createdAt"], + "entitlement_limit": benefit["entitlementLimit"], + "image_url": benefit["imageAssetURL"], + "is_ios_available": benefit["isIosAvailable"], + "name": benefit["name"], + "time_based_drop": time_based_drop, + }, + ) + if created: + logger.info("Added benefit %s", our_benefit.id) + else: + logger.info("Updated benefit %s", our_benefit.id) + + +async def add_drop_campaign(drop_campaign: dict) -> None: + """Add a drop campaign to the database. + + Args: + drop_campaign (dict): The drop campaign to add. + """ + logger.info("Adding drop campaign to database") + owner: Owner | None = await add_or_update_owner(drop_campaign["owner"]) + game: Game | None = await add_or_update_game(drop_campaign["game"], owner) + channels: list[Channel] | None = await add_or_update_channels(drop_campaign["allow"]["channels"]) + + our_drop_campaign, created = await DropCampaign.objects.aget_or_create( + id=drop_campaign["id"], + defaults={ + "account_link_url": drop_campaign["accountLinkURL"], + "description": drop_campaign["description"], + "details_url": drop_campaign["detailsURL"], + "ends_at": drop_campaign["endAt"], + "starts_at": drop_campaign["startAt"], + "image_url": drop_campaign["imageURL"], + "name": drop_campaign["name"], + "status": drop_campaign["status"], + "game": game, + }, + ) + if created: + logger.info("Added drop campaign %s", our_drop_campaign.id) + else: + logger.info("Updated drop campaign %s", our_drop_campaign.id) + + if channels: + our_drop_campaign.channels.aset(channels) # type: ignore # noqa: PGH003 + + # Add time-based drops + for time_based_drop in drop_campaign["timeBasedDrops"]: + if time_based_drop["preconditionDrops"]: + # TODO(TheLovinator): Add precondition drops to time-based drop # noqa: TD003 + # TODO(TheLovinator): Send JSON to Discord # noqa: TD003 + logger.error("Not implemented: Add precondition drops to time-based drop, JSON: %s", time_based_drop) + + our_time_based_drop, created = await TimeBasedDrop.objects.aget_or_create( + id=time_based_drop["id"], + defaults={ + "required_subs": time_based_drop["requiredSubs"], + "ends_at": time_based_drop["endAt"], + "name": time_based_drop["name"], + "required_minutes_watched": time_based_drop["requiredMinutesWatched"], + "starts_at": time_based_drop["startAt"], + "drop_campaign": our_drop_campaign, + }, + ) + if created: + logger.info("Added time-based drop %s", our_time_based_drop.id) + else: + logger.info("Updated time-based drop %s", our_time_based_drop.id) + + # Add benefits + for benefit_edge in time_based_drop["benefitEdges"]: + await add_benefit(benefit_edge["benefit"], our_time_based_drop) + + +async def process_json_data(num: int, campaign: dict | None) -> None: """Process JSON data. Args: num (int): The number of the JSON data. campaign (dict): The JSON data to process. - json_data (list[dict]): The list of JSON """ - if not json_data: - logger.warning("No JSON data provided") + logger.info("Processing JSON %d", num) + if not campaign: + logger.warning("No campaign found for JSON %d", num) return - logger.info("Processing JSON %d of %d", num, len(json_data)) if not isinstance(campaign, dict): logger.warning("Campaign is not a dictionary") return + # This is a Reward Campaign if "rewardCampaignsAvailableToUser" in campaign["data"]: save_json(campaign, "reward_campaigns") await add_reward_campaign(campaign) @@ -544,10 +321,10 @@ async def process_json_data(num: int, campaign: dict | None, json_data: list[dic return save_json(campaign, "drop_campaign") - await add_drop_campaign(campaign) + await add_drop_campaign(campaign["data"]["user"]["dropCampaign"]) if "dropCampaigns" in campaign.get("data", {}).get("user", {}): - for drop_campaign in campaign["data"]["user"]["dropCampaigns"]: + for drop_campaign in campaign["data"]["currentUser"]["dropCampaigns"]: save_json(campaign, "drop_campaigns") await add_drop_campaign(drop_campaign) @@ -605,7 +382,7 @@ class Command(BaseCommand): await browser.close() for num, campaign in enumerate(json_data, start=1): - await process_json_data(num, campaign, json_data) + await process_json_data(num=num, campaign=campaign) return json_data diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index d1cd773..937a816 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -1,10 +1,14 @@ -# Generated by Django 5.1 on 2024-08-09 02:49 +# Generated by Django 5.1 on 2024-08-12 03:47 +from __future__ import annotations + +from typing import TYPE_CHECKING -import auto_prefetch import django.db.models.deletion import django.db.models.manager from django.db import migrations, models -from django.db.migrations.operations.base import Operation + +if TYPE_CHECKING: + from django.db.migrations.operations.base import Operation class Migration(migrations.Migration): @@ -14,81 +18,19 @@ class Migration(migrations.Migration): operations: list[Operation] = [ migrations.CreateModel( - name="Benefit", + name="DropCampaign", fields=[ + ("created_at", models.DateTimeField(auto_created=True, null=True)), ("id", models.TextField(primary_key=True, serialize=False)), - ("created_at", models.DateTimeField(null=True)), - ("entitlement_limit", models.TextField(null=True)), - ("image_asset_url", models.URLField(blank=True, null=True)), - ("is_ios_available", models.BooleanField(null=True)), - ("name", models.TextField(blank=True, null=True)), - ("typename", 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="Channel", - fields=[ - ("id", models.TextField(primary_key=True, serialize=False)), - ("display_name", models.TextField(blank=True, null=True)), - ("name", models.TextField(blank=True, null=True)), - ("typename", 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="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)), + ("modified_at", models.DateTimeField(auto_now=True, null=True)), + ("account_link_url", models.URLField(null=True)), + ("description", models.TextField(null=True)), + ("details_url", models.URLField(null=True)), + ("ends_at", models.DateTimeField(null=True)), + ("starts_at", models.DateTimeField(null=True)), + ("image_url", models.URLField(null=True)), + ("name", models.TextField(null=True)), + ("status", models.TextField(null=True)), ], options={ "abstract": False, @@ -102,27 +44,11 @@ class Migration(migrations.Migration): migrations.CreateModel( name="Game", fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("slug", models.TextField(blank=True, null=True)), - ("display_name", models.TextField(blank=True, null=True)), - ("box_art_url", models.URLField(blank=True, null=True)), - ("typename", 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="Image", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("image1_x_url", models.URLField(blank=True, null=True)), - ("typename", models.TextField(blank=True, null=True)), + ("twitch_id", models.TextField(primary_key=True, serialize=False)), + ("game_url", models.URLField(null=True)), + ("name", models.TextField(null=True)), + ("box_art_url", models.URLField(null=True)), + ("slug", models.TextField(null=True)), ], options={ "abstract": False, @@ -137,9 +63,7 @@ class Migration(migrations.Migration): name="Owner", fields=[ ("id", models.TextField(primary_key=True, serialize=False)), - ("slug", models.TextField(blank=True, null=True)), - ("display_name", models.TextField(blank=True, null=True)), - ("typename", models.TextField(blank=True, null=True)), + ("name", models.TextField(null=True)), ], options={ "abstract": False, @@ -151,111 +75,14 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name="UnlockRequirements", + name="Channel", fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("subs_goal", models.TextField(null=True)), - ("minute_watched_goal", models.TextField(null=True)), - ("typename", 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="BenefitEdge", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("entitlement_limit", models.TextField(null=True)), - ("typename", models.TextField(blank=True, null=True)), - ( - "benefit", - auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="benefit_edges", - to="core.benefit", - ), - ), - ], - 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="Allow", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("is_enabled", models.BooleanField(default=True)), - ("typename", models.TextField(blank=True, null=True)), - ("channels", models.ManyToManyField(related_name="allow", to="core.channel")), - ], - 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="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="core.frontendchannel")), - ( - "game", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="drop_campaigns", - to="core.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="core.frontenddropcampaign", - ), - ), + ("twitch_id", models.TextField(primary_key=True, serialize=False)), + ("display_name", models.TextField(null=True)), + ("name", models.TextField(null=True)), + ("twitch_url", models.URLField(null=True)), + ("live", models.BooleanField(default=False)), + ("drop_campaigns", models.ManyToManyField(related_name="channels", to="core.dropcampaign")), ], options={ "abstract": False, @@ -267,60 +94,81 @@ class Migration(migrations.Migration): ], ), migrations.AddField( - model_name="frontendgame", + model_name="dropcampaign", + name="game", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="drop_campaigns", + to="core.game", + ), + ), + migrations.AddField( + model_name="game", name="org", field=models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, related_name="games", - to="core.frontendorg", - ), - ), - migrations.AddField( - model_name="benefit", - name="game", - field=auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="benefits", - to="core.game", - ), - ), - migrations.AddField( - model_name="benefit", - name="owner_organization", - field=auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="benefits", to="core.owner", ), ), + migrations.CreateModel( + name="RewardCampaign", + fields=[ + ("created_at", models.DateTimeField(auto_created=True, null=True)), + ("id", models.TextField(primary_key=True, serialize=False)), + ("modified_at", models.DateTimeField(auto_now=True, null=True)), + ("name", models.TextField(null=True)), + ("brand", models.TextField(null=True)), + ("starts_at", models.DateTimeField(null=True)), + ("ends_at", models.DateTimeField(null=True)), + ("status", models.TextField(null=True)), + ("summary", models.TextField(null=True)), + ("instructions", models.TextField(null=True)), + ("reward_value_url_param", models.TextField(null=True)), + ("external_url", models.URLField(null=True)), + ("about_url", models.URLField(null=True)), + ("is_site_wide", models.BooleanField(null=True)), + ("sub_goal", models.PositiveBigIntegerField(null=True)), + ("minute_watched_goal", models.PositiveBigIntegerField(null=True)), + ("image_url", models.URLField(null=True)), + ( + "game", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="reward_campaigns", + to="core.game", + ), + ), + ], + 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="Reward", fields=[ ("id", models.TextField(primary_key=True, serialize=False)), - ("name", models.TextField(blank=True, null=True)), + ("name", models.TextField(null=True)), + ("banner_image_url", models.URLField(null=True)), + ("thumbnail_image_url", models.URLField(null=True)), ("earnable_until", models.DateTimeField(null=True)), - ("redemption_instructions", models.TextField(blank=True, null=True)), - ("redemption_url", models.URLField(blank=True, null=True)), - ("typename", models.TextField(blank=True, null=True)), + ("redemption_instructions", models.TextField(null=True)), + ("redemption_url", models.URLField(null=True)), ( - "banner_image", - auto_prefetch.ForeignKey( + "campaign", + models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, - related_name="banner_rewards", - to="core.image", - ), - ), - ( - "thumbnail_image", - auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="thumbnail_rewards", - to="core.image", + related_name="rewards", + to="core.rewardcampaign", ), ), ], @@ -336,137 +184,51 @@ class Migration(migrations.Migration): migrations.CreateModel( name="TimeBasedDrop", fields=[ + ("created_at", models.DateTimeField(auto_created=True, null=True)), ("id", models.TextField(primary_key=True, serialize=False)), - ("created_at", models.DateTimeField(null=True)), - ("entitlement_limit", models.TextField(null=True)), - ("image_asset_url", models.URLField(blank=True, null=True)), + ("modified_at", models.DateTimeField(auto_now=True, null=True)), + ("required_subs", models.PositiveBigIntegerField(null=True)), + ("ends_at", models.DateTimeField(null=True)), + ("name", models.TextField(null=True)), + ("required_minutes_watched", models.PositiveBigIntegerField(null=True)), + ("starts_at", models.DateTimeField(null=True)), + ( + "drop_campaign", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="drops", + to="core.dropcampaign", + ), + ), + ], + 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="Benefit", + fields=[ + ("created_at", models.DateTimeField(auto_created=True, null=True)), + ("id", models.TextField(primary_key=True, serialize=False)), + ("modified_at", models.DateTimeField(auto_now=True, null=True)), + ("twitch_created_at", models.DateTimeField(null=True)), + ("entitlement_limit", models.PositiveBigIntegerField(null=True)), + ("image_url", models.URLField(null=True)), ("is_ios_available", models.BooleanField(null=True)), - ("name", models.TextField(blank=True, null=True)), - ("typename", models.TextField(blank=True, null=True)), + ("name", models.TextField(null=True)), ( - "game", - auto_prefetch.ForeignKey( + "time_based_drop", + models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, - related_name="time_based_drops", - to="core.game", - ), - ), - ( - "owner_organization", - auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="time_based_drops", - to="core.owner", - ), - ), - ], - 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="DropCampaign", - fields=[ - ("id", models.TextField(primary_key=True, serialize=False)), - ("account_link_url", models.URLField(blank=True, null=True)), - ("description", models.TextField(blank=True, null=True)), - ("details_url", models.URLField(blank=True, null=True)), - ("ends_at", models.DateTimeField(null=True)), - ("image_url", models.URLField(blank=True, null=True)), - ("name", models.TextField(blank=True, null=True)), - ("starts_at", models.DateTimeField(null=True)), - ( - "status", - models.TextField(blank=True, choices=[("ACTIVE", "Active"), ("EXPIRED", "Expired")], null=True), - ), - ("typename", models.TextField(blank=True, null=True)), - ( - "allow", - auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="drop_campaigns", - to="core.allow", - ), - ), - ( - "game", - auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="drop_campaigns", - to="core.game", - ), - ), - ( - "owner", - auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="drop_campaigns", - to="core.owner", - ), - ), - ("time_based_drops", models.ManyToManyField(related_name="drop_campaigns", to="core.timebaseddrop")), - ], - 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="RewardCampaign", - fields=[ - ("id", models.TextField(primary_key=True, serialize=False)), - ("name", models.TextField(blank=True, null=True)), - ("brand", models.TextField(blank=True, null=True)), - ("starts_at", models.DateTimeField(null=True)), - ("ends_at", models.DateTimeField(null=True)), - ("status", models.TextField(blank=True, null=True)), - ("summary", models.TextField(blank=True, null=True)), - ("instructions", models.TextField(blank=True, null=True)), - ("external_url", models.URLField(blank=True, null=True)), - ("reward_value_url_param", models.TextField(blank=True, null=True)), - ("about_url", models.URLField(blank=True, null=True)), - ("is_sitewide", models.BooleanField(null=True)), - ("typename", models.TextField(blank=True, null=True)), - ( - "game", - auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="reward_campaigns", - to="core.game", - ), - ), - ( - "image", - auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="reward_campaigns", - to="core.image", - ), - ), - ("rewards", models.ManyToManyField(related_name="reward_campaigns", to="core.reward")), - ( - "unlock_requirements", - auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="reward_campaigns", - to="core.unlockrequirements", + related_name="benefits", + to="core.timebaseddrop", ), ), ], diff --git a/core/models.py b/core/models.py deleted file mode 100644 index 9e4bde1..0000000 --- a/core/models.py +++ /dev/null @@ -1,643 +0,0 @@ -from __future__ import annotations - -import logging -import typing - -import auto_prefetch -from django.db import models - -logger: logging.Logger = logging.getLogger(__name__) - - -class Game(auto_prefetch.Model): - """The game that the reward is for. - - Optional for reward campaigns. Required for drop campaigns. - - Attributes: - id (str): The primary key of the game. - slug (str): The slug identifier of the game. - display_name (str): The display name of the game. - typename (str): The type name of the object, typically "Game". - - Example JSON data: - { - "data": { - "currentUser": { - "dropCampaigns": [ - { - "game": { - "id": "263490", - "slug": "rust", - "displayName": "Rust", - "__typename": "Game" - } - } - ] - } - } - } - """ - - id = models.TextField(primary_key=True, unique=True, help_text="The game ID.", verbose_name="Game ID") - slug = models.TextField(null=True, blank=True, help_text="Slug used for building URL where all the streams are.") - display_name = models.TextField( - null=True, - blank=True, - help_text="Game name.", - default="Unknown Game", - verbose_name="Game Name", - ) - typename = models.TextField(null=True, blank=True, help_text="Always 'Game'.", verbose_name="Type Name") - - # Only used for reward campaigns? - box_art_url = models.URLField( - null=True, - blank=True, - help_text="URL to the box art of the game.", - default="https://static-cdn.jtvnw.net/ttv-static/404_boxart.jpg", - verbose_name="Box Art URL", - ) - - def __str__(self) -> str: - return self.display_name or "Unknown" - - def get_twitch_url(self) -> str: - if not self.slug: - logger.error("Game %s has no slug", self.display_name) - return "https://www.twitch.tv/" - return f"https://www.twitch.tv/directory/game/{self.slug}" - - -class Image(auto_prefetch.Model): - """An image model representing URLs and type. - - Attributes: - image1_x_url (str): URL to the image. - typename (str): The type name of the object, typically "RewardCampaignImageSet". - - JSON example: - { - "image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/campaign.png", - "__typename": "RewardCampaignImageSet" - } - """ - - image1_x_url = models.URLField(null=True, blank=True) - typename = models.TextField(null=True, blank=True) - - def __str__(self) -> str: - return self.image1_x_url or "Unknown" - - -class Reward(auto_prefetch.Model): - """The actual reward you get when you complete the requirements. - - Attributes: - id (str): The primary key of the reward. - name (str): The name of the reward. - banner_image (Image): The banner image associated with the reward. - thumbnail_image (Image): The thumbnail image associated with the reward. - earnable_until (datetime): The date and time until the reward can be earned. - redemption_instructions (str): Instructions on how to redeem the reward. - redemption_url (str): URL for redeeming the reward. - typename (str): The type name of the object, typically "Reward". - - JSON example: - { - "id": "374628c6-34b4-11ef-a468-62ece0f03426", - "name": "Twitchy Character Skin", - "bannerImage": { - "image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png", - "__typename": "RewardCampaignImageSet" - }, - "thumbnailImage": { - "image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png", - "__typename": "RewardCampaignImageSet" - }, - "earnableUntil": "2024-07-30T06:59:59Z", - "redemptionInstructions": "", - "redemptionURL": "https://redeem.ubisoft.com/xdefiant/", - "__typename": "Reward" - } - """ - - id = models.TextField(primary_key=True) - name = models.TextField(null=True, blank=True) - banner_image = auto_prefetch.ForeignKey(Image, related_name="banner_rewards", on_delete=models.CASCADE, null=True) - thumbnail_image = auto_prefetch.ForeignKey( - Image, - related_name="thumbnail_rewards", - on_delete=models.CASCADE, - null=True, - ) - earnable_until = models.DateTimeField(null=True) - redemption_instructions = models.TextField(null=True, blank=True) - redemption_url = models.URLField(null=True, blank=True) - typename = models.TextField(null=True, blank=True) - - def __str__(self) -> str: - return self.name or "Unknown" - - -class UnlockRequirements(auto_prefetch.Model): - """Requirements to unlock a reward. - - Attributes: - subs_goal (int): The number of subscriptions needed to unlock the reward. - minute_watched_goal (int): The number of minutes watched needed to unlock the reward. - typename (str): The type name of the object, typically "QuestRewardUnlockRequirements". - - JSON example: - { - "subsGoal": 2, - "minuteWatchedGoal": 0, - "__typename": "QuestRewardUnlockRequirements" - } - """ - - subs_goal = models.TextField(null=True) - minute_watched_goal = models.TextField(null=True) - typename = models.TextField(null=True, blank=True) - - def __str__(self) -> str: - return f"{self.subs_goal} subs and {self.minute_watched_goal} minutes watched" - - -class RewardCampaign(auto_prefetch.Model): - """Represents a reward campaign. - - Attributes: - id (str): The primary key of the reward campaign. - name (str): The name of the reward campaign. - brand (str): The brand associated with the campaign. - starts_at (datetime): The start date and time of the campaign. - ends_at (datetime): The end date and time of the campaign. - status (str): The status of the campaign. - summary (str): A brief summary of the campaign. - instructions (str): Instructions for the campaign. - external_url (str): The external URL related to the campaign. - reward_value_url_param (str): URL parameter for the reward value. - about_url (str): URL with more information about the campaign. - is_sitewide (bool): Indicates if the campaign is sitewide. - game (Game): The game associated with the campaign. - image (Image): The image associated with the campaign. - rewards (ManyToManyField): The rewards available in the campaign. - typename (str): The type name of the object, typically "RewardCampaign". - - JSON example: - { - "id": "3757a2ae-34b4-11ef-a468-62ece0f03426", - "name": "XDefiant Season 1 Launch", - "brand": "Ubisoft", - "startsAt": "2024-07-02T17:00:00Z", - "endsAt": "2024-07-30T06:59:59Z", - "status": "UNKNOWN", - "summary": "Get a redeemable code for the Twitchy Character Skin in XDefiant for gifting or purchasing 2 subscriptions of any tier to participating channels.", - "instructions": "", - "externalURL": "https://redeem.ubisoft.com/xdefiant/", - "rewardValueURLParam": "", - "aboutURL": "https://xdefiant.com/S1-twitch-rewards", - "isSitewide": false, - "game": { - "id": "780302568", - "slug": "xdefiant", - "displayName": "XDefiant", - "__typename": "Game" - }, - "unlockRequirements": { - "subsGoal": 2, - "minuteWatchedGoal": 0, - "__typename": "QuestRewardUnlockRequirements" - }, - "image": { - "image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/campaign.png", - "__typename": "RewardCampaignImageSet" - }, - "rewards": [ - { - "id": "374628c6-34b4-11ef-a468-62ece0f03426", - "name": "Twitchy Character Skin", - "bannerImage": { - "image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png", - "__typename": "RewardCampaignImageSet" - }, - "thumbnailImage": { - "image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png", - "__typename": "RewardCampaignImageSet" - }, - "earnableUntil": "2024-07-30T06:59:59Z", - "redemptionInstructions": "", - "redemptionURL": "https://redeem.ubisoft.com/xdefiant/", - "__typename": "Reward" - } - ], - "__typename": "RewardCampaign" - } - """ # noqa: E501 - - id = models.TextField(primary_key=True) - name = models.TextField(null=True, blank=True) - brand = models.TextField(null=True, blank=True) - starts_at = models.DateTimeField(null=True) - ends_at = models.DateTimeField(null=True) - status = models.TextField(null=True, blank=True) - summary = models.TextField(null=True, blank=True) - instructions = models.TextField(null=True, blank=True) - external_url = models.URLField(null=True, blank=True) - reward_value_url_param = models.TextField(null=True, blank=True) - about_url = models.URLField(null=True, blank=True) - is_sitewide = models.BooleanField(null=True) - game = auto_prefetch.ForeignKey(Game, on_delete=models.CASCADE, related_name="reward_campaigns", null=True) - unlock_requirements = auto_prefetch.ForeignKey( - UnlockRequirements, - on_delete=models.CASCADE, - related_name="reward_campaigns", - null=True, - ) - image = auto_prefetch.ForeignKey(Image, on_delete=models.CASCADE, related_name="reward_campaigns", null=True) - rewards = models.ManyToManyField(Reward, related_name="reward_campaigns") - typename = models.TextField(null=True, blank=True) - - def __str__(self) -> str: - return self.name or "Unknown" - - -class Channel(auto_prefetch.Model): - """Represents a Twitch channel. - - Attributes: - id (int): The primary key of the channel. - display_name (str): The display name of the channel. - name (str): The name of the channel. - typename (str): The type name of the object, typically "Channel". - - Example JSON data: - { - "data": { - "user": { - "dropCampaign": { - "allow": { - "channels": [ - { - "id": "464161875", - "displayName": "Valair", - "name": "valair", - "__typename": "Channel" - } - ] - } - } - } - } - } - """ - - # Used in Drop Campaigns - id = models.TextField(primary_key=True) - display_name = models.TextField(null=True, blank=True) - name = models.TextField(null=True, blank=True) - typename = models.TextField(null=True, blank=True) - - def __str__(self) -> str: - return self.display_name or "Unknown Channel" - - def get_twitch_url(self) -> str: - # TODO(TheLovinator): Use a field instead # noqa: TD003 - return f"https://www.twitch.tv/{self.name}" - - -class Allow(auto_prefetch.Model): - """List of channels that you can watch to earn rewards. - - Attributes: - channels (ManyToManyField): The channels that you can watch to earn rewards. - is_enabled (bool): Indicates if the channel is enabled. - typename (str): The type name of the object, typically "RewardCampaignChannelAllow". - - Example JSON data: - { - "data": { - "user": { - "dropCampaign": { - "allow": { - "channels": [], - "isEnabled": true, - "__typename": "DropCampaignACL" - } - } - } - } - } - """ - - channels = models.ManyToManyField(Channel, related_name="allow") - is_enabled = models.BooleanField(default=True) - typename = models.TextField(null=True, blank=True) - - def __str__(self) -> str: - return f"{self.channels.count()} channels" - - -class Owner(auto_prefetch.Model): - """Represents the owner of the reward campaign. - - Used for: - - Reward campaigns - - Attributes: - id (int): The primary key of the owner. - slug (str): The slug identifier of the owner. - display_name (str): The display name of the owner. - typename (str): The type name of the object, typically "Organization". - - JSON example: - "owner": { - "id": "a1a51d5a-233d-41c3-9acd-a03bdab35159", - "name": "Out of the Park Developments", - "__typename": "Organization" - }, - """ - - id = models.TextField(primary_key=True, unique=True, help_text="The owner ID.") - name = models.TextField(null=True, blank=True, help_text="Owner name.") - slug = models.TextField(null=True, blank=True, help_text="Slug used for building URL where all the streams are.") - display_name = models.TextField(null=True, blank=True, help_text="Owner name.") - typename = models.TextField(null=True, blank=True, help_text="Always 'Organization'.") - - def __str__(self) -> str: - return self.display_name or "Unknown" - - def get_twitch_url(self) -> str: - if not self.slug: - logger.error("Owner %s has no slug", self.display_name) - return "https://www.twitch.tv/" - - return f"https://www.twitch.tv/{self.slug}" - - -class Benefit(auto_prefetch.Model): - """Represents a benefit that you can earn. - - Attributes: - id (int): The primary key of the benefit. - created_at (datetime): The date and time the benefit was created. - entitlement_limit (int): The limit of entitlement. - game (Game): The game associated with the benefit. - image_asset_url (str): URL to the image asset. - is_ios_available (bool): Indicates if the benefit is available on iOS. - name (str): The name of the benefit. - owner_organization (Owner): The owner organization of the benefit. - typename (str): The type name of the object, typically "Benefit". - - JSON example: - "benefit": { - "id": "6da09649-1fda-4446-a061-cacd8e21b886_CUSTOM_ID_S29_Torso008_01", - "createdAt": "2024-07-09T12:57:31.072Z", - "entitlementLimit": 1, - "game": { - "id": "491487", - "name": "Dead by Daylight", - "__typename": "Game" - }, - "imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/ed4a7829-cc2b-44d3-90a4-f73ef7d8d636.png", - "isIosAvailable": false, - "name": "Unwanted Attention", - "ownerOrganization": { - "id": "6da09649-1fda-4446-a061-cacd8e21b886", - "name": "Behaviour Interactive Inc.", - "__typename": "Organization" - }, - "__typename": "DropBenefit" - } - """ - - id = models.TextField(primary_key=True) - created_at = models.DateTimeField(null=True) - entitlement_limit = models.TextField(null=True) - game = auto_prefetch.ForeignKey(Game, on_delete=models.CASCADE, related_name="benefits", null=True) - image_asset_url = models.URLField(null=True, blank=True) - is_ios_available = models.BooleanField(null=True) - name = models.TextField(null=True, blank=True) - owner_organization = auto_prefetch.ForeignKey(Owner, on_delete=models.CASCADE, related_name="benefits", null=True) - typename = models.TextField(null=True, blank=True) - - def __str__(self) -> str: - return self.name or "Unknown" - - -class BenefitEdge(auto_prefetch.Model): - """Represents a benefit edge. - - Attributes: - benefit (Benefit): The benefit associated with the edge. - entitlement_limit (int): The limit of entitlement. - typename (str): The type name of the object, typically "DropBenefitEdge". - - - JSON example: - "benefitEdges": [ - { - "benefit": { - "id": "6da09649-1fda-4446-a061-cacd8e21b886_CUSTOM_ID_S29_Torso008_01", - "createdAt": "2024-07-09T12:57:31.072Z", - "entitlementLimit": 1, - "game": { - "id": "491487", - "name": "Dead by Daylight", - "__typename": "Game" - }, - "imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/ed4a7829-cc2b-44d3-90a4-f73ef7d8d636.png", - "isIosAvailable": false, - "name": "Unwanted Attention", - "ownerOrganization": { - "id": "6da09649-1fda-4446-a061-cacd8e21b886", - "name": "Behaviour Interactive Inc.", - "__typename": "Organization" - }, - "__typename": "DropBenefit" - }, - "entitlementLimit": 1, - "__typename": "DropBenefitEdge" - } - ], - """ - - benefit = auto_prefetch.ForeignKey(Benefit, on_delete=models.CASCADE, related_name="benefit_edges", null=True) - entitlement_limit = models.TextField(null=True) - typename = models.TextField(null=True, blank=True) - - def __str__(self) -> str: - benefit_name: str | None = self.benefit.name if self.benefit else "Unknown" - return f"{benefit_name} - {self.entitlement_limit}" - - -class TimeBasedDrop(auto_prefetch.Model): - """Represents a time-based drop. - - Attributes: - id (int): The primary key of the time-based drop. - name (str): The name of the time-based drop. - starts_at (datetime): The start date and time of the drop. - ends_at (datetime): The end date and time of the drop. - typename (str): The type name of the object, typically "TimeBasedDrop". - - JSON example: - { - "id": "0ebeff68-3df3-11ef-b15b-0a58a9feac02", - "requiredSubs": 0, - "benefitEdges": [ - { - "benefit": { - "id": "6da09649-1fda-4446-a061-cacd8e21b886_CUSTOM_ID_S29_Legs008_01", - "createdAt": "2024-07-09T12:58:03.654Z", - "entitlementLimit": 1, - "game": { - "id": "491487", - "name": "Dead by Daylight", - "__typename": "Game" - }, - "imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/f46acdf5-9515-41eb-805e-86956db0a9e9.png", - "isIosAvailable": false, - "name": "Back Home", - "ownerOrganization": { - "id": "6da09649-1fda-4446-a061-cacd8e21b886", - "name": "Behaviour Interactive Inc.", - "__typename": "Organization" - }, - "__typename": "DropBenefit" - }, - "entitlementLimit": 1, - "__typename": "DropBenefitEdge" - } - ], - "endAt": "2024-07-30T14:59:59.999Z", - "name": "Back Home", - "preconditionDrops": null, - "requiredMinutesWatched": 360, - "startAt": "2024-07-16T15:00:00Z", - "__typename": "TimeBasedDrop" - }, - """ - - id = models.TextField(primary_key=True) - created_at = models.DateTimeField(null=True) - entitlement_limit = models.TextField(null=True) - game = auto_prefetch.ForeignKey(Game, on_delete=models.CASCADE, related_name="time_based_drops", null=True) - image_asset_url = models.URLField(null=True, blank=True) - is_ios_available = models.BooleanField(null=True) - name = models.TextField(null=True, blank=True) - owner_organization = auto_prefetch.ForeignKey( - Owner, - on_delete=models.CASCADE, - related_name="time_based_drops", - null=True, - ) - typename = models.TextField(null=True, blank=True) - - def __str__(self) -> str: - return self.name or "Unknown" - - -class DropCampaign(auto_prefetch.Model): - """Represents a drop campaign. - - Attributes: - id (int): The primary key of the drop campaign. - allow (Allow): The channels that you can watch to earn rewards. - account_link_url (str): URL to link your account. - description (str): The description of the drop campaign. - details_url (str): URL with more details about the drop campaign. - ends_at (datetime): The end date and time of the drop campaign. - game (Game): The game associated with the drop campaign. - image_url (str): URL to the image associated with the drop campaign. - name (str): The name of the drop campaign. - owner (Owner): The owner of the drop campaign. - starts_at (datetime): The start date and time of the drop campaign. - status (str): The status of the drop campaign. - time_based_drops (ManyToManyField): The time-based drops associated with the campaign. - 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) - description = models.TextField(null=True, blank=True) - details_url = models.URLField(null=True, blank=True) - ends_at = models.DateTimeField(null=True) - # event_based_drops = ???? - game = auto_prefetch.ForeignKey(Game, on_delete=models.CASCADE, related_name="drop_campaigns", null=True) - image_url = models.URLField(null=True, blank=True) - 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(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) diff --git a/core/models/__init__.py b/core/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/models/twitch.py b/core/models/twitch.py new file mode 100644 index 0000000..a90b40a --- /dev/null +++ b/core/models/twitch.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import logging + +import auto_prefetch +from django.db import models + +logger: logging.Logger = logging.getLogger(__name__) + + +class Owner(auto_prefetch.Model): + """The company or person that owns the game. + + Drops will be grouped by the owner. Users can also subscribe to owners. + """ + + id = models.TextField(primary_key=True) # "ad299ac0-f1a5-417d-881d-952c9aed00e9" + name = models.TextField(null=True) # "Microsoft" + + +class Game(auto_prefetch.Model): + """This is the game we will see on the front end.""" + + twitch_id = models.TextField(primary_key=True) # "509658" + game_url = models.URLField(null=True) # "https://www.twitch.tv/directory/category/halo-infinite" + name = models.TextField(null=True) # "Halo Infinite" + box_art_url = models.URLField(null=True) # "https://static-cdn.jtvnw.net/ttv-boxart/Halo%20Infinite.jpg" + slug = models.TextField(null=True) # "halo-infinite" + + org = models.ForeignKey(Owner, on_delete=models.CASCADE, related_name="games", null=True) + + def __str__(self) -> str: + return self.name or "Game name unknown" + + +class DropCampaign(auto_prefetch.Model): + """This is the drop campaign we will see on the front end.""" + + id = models.TextField(primary_key=True) # "f257ce6e-502a-11ef-816e-0a58a9feac02" + created_at = models.DateTimeField(null=True, auto_created=True) # "2024-08-11T00:00:00Z" + modified_at = models.DateTimeField(null=True, auto_now=True) # "2024-08-12T00:00:00Z" + + account_link_url = models.URLField(null=True) # "https://www.halowaypoint.com/settings/linked-accounts" + + # "Tune into this HCS Grassroots event to earn Halo Infinite in-game content!" + description = models.TextField(null=True) + details_url = models.URLField(null=True) # "https://www.halowaypoint.com" + + ends_at = models.DateTimeField(null=True) # "2024-08-12T05:59:59.999Z" + starts_at = models.DateTimeField(null=True) # "2024-08-11T11:00:00Z"" + + game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name="drop_campaigns", null=True) + + # "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/c8e02666-8b86-471f-bf38-7ece29a758e4.png" + image_url = models.URLField(null=True) + + name = models.TextField(null=True) # "HCS Open Series - Week 1 - DAY 2 - AUG11" + status = models.TextField(null=True) # "ACTIVE" + + +class Channel(auto_prefetch.Model): + """This is the channel we will see on the front end.""" + + twitch_id = models.TextField(primary_key=True) # "222719079" + display_name = models.TextField(null=True) # "LVTHalo" + name = models.TextField(null=True) # "lvthalo" + twitch_url = models.URLField(null=True) # "https://www.twitch.tv/lvthalo" + live = models.BooleanField(default=False) # "True" + + drop_campaigns = models.ManyToManyField(DropCampaign, related_name="channels") + + +class TimeBasedDrop(auto_prefetch.Model): + """This is the drop we will see on the front end.""" + + id = models.TextField(primary_key=True) # "d5cdf372-502b-11ef-bafd-0a58a9feac02" + created_at = models.DateTimeField(null=True, auto_created=True) # "2024-08-11T00:00:00Z" + modified_at = models.DateTimeField(null=True, auto_now=True) # "2024-08-12T00:00:00Z" + + required_subs = models.PositiveBigIntegerField(null=True) # "1" + ends_at = models.DateTimeField(null=True) # "2024-08-12T05:59:59.999Z" + name = models.TextField(null=True) # "Cosmic Nexus Chimera" + required_minutes_watched = models.PositiveBigIntegerField(null=True) # "120" + starts_at = models.DateTimeField(null=True) # "2024-08-11T11:00:00Z" + + drop_campaign = models.ForeignKey(DropCampaign, on_delete=models.CASCADE, related_name="drops", null=True) + + +class Benefit(auto_prefetch.Model): + """This is the benefit we will see on the front end.""" + + id = models.TextField(primary_key=True) # "d5cdf372-502b-11ef-bafd-0a58a9feac02" + created_at = models.DateTimeField(null=True, auto_created=True) # "2024-08-11T00:00:00Z" + modified_at = models.DateTimeField(null=True, auto_now=True) # "2024-08-12T00:00:00Z" + + # Note: This is Twitch's created_at from the API. + twitch_created_at = models.DateTimeField(null=True) # "2023-11-09T01:18:00.126Z" + + entitlement_limit = models.PositiveBigIntegerField(null=True) # "1" + + # "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/e58ad175-73f6-4392-80b8-fb0223163733.png" + image_url = models.URLField(null=True) + is_ios_available = models.BooleanField(null=True) # "True" + + name = models.TextField(null=True) # "Cosmic Nexus Chimera" + + time_based_drop = models.ForeignKey(TimeBasedDrop, on_delete=models.CASCADE, related_name="benefits", null=True) + + +class RewardCampaign(auto_prefetch.Model): + """Buy subscriptions to earn rewards.""" + + id = models.TextField(primary_key=True) # "dc4ff0b4-4de0-11ef-9ec3-621fb0811846" + created_at = models.DateTimeField(null=True, auto_created=True) # "2024-08-11T00:00:00Z" + modified_at = models.DateTimeField(null=True, auto_now=True) # "2024-08-12T00:00:00Z" + + name = models.TextField(null=True) # "Buy 1 new sub, get 3 months of Apple TV+" + brand = models.TextField(null=True) # "Apple TV+" + starts_at = models.DateTimeField(null=True) # "2024-08-11T11:00:00Z" + ends_at = models.DateTimeField(null=True) # "2024-08-12T05:59:59.999Z" + status = models.TextField(null=True) # "UNKNOWN" + summary = models.TextField(null=True) # "Get 3 months of Apple TV+ with the purchase of a new sub" + instructions = models.TextField(null=True) # "Buy a new sub to get 3 months of Apple TV+" + reward_value_url_param = models.TextField(null=True) # "" + external_url = models.URLField(null=True) # "https://tv.apple.com/includes/commerce/redeem/code-entry" + about_url = models.URLField(null=True) # "https://blog.twitch.tv/2024/07/26/sub-and-get-apple-tv/" + is_site_wide = models.BooleanField(null=True) # "True" + game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name="reward_campaigns", null=True) + + sub_goal = models.PositiveBigIntegerField(null=True) # "1" + minute_watched_goal = models.PositiveBigIntegerField(null=True) # "0" + + # "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_appletv_q3_2024/apple_150x200.png" + image_url = models.URLField(null=True) + + +class Reward(auto_prefetch.Model): + """This from the RewardCampaign.""" + + id = models.TextField(primary_key=True) # "dc2e9810-4de0-11ef-9ec3-621fb0811846" + name = models.TextField(null=True) # "3 months of Apple TV+" + + # "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_appletv_q3_2024/apple_200x200.png" + banner_image_url = models.URLField(null=True) + # "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_appletv_q3_2024/apple_200x200.png" + thumbnail_image_url = models.URLField(null=True) + + earnable_until = models.DateTimeField(null=True) # "2024-08-19T19:00:00Z" + redemption_instructions = models.TextField(null=True) # "" + redemption_url = models.URLField(null=True) # "https://tv.apple.com/includes/commerce/redeem/code-entry" + + campaign = models.ForeignKey(RewardCampaign, on_delete=models.CASCADE, related_name="rewards", null=True) diff --git a/core/settings.py b/core/settings.py index be9baa7..5f47bce 100644 --- a/core/settings.py +++ b/core/settings.py @@ -68,6 +68,8 @@ DISCORD_WEBHOOK_URL: str = os.getenv(key="DISCORD_WEBHOOK_URL", default="") INSTALLED_APPS: list[str] = [ "core.apps.CoreConfig", "whitenoise.runserver_nostatic", + "django.contrib.admin", + "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", @@ -80,9 +82,11 @@ MIDDLEWARE: list[str] = [ "django.middleware.gzip.GZipMiddleware", "debug_toolbar.middleware.DebugToolbarMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", + "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "simple_history.middleware.HistoryRequestMiddleware", ] @@ -93,6 +97,8 @@ TEMPLATES = [ "DIRS": [BASE_DIR / "templates"], "OPTIONS": { "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.i18n", "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.messages.context_processors.messages", diff --git a/core/templates/index.html b/core/templates/index.html index 12c0268..0f562a0 100644 --- a/core/templates/index.html +++ b/core/templates/index.html @@ -12,9 +12,9 @@ {% include "partials/reward_campaign_card.html" %} {% endfor %}

Organizations

- {% for org in orgs %} -

- {{ org }} + {% for org in owners %} +

+ {{ org.name }}

{% for game in org.games.all %} {% include "partials/game_card.html" %} diff --git a/core/templates/partials/game_card.html b/core/templates/partials/game_card.html index 1a27e2b..bf6d882 100644 --- a/core/templates/partials/game_card.html +++ b/core/templates/partials/game_card.html @@ -1,7 +1,7 @@ -
+
- {{ game.display_name }}

- {{ game.display_name }} + {{ game.name }}

- {% for webhook in webhooks %} -
- {{ webhook.name }} - {{ webhook.name }} -
- - -
-
- - -
-
- {% endfor %} +
{% for campaign in game.drop_campaigns.all %} {% if not forloop.first %}
{% endif %}

{{ campaign.name }}

- Ends in: {{ campaign.end_at|timeuntil }} + Ends in: {{ campaign.ends_at|timeuntil }}

{% if campaign.description != campaign.name %} - {% if campaign.description|length > 100 %} + {% if campaign.description|length > 200 %}

+ aria-controls="collapseDescription{{ campaign.id }}" + aria-label="Show Description">Show Description

-
+
{{ campaign.description }}

@@ -62,15 +45,17 @@ {% endif %}
{% for drop in campaign.drops.all %} -
- {{ drop.name }} drop image - {{ drop.name }} - {{ drop.required_minutes_watched }} -
+ {% for benefit in drop.benefits.all %} +
+ {{ benefit.name }} drop image + {{ benefit.name }} +
+ {% endfor %} {% endfor %}
diff --git a/core/templates/partials/reward_campaign_card.html b/core/templates/partials/reward_campaign_card.html index dcd174a..bed1a3d 100644 --- a/core/templates/partials/reward_campaign_card.html +++ b/core/templates/partials/reward_campaign_card.html @@ -1,7 +1,7 @@
- {{ campaign.name }} {% for reward in campaign.rewards.all %}
- {{ reward.name }} reward image HttpResponse: HttpResponse: The response object """ reward_campaigns: BaseManager[RewardCampaign] = RewardCampaign.objects.all() + owners: BaseManager[Owner] = Owner.objects.all() 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} + context: dict[str, BaseManager[RewardCampaign] | str | BaseManager[Owner]] = { + "reward_campaigns": reward_campaigns, + "toc": toc, + "owners": owners, + } return TemplateResponse(request=request, template="index.html", context=context) @@ -79,9 +84,7 @@ def game_view(request: HttpRequest) -> HttpResponse: """ 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 - ] + tocs: list[TOCItem] = [TOCItem(name=game.name, toc_id=game.slug) for game in games if game.name and game.slug] toc: str = build_toc(tocs) context: dict[str, BaseManager[Game] | str] = {"games": games, "toc": toc}