Save channels to the database

This commit is contained in:
Joakim Hellsén 2025-09-08 22:18:32 +02:00
commit 48783fadc2
5 changed files with 162 additions and 5 deletions

View file

@ -85,6 +85,34 @@ Hover over the end time to see the exact date and time.
text-align: left"> text-align: left">
Duration: {{ campaign.start_at|timesince:campaign.end_at }} Duration: {{ campaign.start_at|timesince:campaign.end_at }}
</time> </time>
{% if campaign.allow_channels.all %}
<div style="margin-top: 0.5rem; font-size: 0.8rem; color: #444;">
<strong>Channels:</strong>
<ul style="margin: 0.25rem 0 0 0;
padding-left: 1rem;
list-style-type: none">
{% for channel in campaign.allow_channels.all %}
{% if forloop.counter <= 5 %}
<li style="margin-bottom: 0.1rem;">
<a href="https://twitch.tv/{{ channel.name }}"
target="_blank"
rel="noopener noreferrer"
style="color: #9146ff;
text-decoration: none"
title="Watch {{ channel.display_name }} on Twitch">
{{ channel.display_name }}
</a>
</li>
{% endif %}
{% endfor %}
{% if campaign.allow_channels.all|length > 5 %}
<li style="margin-bottom: 0.1rem; color: #666; font-style: italic;">
... and {{ campaign.allow_channels.all|length|add:"-5" }} more
</li>
{% endif %}
</ul>
</div>
{% endif %}
</div> </div>
</article> </article>
{% endfor %} {% endfor %}

View file

@ -12,7 +12,7 @@ from django.core.management.base import BaseCommand, CommandError, CommandParser
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
from twitch.models import DropBenefit, DropBenefitEdge, DropCampaign, Game, Organization, TimeBasedDrop from twitch.models import Channel, DropBenefit, DropBenefitEdge, DropCampaign, Game, Organization, TimeBasedDrop
if TYPE_CHECKING: if TYPE_CHECKING:
from datetime import datetime from datetime import datetime
@ -97,10 +97,15 @@ class Command(BaseCommand):
for p in paths: for p in paths:
try: try:
path: Path = Path(p) path: Path = Path(p)
processed_path: Path = path / processed_dir
processed_path.mkdir(exist_ok=True)
self.validate_path(path) self.validate_path(path)
# For files, use the parent directory for processed files
if path.is_file():
processed_path: Path = path.parent / processed_dir
else:
processed_path: Path = path / processed_dir
processed_path.mkdir(exist_ok=True)
self.process_drops(continue_on_error=continue_on_error, path=path, processed_path=processed_path) self.process_drops(continue_on_error=continue_on_error, path=path, processed_path=processed_path)
except CommandError as e: except CommandError as e:
@ -528,6 +533,10 @@ class Command(BaseCommand):
Returns: Returns:
Returns the DropCampaign object. Returns the DropCampaign object.
""" """
# Extract allow data from campaign_data
allow_data = campaign_data.get("allow", {})
allow_is_enabled = allow_data.get("isEnabled")
drop_campaign_defaults: dict[str, Any] = { drop_campaign_defaults: dict[str, Any] = {
"game": game, "game": game,
"name": campaign_data.get("name"), "name": campaign_data.get("name"),
@ -538,6 +547,7 @@ class Command(BaseCommand):
"start_at": parse_date(campaign_data.get("startAt") or campaign_data.get("startsAt")), "start_at": parse_date(campaign_data.get("startAt") or campaign_data.get("startsAt")),
"end_at": parse_date(campaign_data.get("endAt") or campaign_data.get("endsAt")), "end_at": parse_date(campaign_data.get("endAt") or campaign_data.get("endsAt")),
"is_account_connected": campaign_data.get("self", {}).get("isAccountConnected"), "is_account_connected": campaign_data.get("self", {}).get("isAccountConnected"),
"allow_is_enabled": allow_is_enabled,
} }
# Run .strip() on all string fields to remove leading/trailing whitespace # Run .strip() on all string fields to remove leading/trailing whitespace
for key, value in drop_campaign_defaults.items(): for key, value in drop_campaign_defaults.items():
@ -551,6 +561,33 @@ class Command(BaseCommand):
id=campaign_data["id"], id=campaign_data["id"],
defaults=drop_campaign_defaults, defaults=drop_campaign_defaults,
) )
# Handle allow_channels (many-to-many relationship)
allow_channels: list[dict[str, str]] = allow_data.get("channels", [])
if allow_channels:
channel_objects: list[Channel] = []
for channel_data in allow_channels:
channel_defaults: dict[str, str | None] = {
"name": channel_data.get("name"),
"display_name": channel_data.get("displayName"),
}
# Run .strip() on all string fields to remove leading/trailing whitespace
for key, value in channel_defaults.items():
if isinstance(value, str):
channel_defaults[key] = value.strip()
# Filter out None values
channel_defaults = {k: v for k, v in channel_defaults.items() if v is not None}
channel, _ = Channel.objects.update_or_create(
id=channel_data["id"],
defaults=channel_defaults,
)
channel_objects.append(channel)
# Set the many-to-many relationship
drop_campaign.allow_channels.set(channel_objects)
if created: if created:
self.stdout.write(self.style.SUCCESS(f"Created new drop campaign: {drop_campaign.name} (ID: {drop_campaign.id})")) self.stdout.write(self.style.SUCCESS(f"Created new drop campaign: {drop_campaign.name} (ID: {drop_campaign.id})"))
return drop_campaign return drop_campaign

