From 48783fadc2ba1aedc41f11c1db486bac09925965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Mon, 8 Sep 2025 22:18:32 +0200 Subject: [PATCH] Save channels to the database --- templates/twitch/dashboard.html | 28 ++++++++++ twitch/management/commands/import_drops.py | 45 ++++++++++++++-- ...paign_allow_is_enabled_channel_and_more.py | 37 +++++++++++++ twitch/models.py | 54 +++++++++++++++++++ twitch/views.py | 3 +- 5 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 twitch/migrations/0013_dropcampaign_allow_is_enabled_channel_and_more.py diff --git a/templates/twitch/dashboard.html b/templates/twitch/dashboard.html index 6219c3b..f7ca59c 100644 --- a/templates/twitch/dashboard.html +++ b/templates/twitch/dashboard.html @@ -85,6 +85,34 @@ Hover over the end time to see the exact date and time. text-align: left"> Duration: {{ campaign.start_at|timesince:campaign.end_at }} + {% if campaign.allow_channels.all %} +
+ Channels: + +
+ {% endif %} {% endfor %} diff --git a/twitch/management/commands/import_drops.py b/twitch/management/commands/import_drops.py index 939d3b5..d86cacd 100644 --- a/twitch/management/commands/import_drops.py +++ b/twitch/management/commands/import_drops.py @@ -12,7 +12,7 @@ from django.core.management.base import BaseCommand, CommandError, CommandParser from django.db import transaction 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: from datetime import datetime @@ -97,10 +97,15 @@ class Command(BaseCommand): for p in paths: try: path: Path = Path(p) - processed_path: Path = path / processed_dir - processed_path.mkdir(exist_ok=True) - 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) except CommandError as e: @@ -528,6 +533,10 @@ class Command(BaseCommand): Returns: 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] = { "game": game, "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")), "end_at": parse_date(campaign_data.get("endAt") or campaign_data.get("endsAt")), "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 for key, value in drop_campaign_defaults.items(): @@ -551,6 +561,33 @@ class Command(BaseCommand): id=campaign_data["id"], 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: self.stdout.write(self.style.SUCCESS(f"Created new drop campaign: {drop_campaign.name} (ID: {drop_campaign.id})")) return drop_campaign diff --git a/twitch/migrations/0013_dropcampaign_allow_is_enabled_channel_and_more.py b/twitch/migrations/0013_dropcampaign_allow_is_enabled_channel_and_more.py new file mode 100644 index 0000000..aeb5d59 --- /dev/null +++ b/twitch/migrations/0013_dropcampaign_allow_is_enabled_channel_and_more.py @@ -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'), + ), + ] diff --git a/twitch/models.py b/twitch/models.py index 47cf818..153e578 100644 --- a/twitch/models.py +++ b/twitch/models.py @@ -173,6 +173,50 @@ class Game(models.Model): 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): """Represents a Twitch drop campaign.""" @@ -224,6 +268,16 @@ class DropCampaign(models.Model): default=False, 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 search_vector = SearchVectorField(null=True, blank=True) diff --git a/twitch/views.py b/twitch/views.py index 14abb08..492b97a 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -532,7 +532,8 @@ def dashboard(request: HttpRequest) -> HttpResponse: Prefetch( "time_based_drops", queryset=TimeBasedDrop.objects.prefetch_related("benefits"), - ) + ), + "allow_channels", ) )