Remove bloat
This commit is contained in:
parent
011c617328
commit
715cbf4bf0
51 changed files with 691 additions and 3032 deletions
213
twitch/models.py
213
twitch/models.py
|
|
@ -1,18 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
import auto_prefetch
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVectorField
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from accounts.models import User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
|
||||
|
|
@ -20,17 +13,15 @@ logger: logging.Logger = logging.getLogger("ttvdrops")
|
|||
|
||||
|
||||
# MARK: Organization
|
||||
class Organization(auto_prefetch.Model):
|
||||
class Organization(models.Model):
|
||||
"""Represents an organization on Twitch that can own drop campaigns."""
|
||||
|
||||
id = models.CharField(
|
||||
max_length=255,
|
||||
id = models.TextField(
|
||||
primary_key=True,
|
||||
verbose_name="Organization ID",
|
||||
help_text="The unique Twitch identifier for the organization.",
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
name = models.TextField(
|
||||
db_index=True,
|
||||
unique=True,
|
||||
verbose_name="Name",
|
||||
|
|
@ -47,16 +38,10 @@ class Organization(auto_prefetch.Model):
|
|||
help_text="Timestamp when this organization record was last updated.",
|
||||
)
|
||||
|
||||
# PostgreSQL full-text search field
|
||||
search_vector = SearchVectorField(null=True, blank=True)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
indexes: ClassVar[list] = [
|
||||
# Regular B-tree index for name lookups
|
||||
models.Index(fields=["name"]),
|
||||
# Full-text search index (GIN works with SearchVectorField)
|
||||
GinIndex(fields=["search_vector"], name="org_search_vector_idx"),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
@ -65,11 +50,11 @@ class Organization(auto_prefetch.Model):
|
|||
|
||||
|
||||
# MARK: Game
|
||||
class Game(auto_prefetch.Model):
|
||||
class Game(models.Model):
|
||||
"""Represents a game on Twitch."""
|
||||
|
||||
id = models.CharField(max_length=255, primary_key=True, verbose_name="Game ID")
|
||||
slug = models.CharField(
|
||||
id = models.TextField(primary_key=True, verbose_name="Game ID")
|
||||
slug = models.TextField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
default="",
|
||||
|
|
@ -77,15 +62,13 @@ class Game(auto_prefetch.Model):
|
|||
verbose_name="Slug",
|
||||
help_text="Short unique identifier for the game.",
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
name = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
db_index=True,
|
||||
verbose_name="Name",
|
||||
)
|
||||
display_name = models.CharField(
|
||||
max_length=255,
|
||||
display_name = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
db_index=True,
|
||||
|
|
@ -97,7 +80,7 @@ class Game(auto_prefetch.Model):
|
|||
default="",
|
||||
verbose_name="Box art URL",
|
||||
)
|
||||
# Locally cached image file for the game's box art
|
||||
|
||||
box_art_file = models.FileField(
|
||||
upload_to="games/box_art/",
|
||||
blank=True,
|
||||
|
|
@ -105,10 +88,7 @@ class Game(auto_prefetch.Model):
|
|||
help_text="Locally cached box art image served from this site.",
|
||||
)
|
||||
|
||||
# PostgreSQL full-text search field
|
||||
search_vector = SearchVectorField(null=True, blank=True)
|
||||
|
||||
owner = auto_prefetch.ForeignKey(
|
||||
owner = models.ForeignKey(
|
||||
Organization,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="games",
|
||||
|
|
@ -128,16 +108,12 @@ class Game(auto_prefetch.Model):
|
|||
help_text="Timestamp when this game record was last updated.",
|
||||
)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
class Meta:
|
||||
ordering = ["display_name"]
|
||||
indexes: ClassVar[list] = [
|
||||
models.Index(fields=["slug"]),
|
||||
# Regular B-tree indexes for name lookups
|
||||
models.Index(fields=["display_name"]),
|
||||
models.Index(fields=["name"]),
|
||||
# Full-text search index (GIN works with SearchVectorField)
|
||||
GinIndex(fields=["search_vector"], name="game_search_vector_idx"),
|
||||
# Partial index for games with owners only
|
||||
models.Index(fields=["slug"]),
|
||||
models.Index(fields=["owner"], condition=models.Q(owner__isnull=False), name="game_owner_partial_idx"),
|
||||
]
|
||||
|
||||
|
|
@ -157,36 +133,6 @@ class Game(auto_prefetch.Model):
|
|||
"""Return all organizations that own games with campaigns for this game."""
|
||||
return Organization.objects.filter(games__drop_campaigns__game=self).distinct()
|
||||
|
||||
@property
|
||||
def box_art_base_url(self) -> str:
|
||||
"""Return the base box art URL without Twitch size suffixes."""
|
||||
if not self.box_art:
|
||||
return ""
|
||||
parts = urlsplit(self.box_art)
|
||||
path = re.sub(
|
||||
r"(-\d+x\d+)(\.(?:jpg|jpeg|png|gif|webp))$",
|
||||
r"\2",
|
||||
parts.path,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
return urlunsplit((parts.scheme, parts.netloc, path, "", ""))
|
||||
|
||||
@property
|
||||
def box_art_best_url(self) -> str:
|
||||
"""Return the best available URL for the game's box art.
|
||||
|
||||
Preference order:
|
||||
1) Local cached file (MEDIA)
|
||||
2) Remote Twitch base URL
|
||||
3) Empty string
|
||||
"""
|
||||
try:
|
||||
if self.box_art_file and getattr(self.box_art_file, "url", None):
|
||||
return self.box_art_file.url
|
||||
except (AttributeError, OSError, ValueError) as exc: # storage might not be configured in some contexts
|
||||
logger.debug("Failed to resolve Game.box_art_file url: %s", exc)
|
||||
return self.box_art_base_url
|
||||
|
||||
@property
|
||||
def get_game_name(self) -> str:
|
||||
"""Return the best available name for the game."""
|
||||
|
|
@ -207,7 +153,7 @@ class Game(auto_prefetch.Model):
|
|||
|
||||
|
||||
# MARK: TwitchGame
|
||||
class TwitchGameData(auto_prefetch.Model):
|
||||
class TwitchGameData(models.Model):
|
||||
"""Represents game metadata returned from the Twitch API.
|
||||
|
||||
This mirrors the public Twitch API fields for a game and is tied to the local `Game` model where possible.
|
||||
|
|
@ -220,8 +166,8 @@ class TwitchGameData(auto_prefetch.Model):
|
|||
igdb_id: Optional IGDB id for the game
|
||||
"""
|
||||
|
||||
id = models.CharField(max_length=255, primary_key=True, verbose_name="Twitch Game ID")
|
||||
game = auto_prefetch.ForeignKey(
|
||||
id = models.TextField(primary_key=True, verbose_name="Twitch Game ID")
|
||||
game = models.ForeignKey(
|
||||
Game,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="twitch_game_data",
|
||||
|
|
@ -231,7 +177,7 @@ class TwitchGameData(auto_prefetch.Model):
|
|||
help_text="Optional link to the local Game record for this Twitch game.",
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=255, blank=True, default="", db_index=True, verbose_name="Name")
|
||||
name = models.TextField(blank=True, default="", db_index=True, verbose_name="Name")
|
||||
box_art_url = models.URLField(
|
||||
max_length=500,
|
||||
blank=True,
|
||||
|
|
@ -239,39 +185,36 @@ class TwitchGameData(auto_prefetch.Model):
|
|||
verbose_name="Box art URL",
|
||||
help_text="URL template with {width}x{height} placeholders for the box art image.",
|
||||
)
|
||||
igdb_id = models.CharField(max_length=255, blank=True, default="", verbose_name="IGDB ID")
|
||||
igdb_id = models.TextField(blank=True, default="", verbose_name="IGDB ID")
|
||||
|
||||
added_at = models.DateTimeField(auto_now_add=True, db_index=True, help_text="Record creation time.")
|
||||
updated_at = models.DateTimeField(auto_now=True, help_text="Record last update time.")
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
indexes: ClassVar[list] = [
|
||||
models.Index(fields=["name"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str: # pragma: no cover - trivial
|
||||
def __str__(self) -> str:
|
||||
return self.name or self.id
|
||||
|
||||
|
||||
# MARK: Channel
|
||||
class Channel(auto_prefetch.Model):
|
||||
class Channel(models.Model):
|
||||
"""Represents a Twitch channel that can participate in drop campaigns."""
|
||||
|
||||
id = models.CharField(
|
||||
max_length=255,
|
||||
id = models.TextField(
|
||||
primary_key=True,
|
||||
verbose_name="Channel ID",
|
||||
help_text="The unique Twitch identifier for the channel.",
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
name = models.TextField(
|
||||
db_index=True,
|
||||
verbose_name="Username",
|
||||
help_text="The lowercase username of the channel.",
|
||||
)
|
||||
display_name = models.CharField(
|
||||
max_length=255,
|
||||
display_name = models.TextField(
|
||||
db_index=True,
|
||||
verbose_name="Display Name",
|
||||
help_text="The display name of the channel (with proper capitalization).",
|
||||
|
|
@ -287,7 +230,7 @@ class Channel(auto_prefetch.Model):
|
|||
help_text="Timestamp when this channel record was last updated.",
|
||||
)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
class Meta:
|
||||
ordering = ["display_name"]
|
||||
indexes: ClassVar[list] = [
|
||||
models.Index(fields=["name"]),
|
||||
|
|
@ -300,16 +243,14 @@ class Channel(auto_prefetch.Model):
|
|||
|
||||
|
||||
# MARK: DropCampaign
|
||||
class DropCampaign(auto_prefetch.Model):
|
||||
class DropCampaign(models.Model):
|
||||
"""Represents a Twitch drop campaign."""
|
||||
|
||||
id = models.CharField(
|
||||
max_length=255,
|
||||
id = models.TextField(
|
||||
primary_key=True,
|
||||
help_text="Unique Twitch identifier for the campaign.",
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
name = models.TextField(
|
||||
db_index=True,
|
||||
help_text="Name of the drop campaign.",
|
||||
)
|
||||
|
|
@ -335,7 +276,6 @@ class DropCampaign(auto_prefetch.Model):
|
|||
default="",
|
||||
help_text="URL to an image representing the campaign.",
|
||||
)
|
||||
# Locally cached campaign image
|
||||
image_file = models.FileField(
|
||||
upload_to="campaigns/images/",
|
||||
blank=True,
|
||||
|
|
@ -369,10 +309,7 @@ class DropCampaign(auto_prefetch.Model):
|
|||
help_text="Channels that are allowed to participate in this campaign.",
|
||||
)
|
||||
|
||||
# PostgreSQL full-text search field
|
||||
search_vector = SearchVectorField(null=True, blank=True)
|
||||
|
||||
game = auto_prefetch.ForeignKey(
|
||||
game = models.ForeignKey(
|
||||
Game,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="drop_campaigns",
|
||||
|
|
@ -390,7 +327,7 @@ class DropCampaign(auto_prefetch.Model):
|
|||
help_text="Timestamp when this campaign record was last updated.",
|
||||
)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
class Meta:
|
||||
ordering = ["-start_at"]
|
||||
constraints = [
|
||||
# Ensure end_at is after start_at when both are set
|
||||
|
|
@ -400,13 +337,8 @@ class DropCampaign(auto_prefetch.Model):
|
|||
),
|
||||
]
|
||||
indexes: ClassVar[list] = [
|
||||
# Regular B-tree index for campaign name lookups
|
||||
models.Index(fields=["name"]),
|
||||
# Full-text search index (GIN works with SearchVectorField)
|
||||
GinIndex(fields=["search_vector"], name="campaign_search_vector_idx"),
|
||||
# Composite index for time range queries
|
||||
models.Index(fields=["start_at", "end_at"]),
|
||||
# Partial index for active campaigns
|
||||
models.Index(fields=["start_at", "end_at"], condition=models.Q(start_at__isnull=False, end_at__isnull=False), name="campaign_active_partial_idx"),
|
||||
]
|
||||
|
||||
|
|
@ -462,16 +394,14 @@ class DropCampaign(auto_prefetch.Model):
|
|||
|
||||
|
||||
# MARK: DropBenefit
|
||||
class DropBenefit(auto_prefetch.Model):
|
||||
class DropBenefit(models.Model):
|
||||
"""Represents a benefit that can be earned from a drop."""
|
||||
|
||||
id = models.CharField(
|
||||
max_length=255,
|
||||
id = models.TextField(
|
||||
primary_key=True,
|
||||
help_text="Unique Twitch identifier for the benefit.",
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
name = models.TextField(
|
||||
db_index=True,
|
||||
blank=True,
|
||||
default="N/A",
|
||||
|
|
@ -483,7 +413,6 @@ class DropBenefit(auto_prefetch.Model):
|
|||
default="",
|
||||
help_text="URL to the benefit's image asset.",
|
||||
)
|
||||
# Locally cached benefit image
|
||||
image_file = models.FileField(
|
||||
upload_to="benefits/images/",
|
||||
blank=True,
|
||||
|
|
@ -505,7 +434,7 @@ class DropBenefit(auto_prefetch.Model):
|
|||
default=False,
|
||||
help_text="Whether the benefit is available on iOS.",
|
||||
)
|
||||
distribution_type = models.CharField(
|
||||
distribution_type = models.TextField(
|
||||
max_length=50,
|
||||
db_index=True,
|
||||
blank=True,
|
||||
|
|
@ -513,9 +442,6 @@ class DropBenefit(auto_prefetch.Model):
|
|||
help_text="Type of distribution for this benefit.",
|
||||
)
|
||||
|
||||
# PostgreSQL full-text search field
|
||||
search_vector = SearchVectorField(null=True, blank=True)
|
||||
|
||||
added_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
db_index=True,
|
||||
|
|
@ -526,16 +452,12 @@ class DropBenefit(auto_prefetch.Model):
|
|||
help_text="Timestamp when this benefit record was last updated.",
|
||||
)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
indexes: ClassVar[list] = [
|
||||
# Regular B-tree index for benefit name lookups
|
||||
models.Index(fields=["name"]),
|
||||
# Full-text search index (GIN works with SearchVectorField)
|
||||
GinIndex(fields=["search_vector"], name="benefit_search_vector_idx"),
|
||||
models.Index(fields=["created_at"]),
|
||||
models.Index(fields=["distribution_type"]),
|
||||
# Partial index for iOS available benefits
|
||||
models.Index(fields=["is_ios_available"], condition=models.Q(is_ios_available=True), name="benefit_ios_available_idx"),
|
||||
]
|
||||
|
||||
|
|
@ -543,28 +465,16 @@ class DropBenefit(auto_prefetch.Model):
|
|||
"""Return a string representation of the drop benefit."""
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def image_best_url(self) -> str:
|
||||
"""Return the best available URL for the benefit image (local first)."""
|
||||
try:
|
||||
if self.image_file and getattr(self.image_file, "url", None):
|
||||
return self.image_file.url
|
||||
except (AttributeError, OSError, ValueError) as exc:
|
||||
logger.debug("Failed to resolve DropBenefit.image_file url: %s", exc)
|
||||
return self.image_asset_url or ""
|
||||
|
||||
|
||||
# MARK: TimeBasedDrop
|
||||
class TimeBasedDrop(auto_prefetch.Model):
|
||||
class TimeBasedDrop(models.Model):
|
||||
"""Represents a time-based drop in a drop campaign."""
|
||||
|
||||
id = models.CharField(
|
||||
max_length=255,
|
||||
id = models.TextField(
|
||||
primary_key=True,
|
||||
help_text="Unique Twitch identifier for the time-based drop.",
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
name = models.TextField(
|
||||
db_index=True,
|
||||
help_text="Name of the time-based drop.",
|
||||
)
|
||||
|
|
@ -591,11 +501,8 @@ class TimeBasedDrop(auto_prefetch.Model):
|
|||
help_text="Datetime when this drop expires.",
|
||||
)
|
||||
|
||||
# PostgreSQL full-text search field
|
||||
search_vector = SearchVectorField(null=True, blank=True)
|
||||
|
||||
# Foreign keys
|
||||
campaign = auto_prefetch.ForeignKey(
|
||||
campaign = models.ForeignKey(
|
||||
DropCampaign,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="time_based_drops",
|
||||
|
|
@ -618,7 +525,7 @@ class TimeBasedDrop(auto_prefetch.Model):
|
|||
help_text="Timestamp when this time-based drop record was last updated.",
|
||||
)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
class Meta:
|
||||
ordering = ["start_at"]
|
||||
constraints = [
|
||||
# Ensure end_at is after start_at when both are set
|
||||
|
|
@ -632,13 +539,9 @@ class TimeBasedDrop(auto_prefetch.Model):
|
|||
),
|
||||
]
|
||||
indexes: ClassVar[list] = [
|
||||
# Regular B-tree index for drop name lookups
|
||||
models.Index(fields=["name"]),
|
||||
# Full-text search index (GIN works with SearchVectorField)
|
||||
GinIndex(fields=["search_vector"], name="drop_search_vector_idx"),
|
||||
models.Index(fields=["start_at", "end_at"]),
|
||||
models.Index(fields=["required_minutes_watched"]),
|
||||
# Covering index for common queries (includes campaign_id from FK)
|
||||
models.Index(fields=["campaign", "start_at", "required_minutes_watched"]),
|
||||
]
|
||||
|
||||
|
|
@ -648,15 +551,15 @@ class TimeBasedDrop(auto_prefetch.Model):
|
|||
|
||||
|
||||
# MARK: DropBenefitEdge
|
||||
class DropBenefitEdge(auto_prefetch.Model):
|
||||
class DropBenefitEdge(models.Model):
|
||||
"""Represents the relationship between a TimeBasedDrop and a DropBenefit."""
|
||||
|
||||
drop = auto_prefetch.ForeignKey(
|
||||
drop = models.ForeignKey(
|
||||
TimeBasedDrop,
|
||||
on_delete=models.CASCADE,
|
||||
help_text="The time-based drop in this relationship.",
|
||||
)
|
||||
benefit = auto_prefetch.ForeignKey(
|
||||
benefit = models.ForeignKey(
|
||||
DropBenefit,
|
||||
on_delete=models.CASCADE,
|
||||
help_text="The benefit in this relationship.",
|
||||
|
|
@ -676,7 +579,7 @@ class DropBenefitEdge(auto_prefetch.Model):
|
|||
help_text="Timestamp when this drop-benefit edge was last updated.",
|
||||
)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=("drop", "benefit"), name="unique_drop_benefit"),
|
||||
]
|
||||
|
|
@ -687,31 +590,3 @@ class DropBenefitEdge(auto_prefetch.Model):
|
|||
def __str__(self) -> str:
|
||||
"""Return a string representation of the drop benefit edge."""
|
||||
return f"{self.drop.name} - {self.benefit.name}"
|
||||
|
||||
|
||||
# MARK: NotificationSubscription
|
||||
class NotificationSubscription(auto_prefetch.Model):
|
||||
"""Users can subscribe to games to get notified."""
|
||||
|
||||
user = auto_prefetch.ForeignKey(User, on_delete=models.CASCADE)
|
||||
game = auto_prefetch.ForeignKey(Game, null=True, blank=True, on_delete=models.CASCADE)
|
||||
organization = auto_prefetch.ForeignKey(Organization, null=True, blank=True, on_delete=models.CASCADE)
|
||||
|
||||
notify_found = models.BooleanField(default=False)
|
||||
notify_live = models.BooleanField(default=False)
|
||||
|
||||
added_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
unique_together: ClassVar[list[tuple[str, str]]] = [
|
||||
("user", "game"),
|
||||
("user", "organization"),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.game:
|
||||
return f"{self.user} subscription to game: {self.game.display_name}"
|
||||
if self.organization:
|
||||
return f"{self.user} subscription to organization: {self.organization.name}"
|
||||
return f"{self.user} subscription"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue