initial commit

This commit is contained in:
2025-07-21 21:31:13 -05:00
commit fb35c70ef0
34 changed files with 1287 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
__pycache__
*.sqlite3
media/
*.csv
.env

View File

@@ -0,0 +1,124 @@
{
"folders": [
{
"path": "."
}
],
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "Run Django Server",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": ["runserver"],
"django": true,
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env"
},
{
"name": "Launch Chrome",
"type": "chrome",
"request": "launch",
"url": "http://127.0.0.1:8000", // adjust based on your local server
"webRoot": "${workspaceFolder}",
"sourceMaps": true,
"trace": true
},
{
"name": "Debug: Import 2014-2019.csv",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": ["import_legacy", "./data/2014-2019.csv"],
"django": true,
"console": "integratedTerminal"
}
],
"compounds":[{
"name": "Django + Chrome",
"configurations": ["Run Django Server", "Launch Chrome"],
"type": "compound"
}]
},
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "🗑️ Delete all Movies",
"type": "shell",
"command": "${config:python.defaultInterpreterPath}",
"args": [
"manage.py",
"shell",
"-c",
"from boxofficefantasy.models import Movie; Movie.objects.all().delete()"
],
"group": "build",
"problemMatcher": []
},
{
"label": "Import 2014-2019.csv",
"type": "shell",
"command": "${config:python.defaultInterpreterPath}",
"args": ["manage.py", "import_legacy", "./data/2014-2019.csv"],
"problemMatcher": []
},
{
"label": "📦 Make Migrations",
"type": "shell",
"command": "${config:python.defaultInterpreterPath}",
"args": ["manage.py", "makemigrations"],
"group": "build",
"problemMatcher": []
},
{
"label": "🔄 Apply Migrations",
"type": "shell",
"command": "${config:python.defaultInterpreterPath}",
"args": ["manage.py", "migrate"],
"group": "build",
"problemMatcher": []
},
{
"label": "Import ",
"type": "shell",
"command": "${config:python.defaultInterpreterPath}",
"args": ["manage.py", "migrate"],
"group": "build",
"problemMatcher": []
},
{
"label": "📦 🔄 Make & Apply Migratations",
"dependsOn": ["📦 Make Migrations", "🔄 Apply Migrations"],
"dependsOrder": "sequence",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
}
]
},
"settings": {
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"[django-html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
}
},
"files.associations": {
"*.dj.html": "django-html"
},
"emmet.includeLanguages": {
"django-html": "html"
}
}
}

View File

11
boxofficefantasy/admin.py Normal file
View File

@@ -0,0 +1,11 @@
from django.contrib import admin
from .models import League, Season, UserSeasonEntry, Movie, MovieMetric, ScoringRule, Pick
# Register your models here.
admin.site.register(League)
admin.site.register(Season)
admin.site.register(UserSeasonEntry)
admin.site.register(Movie)
admin.site.register(MovieMetric)
admin.site.register(ScoringRule)
admin.site.register(Pick)

6
boxofficefantasy/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BoxofficefantasyConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'boxofficefantasy'

View File

@@ -0,0 +1,51 @@
import requests
from django.core.cache import cache
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from tmdbv3api import TMDb, Movie
# Configure TMDb API client
tmdb = TMDb()
tmdb.api_key = settings.TMDB_API_KEY
tmdb.language = "en"
TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/w500"
def get_tmdb_movie_by_imdb(imdb_id):
"""
Fetch TMDb metadata by IMDb ID, using cache to avoid redundant API calls.
"""
cache_key = f"tmdb:movie:{imdb_id}"
cached = cache.get(cache_key)
if cached:
return cached
results = Movie().external(external_id=imdb_id, external_source="imdb_id")
if not results:
return None
movie_data = results.movie_results[0]
cache.set(cache_key, movie_data, timeout=60 * 60 * 24) # 1 day
return movie_data
def cache_tmdb_poster(poster_path):
"""
Download and cache the TMDb poster locally, returning a local URL.
"""
if not poster_path:
return None
filename = f"tmdb_posters/{poster_path.strip('/')}"
if default_storage.exists(filename):
return default_storage.url(filename)
url = f"{TMDB_IMAGE_BASE}{poster_path}"
response = requests.get(url)
if response.status_code == 200:
content = ContentFile(response.content)
default_storage.save(filename, content)
return default_storage.url(filename)
return None

