From 534de60f9f3c9ebb771644afc26fdb237a35b058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Sun, 18 Aug 2024 05:41:50 +0200 Subject: [PATCH] Use template tags instead of partial HTML --- .pre-commit-config.yaml | 2 +- core/management/commands/scrape_twitch.py | 40 +++--- core/settings.py | 14 +- core/templates/index.html | 49 +++---- core/templates/partials/game_card.html | 52 ------- .../partials/reward_campaign_card.html | 52 ------- core/templatetags/__init__.py | 0 core/templatetags/campaign_tags.py | 93 +++++++++++++ core/templatetags/game_tags.py | 130 ++++++++++++++++++ 9 files changed, 277 insertions(+), 155 deletions(-) delete mode 100644 core/templates/partials/game_card.html delete mode 100644 core/templates/partials/reward_campaign_card.html create mode 100644 core/templatetags/__init__.py create mode 100644 core/templatetags/campaign_tags.py create mode 100644 core/templatetags/game_tags.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 414816f..2faedde 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,7 +43,7 @@ repos: # An extremely fast Python linter and formatter. - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.7 + rev: v0.6.1 hooks: - id: ruff-format - id: ruff diff --git a/core/management/commands/scrape_twitch.py b/core/management/commands/scrape_twitch.py index 50f3e76..dd955f9 100644 --- a/core/management/commands/scrape_twitch.py +++ b/core/management/commands/scrape_twitch.py @@ -76,11 +76,9 @@ async def add_reward_campaign(campaign: dict | None) -> None: """ if not campaign: return - - logger.info("Adding reward campaign to database %s", campaign["id"]) if "data" in campaign and "rewardCampaignsAvailableToUser" in campaign["data"]: for reward_campaign in campaign["data"]["rewardCampaignsAvailableToUser"]: - our_reward_campaign = await handle_reward_campaign(reward_campaign) + our_reward_campaign: RewardCampaign = await handle_reward_campaign(reward_campaign) if "rewards" in reward_campaign: for reward in reward_campaign["rewards"]: @@ -126,6 +124,7 @@ async def handle_reward_campaign(reward_campaign: dict) -> RewardCampaign: RewardCampaign: The reward campaign that was added or updated. """ mappings: dict[str, str] = { + "name": "name", "brand": "brand", "createdAt": "created_at", "startsAt": "starts_at", @@ -209,8 +208,7 @@ async def add_or_update_owner(owner_json: dict | None) -> Owner | None: if not owner_json: return None - defaults = {} - + defaults: dict[str, str] = {} if owner_json.get("name"): defaults["name"] = owner_json["name"] @@ -235,7 +233,7 @@ async def add_or_update_channels(channels_json: list[dict]) -> list[Channel] | N channels: list[Channel] = [] for channel_json in channels_json: - defaults = {} + defaults: dict[str, str] = {} if channel_json.get("displayName"): defaults["display_name"] = channel_json["displayName"] @@ -266,7 +264,7 @@ async def add_benefit(benefit: dict, time_based_drop: TimeBasedDrop) -> None: "isIosAvailable": "is_ios_available", "name": "name", } - defaults = {new_key: benefit[key] for key, new_key in mappings.items() if benefit.get(key)} + defaults: dict[str, str] = {new_key: benefit[key] for key, new_key in mappings.items() if benefit.get(key)} our_benefit, created = await Benefit.objects.aupdate_or_create(id=benefit["id"], defaults=defaults) if created: logger.info("Added benefit %s", our_benefit.id) @@ -284,10 +282,11 @@ async def add_drop_campaign(drop_campaign: dict | None) -> None: if not drop_campaign: return - defaults = {} + defaults: dict[str, str | Game] = {} if drop_campaign.get("game"): game: Game | None = await add_or_update_game(drop_campaign["game"]) - defaults["game"] = game + if game: + defaults["game"] = game mappings: dict[str, str] = { "accountLinkURL": "account_link_url", @@ -327,19 +326,22 @@ async def add_time_based_drops(drop_campaign: dict, our_drop_campaign: DropCampa our_drop_campaign (DropCampaign): The drop campaign object in the database. """ for time_based_drop in drop_campaign.get("timeBasedDrops", []): + time_based_drop: dict[str, typing.Any] if time_based_drop.get("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) - mappings = { + mappings: dict[str, str] = { "requiredSubs": "required_subs", "endAt": "ends_at", "name": "name", "requiredMinutesWatched": "required_minutes_watched", "startAt": "starts_at", } - defaults = {new_key: time_based_drop[key] for key, new_key in mappings.items() if time_based_drop.get(key)} + defaults: dict[str, str | DropCampaign] = { + new_key: time_based_drop[key] for key, new_key in mappings.items() if time_based_drop.get(key) + } if our_drop_campaign: defaults["drop_campaign"] = our_drop_campaign @@ -373,18 +375,18 @@ async def process_json_data(num: int, campaign: dict | None) -> None: # This is a Reward Campaign if "rewardCampaignsAvailableToUser" in campaign.get("data", {}): - save_json(campaign, "reward_campaigns") - await add_reward_campaign(campaign) + save_json(campaign=campaign, dir_name="reward_campaigns") + await add_reward_campaign(campaign=campaign) if "dropCampaign" in campaign.get("data", {}).get("user", {}): - save_json(campaign, "drop_campaign") + save_json(campaign=campaign, dir_name="drop_campaign") if campaign.get("data", {}).get("user", {}).get("dropCampaign"): - await add_drop_campaign(campaign["data"]["user"]["dropCampaign"]) + await add_drop_campaign(drop_campaign=campaign["data"]["user"]["dropCampaign"]) if "dropCampaigns" in campaign.get("data", {}).get("currentUser", {}): for drop_campaign in campaign["data"]["currentUser"]["dropCampaigns"]: - save_json(campaign, "drop_campaigns") - await add_drop_campaign(drop_campaign) + save_json(campaign=campaign, dir_name="drop_campaigns") + await add_drop_campaign(drop_campaign=drop_campaign) class Command(BaseCommand): @@ -423,7 +425,7 @@ class Command(BaseCommand): while not logged_in: try: await page.wait_for_selector( - 'div[data-a-target="top-nav-avatar"]', + selector='div[data-a-target="top-nav-avatar"]', timeout=300000, ) logged_in = True @@ -449,7 +451,7 @@ class Command(BaseCommand): async def run_with_playwright(self) -> None: async with async_playwright() as playwright: - await self.run(playwright) + await self.run(playwright=playwright) if __name__ == "__main__": diff --git a/core/settings.py b/core/settings.py index 864d908..4e4e9c3 100644 --- a/core/settings.py +++ b/core/settings.py @@ -171,13 +171,13 @@ MESSAGE_TAGS: dict[int, str] = { messages.ERROR: "alert-danger", } -CACHE_MIDDLEWARE_SECONDS = 60 * 60 * 24 # 1 day -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", - "LOCATION": DATA_DIR / "django_cache", - }, -} +# CACHE_MIDDLEWARE_SECONDS = 60 * 60 * 24 # 1 day +# CACHES = { +# "default": { +# "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", +# "LOCATION": DATA_DIR / "django_cache", +# }, +# } SITE_ID = 1 diff --git a/core/templates/index.html b/core/templates/index.html index 83ba268..f9967cb 100644 --- a/core/templates/index.html +++ b/core/templates/index.html @@ -1,29 +1,30 @@ {% extends "base.html" %} -{% load static %} +{% load static campaign_tags game_tags %} {% block content %} -
-
-
{{ toc|safe }}
-
- {% include "partials/info_box.html" %} - {% include "partials/news.html" %} -

