Merge branch 'feature/phase-2-initial-django-models' into develop
This commit is contained in:
8
.env.example
Normal file
8
.env.example
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
DJANGO_SECRET_KEY=dev-only-change-me
|
||||||
|
DJANGO_DEBUG=1
|
||||||
|
DJANGO_ALLOWED_HOSTS=*
|
||||||
|
POSTGRES_DB=hoopscout
|
||||||
|
POSTGRES_USER=hoopscout
|
||||||
|
POSTGRES_PASSWORD=hoopscout
|
||||||
|
POSTGRES_HOST=db
|
||||||
|
POSTGRES_PORT=5432
|
||||||
0
app/hoopscout/__init__.py
Normal file
0
app/hoopscout/__init__.py
Normal file
7
app/hoopscout/asgi.py
Normal file
7
app/hoopscout/asgi.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hoopscout.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
68
app/hoopscout/settings.py
Normal file
68
app/hoopscout/settings.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-only-insecure-secret-key")
|
||||||
|
DEBUG = os.getenv("DJANGO_DEBUG", "1") == "1"
|
||||||
|
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(",")
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"scouting.apps.ScoutingConfig",
|
||||||
|
]
|
||||||
|
|
||||||
|
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 = "hoopscout.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "hoopscout.wsgi.application"
|
||||||
|
ASGI_APPLICATION = "hoopscout.asgi.application"
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"NAME": os.getenv("POSTGRES_DB", "hoopscout"),
|
||||||
|
"USER": os.getenv("POSTGRES_USER", "hoopscout"),
|
||||||
|
"PASSWORD": os.getenv("POSTGRES_PASSWORD", "hoopscout"),
|
||||||
|
"HOST": os.getenv("POSTGRES_HOST", "db"),
|
||||||
|
"PORT": os.getenv("POSTGRES_PORT", "5432"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
6
app/hoopscout/urls.py
Normal file
6
app/hoopscout/urls.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
]
|
||||||
7
app/hoopscout/wsgi.py
Normal file
7
app/hoopscout/wsgi.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hoopscout.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
14
app/manage.py
Normal file
14
app/manage.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hoopscout.settings")
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
2
app/requirements.txt
Normal file
2
app/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Django==5.2.2
|
||||||
|
psycopg[binary]==3.2.9
|
||||||
0
app/scouting/__init__.py
Normal file
0
app/scouting/__init__.py
Normal file
70
app/scouting/admin.py
Normal file
70
app/scouting/admin.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
Competition,
|
||||||
|
Player,
|
||||||
|
PlayerSeason,
|
||||||
|
PlayerSeasonStats,
|
||||||
|
Role,
|
||||||
|
Season,
|
||||||
|
Specialty,
|
||||||
|
Team,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Role)
|
||||||
|
class RoleAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "slug")
|
||||||
|
search_fields = ("name", "slug")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Specialty)
|
||||||
|
class SpecialtyAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "slug")
|
||||||
|
search_fields = ("name", "slug")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Player)
|
||||||
|
class PlayerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("full_name", "nationality", "birth_date")
|
||||||
|
search_fields = ("full_name", "first_name", "last_name", "nationality")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Competition)
|
||||||
|
class CompetitionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "country", "level")
|
||||||
|
search_fields = ("name", "country", "level")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Team)
|
||||||
|
class TeamAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "competition", "country")
|
||||||
|
search_fields = ("name", "country")
|
||||||
|
list_filter = ("competition",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Season)
|
||||||
|
class SeasonAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "start_year", "end_year")
|
||||||
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(PlayerSeason)
|
||||||
|
class PlayerSeasonAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("player", "season", "team", "competition", "position")
|
||||||
|
list_filter = ("season", "competition", "position")
|
||||||
|
search_fields = ("player__full_name", "team__name", "competition__name")
|
||||||
|
filter_horizontal = ("roles", "specialties")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(PlayerSeasonStats)
|
||||||
|
class PlayerSeasonStatsAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"player_season",
|
||||||
|
"points",
|
||||||
|
"assists",
|
||||||
|
"steals",
|
||||||
|
"turnovers",
|
||||||
|
"blocks",
|
||||||
|
)
|
||||||
|
search_fields = ("player_season__player__full_name", "player_season__season__name")
|
||||||
6
app/scouting/apps.py
Normal file
6
app/scouting/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ScoutingConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "scouting"
|
||||||
145
app/scouting/migrations/0001_initial.py
Normal file
145
app/scouting/migrations/0001_initial.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# Generated by Django 5.2.2 on 2026-04-06 17:05
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Competition',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=150, unique=True)),
|
||||||
|
('country', models.CharField(blank=True, max_length=100)),
|
||||||
|
('level', models.CharField(blank=True, max_length=100)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Player',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('full_name', models.CharField(max_length=255)),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=100)),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=100)),
|
||||||
|
('birth_date', models.DateField(blank=True, null=True)),
|
||||||
|
('nationality', models.CharField(blank=True, max_length=100)),
|
||||||
|
('height_cm', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
|
||||||
|
('weight_kg', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
|
||||||
|
('wingspan_cm', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['full_name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Role',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('slug', models.SlugField(max_length=120, unique=True)),
|
||||||
|
('description', models.TextField(blank=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Season',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=20, unique=True)),
|
||||||
|
('start_year', models.PositiveSmallIntegerField()),
|
||||||
|
('end_year', models.PositiveSmallIntegerField()),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-start_year', '-end_year'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Specialty',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('slug', models.SlugField(max_length=120, unique=True)),
|
||||||
|
('description', models.TextField(blank=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PlayerSeason',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('position', models.CharField(choices=[('PG', 'PG'), ('SG', 'SG'), ('SF', 'SF'), ('PF', 'PF'), ('C', 'C')], max_length=2)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('competition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='player_seasons', to='scouting.competition')),
|
||||||
|
('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='player_seasons', to='scouting.player')),
|
||||||
|
('roles', models.ManyToManyField(blank=True, related_name='player_seasons', to='scouting.role')),
|
||||||
|
('season', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='player_seasons', to='scouting.season')),
|
||||||
|
('specialties', models.ManyToManyField(blank=True, related_name='player_seasons', to='scouting.specialty')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['player__full_name', '-season__start_year'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PlayerSeasonStats',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('points', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
|
||||||
|
('assists', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
|
||||||
|
('steals', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
|
||||||
|
('turnovers', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
|
||||||
|
('blocks', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
|
||||||
|
('efg_pct', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
|
||||||
|
('ts_pct', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
|
||||||
|
('plus_minus', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
|
||||||
|
('offensive_rating', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
|
||||||
|
('defensive_rating', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('player_season', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='stats', to='scouting.playerseason')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Player season stats',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Team',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=150)),
|
||||||
|
('country', models.CharField(blank=True, max_length=100)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('competition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teams', to='scouting.competition')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['name'],
|
||||||
|
'unique_together': {('name', 'competition')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='playerseason',
|
||||||
|
name='team',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='player_seasons', to='scouting.team'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
app/scouting/migrations/__init__.py
Normal file
0
app/scouting/migrations/__init__.py
Normal file
159
app/scouting/models.py
Normal file
159
app/scouting/models.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Role(models.Model):
|
||||||
|
name = models.CharField(max_length=100, unique=True)
|
||||||
|
slug = models.SlugField(max_length=120, unique=True)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Specialty(models.Model):
|
||||||
|
name = models.CharField(max_length=100, unique=True)
|
||||||
|
slug = models.SlugField(max_length=120, unique=True)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Player(models.Model):
|
||||||
|
full_name = models.CharField(max_length=255)
|
||||||
|
first_name = models.CharField(max_length=100, blank=True)
|
||||||
|
last_name = models.CharField(max_length=100, blank=True)
|
||||||
|
birth_date = models.DateField(null=True, blank=True)
|
||||||
|
nationality = models.CharField(max_length=100, blank=True)
|
||||||
|
height_cm = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
||||||
|
weight_kg = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
||||||
|
wingspan_cm = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["full_name"]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.full_name
|
||||||
|
|
||||||
|
|
||||||
|
class Competition(models.Model):
|
||||||
|
name = models.CharField(max_length=150, unique=True)
|
||||||
|
country = models.CharField(max_length=100, blank=True)
|
||||||
|
level = models.CharField(max_length=100, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Team(models.Model):
|
||||||
|
name = models.CharField(max_length=150)
|
||||||
|
competition = models.ForeignKey(
|
||||||
|
Competition,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="teams",
|
||||||
|
)
|
||||||
|
country = models.CharField(max_length=100, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["name"]
|
||||||
|
unique_together = ("name", "competition")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Season(models.Model):
|
||||||
|
name = models.CharField(max_length=20, unique=True)
|
||||||
|
start_year = models.PositiveSmallIntegerField()
|
||||||
|
end_year = models.PositiveSmallIntegerField()
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-start_year", "-end_year"]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerSeason(models.Model):
|
||||||
|
class Position(models.TextChoices):
|
||||||
|
PG = "PG", "PG"
|
||||||
|
SG = "SG", "SG"
|
||||||
|
SF = "SF", "SF"
|
||||||
|
PF = "PF", "PF"
|
||||||
|
C = "C", "C"
|
||||||
|
|
||||||
|
player = models.ForeignKey(Player, on_delete=models.CASCADE, related_name="player_seasons")
|
||||||
|
season = models.ForeignKey(Season, on_delete=models.CASCADE, related_name="player_seasons")
|
||||||
|
team = models.ForeignKey(
|
||||||
|
Team,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="player_seasons",
|
||||||
|
)
|
||||||
|
competition = models.ForeignKey(
|
||||||
|
Competition,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="player_seasons",
|
||||||
|
)
|
||||||
|
position = models.CharField(max_length=2, choices=Position.choices)
|
||||||
|
roles = models.ManyToManyField(Role, blank=True, related_name="player_seasons")
|
||||||
|
specialties = models.ManyToManyField(Specialty, blank=True, related_name="player_seasons")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["player__full_name", "-season__start_year"]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.player.full_name} - {self.season.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerSeasonStats(models.Model):
|
||||||
|
player_season = models.OneToOneField(
|
||||||
|
PlayerSeason,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="stats",
|
||||||
|
)
|
||||||
|
|
||||||
|
points = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
|
||||||
|
assists = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
|
||||||
|
steals = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
|
||||||
|
turnovers = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
|
||||||
|
blocks = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
|
||||||
|
|
||||||
|
efg_pct = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
||||||
|
ts_pct = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
|
||||||
|
plus_minus = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||||
|
offensive_rating = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||||
|
defensive_rating = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "Player season stats"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Stats for {self.player_season}"
|
||||||
35
infra/docker-compose.yml
Normal file
35
infra/docker-compose.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: infra/docker/Dockerfile
|
||||||
|
working_dir: /app
|
||||||
|
command: python manage.py runserver 0.0.0.0:8000
|
||||||
|
volumes:
|
||||||
|
- ../app:/app
|
||||||
|
environment:
|
||||||
|
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
|
||||||
|
DJANGO_DEBUG: ${DJANGO_DEBUG}
|
||||||
|
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_HOST: ${POSTGRES_HOST}
|
||||||
|
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
13
infra/docker/Dockerfile
Normal file
13
infra/docker/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY app/requirements.txt /tmp/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
||||||
|
|
||||||
|
COPY app/ /app/
|
||||||
|
|
||||||
|
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||||
Reference in New Issue
Block a user