From 041b2ddb956a9f2c99b9f1285f48b691e79c8679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Tue, 13 Aug 2024 05:37:53 +0200 Subject: [PATCH] Make index view faster --- .vscode/settings.json | 1 + ...options_alter_channel_options_and_more.py} | 176 +++++++----------- ..._alter_benefit_time_based_drop_and_more.py | 75 -------- core/models/twitch.py | 50 +++-- core/templates/index.html | 15 +- core/templates/partials/game_card.html | 13 +- core/views.py | 37 ++-- poetry.lock | 16 +- pyproject.toml | 1 - requirements-dev.txt | 6 - requirements.txt | 13 -- 11 files changed, 146 insertions(+), 257 deletions(-) rename core/migrations/{0001_initial.py => 0001_squashed_0003_alter_benefit_options_alter_channel_options_and_more.py} (73%) delete mode 100644 core/migrations/0002_alter_benefit_time_based_drop_and_more.py delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt diff --git a/.vscode/settings.json b/.vscode/settings.json index c24bd27..efa4321 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,6 +17,7 @@ "sitewide", "socialaccount", "Stresss", + "tocs", "ttvdrops", "ulimits", "Valair", diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_squashed_0003_alter_benefit_options_alter_channel_options_and_more.py similarity index 73% rename from core/migrations/0001_initial.py rename to core/migrations/0001_squashed_0003_alter_benefit_options_alter_channel_options_and_more.py index 937a816..56e9800 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_squashed_0003_alter_benefit_options_alter_channel_options_and_more.py @@ -1,45 +1,31 @@ -# Generated by Django 5.1 on 2024-08-12 03:47 -from __future__ import annotations - -from typing import TYPE_CHECKING +# Generated by Django 5.1 on 2024-08-12 23:16 import django.db.models.deletion -import django.db.models.manager from django.db import migrations, models - -if TYPE_CHECKING: - from django.db.migrations.operations.base import Operation +from django.db.migrations.operations.base import Operation class Migration(migrations.Migration): + replaces: list[tuple[str, str]] = [ + ("core", "0001_initial"), + ("core", "0002_alter_benefit_time_based_drop_and_more"), + ("core", "0003_alter_benefit_options_alter_channel_options_and_more"), + ] + initial = True dependencies: list[tuple[str, str]] = [] operations: list[Operation] = [ migrations.CreateModel( - name="DropCampaign", + name="Owner", 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)), - ("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, - "base_manager_name": "prefetch_manager", }, - managers=[ - ("objects", django.db.models.manager.Manager()), - ("prefetch_manager", django.db.models.manager.Manager()), - ], ), migrations.CreateModel( name="Game", @@ -49,69 +35,19 @@ class Migration(migrations.Migration): ("name", models.TextField(null=True)), ("box_art_url", models.URLField(null=True)), ("slug", models.TextField(null=True)), + ( + "org", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="games", + 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="Owner", - fields=[ - ("id", models.TextField(primary_key=True, serialize=False)), - ("name", models.TextField(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=[ - ("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, - "base_manager_name": "prefetch_manager", - }, - managers=[ - ("objects", django.db.models.manager.Manager()), - ("prefetch_manager", django.db.models.manager.Manager()), - ], - ), - migrations.AddField( - 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.owner", - ), ), migrations.CreateModel( name="RewardCampaign", @@ -145,41 +81,35 @@ class Migration(migrations.Migration): ], 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", + name="DropCampaign", 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)), + ("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)), - ("banner_image_url", models.URLField(null=True)), - ("thumbnail_image_url", models.URLField(null=True)), - ("earnable_until", models.DateTimeField(null=True)), - ("redemption_instructions", models.TextField(null=True)), - ("redemption_url", models.URLField(null=True)), + ("status", models.TextField(null=True)), ( - "campaign", + "game", models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, - related_name="rewards", - to="core.rewardcampaign", + related_name="drop_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="TimeBasedDrop", @@ -204,12 +134,21 @@ class Migration(migrations.Migration): ], 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=[ + ("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, + }, ), migrations.CreateModel( name="Benefit", @@ -234,11 +173,30 @@ class Migration(migrations.Migration): ], 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(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(null=True)), + ("redemption_url", models.URLField(null=True)), + ( + "campaign", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="rewards", + to="core.rewardcampaign", + ), + ), ], + options={ + "abstract": False, + }, ), ] diff --git a/core/migrations/0002_alter_benefit_time_based_drop_and_more.py b/core/migrations/0002_alter_benefit_time_based_drop_and_more.py deleted file mode 100644 index 65eb82a..0000000 --- a/core/migrations/0002_alter_benefit_time_based_drop_and_more.py +++ /dev/null @@ -1,75 +0,0 @@ -# Generated by Django 5.1 on 2024-08-12 22:19 - -import auto_prefetch -import django.db.models.deletion -from django.db import migrations -from django.db.migrations.operations.base import Operation - - -class Migration(migrations.Migration): - dependencies: list[tuple[str, str]] = [ - ("core", "0001_initial"), - ] - - operations: list[Operation] = [ - migrations.AlterField( - model_name="benefit", - name="time_based_drop", - field=auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="benefits", - to="core.timebaseddrop", - ), - ), - migrations.AlterField( - model_name="dropcampaign", - name="game", - field=auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="drop_campaigns", - to="core.game", - ), - ), - migrations.AlterField( - model_name="game", - name="org", - field=auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="games", - to="core.owner", - ), - ), - migrations.AlterField( - model_name="reward", - name="campaign", - field=auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="rewards", - to="core.rewardcampaign", - ), - ), - migrations.AlterField( - model_name="rewardcampaign", - name="game", - field=auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="reward_campaigns", - to="core.game", - ), - ), - migrations.AlterField( - model_name="timebaseddrop", - name="drop_campaign", - field=auto_prefetch.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="drops", - to="core.dropcampaign", - ), - ), - ] diff --git a/core/models/twitch.py b/core/models/twitch.py index 5702ddd..f486b96 100644 --- a/core/models/twitch.py +++ b/core/models/twitch.py @@ -2,13 +2,12 @@ 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): +class Owner(models.Model): """The company or person that owns the game. Drops will be grouped by the owner. Users can also subscribe to owners. @@ -17,8 +16,11 @@ class Owner(auto_prefetch.Model): id = models.TextField(primary_key=True) # "ad299ac0-f1a5-417d-881d-952c9aed00e9" name = models.TextField(null=True) # "Microsoft" + def __str__(self) -> str: + return self.name or "Owner name unknown" -class Game(auto_prefetch.Model): + +class Game(models.Model): """This is the game we will see on the front end.""" twitch_id = models.TextField(primary_key=True) # "509658" @@ -27,13 +29,13 @@ class Game(auto_prefetch.Model): 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 = auto_prefetch.ForeignKey(Owner, on_delete=models.CASCADE, related_name="games", null=True) + 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): +class DropCampaign(models.Model): """This is the drop campaign we will see on the front end.""" id = models.TextField(primary_key=True) # "f257ce6e-502a-11ef-816e-0a58a9feac02" @@ -49,7 +51,7 @@ class DropCampaign(auto_prefetch.Model): ends_at = models.DateTimeField(null=True) # "2024-08-12T05:59:59.999Z" starts_at = models.DateTimeField(null=True) # "2024-08-11T11:00:00Z"" - game = auto_prefetch.ForeignKey(Game, on_delete=models.CASCADE, related_name="drop_campaigns", null=True) + 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) @@ -57,8 +59,11 @@ class DropCampaign(auto_prefetch.Model): name = models.TextField(null=True) # "HCS Open Series - Week 1 - DAY 2 - AUG11" status = models.TextField(null=True) # "ACTIVE" + def __str__(self) -> str: + return self.name or "Drop campaign name unknown" -class Channel(auto_prefetch.Model): + +class Channel(models.Model): """This is the channel we will see on the front end.""" twitch_id = models.TextField(primary_key=True) # "222719079" @@ -69,8 +74,11 @@ class Channel(auto_prefetch.Model): drop_campaigns = models.ManyToManyField(DropCampaign, related_name="channels") + def __str__(self) -> str: + return self.display_name or "Channel name unknown" -class TimeBasedDrop(auto_prefetch.Model): + +class TimeBasedDrop(models.Model): """This is the drop we will see on the front end.""" id = models.TextField(primary_key=True) # "d5cdf372-502b-11ef-bafd-0a58a9feac02" @@ -83,10 +91,13 @@ class TimeBasedDrop(auto_prefetch.Model): required_minutes_watched = models.PositiveBigIntegerField(null=True) # "120" starts_at = models.DateTimeField(null=True) # "2024-08-11T11:00:00Z" - drop_campaign = auto_prefetch.ForeignKey(DropCampaign, on_delete=models.CASCADE, related_name="drops", null=True) + drop_campaign = models.ForeignKey(DropCampaign, on_delete=models.CASCADE, related_name="drops", null=True) + + def __str__(self) -> str: + return self.name or "Drop name unknown" -class Benefit(auto_prefetch.Model): +class Benefit(models.Model): """This is the benefit we will see on the front end.""" id = models.TextField(primary_key=True) # "d5cdf372-502b-11ef-bafd-0a58a9feac02" @@ -104,15 +115,18 @@ class Benefit(auto_prefetch.Model): name = models.TextField(null=True) # "Cosmic Nexus Chimera" - time_based_drop = auto_prefetch.ForeignKey( + time_based_drop = models.ForeignKey( TimeBasedDrop, on_delete=models.CASCADE, related_name="benefits", null=True, ) + def __str__(self) -> str: + return self.name or "Benefit name unknown" -class RewardCampaign(auto_prefetch.Model): + +class RewardCampaign(models.Model): """Buy subscriptions to earn rewards.""" id = models.TextField(primary_key=True) # "dc4ff0b4-4de0-11ef-9ec3-621fb0811846" @@ -130,7 +144,7 @@ class RewardCampaign(auto_prefetch.Model): 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 = auto_prefetch.ForeignKey(Game, on_delete=models.CASCADE, related_name="reward_campaigns", null=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" @@ -138,8 +152,11 @@ class RewardCampaign(auto_prefetch.Model): # "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_appletv_q3_2024/apple_150x200.png" image_url = models.URLField(null=True) + def __str__(self) -> str: + return self.name or "Reward campaign name unknown" -class Reward(auto_prefetch.Model): + +class Reward(models.Model): """This from the RewardCampaign.""" id = models.TextField(primary_key=True) # "dc2e9810-4de0-11ef-9ec3-621fb0811846" @@ -154,4 +171,7 @@ class Reward(auto_prefetch.Model): redemption_instructions = models.TextField(null=True) # "" redemption_url = models.URLField(null=True) # "https://tv.apple.com/includes/commerce/redeem/code-entry" - campaign = auto_prefetch.ForeignKey(RewardCampaign, on_delete=models.CASCADE, related_name="rewards", null=True) + campaign = models.ForeignKey(RewardCampaign, on_delete=models.CASCADE, related_name="rewards", null=True) + + def __str__(self) -> str: + return self.name or "Reward name unknown" diff --git a/core/templates/index.html b/core/templates/index.html index 0f562a0..15b93cc 100644 --- a/core/templates/index.html +++ b/core/templates/index.html @@ -11,14 +11,15 @@ {% for campaign in reward_campaigns %} {% include "partials/reward_campaign_card.html" %} {% endfor %} -