- Reward campaigns - -
{{ reward_campaigns.count }} campaigns
-

- {% for campaign in reward_campaigns %} - {% include "partials/reward_campaign_card.html" %} - {% endfor %} -

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

- {% for game in games %} - {% if game.drop_campaigns.count > 0 %} - {% include "partials/game_card.html" %} - {% endif %} - {% endfor %} -
+
+
+
{{ toc|safe }}
+
+ {% include "partials/info_box.html" %} + {% include "partials/news.html" %} +

+ Reward campaign - +
+ {{ reward_campaigns.count }} + campaign{{ reward_campaigns.count|pluralize }} +
+

+ {% for campaign in reward_campaigns %} + {% render_campaign campaign %} + {% endfor %} +

+ Drop campaigns - +
{{ games.count }} game{{ games.count|pluralize }}
+

+ {% for game in games %} + {% render_game_card game %} + {% endfor %}
+
{% endblock content %} diff --git a/core/templates/partials/game_card.html b/core/templates/partials/game_card.html deleted file mode 100644 index 9e2e29d..0000000 --- a/core/templates/partials/game_card.html +++ /dev/null @@ -1,52 +0,0 @@ -
-
-
- {{ game.name }} box art -
-
-
-

- {{ game.name }} -

-
- -
- {% for campaign in game.drop_campaigns.all %} -
- {% if campaign.details_url == campaign.account_link_url %} - Details - {% else %} - Details - | - Link Account - {% endif %} -

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

-
- {% for drop in campaign.drops.all %} - {% for benefit in drop.benefits.all %} -
- {{ benefit.name }} drop image - {{ benefit.name }} -
- {% endfor %} - {% endfor %} -
-
- {% endfor %} -
-
-
-
diff --git a/core/templates/partials/reward_campaign_card.html b/core/templates/partials/reward_campaign_card.html deleted file mode 100644 index bed1a3d..0000000 --- a/core/templates/partials/reward_campaign_card.html +++ /dev/null @@ -1,52 +0,0 @@ -
-
-
- {{ campaign.name }} -
-
-
-

- {{ campaign.name }} -

-

{{ campaign.summary }}

-

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

- Learn More - {% if campaign.instructions %} -
-

Instructions

-

{{ campaign.instructions }}

-
- {% endif %} - {% if campaign.rewards.exists %} -
-

Rewards