View File

View File

@@ -0,0 +1,64 @@
import csv
from django.core.management.base import BaseCommand
from boxofficefantasy.models import Movie, MovieMetric, UserSeasonEntry, Pick, Season
from django.contrib.auth.models import User
class Command(BaseCommand):
help = "Import legacy draft/score CSV into boxofficefantasy app"
def add_arguments(self, parser):
parser.add_argument("csv_path", type=str)
def handle(self, *args, **options):
path = options["csv_path"]
with open(path, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
# 1. Get or create the Movie
movie, _ = Movie.objects.get_or_create(
title=row["Title"] if row["Title"] != 'NULL' else row["draftFilmTitle"],
defaults={
"imdb_id": row["imdbID"] if row["imdbID"] != 'NULL' else None,
"bom_legacy_id": row["mojoID"] if row["mojoID"] != 'NULL' else None,
"bom_id": None,
},
)
# 2. Add the MovieStat for domestic gross (if present)
if row.get("domesticgross"):
MovieMetric.objects.get_or_create(
movie=movie,
key="domestic_gross",
value=float(row["domesticgross"]) if row["domesticgross"] != 'NULL' else 0,
)
# 3. Create the Season (if needed)
season, _ = Season.objects.get_or_create(
year=row["year"],
defaults={"league_id": 1}, # Change if you have multiple leagues
)
# 3.5 Create the User (if needed)
user, did_create_new_user = User.objects.get_or_create(
username=row["ownerId"], email=f"{row['ownerId']}@not_an_email.com"
)
if did_create_new_user:
user.set_unusable_password()
# 4. Create the UserSeasonEntry
season_entry, _ = UserSeasonEntry.objects.get_or_create(
user=user, # Assumes user with that ID exists
season=season,
defaults={"team_name": row["teamname"]},
)
# # 5. Create the Pick
Pick.objects.get_or_create(
season_entry=season_entry,
season=season,
movie=movie,
defaults={"bid_amount": float(row["purchaseprice"])},
)
self.stdout.write(self.style.SUCCESS("Import completed."))

View File

@@ -0,0 +1,99 @@
# Generated by Django 5.2.4 on 2025-07-18 20:01
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Movie',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('imdb_id', models.CharField(max_length=20, unique=True)),
('bom_id', models.CharField(blank=True, max_length=50, null=True)),
('bom_legacy_id', models.CharField(blank=True, max_length=50, null=True)),
],
),
migrations.CreateModel(
name='League',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('commissioner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Season',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.IntegerField()),
('start_date', models.DateField()),
('end_date', models.DateField()),
('league', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boxofficefantasy.league')),
],
options={
'unique_together': {('league', 'year')},
},
),
migrations.CreateModel(
name='ScoringRule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('formula', models.TextField(help_text="Python expression using keys like 'domestic_gross', 'oscars', 'multiplier'.")),
('season', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='boxofficefantasy.season')),
],
),
migrations.CreateModel(
name='UserSeasonEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('team_name', models.CharField(max_length=100)),
('season', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boxofficefantasy.season')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Pick',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('bid_amount', models.DecimalField(decimal_places=2, max_digits=10)),
('movie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boxofficefantasy.movie')),
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boxofficefantasy.userseasonentry')),
],
),
migrations.CreateModel(
name='MovieMetric',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=100)),
('value', models.FloatField()),
('updated_at', models.DateTimeField(auto_now=True)),
('movie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boxofficefantasy.movie')),
],
options={
'unique_together': {('movie', 'key')},
},
),
migrations.AddConstraint(
model_name='userseasonentry',
constraint=models.UniqueConstraint(fields=('season', 'team_name'), name='unique_team_name_per_season'),
),
migrations.AlterUniqueTogether(
name='userseasonentry',
unique_together={('user', 'season')},
),
migrations.AlterUniqueTogether(
name='pick',
unique_together={('entry', 'movie')},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.4 on 2025-07-18 20:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('boxofficefantasy', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='userseasonentry',
options={'verbose_name': 'User Season Entry', 'verbose_name_plural': 'User Season Entries'},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-07-19 00:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boxofficefantasy', '0002_alter_userseasonentry_options'),
]
operations = [
migrations.AlterField(
model_name='season',
name='end_date',
field=models.DateField(null=True),
),
migrations.AlterField(
model_name='season',
name='start_date',
field=models.DateField(null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-07-19 14:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boxofficefantasy', '0003_alter_season_end_date_alter_season_start_date'),
]
operations = [
migrations.AddField(
model_name='league',
name='slug',
field=models.SlugField(blank=True, unique=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.4 on 2025-07-19 14:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boxofficefantasy', '0004_league_slug'),
]
operations = [
migrations.AddField(
model_name='season',
name='label',
field=models.CharField(default='Summer', max_length=50),
preserve_default=False,
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.2.4 on 2025-07-19 15:59
import django.db.models.deletion
from django.db import migrations, models
def set_season_from_season_entry(apps, schema_editor):
Pick = apps.get_model("boxofficefantasy", "Pick")
for pick in Pick.objects.all():
pick.season = pick.season_entry.season
pick.save()
class Migration(migrations.Migration):
dependencies = [
("boxofficefantasy", "0005_season_label"),
]
operations = [
migrations.RenameField(
model_name="pick",
old_name="entry",
new_name="season_entry",
),
migrations.AlterUniqueTogether(
name="pick",
unique_together={("season_entry", "movie")},
),
migrations.AddField(
model_name="pick",
name="season",
field=models.ForeignKey(
default=None,
on_delete=django.db.models.deletion.CASCADE,
to="boxofficefantasy.season",
null=True,
),
preserve_default=False,
),
migrations.RunPython(set_season_from_season_entry),
migrations.AlterField(
model_name="pick",
name="season",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="boxofficefantasy.season",
null=True,
),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.4 on 2025-07-19 16:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boxofficefantasy', '0006_rename_entry_pick_season_entry_and_more'),
]
operations = [
migrations.AlterField(
model_name='pick',
name='season',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boxofficefantasy.season'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-07-19 16:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boxofficefantasy', '0007_alter_pick_season'),
]
operations = [
migrations.AlterField(
model_name='movie',
name='imdb_id',
field=models.CharField(blank=True, max_length=20, null=True),
),
]

View File

106
boxofficefantasy/models.py Normal file
View File

@@ -0,0 +1,106 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils.text import slugify
from django.core.exceptions import ValidationError
# Create your models here.
class League(models.Model):
name = models.CharField(max_length=100)
commissioner = models.ForeignKey(User, on_delete=models.CASCADE)
slug = models.SlugField(unique=True, blank=True)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Season(models.Model):
league = models.ForeignKey(League, on_delete=models.CASCADE)
year = models.IntegerField()
start_date = models.DateField(null=True)
end_date = models.DateField(null=True)
label = models.CharField(max_length=50)
class Meta:
unique_together = ('league', 'year')
def __str__(self):
return f"{self.league.name} - {self.year}"
@property
def slug(self):
return f"{slugify(self.label)}-{self.year}"
class UserSeasonEntry(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
season = models.ForeignKey(Season, on_delete=models.CASCADE)
team_name = models.CharField(max_length=100)
class Meta:
unique_together = ('user', 'season')
constraints = [
models.UniqueConstraint(fields=['season', 'team_name'], name='unique_team_name_per_season')
]
verbose_name = "User Season Entry"
verbose_name_plural = "User Season Entries"
def __str__(self):
return f"{self.team_name} ({self.user.username})"
class Movie(models.Model):
title = models.CharField(max_length=255)
imdb_id = models.CharField(max_length=20, blank=True, null=True)
bom_id = models.CharField(max_length=50, blank=True, null=True)
bom_legacy_id = models.CharField(max_length=50, blank=True, null=True)
def __str__(self):
return self.title
def clean(self):
for field in ["imdb_id", "bom_id", "bom_legacy_id"]:
if getattr(self, field):
if Movie.objects.exclude(pk=self.pk).filter(**{field:getattr(self,field)}).exists():
raise ValidationError({field: f'{field} must be unique.'})
class Pick(models.Model):
season_entry = models.ForeignKey(UserSeasonEntry, on_delete=models.CASCADE)
season = models.ForeignKey(Season, on_delete=models.CASCADE)
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
bid_amount = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
unique_together = ('season_entry', 'movie')
def __str__(self):
return f"{self.season_entry} - {self.movie}"
class MovieMetric(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
key = models.CharField(max_length=100)
value = models.FloatField()
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ('movie', 'key')
def __str__(self):
return f"{self.movie.title} - {self.key}: {self.value}"
class ScoringRule(models.Model):
season = models.OneToOneField(Season, on_delete=models.CASCADE)
formula = models.TextField(
help_text="Python expression using keys like 'domestic_gross', 'oscars', 'multiplier'."
)
def __str__(self):
return f"Scoring for {self.season}"

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Site{% endblock %}</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
/>
<link
rel="stylesheet"
href="https://cdn.datatables.net/2.3.2/css/dataTables.bootstrap5.css"
/>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.3/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.datatables.net/2.3.2/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.3.2/js/dataTables.bootstrap5.js"></script>
</head>
<body>
<header class="p-3 bg-dark text-white">
<div class="container">My Site Header</div>
</header>
<main class="container mt-4">
{% block content %}
<!-- Default content -->
{% endblock %}
</main>
<footer class="text-muted text-center mt-5">
<small>&copy; 2025 MySite</small>
</footer>
</body>
</html>

View File

@@ -0,0 +1,15 @@
{% extends "base.dj.html" %} {% load humanize %} {% block title %}Scoreboard
{{ season.label }} {{season.year }}{% endblock %} {% block content %}
{{scoreboard|json_script:"scoreboard-data" }}
<h1>
{{tmdb_data.title}}
</h1>
<script>
{{tmdb_data}}
</script>
<div><img src="{{poster_path}}"></div>
<div>{{tmdb_data.overview}}</div>
{%endblock%}

View File

@@ -0,0 +1,34 @@
{% extends "base.dj.html" %} {% load humanize %} {% block title %}Scoreboard
{{ season.label }} {{season.year }}{% endblock %} {% block content %}
{{movies|json_script:"movie_data" }}
<h1>
{{ season.league.name }} {{ season.label }} {{ season.year }} Movies
</h1>
<table id="datatable" class="table"></table>
<script>
window.addEventListener("load", () => {
const columns = [
{ title: "Title", data: "title" },
{ title: "Team", data:"team_name"},
{ title: "Score", data: "score",render: function (data, type, row) {
// Only format for display type
if (type === "display" || type === "filter") {
return `$${parseFloat(data).toLocaleString("en-US", {
minimumFractionDigits: 0,
})}`;
}
return data;
},},
];
const data = JSON.parse(
document.getElementById("movie_data").textContent
);
const dataTableEl = document.querySelector("#datatable");
const dataTable = $("#datatable").DataTable({
data,
columns,
order: [[2, 'desc']]
});
});
</script>
{%endblock%}

View File

@@ -0,0 +1,52 @@
{% extends "base.dj.html" %} {% load humanize %} {% block title %}Scoreboard
{{ season.label }} {{season.year }}{% endblock %} {% block content %}
{{scoreboard|json_script:"scoreboard-data" }}
<h1>
{{ season.league.name }} {{ season.label }} {{ season.year }} Scoreboard
</h1>
<table id="datatable" class="table"></table>
{% for entry in scoreboard %}
<h3>{{ entry.team }} ({{ entry.user }})</h3>
<ul>
{% for pick in entry.picks %}
<li>
<a href="{% url 'season:movie' league.slug season.slug pick.imdb_id %}">{{ pick.movie.title }}</a> ${{ pick.score|floatformat:0|intcomma }} (Bid:
${{pick.bid | floatformat:0}}M)
</li>
{% endfor %}
</ul>
{% endfor %}
<script>
window.addEventListener("load", () => {
const columns = [
{ title: "Team", data: "team" },
{ title: "User", data: "user" },
{
title: "Total Score",
data: "total",
class: "font-monospace",
render: function (data, type, row) {
// Only format for display type
if (type === "display" || type === "filter") {
return `$${parseFloat(data).toLocaleString("en-US", {
minimumFractionDigits: 0,
})}`;
}
return data;
},
},
];
const data = JSON.parse(
document.getElementById("scoreboard-data").textContent
);
const dataTableEl = document.querySelector("#datatable");
const dataTable = $("#datatable").DataTable({
data,
columns,
order: [[2, 'desc']]
});
});
</script>
{%endblock%}

View File

@@ -0,0 +1,14 @@
{% extends "base.dj.html" %} {% load humanize %} {% block content%}
<script id="season-data" type="application/json">
{{ seasons|json_script:"seasons-data" }}
</script>
<ul>
{% for season in seasons %}
<li>
<a href="{% url 'season:scoreboard' season.league.slug season.slug %}">
{{ season.league.name }} {{ season.label }} {{ season.year }}
</a>
</li>
{% endfor %}
</ul>
{%endblock%}

View File

@@ -0,0 +1,39 @@
{% extends "base.dj.html" %} {% load humanize %} {% block title %}Scoreboard
{{ season.label }} {{season.year }}{% endblock %} {% block content %}
{{movies|json_script:"movie_data" }}
<h1>
Teams
</h1>
<table id="datatable" class="table d-none"></table>
<ul>
{%for entry in user_season_entries%}
<li>{{entry.username}} - {{entry.team_name}}</li>
{%endfor%}
</ul>
<script>
window.addEventListener("xxx", () => {
const columns = [
{ title: "Title", data: "title" },
{ title: "Team", data:"team_name"},
{ title: "Score", data: "score",render: function (data, type, row) {
// Only format for display type
if (type === "display" || type === "filter") {
return `$${parseFloat(data).toLocaleString("en-US", {
minimumFractionDigits: 0,
})}`;
}
return data;
},},
];
const data = JSON.parse(
document.getElementById("movie_data").textContent
);
const dataTableEl = document.querySelector("#datatable");
const dataTable = $("#datatable").DataTable({
data,
columns,
order: [[2, 'desc']]
});
});
</script>
{%endblock%}

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

29
boxofficefantasy/urls.py Normal file
View File

@@ -0,0 +1,29 @@
from django.urls import path, include
from . import views
league_patterns = [
# path("", views.league_view, name="league" ),
path("season/", views.season_view, name="seasons"),
path("movie/", views.movie_view, name="movies"),
path("team/", views.team_view, name="teams"),
]
season_patterns = [
path("", views.season_view, name="season"),
path("scoreboard/", views.scoreboard_view, name="scoreboard"),
path("team/<str:username>/", views.team_view, name="team"),
path("team/", views.team_view, name="teams"),
path("movie/<str:imdb_id>/", views.movie_view, name="movie"),
path("movie/", views.movie_view, name="movies")
]
urlpatterns = [
path(
"league/<slug:league_slug>/season/<slug:season_slug>/",
include((season_patterns, "boxofficefantasy"), namespace="season")
),
path(
"league/<slug:league_slug>/",
include((league_patterns, "boxofficefantasy"), namespace="league")
),
]

221
boxofficefantasy/views.py Normal file
View File

@@ -0,0 +1,221 @@
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 boxofficefantasy.models import League, Season, UserSeasonEntry, Movie, Pick
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.")
# 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 = []
for season_entry in 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 render(
request,
"scoreboard.dj.html",
{
"league": league,
"season": season,
"scoreboard": scoreboard,
},
)
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
)
return render(request, "scoreboard.dj.html", {
"season": season,
"league": league,
})

View File

View File

@@ -0,0 +1,16 @@
"""
ASGI config for boxofficefantasy_project project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'boxofficefantasy_project.settings')
application = get_asgi_application()

View File

@@ -0,0 +1,131 @@
"""
Django settings for boxofficefantasy_project project.
Generated by 'django-admin startproject' using Django 5.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-_rrxhe5i6uqap!52u(1zi8x$820duvf5s_!9!bc4ghbyyktol0'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# TMDB API KEY
TMDB_API_KEY = os.environ.get("TMDB_API_KEY")
# Application definition
INSTALLED_APPS = [
'boxofficefantasy',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'boxofficefantasy_project.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'boxofficefantasy_project.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/'
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@@ -0,0 +1,29 @@
"""
URL configuration for boxofficefantasy_project project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path("", include("boxofficefantasy.urls"))
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -0,0 +1,16 @@
"""
WSGI config for boxofficefantasy_project project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'boxofficefantasy_project.settings')
application = get_wsgi_application()

22
manage.py Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'boxofficefantasy_project.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()