View file

@ -0,0 +1,37 @@
# Generated by Django 5.2.5 on 2025-09-08 17:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('twitch', '0012_dropbenefit_search_vector_game_search_vector_and_more'),
]
operations = [
migrations.AddField(
model_name='dropcampaign',
name='allow_is_enabled',
field=models.BooleanField(default=True, help_text='Whether the campaign allows participation.'),
),
migrations.CreateModel(
name='Channel',
fields=[
('id', models.CharField(help_text='The unique Twitch identifier for the channel.', max_length=255, primary_key=True, serialize=False, verbose_name='Channel ID')),
('name', models.CharField(db_index=True, help_text='The lowercase username of the channel.', max_length=255, verbose_name='Username')),
('display_name', models.CharField(db_index=True, help_text='The display name of the channel (with proper capitalization).', max_length=255, verbose_name='Display Name')),
('added_at', models.DateTimeField(auto_now_add=True, db_index=True, help_text='Timestamp when this channel record was created.')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Timestamp when this channel record was last updated.')),
],
options={
'ordering': ['display_name'],
'indexes': [models.Index(fields=['name'], name='twitch_chan_name_15d566_idx'), models.Index(fields=['display_name'], name='twitch_chan_display_2bf213_idx')],
},
),
migrations.AddField(
model_name='dropcampaign',
name='allow_channels',
field=models.ManyToManyField(blank=True, help_text='Channels that are allowed to participate in this campaign.', related_name='allowed_campaigns', to='twitch.channel'),
),
]

View file

@ -173,6 +173,50 @@ class Game(models.Model):
return self.id return self.id
class Channel(models.Model):
"""Represents a Twitch channel that can participate in drop campaigns."""
id = models.CharField(
max_length=255,
primary_key=True,
verbose_name="Channel ID",
help_text="The unique Twitch identifier for the channel.",
)
name = models.CharField(
max_length=255,
db_index=True,
verbose_name="Username",
help_text="The lowercase username of the channel.",
)
display_name = models.CharField(
max_length=255,
db_index=True,
verbose_name="Display Name",
help_text="The display name of the channel (with proper capitalization).",
)
added_at = models.DateTimeField(
auto_now_add=True,
db_index=True,
help_text="Timestamp when this channel record was created.",
)
updated_at = models.DateTimeField(
auto_now=True,
help_text="Timestamp when this channel record was last updated.",
)
class Meta:
ordering = ["display_name"]
indexes: ClassVar[list] = [
models.Index(fields=["name"]),
models.Index(fields=["display_name"]),
]
def __str__(self) -> str:
"""Return a string representation of the channel."""
return self.display_name or self.name or self.id
class DropCampaign(models.Model): class DropCampaign(models.Model):
"""Represents a Twitch drop campaign.""" """Represents a Twitch drop campaign."""
@ -224,6 +268,16 @@ class DropCampaign(models.Model):
default=False, default=False,
help_text="Indicates if the user account is linked.", help_text="Indicates if the user account is linked.",
) )
allow_is_enabled = models.BooleanField(
default=True,
help_text="Whether the campaign allows participation.",
)
allow_channels = models.ManyToManyField(
Channel,
blank=True,
related_name="allowed_campaigns",
help_text="Channels that are allowed to participate in this campaign.",
)
# PostgreSQL full-text search field # PostgreSQL full-text search field
search_vector = SearchVectorField(null=True, blank=True) search_vector = SearchVectorField(null=True, blank=True)

View file

@ -532,7 +532,8 @@ def dashboard(request: HttpRequest) -> HttpResponse:
Prefetch( Prefetch(
"time_based_drops", "time_based_drops",
queryset=TimeBasedDrop.objects.prefetch_related("benefits"), queryset=TimeBasedDrop.objects.prefetch_related("benefits"),
) ),
"allow_channels",
) )
) )