-
- {% for reward in campaign.rewards.all %} -
- {{ reward.name }} reward image -
- {{ reward.name }} -
-
- {% endfor %} -
-
- {% endif %} -
-
-
-
diff --git a/core/templatetags/__init__.py b/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/templatetags/campaign_tags.py b/core/templatetags/campaign_tags.py new file mode 100644 index 0000000..f55253b --- /dev/null +++ b/core/templatetags/campaign_tags.py @@ -0,0 +1,93 @@ +from django import template +from django.utils.html import format_html +from django.utils.safestring import SafeText +from django.utils.timesince import timesince +from django.utils.timezone import now + +from core.models import Reward, RewardCampaign + +register = template.Library() + + +@register.simple_tag +def render_campaign(campaign: RewardCampaign) -> SafeText: + """Render the campaign HTML. + + Args: + campaign: The campaign object. + + Returns: + The rendered HTML string. + """ + time_remaining = timesince(now(), campaign.ends_at) + ends_in: str = f'{campaign.ends_at.strftime("%A %d %B %H:%M %Z")}' if campaign.ends_at else "" + starts_in: str = f'{campaign.starts_at.strftime("%A %d %B %H:%M %Z")}' if campaign.starts_at else "" + + # Start building the HTML + html: str = f""" +
+
+
+ {campaign.name} +
+
+
+

+ {campaign.name} +

+

{campaign.summary}

+

+ Ends in: {time_remaining} +

+ Learn More + """ + + # Add instructions if present + if campaign.instructions: + html += f""" +
+

Instructions

+

{campaign.instructions}

+
+ """ + + # Add rewards if present + if campaign.rewards.exists(): # type: ignore # noqa: PGH003 + html += """ +
+

Rewards

+
+ """ + for reward in campaign.rewards.all(): # type: ignore # noqa: PGH003 + reward: Reward + html += f""" +
+ {reward.name} reward image +
+ {reward.name} +
+
+ """ + html += "
" + + # Close the main divs + html += """ +
+
+
+
+ """ + + return format_html(html) diff --git a/core/templatetags/game_tags.py b/core/templatetags/game_tags.py new file mode 100644 index 0000000..deaafdc --- /dev/null +++ b/core/templatetags/game_tags.py @@ -0,0 +1,130 @@ +from django import template +from django.utils.html import format_html +from django.utils.safestring import SafeText +from django.utils.timesince import timesince +from django.utils.timezone import now + +from core.models import Benefit, DropCampaign, Game, TimeBasedDrop + +register = template.Library() + + +@register.simple_tag +def render_game_card(game: Game) -> SafeText: + """Render the game card HTML. + + Args: + game: The game object. + + Returns: + The rendered HTML string. + """ + twitch_id: str = game.twitch_id + box_art_url: str = game.box_art_url or "https://static-cdn.jtvnw.net/ttv-static/404_boxart.jpg" + name: str = game.name or "Game name unknown" + slug: str = game.slug or "game-name-unknown" + drop_campaigns = game.drop_campaigns.all() # type: ignore # noqa: PGH003 + return format_html( + """ +
+
+
+ {} box art +
+
+
+

+ {} +

+
+ +
+ {} +
+
+
+
+ """, + twitch_id, + box_art_url, + name, + slug, + name, + render_campaigns(drop_campaigns), + ) + + +def render_campaigns(campaigns: list[DropCampaign]) -> SafeText: + """Render the campaigns HTML. + + Args: + campaigns: The list of campaigns. + + Returns: + The rendered HTML string. + """ + campaign_html: str = "" + for campaign in campaigns: + if campaign.details_url == campaign.account_link_url: + link_html: SafeText = format_html( + 'Details', + campaign.details_url, + ) + else: + link_html: SafeText = format_html( + 'Details | Link Account', # noqa: E501 + campaign.details_url, + campaign.account_link_url, + ) + + time_until: str = timesince(campaign.ends_at, now()) if campaign.ends_at else "" + starts_at: str = campaign.starts_at.strftime("%A %d %B %H:%M") if campaign.starts_at else "" + ends_at: str = campaign.ends_at.strftime("%A %d %B %H:%M") if campaign.ends_at else "" + drops: list[TimeBasedDrop] = campaign.drops.all() # type: ignore # noqa: PGH003 + campaign_html += format_html( + """ +
+ {} +

Ends in: {}

+
+ {} +
+
+ """, + link_html, + starts_at, + ends_at, + time_until, + render_drops(drops), + ) + + return format_html(campaign_html) + + +def render_drops(drops: list[TimeBasedDrop]) -> SafeText: + """Render the drops HTML. + + Args: + drops: The list of drops. + + Returns: + The rendered HTML string. + """ + drop_html: str = "" + for drop in drops: + benefits: list[Benefit] = drop.benefits.all() # type: ignore # noqa: PGH003 + for benefit in benefits: + image_url: str = benefit.image_url or "https://static-cdn.jtvnw.net/ttv-static/404_boxart.jpg" + name = benefit.name or "Drop name unknown" + drop_html += format_html( + """ +
+ {} drop image + {} +
+ """, + image_url, + name, + name, + ) + return format_html(drop_html)