Organizations

- {% for org in owners %} -

- {{ org.name }} -

- {% for game in org.games.all %} +

+ Drop campaigns - +
{{ games.count }} games
+

+ {% for game in games %} + {# Only show games with drop campaigns #} + {% if game.drop_campaigns.count > 0 %} {% include "partials/game_card.html" %} - {% endfor %} + {% endif %} {% endfor %} diff --git a/core/templates/partials/game_card.html b/core/templates/partials/game_card.html index bf6d882..b9cb29c 100644 --- a/core/templates/partials/game_card.html +++ b/core/templates/partials/game_card.html @@ -1,8 +1,8 @@ -
+
{{ game.display_name }}{% endif %}

{{ campaign.name }}

+ {% if campaign.details_url == campaign.account_link_url %} + Details + {% else %} + Details + | + Link Account + {% endif %}

Ends in: {{ campaign.ends_at|timeuntil }}

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

HttpResponse: Returns: HttpResponse: The response object """ - reward_campaigns: BaseManager[RewardCampaign] = RewardCampaign.objects.all() - owners: BaseManager[Owner] = Owner.objects.all() + reward_campaigns: BaseManager[RewardCampaign] = ( + RewardCampaign.objects.all() + .prefetch_related("rewards") + .filter(ends_at__gt=timezone.now(), starts_at__lt=timezone.now()) + ) + future_campaigns: BaseManager[DropCampaign] = DropCampaign.objects.filter( + ends_at__gt=timezone.now(), + starts_at__lt=timezone.now(), + ) - toc: str = build_toc([ - TOCItem(name="Information", toc_id="#info-box"), - TOCItem(name="Games", toc_id="#games"), - ]) + games: BaseManager[Game] = Game.objects.all().prefetch_related( + Prefetch("drop_campaigns", queryset=future_campaigns.prefetch_related("drops__benefits")), + ) + tocs: list[TOCItem] = [] + for game in games.all(): + game_name: str = game.name or "

" + tocs.append(TOCItem(name=game_name, toc_id=f"#{game.twitch_id}")) - context: dict[str, BaseManager[RewardCampaign] | str | BaseManager[Owner]] = { + toc: str = build_toc(tocs) + + context: dict[str, BaseManager[RewardCampaign] | str | BaseManager[Game]] = { "reward_campaigns": reward_campaigns, + "games": games, "toc": toc, - "owners": owners, } return TemplateResponse(request=request, template="index.html", context=context) @@ -84,10 +98,7 @@ def game_view(request: HttpRequest) -> HttpResponse: """ games: BaseManager[Game] = Game.objects.all() - 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} + context: dict[str, BaseManager[Game] | str] = {"games": games} return TemplateResponse(request=request, template="games.html", context=context) diff --git a/poetry.lock b/poetry.lock index c72dc7e..3d4ffd8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -315,20 +315,6 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] -[[package]] -name = "django-auto-prefetch" -version = "1.9.0" -description = "Automatically prefetch foreign key values as needed." -optional = false -python-versions = ">=3.8" -files = [ - {file = "django_auto_prefetch-1.9.0-py3-none-any.whl", hash = "sha256:2bcc1d4551a952cc73124d339319570392aece390cedffbd9585d94f802eef47"}, - {file = "django_auto_prefetch-1.9.0.tar.gz", hash = "sha256:b447a0342a734bd3c718f9721598042721edce1a95b81df5b5a167dd0997f347"}, -] - -[package.dependencies] -django = ">=3.2" - [[package]] name = "django-debug-toolbar" version = "4.4.6" @@ -1217,4 +1203,4 @@ brotli = ["brotli"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "4efd9a959fea305be05a55cc0041474ab83ba202dc4c55479bb9d2b66387393b" +content-hash = "36def17b6380edc474dbfa9cd469124d4ae2d3b0c27ea4fc8cb4c6c54dddb032" diff --git a/pyproject.toml b/pyproject.toml index 42c260c..d50a7b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ package-mode = false python = "^3.12" discord-webhook = "^1.3.1" django = { version = "^5.1", allow-prereleases = true } -django-auto-prefetch = "^1.9.0" django-debug-toolbar = "^4.4.6" django-simple-history = "^3.7.0" pillow = "^10.4.0" diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 82be923..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -djlint -pip -pre-commit -pytest -pytest-django -ruff diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4abe69c..0000000 --- a/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -discord-webhook -django-auto-prefetch -django-debug-toolbar -django-simple-history -django>=0.0.0.dev0 -hishel -httpx -pillow -platformdirs -python-dotenv -sentry-sdk[django] -undetected-playwright-patch -whitenoise[brotli]