- Added user authentication UI in the base template for navbar. - Expanded `league.dj.html` to include a new "Draft Sessions" tab showing active drafts. - Refactored Django views and models to support `DraftSession` with participants and movies. - Replaced deprecated models like `DraftParticipant` and `DraftMoviePool` with a new schema using `DraftSessionParticipant`. - Introduced WebSocket consumers (`DraftAdminConsumer`, `DraftParticipantConsumer`) with structured phase logic and caching. - Added `DraftStateManager` for managing draft state in Django cache. - Created frontend UI components in React for draft admin and participants, including phase control and WebSocket message logging. - Updated SCSS styles for improved UI structure and messaging area.
366 lines
12 KiB
Python
366 lines
12 KiB
Python
from django.shortcuts import render, get_object_or_404
|
||
from django.contrib.auth import get_user_model
|
||
from django.http.response import Http404, HttpResponse
|
||
from django.urls import reverse
|
||
from django.db.models import OuterRef, Subquery, Sum, Q
|
||
from boxofficefantasy.models import League, Season, UserSeasonEntry, Movie, Pick
|
||
from draft.models import DraftSession
|
||
from .integrations.tmdb import get_tmdb_movie_by_imdb, cache_tmdb_poster
|
||
|
||
User = get_user_model()
|
||
|
||
|
||
def parse_season_slug(season_slug: str) -> tuple[str, str]:
|
||
try:
|
||
label, year = season_slug.rsplit("-", 1)
|
||
year = int(year)
|
||
return (label, year)
|
||
except ValueError:
|
||
raise Http404("Invalid season format.")
|
||
|
||
|
||
def get_scoreboard(user_season_entries=list[UserSeasonEntry]):
|
||
scoreboard = []
|
||
|
||
for season_entry in user_season_entries:
|
||
picks = Pick.objects.filter(season_entry=season_entry).select_related("movie")
|
||
total_score = 0
|
||
pick_data = []
|
||
|
||
for pick in picks:
|
||
movie = pick.movie
|
||
metrics = {m.key: m.value for m in movie.moviemetric_set.all()}
|
||
score = metrics.get("domestic_gross", 0)
|
||
total_score += score
|
||
pick_data.append(
|
||
{
|
||
"imdb_id": movie.imdb_id,
|
||
"movie": movie.title,
|
||
"score": score,
|
||
"bid": pick.bid_amount,
|
||
}
|
||
)
|
||
pick_data.sort(key=lambda e: e["score"], reverse=True)
|
||
scoreboard.append(
|
||
{
|
||
"team": season_entry.team_name,
|
||
"user": season_entry.user.username,
|
||
"total": total_score,
|
||
"picks": pick_data,
|
||
}
|
||
)
|
||
|
||
scoreboard.sort(key=lambda e: e["total"], reverse=True)
|
||
return scoreboard
|
||
|
||
|
||
# Create your views here.
|
||
def scoreboard_view(request, league_slug, season_slug):
|
||
# season_slug is something like "summer-2025"
|
||
|
||
season_label, season_year = parse_season_slug(season_slug)
|
||
|
||
league = get_object_or_404(League, slug=league_slug)
|
||
season = get_object_or_404(
|
||
Season, league=league, year=season_year, label__iexact=season_label
|
||
)
|
||
entries = UserSeasonEntry.objects.filter(season=season).select_related("user")
|
||
scoreboard = get_scoreboard(entries)
|
||
|
||
return render(
|
||
request,
|
||
"scoreboard.dj.html",
|
||
{
|
||
"league": league,
|
||
"season": season,
|
||
"scoreboard": scoreboard,
|
||
"breadcrumbs": [
|
||
{"label": league.name, "url": "#"},
|
||
{
|
||
"label": "seasons",
|
||
"url": reverse("league:seasons", args=[league.slug]),
|
||
},
|
||
{"label": f"{season.label} {season.year}", "url": "#"},
|
||
],
|
||
},
|
||
)
|
||
|
||
|
||
def team_view(request, league_slug=None, season_slug=None, username=None):
|
||
if not league_slug:
|
||
return HttpResponse(
|
||
"Team View: League not specified", content_type="text/plain"
|
||
)
|
||
|
||
league = get_object_or_404(League, slug=league_slug)
|
||
|
||
# 1️⃣ League only – all teams across all seasons in the league
|
||
if not season_slug:
|
||
entries = UserSeasonEntry.objects.filter(season__league=league).select_related(
|
||
"user", "season"
|
||
)
|
||
return render(
|
||
request,
|
||
"teams.dj.html",
|
||
{
|
||
"entries": entries,
|
||
"league": league,
|
||
},
|
||
)
|
||
|
||
# 2️⃣ League + Season – all teams in that season
|
||
season_label, season_year = parse_season_slug(season_slug)
|
||
season = get_object_or_404(
|
||
Season, league=league, year=season_year, label__iexact=season_label
|
||
)
|
||
|
||
if not username:
|
||
entries = UserSeasonEntry.objects.filter(season=season).select_related("user")
|
||
return render(
|
||
request,
|
||
"teams.dj.html",
|
||
{
|
||
"user_season_entries": [
|
||
{
|
||
"name": user_season_entry.user.get_full_name(),
|
||
"team_name": user_season_entry.team_name,
|
||
"username": user_season_entry.user.username,
|
||
}
|
||
for user_season_entry in entries
|
||
],
|
||
"league": {"name": league.name},
|
||
"season": {"label": season.label, "year": season.year},
|
||
},
|
||
)
|
||
|
||
# 3️⃣ League + Season + Username – one team and its picks
|
||
user = get_object_or_404(User, username=username)
|
||
entry = get_object_or_404(UserSeasonEntry, season=season, user=user)
|
||
|
||
picks = (
|
||
Pick.objects.filter(season_entry=entry)
|
||
.select_related("movie")
|
||
.prefetch_related("movie__moviemetric_set")
|
||
)
|
||
movie_data = []
|
||
for pick in picks:
|
||
metrics = {m.key: m.value for m in pick.movie.moviemetric_set.all()}
|
||
movie_data.append(
|
||
{
|
||
"movie": pick.movie,
|
||
"bid": pick.bid_amount,
|
||
"score": metrics.get("domestic_gross", 0),
|
||
}
|
||
)
|
||
|
||
return render(
|
||
request,
|
||
"team_detail.dj.html",
|
||
{
|
||
"entry": entry,
|
||
"picks": movie_data,
|
||
"league": league,
|
||
"season": season,
|
||
"user": user,
|
||
},
|
||
)
|
||
|
||
|
||
def movie_view(request, league_slug=None, season_slug=None, imdb_id=None):
|
||
if not league_slug:
|
||
return HttpResponse("Movie View: No league provided", content_type="text/plain")
|
||
|
||
# 1️⃣ League only — all movies across seasons
|
||
if league_slug and not season_slug and not imdb_id:
|
||
league = get_object_or_404(League, slug=league_slug)
|
||
picks = Pick.objects.filter(season__league=league)
|
||
movie_data = [
|
||
{
|
||
"title": pick.movie.title,
|
||
"score": (
|
||
pick.movie.moviemetric_set.filter(key="domestic_gross")
|
||
.first()
|
||
.value
|
||
if pick.movie
|
||
else 0
|
||
),
|
||
"bid": pick.bid_amount,
|
||
"team_name": pick.season_entry.team_name,
|
||
"user": pick.season_entry.user.username,
|
||
}
|
||
for pick in picks
|
||
]
|
||
return render(
|
||
request, "movies.dj.html", {"movies": movie_data, "league": league}
|
||
)
|
||
|
||
# 2️⃣ League + Season — all movies in that season
|
||
if league_slug and season_slug and not imdb_id:
|
||
season_label, season_year = parse_season_slug(season_slug)
|
||
league = get_object_or_404(League, slug=league_slug)
|
||
season = get_object_or_404(
|
||
Season, league=league, year=season_year, label__iexact=season_label
|
||
)
|
||
picks = season.pick_set.select_related(
|
||
"movie", "season_entry", "season_entry__user"
|
||
)
|
||
movie_data = [
|
||
{
|
||
"id": pick.movie.bom_legacy_id,
|
||
"title": pick.movie.title,
|
||
"score": (
|
||
pick.movie.moviemetric_set.filter(key="domestic_gross")
|
||
.first()
|
||
.value
|
||
if pick.movie
|
||
else 0
|
||
),
|
||
"bid": pick.bid_amount,
|
||
"team_name": pick.season_entry.team_name,
|
||
"user": pick.season_entry.user.username,
|
||
}
|
||
for pick in picks
|
||
]
|
||
return render(
|
||
request,
|
||
"movies.dj.html",
|
||
{"movies": movie_data, "season": season, "league": league},
|
||
)
|
||
|
||
# 3️⃣ League + Season + Movie — show movie details
|
||
if league_slug and season_slug and imdb_id:
|
||
season_label, season_year = parse_season_slug(season_slug)
|
||
league = get_object_or_404(League, slug=league_slug)
|
||
season = get_object_or_404(
|
||
Season, league=league, year=season_year, label__iexact=season_label
|
||
)
|
||
movie = get_object_or_404(Movie, imdb_id=imdb_id)
|
||
|
||
picks = movie.pick_set.filter(season=season).select_related(
|
||
"season_entry", "season_entry__user"
|
||
)
|
||
metrics = {m.key: m.value for m in movie.moviemetric_set.all()}
|
||
tmdb_data = get_tmdb_movie_by_imdb(movie.imdb_id)
|
||
data = {
|
||
"movie": movie,
|
||
"tmdb_data": tmdb_data,
|
||
"poster_path": cache_tmdb_poster(tmdb_data.poster_path),
|
||
"metrics": metrics,
|
||
"picks": picks,
|
||
"season": season,
|
||
"league": league,
|
||
}
|
||
return render(request, "movie.dj.html", data)
|
||
|
||
return HttpResponse("Invalid parameter combination.", content_type="text/plain")
|
||
|
||
|
||
def season_view(request, league_slug, season_slug=None):
|
||
if not league_slug:
|
||
return HttpResponse("League not specified", content_type="text/plain")
|
||
|
||
league = get_object_or_404(League, slug=league_slug)
|
||
|
||
# 1️⃣ League only – list all seasons in the league
|
||
if not season_slug:
|
||
seasons = [
|
||
{
|
||
"label": season.label,
|
||
"year": season.year,
|
||
"slug": season.slug,
|
||
"league": {"name": season.league.name, "slug": season.league.slug},
|
||
}
|
||
for season in league.season_set.all()
|
||
]
|
||
return render(
|
||
request,
|
||
"seasons.dj.html",
|
||
{
|
||
"seasons": seasons,
|
||
"league": league,
|
||
},
|
||
)
|
||
|
||
# 2️⃣ League + season – show a basic detail page or placeholder
|
||
season_label, season_year = parse_season_slug(season_slug)
|
||
season = get_object_or_404(
|
||
Season, league=league, year=season_year, label__iexact=season_label
|
||
)
|
||
entries = UserSeasonEntry.objects.filter(season=season).select_related("user")
|
||
|
||
picks = season.pick_set.select_related(
|
||
"season_entry", "season_entry__user"
|
||
).annotate(
|
||
domestic_gross=Sum(
|
||
"movie__moviemetric__value",
|
||
filter=Q(movie__moviemetric__key="domestic_gross"),
|
||
)
|
||
)
|
||
|
||
return render(
|
||
request,
|
||
"season.dj.html",
|
||
{
|
||
"season": season,
|
||
"league": league,
|
||
"scoreboard": get_scoreboard(entries),
|
||
"picks": picks,
|
||
},
|
||
)
|
||
|
||
|
||
def league_view(request, league_slug=None):
|
||
# 1️⃣ League only – list all seasons in the league
|
||
if not league_slug:
|
||
return render(request, "leagues.dj.html", {"leagues": League.objects.all()})
|
||
league = get_object_or_404(League, slug=league_slug)
|
||
|
||
# Subquery: top entry per season by total score
|
||
top_entry = (
|
||
UserSeasonEntry.objects.filter(season=OuterRef("pk"))
|
||
.annotate(
|
||
total_score=Sum(
|
||
"pick__movie__moviemetric__value",
|
||
filter=Q(pick__movie__moviemetric__key="domestic_gross"),
|
||
)
|
||
)
|
||
.order_by("-total_score")
|
||
)
|
||
|
||
winner_team = top_entry.values("team_name")[:1]
|
||
winner_score = top_entry.values("total_score")[:1]
|
||
|
||
# Subquery: pick with top-grossing movie for the season
|
||
top_pick = (
|
||
Pick.objects.filter(season=OuterRef("pk"))
|
||
.annotate(
|
||
gross=Sum(
|
||
"movie__moviemetric__value",
|
||
filter=Q(movie__moviemetric__key="domestic_gross"),
|
||
)
|
||
)
|
||
.order_by("-gross")
|
||
)
|
||
|
||
top_movie = top_pick.values("movie__title")[:1]
|
||
top_gross = top_pick.values("gross")[:1]
|
||
|
||
seasons = Season.objects.annotate(
|
||
winner_team=Subquery(winner_team),
|
||
winner_score=Subquery(winner_score),
|
||
top_movie=Subquery(top_movie),
|
||
top_movie_score=Subquery(top_gross),
|
||
).order_by("-year")
|
||
|
||
draft_sessions = DraftSession.objects.filter(season__league=league)
|
||
|
||
return render(
|
||
request,
|
||
"league.dj.html",
|
||
{
|
||
"league": league,
|
||
"seasons": seasons,
|
||
"draft_sessions": draft_sessions
|
||
},
|
||
)
|