Start subscription

This commit is contained in:
2024-08-15 03:18:29 +02:00
parent 7d6333183d
commit d4d8567ef8
11 changed files with 518 additions and 245 deletions

View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models.twitch import Benefit, Channel, DropCampaign, Game, Owner, Reward, RewardCampaign, TimeBasedDrop from .models import Benefit, Channel, DropCampaign, Game, Owner, Reward, RewardCampaign, TimeBasedDrop
admin.site.register(Game) admin.site.register(Game)
admin.site.register(Owner) admin.site.register(Owner)

View File

@ -10,7 +10,7 @@ from platformdirs import user_data_dir
from playwright.async_api import Playwright, async_playwright from playwright.async_api import Playwright, async_playwright
from playwright.async_api._generated import Response from playwright.async_api._generated import Response
from core.models.twitch import Benefit, Channel, DropCampaign, Game, Owner, Reward, RewardCampaign, TimeBasedDrop from core.models import Benefit, Channel, DropCampaign, Game, Owner, Reward, RewardCampaign, TimeBasedDrop
if TYPE_CHECKING: if TYPE_CHECKING:
from playwright.async_api._generated import BrowserContext, Page from playwright.async_api._generated import BrowserContext, Page
@ -74,6 +74,7 @@ async def add_reward_campaign(campaign: dict | None) -> None: # noqa: C901
Args: Args:
campaign (dict): The reward campaign to add. campaign (dict): The reward campaign to add.
""" """
# sourcery skip: low-code-quality
if not campaign: if not campaign:
return return

View File

@ -0,0 +1,322 @@
# Generated by Django 5.1 on 2024-08-15 00:28
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration):
initial = True
dependencies: list[tuple[str, str]] = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations: list[Operation] = [
migrations.CreateModel(
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)),
("status", models.TextField(null=True)),
],
),
migrations.CreateModel(
name="Game",
fields=[
("twitch_id", models.TextField(primary_key=True, serialize=False)),
("game_url", models.URLField(default="https://www.twitch.tv/", null=True)),
("name", models.TextField(default="Game name unknown", null=True)),
(
"box_art_url",
models.URLField(default="https://static-cdn.jtvnw.net/ttv-static/404_boxart.jpg", null=True),
),
("slug", models.TextField(null=True)),
],
),
migrations.CreateModel(
name="Owner",
fields=[
("id", models.TextField(primary_key=True, serialize=False)),
("name", models.TextField(null=True)),
],
),
migrations.CreateModel(
name="User",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("password", models.CharField(max_length=128, verbose_name="password")),
("last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login")),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={"unique": "A user with that username already exists."},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
verbose_name="username",
),
),
("first_name", models.CharField(blank=True, max_length=150, verbose_name="first name")),
("last_name", models.CharField(blank=True, max_length=150, verbose_name="last name")),
("email", models.EmailField(blank=True, max_length=254, verbose_name="email address")),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", # noqa: E501
verbose_name="active",
),
),
("date_joined", models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined")),
("subscribe_to_news", models.BooleanField(default=False, help_text="Subscribe to news")),
("subscribe_to_new_games", models.BooleanField(default=False, help_text="Subscribe to new games")),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", # noqa: E501
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
],
options={
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name="DiscordWebhook",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("url", models.URLField()),
("name", models.CharField(max_length=255)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="discord_webhooks",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="Channel",
fields=[
("twitch_id", models.TextField(primary_key=True, serialize=False)),
("display_name", models.TextField(default="Channel name unknown", null=True)),
("name", models.TextField(null=True)),
("twitch_url", models.URLField(default="https://www.twitch.tv/", null=True)),
("live", models.BooleanField(default=False)),
("drop_campaigns", models.ManyToManyField(related_name="channels", to="core.dropcampaign")),
],
),
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.CreateModel(
name="GameSubscription",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("game", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="core.game")),
("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
("webhook", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="core.discordwebhook")),
],
options={
"unique_together": {("user", "game")},
},
),
migrations.AddField(
model_name="user",
name="subscribed_games",
field=models.ManyToManyField(
blank=True,
related_name="subscribed_users",
through="core.GameSubscription",
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="OwnerSubscription",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="core.owner")),
("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
("webhook", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="core.discordwebhook")),
],
options={
"unique_together": {("user", "owner")},
},
),
migrations.AddField(
model_name="user",
name="subscribed_owners",
field=models.ManyToManyField(
blank=True,
related_name="subscribed_users",
through="core.OwnerSubscription",
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",
),
),
],
),
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",
),
),
],
),
migrations.CreateModel(
name="TimeBasedDrop",
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)),
("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",
),
),
],
),
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(null=True)),
(
"time_based_drop",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="benefits",
to="core.timebaseddrop",
),
),
],
),
]

View File

