235 lines
9.5 KiB
Python
235 lines
9.5 KiB
Python
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}")
|