import calendar from django.contrib.auth import get_user_model from django.db import models from django.db.models import Q from django.urls import reverse from django_extensions.db.models import TimeStampedModel User = get_user_model() BNULL = {"blank": True, "null": True} class ChartRecord(TimeStampedModel): """Stores rankings for a user/media/time period combination. Example entries: - Bjork was #3 artist in 2022 - Homogenic was #10 album in April 2025 - The Office was #1 TV series in Week #34 2024 """ user = models.ForeignKey(User, on_delete=models.CASCADE, **BNULL) year = models.IntegerField() month = models.IntegerField(**BNULL) week = models.IntegerField(**BNULL) day = models.IntegerField(**BNULL) artist = models.ForeignKey("music.Artist", on_delete=models.CASCADE, **BNULL) album = models.ForeignKey("music.Album", on_delete=models.CASCADE, **BNULL) track = models.ForeignKey("music.Track", on_delete=models.CASCADE, **BNULL) tv_series = models.ForeignKey("videos.Series", on_delete=models.CASCADE, **BNULL) video = models.ForeignKey("videos.Video", on_delete=models.CASCADE, **BNULL) podcast = models.ForeignKey("podcasts.Podcast", on_delete=models.CASCADE, **BNULL) podcast_episode = models.ForeignKey( "podcasts.PodcastEpisode", on_delete=models.CASCADE, **BNULL ) board_game = models.ForeignKey( "boardgames.BoardGame", on_delete=models.CASCADE, **BNULL ) trail = models.ForeignKey("trails.Trail", on_delete=models.CASCADE, **BNULL) geo_location = models.ForeignKey( "locations.GeoLocation", on_delete=models.CASCADE, **BNULL ) food = models.ForeignKey("foods.Food", on_delete=models.CASCADE, **BNULL) book = models.ForeignKey("books.Book", on_delete=models.CASCADE, **BNULL) rank = models.IntegerField() count = models.IntegerField(default=0) class Meta: indexes = [ models.Index(fields=["user", "year", "artist", "rank"]), models.Index(fields=["user", "year", "album", "rank"]), models.Index(fields=["user", "year", "track", "rank"]), models.Index(fields=["user", "year", "tv_series", "rank"]), models.Index(fields=["user", "year", "video", "rank"]), models.Index(fields=["user", "year", "podcast", "rank"]), models.Index(fields=["user", "year", "board_game", "rank"]), models.Index(fields=["user", "year", "trail", "rank"]), models.Index(fields=["user", "year", "geo_location", "rank"]), models.Index(fields=["user", "year", "food", "rank"]), models.Index(fields=["user", "year", "book", "rank"]), models.Index(fields=["user", "year", "month", "artist", "rank"]), models.Index(fields=["user", "year", "month", "album", "rank"]), models.Index(fields=["user", "year", "month", "track", "rank"]), models.Index(fields=["user", "year", "month", "tv_series", "rank"]), models.Index(fields=["user", "year", "month", "video", "rank"]), models.Index(fields=["user", "year", "month", "board_game", "rank"]), models.Index(fields=["user", "year", "month", "book", "rank"]), models.Index(fields=["user", "year", "month", "food", "rank"]), models.Index(fields=["user", "year", "month", "podcast", "rank"]), models.Index(fields=["user", "year", "month", "trail", "rank"]), models.Index(fields=["user", "year", "week", "artist", "rank"]), models.Index(fields=["user", "year", "week", "album", "rank"]), models.Index(fields=["user", "year", "week", "track", "rank"]), models.Index(fields=["user", "year", "week", "tv_series", "rank"]), models.Index(fields=["user", "year", "week", "video", "rank"]), models.Index(fields=["user", "year", "week", "board_game", "rank"]), models.Index(fields=["user", "year", "week", "book", "rank"]), models.Index(fields=["user", "year", "week", "food", "rank"]), models.Index(fields=["user", "year", "week", "podcast", "rank"]), models.Index(fields=["user", "year", "week", "trail", "rank"]), models.Index(fields=["user", "year", "month", "day", "artist", "rank"]), models.Index(fields=["user", "year", "month", "day", "album", "rank"]), models.Index(fields=["user", "year", "month", "day", "tv_series", "rank"]), ] constraints = [ models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "artist"], condition=Q(artist__isnull=False), name="unique_chart_artist_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "album"], condition=Q(album__isnull=False), name="unique_chart_album_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "track"], condition=Q(track__isnull=False), name="unique_chart_track_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "tv_series"], condition=Q(tv_series__isnull=False), name="unique_chart_tv_series_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "video"], condition=Q(video__isnull=False), name="unique_chart_video_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "podcast"], condition=Q(podcast__isnull=False), name="unique_chart_podcast_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "podcast_episode"], condition=Q(podcast_episode__isnull=False), name="unique_chart_podcast_episode_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "board_game"], condition=Q(board_game__isnull=False), name="unique_chart_board_game_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "trail"], condition=Q(trail__isnull=False), name="unique_chart_trail_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "geo_location"], condition=Q(geo_location__isnull=False), name="unique_chart_geo_location_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "food"], condition=Q(food__isnull=False), name="unique_chart_food_period", ), models.UniqueConstraint( fields=["user", "year", "month", "week", "day", "book"], condition=Q(book__isnull=False), name="unique_chart_book_period", ), ] @property def media_obj(self): for attr in [ "artist", "album", "track", "tv_series", "video", "podcast", "podcast_episode", "board_game", "trail", "geo_location", "food", "book", ]: obj = getattr(self, attr) if obj: return obj return None @property def media_type(self): mapping = { "artist": "Artist", "album": "Album", "track": "Track", "tv_series": "TV Series", "video": "Video", "podcast": "Podcast", "podcast_episode": "Podcast Episode", "board_game": "Board Game", "trail": "Trail", "geo_location": "Location", "food": "Food", "book": "Book", } for attr, name in mapping.items(): if getattr(self, attr) is not None: return name return None @property def period_str(self) -> str: if self.day: return f"{calendar.month_name[self.month]} {self.day}, {self.year}" if self.week: return f"Week {self.week}, {self.year}" if self.month: return f"{calendar.month_name[self.month]} {self.year}" return str(self.year) @property def period_type(self) -> str: if self.day: return "day" if self.week: return "week" if self.month: return "month" return "year" def __str__(self): obj = self.media_obj obj_name = str(obj) if obj else "Unknown" return f"#{self.rank} {obj_name} in {self.period_str}" def get_absolute_url(self): return reverse("charts:charts-home") @property def chart_url(self) -> str: url = reverse("charts:charts-home") date_str = str(self.year) if self.week: date_str = f"{self.year}-W{self.week}" elif self.month: date_str = f"{self.year}-{self.month:02d}" return f"{url}?date={date_str}" @property def rank_emoji(self) -> str: emojis = {1: "🥇", 2: "🥈", 3: "🥉"} return emojis.get(self.rank, f"#{self.rank}")