@ -1,202 +0,0 @@
# Generated by Django 5.1 on 2024-08-12 23:16
import django.db.models.deletion
from django.db import migrations, models
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="Owner",
fields=[
("id", models.TextField(primary_key=True, serialize=False)),
("name", models.TextField(null=True)),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="Game",
fields=[
("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)),
(
"org",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="games",
to="core.owner",
),
),
],
options={
"abstract": False,
},
),
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,
},
),
migrations.CreateModel(
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)),
("status", models.TextField(null=True)),
(
"game",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="drop_campaigns",
to="core.game",
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="TimeBasedDrop",
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)),
("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,
},
),
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",
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(null=True)),
(
"time_based_drop",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="benefits",
to="core.timebaseddrop",
),
),
],
options={
"abstract": False,
},
),
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,
},
),
]

View File

@ -1,37 +0,0 @@
# Generated by Django 5.1 on 2024-08-13 18:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0001_squashed_0003_alter_benefit_options_alter_channel_options_and_more"),
]
operations = [
migrations.AlterField(
model_name="channel",
name="display_name",
field=models.TextField(default="Channel name unknown", null=True),
),
migrations.AlterField(
model_name="channel",
name="twitch_url",
field=models.URLField(default="https://www.twitch.tv/", null=True),
),
migrations.AlterField(
model_name="game",
name="box_art_url",
field=models.URLField(default="https://static-cdn.jtvnw.net/ttv-static/404_boxart.jpg", null=True),
),
migrations.AlterField(
model_name="game",
name="game_url",
field=models.URLField(default="https://www.twitch.tv/", null=True),
),
migrations.AlterField(
model_name="game",
name="name",
field=models.TextField(default="Game name unknown", null=True),
),
]

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import logging import logging
from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
logger: logging.Logger = logging.getLogger(__name__) logger: logging.Logger = logging.getLogger(__name__)
@ -179,3 +180,64 @@ class Reward(models.Model):
def __str__(self) -> str: def __str__(self) -> str:
return self.name or "Reward name unknown" return self.name or "Reward name unknown"
class User(AbstractUser):
"""Extended User model to include subscriptions."""
subscribed_games = models.ManyToManyField(
"Game",
through="GameSubscription",
related_name="subscribed_users",
blank=True,
)
subscribed_owners = models.ManyToManyField(
"Owner",
through="OwnerSubscription",
related_name="subscribed_users",
blank=True,
)
subscribe_to_news = models.BooleanField(default=False, help_text="Subscribe to news")
subscribe_to_new_games = models.BooleanField(default=False, help_text="Subscribe to new games")
def __str__(self) -> str:
return self.username
class DiscordWebhook(models.Model):
"""A Discord webhook for sending notifications."""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="discord_webhooks")
url = models.URLField()
name = models.CharField(max_length=255)
def __str__(self) -> str:
return f"{self.name} ({self.user.username})"
class GameSubscription(models.Model):
"""A subscription to a specific game with a chosen webhook."""
user = models.ForeignKey(User, on_delete=models.CASCADE)
game = models.ForeignKey(Game, on_delete=models.CASCADE)
webhook = models.ForeignKey(DiscordWebhook, on_delete=models.CASCADE)
class Meta:
unique_together = ("user", "game")
def __str__(self) -> str:
return f"{self.user.username} -> {self.game.name} via {self.webhook.name}"
class OwnerSubscription(models.Model):
"""A subscription to a specific owner with a chosen webhook."""
user = models.ForeignKey(User, on_delete=models.CASCADE)
owner = models.ForeignKey(Owner, on_delete=models.CASCADE)
webhook = models.ForeignKey(DiscordWebhook, on_delete=models.CASCADE)
class Meta:
unique_together = ("user", "owner")
def __str__(self) -> str:
return f"{self.user.username} -> {self.owner.name} via {self.webhook.name}"

View File

@ -43,7 +43,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"] STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"]
STATIC_ROOT: Path = BASE_DIR / "staticfiles" STATIC_ROOT: Path = BASE_DIR / "staticfiles"
STATIC_ROOT.mkdir(exist_ok=True) STATIC_ROOT.mkdir(exist_ok=True)
AUTH_USER_MODEL = "core.User"
if DEBUG: if DEBUG:
INTERNAL_IPS: list[str] = ["127.0.0.1"] INTERNAL_IPS: list[str] = ["127.0.0.1"]

125
core/signals.py Normal file
View File

@ -0,0 +1,125 @@
import logging
from typing import TYPE_CHECKING
from discord_webhook import DiscordWebhook
from django.db.models.manager import BaseManager
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from core.models import DropCampaign, Game, GameSubscription, Owner, OwnerSubscription
if TYPE_CHECKING:
import requests
from django.db.models.manager import BaseManager
logger: logging.Logger = logging.getLogger(__name__)
def convert_time_to_discord_timestamp(time: timezone.datetime | None) -> str:
"""Discord uses <t:UNIX_TIMESTAMP:R> for timestamps.
Args:
time: The time to convert to a Discord timestamp.
Returns:
str: The Discord timestamp string. If time is None, returns "Unknown".
"""
return f"<t:{int(time.timestamp())}:R>" if time else "Unknown"
@receiver(signal=post_save, sender=DropCampaign)
def notify_users_of_new_drop(sender: DropCampaign, instance: DropCampaign, created: bool, **kwargs) -> None: # noqa: ANN003, ARG001, FBT001
"""Notify users of a new drop campaign.
Args:
sender (DropCampaign): The model we are sending the signal from.
instance (DropCampaign): The instance of the model that was created.
created (bool): Whether the instance was created or updated.
**kwargs: Additional keyword arguments.
"""
if not created:
logger.debug("Drop campaign '%s' was updated.", instance.name)
return
game: Game | None = instance.game
if not game:
return
owner: Owner = game.owner # type: ignore # noqa: PGH003
# Notify users subscribed to the game
game_subs: BaseManager[GameSubscription] = GameSubscription.objects.filter(game=game)
for sub in game_subs:
if not sub.webhook.url:
logger.error("No webhook URL provided.")
return
webhook = DiscordWebhook(
url=sub.webhook.url,
content=generate_game_message(instance=instance, game=game, sub=sub),
username=f"{game.name} drop.",
rate_limit_retry=True,
)
response: requests.Response = webhook.execute()
logger.debug(response)
# Notify users subscribed to the owner
owner_subs: BaseManager[OwnerSubscription] = OwnerSubscription.objects.filter(owner=owner)
for sub in owner_subs:
if not sub.webhook.url:
logger.error("No webhook URL provided.")
return
webhook = DiscordWebhook(
url=sub.webhook.url,
content=generate_owner_message(instance=instance, owner=owner, sub=sub),
username=f"{owner.name} drop.",
rate_limit_retry=True,
)
response: requests.Response = webhook.execute()
logger.debug(response)
def generate_game_message(instance: DropCampaign, game: Game, sub: GameSubscription) -> str:
"""Generate a message for a drop campaign.
Args:
instance (DropCampaign): Drop campaign instance.
game (Game): Game instance.
sub (GameSubscription): Game subscription instance.
Returns:
str: The message to send to Discord.
"""
game_name: str = game.name or "Unknown"
description: str = instance.description or "No description provided."
start_at: str = convert_time_to_discord_timestamp(instance.starts_at)
end_at: str = convert_time_to_discord_timestamp(instance.ends_at)
msg: str = f"{game_name}: {instance.name}\n{description}\nStarts: {start_at}\nEnds: {end_at}"
logger.info("Discord message: '%s' to '%s'", msg, sub.webhook.url)
return msg
def generate_owner_message(instance: DropCampaign, owner: Owner, sub: OwnerSubscription) -> str:
"""Generate a message for a drop campaign.
Args:
instance (DropCampaign): Drop campaign instance.
owner (Owner): Owner instance.
sub (OwnerSubscription): Owner subscription instance.
Returns:
str: The message to send to Discord.
"""
owner_name: str = owner.name or "Unknown"
description: str = instance.description or "No description provided."
start_at: str = convert_time_to_discord_timestamp(instance.starts_at)
end_at: str = convert_time_to_discord_timestamp(instance.ends_at)
msg: str = f"{owner_name}: {instance.name}\n{description}\nStarts: {start_at}\nEnds: {end_at}"
logger.info("Discord message: '%s' to '%s'", msg, sub.webhook.url)
return msg

View File

@ -7,7 +7,10 @@
<div class="col-lg-9"> <div class="col-lg-9">
{% include "partials/info_box.html" %} {% include "partials/info_box.html" %}
{% include "partials/news.html" %} {% include "partials/news.html" %}
<h2>Reward Campaigns</h2> <h2>
Reward campaigns -
<div class="d-inline text-muted">{{ reward_campaigns.count }} campaigns</div>
</h2>
{% for campaign in reward_campaigns %} {% for campaign in reward_campaigns %}
{% include "partials/reward_campaign_card.html" %} {% include "partials/reward_campaign_card.html" %}
{% endfor %} {% endfor %}
@ -16,7 +19,6 @@
<div class="d-inline text-muted ">{{ games.count }} games</div> <div class="d-inline text-muted ">{{ games.count }} games</div>
</h2> </h2>
{% for game in games %} {% for game in games %}
{# Only show games with drop campaigns #}
{% if game.drop_campaigns.count > 0 %} {% if game.drop_campaigns.count > 0 %}
{% include "partials/game_card.html" %} {% include "partials/game_card.html" %}
{% endif %} {% endif %}

View File

@ -9,7 +9,7 @@ from django.db.models.manager import BaseManager
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone # type: ignore # noqa: PGH003 from django.utils import timezone # type: ignore # noqa: PGH003
from core.models.twitch import DropCampaign, Game, RewardCampaign from core.models import DropCampaign, Game, RewardCampaign
if TYPE_CHECKING: if TYPE_CHECKING:
from django.db.models.manager import BaseManager from django.db.models.manager import BaseManager