feat: add Django web app, CloudKit sync, dashboard, and game_datetime_utc export
Adds the full Django application layer on top of sportstime_parser: - core: Sport, Team, Stadium, Game models with aliases and league structure - scraper: orchestration engine, adapter, job management, Celery tasks - cloudkit: CloudKit sync client, sync state tracking, sync jobs - dashboard: staff dashboard for monitoring scrapers, sync, review queue - notifications: email reports for scrape/sync results - Docker setup for deployment (Dockerfile, docker-compose, entrypoint) Game exports now use game_datetime_utc (ISO 8601 UTC) instead of venue-local date+time strings, matching the canonical format used by the iOS app. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
438
core/migrations/0001_initial.py
Executable file
438
core/migrations/0001_initial.py
Executable file
@@ -0,0 +1,438 @@
|
||||
# Generated by Django 5.1.15 on 2026-01-26 08:59
|
||||
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
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='Conference',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('short_name', models.CharField(blank=True, help_text='Short name (e.g., East, West)', max_length=10)),
|
||||
('order', models.PositiveSmallIntegerField(default=0, help_text='Display order')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Conference',
|
||||
'verbose_name_plural': 'Conferences',
|
||||
'ordering': ['sport', 'order', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Sport',
|
||||
fields=[
|
||||
('code', models.CharField(help_text='Sport code (e.g., nba, mlb, nfl)', max_length=10, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(help_text='Full name (e.g., National Basketball Association)', max_length=100)),
|
||||
('short_name', models.CharField(help_text='Short name (e.g., NBA)', max_length=20)),
|
||||
('season_type', models.CharField(choices=[('split', 'Split Year (e.g., 2024-25)'), ('single', 'Single Year (e.g., 2024)')], help_text='Whether season spans two years or one', max_length=10)),
|
||||
('expected_game_count', models.PositiveIntegerField(default=0, help_text='Expected number of regular season games')),
|
||||
('season_start_month', models.PositiveSmallIntegerField(default=1, help_text='Month when season typically starts (1-12)')),
|
||||
('season_end_month', models.PositiveSmallIntegerField(default=12, help_text='Month when season typically ends (1-12)')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Whether this sport is actively being scraped')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Sport',
|
||||
'verbose_name_plural': 'Sports',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Division',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('short_name', models.CharField(blank=True, help_text='Short name', max_length=10)),
|
||||
('order', models.PositiveSmallIntegerField(default=0, help_text='Display order')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('conference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='divisions', to='core.conference')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Division',
|
||||
'verbose_name_plural': 'Divisions',
|
||||
'ordering': ['conference', 'order', 'name'],
|
||||
'unique_together': {('conference', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalDivision',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('short_name', models.CharField(blank=True, help_text='Short name', max_length=10)),
|
||||
('order', models.PositiveSmallIntegerField(default=0, help_text='Display order')),
|
||||
('created_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('updated_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('conference', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.conference')),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Division',
|
||||
'verbose_name_plural': 'historical Divisions',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalSport',
|
||||
fields=[
|
||||
('code', models.CharField(db_index=True, help_text='Sport code (e.g., nba, mlb, nfl)', max_length=10)),
|
||||
('name', models.CharField(help_text='Full name (e.g., National Basketball Association)', max_length=100)),
|
||||
('short_name', models.CharField(help_text='Short name (e.g., NBA)', max_length=20)),
|
||||
('season_type', models.CharField(choices=[('split', 'Split Year (e.g., 2024-25)'), ('single', 'Single Year (e.g., 2024)')], help_text='Whether season spans two years or one', max_length=10)),
|
||||
('expected_game_count', models.PositiveIntegerField(default=0, help_text='Expected number of regular season games')),
|
||||
('season_start_month', models.PositiveSmallIntegerField(default=1, help_text='Month when season typically starts (1-12)')),
|
||||
('season_end_month', models.PositiveSmallIntegerField(default=12, help_text='Month when season typically ends (1-12)')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Whether this sport is actively being scraped')),
|
||||
('created_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('updated_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Sport',
|
||||
'verbose_name_plural': 'historical Sports',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalStadium',
|
||||
fields=[
|
||||
('id', models.CharField(db_index=True, help_text='Canonical ID (e.g., stadium_nba_los_angeles_lakers)', max_length=100)),
|
||||
('name', models.CharField(help_text='Current stadium name', max_length=200)),
|
||||
('city', models.CharField(max_length=100)),
|
||||
('state', models.CharField(blank=True, help_text='State/Province (blank for international)', max_length=100)),
|
||||
('country', models.CharField(default='USA', max_length=100)),
|
||||
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
('capacity', models.PositiveIntegerField(blank=True, help_text='Seating capacity', null=True)),
|
||||
('surface', models.CharField(blank=True, choices=[('grass', 'Natural Grass'), ('turf', 'Artificial Turf'), ('ice', 'Ice'), ('hardwood', 'Hardwood'), ('other', 'Other')], max_length=20)),
|
||||
('roof_type', models.CharField(blank=True, choices=[('dome', 'Dome (Closed)'), ('retractable', 'Retractable'), ('open', 'Open Air')], max_length=20)),
|
||||
('opened_year', models.PositiveSmallIntegerField(blank=True, help_text='Year stadium opened', null=True)),
|
||||
('timezone', models.CharField(blank=True, help_text='IANA timezone (e.g., America/Los_Angeles)', max_length=50)),
|
||||
('image_url', models.URLField(blank=True, help_text='URL to stadium image')),
|
||||
('created_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('updated_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('sport', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.sport')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Stadium',
|
||||
'verbose_name_plural': 'historical Stadiums',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalConference',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('short_name', models.CharField(blank=True, help_text='Short name (e.g., East, West)', max_length=10)),
|
||||
('order', models.PositiveSmallIntegerField(default=0, help_text='Display order')),
|
||||
('created_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('updated_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('sport', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.sport')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Conference',
|
||||
'verbose_name_plural': 'historical Conferences',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='conference',
|
||||
name='sport',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conferences', to='core.sport'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Stadium',
|
||||
fields=[
|
||||
('id', models.CharField(help_text='Canonical ID (e.g., stadium_nba_los_angeles_lakers)', max_length=100, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(help_text='Current stadium name', max_length=200)),
|
||||
('city', models.CharField(max_length=100)),
|
||||
('state', models.CharField(blank=True, help_text='State/Province (blank for international)', max_length=100)),
|
||||
('country', models.CharField(default='USA', max_length=100)),
|
||||
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
('capacity', models.PositiveIntegerField(blank=True, help_text='Seating capacity', null=True)),
|
||||
('surface', models.CharField(blank=True, choices=[('grass', 'Natural Grass'), ('turf', 'Artificial Turf'), ('ice', 'Ice'), ('hardwood', 'Hardwood'), ('other', 'Other')], max_length=20)),
|
||||
('roof_type', models.CharField(blank=True, choices=[('dome', 'Dome (Closed)'), ('retractable', 'Retractable'), ('open', 'Open Air')], max_length=20)),
|
||||
('opened_year', models.PositiveSmallIntegerField(blank=True, help_text='Year stadium opened', null=True)),
|
||||
('timezone', models.CharField(blank=True, help_text='IANA timezone (e.g., America/Los_Angeles)', max_length=50)),
|
||||
('image_url', models.URLField(blank=True, help_text='URL to stadium image')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('sport', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stadiums', to='core.sport')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Stadium',
|
||||
'verbose_name_plural': 'Stadiums',
|
||||
'ordering': ['sport', 'city', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalTeam',
|
||||
fields=[
|
||||
('id', models.CharField(db_index=True, help_text='Canonical ID (e.g., team_nba_lal)', max_length=50)),
|
||||
('city', models.CharField(help_text='Team city (e.g., Los Angeles)', max_length=100)),
|
||||
('name', models.CharField(help_text='Team name (e.g., Lakers)', max_length=100)),
|
||||
('full_name', models.CharField(help_text='Full team name (e.g., Los Angeles Lakers)', max_length=200)),
|
||||
('abbreviation', models.CharField(help_text='Team abbreviation (e.g., LAL)', max_length=10)),
|
||||
('primary_color', models.CharField(blank=True, help_text='Primary color hex (e.g., #552583)', max_length=7)),
|
||||
('secondary_color', models.CharField(blank=True, help_text='Secondary color hex (e.g., #FDB927)', max_length=7)),
|
||||
('logo_url', models.URLField(blank=True, help_text='URL to team logo')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Whether team is currently active')),
|
||||
('created_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('updated_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('division', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.division')),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('sport', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.sport')),
|
||||
('home_stadium', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.stadium')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Team',
|
||||
'verbose_name_plural': 'historical Teams',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalStadiumAlias',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||
('alias', models.CharField(help_text='The alias text to match against', max_length=200)),
|
||||
('alias_type', models.CharField(choices=[('official', 'Official Name'), ('former', 'Former Name'), ('nickname', 'Nickname'), ('abbreviation', 'Abbreviation')], default='official', max_length=20)),
|
||||
('valid_from', models.DateField(blank=True, help_text='Date from which this alias is valid (inclusive)', null=True)),
|
||||
('valid_until', models.DateField(blank=True, help_text='Date until which this alias is valid (inclusive)', null=True)),
|
||||
('is_primary', models.BooleanField(default=False, help_text='Whether this is the current/primary name')),
|
||||
('source', models.CharField(blank=True, help_text='Source of this alias', max_length=200)),
|
||||
('notes', models.TextField(blank=True, help_text='Notes about this alias (e.g., naming rights deal)')),
|
||||
('created_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('updated_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('stadium', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.stadium')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Stadium Alias',
|
||||
'verbose_name_plural': 'historical Stadium Aliases',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('id', models.CharField(help_text='Canonical ID (e.g., team_nba_lal)', max_length=50, primary_key=True, serialize=False)),
|
||||
('city', models.CharField(help_text='Team city (e.g., Los Angeles)', max_length=100)),
|
||||
('name', models.CharField(help_text='Team name (e.g., Lakers)', max_length=100)),
|
||||
('full_name', models.CharField(help_text='Full team name (e.g., Los Angeles Lakers)', max_length=200)),
|
||||
('abbreviation', models.CharField(help_text='Team abbreviation (e.g., LAL)', max_length=10)),
|
||||
('primary_color', models.CharField(blank=True, help_text='Primary color hex (e.g., #552583)', max_length=7)),
|
||||
('secondary_color', models.CharField(blank=True, help_text='Secondary color hex (e.g., #FDB927)', max_length=7)),
|
||||
('logo_url', models.URLField(blank=True, help_text='URL to team logo')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Whether team is currently active')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('division', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teams', to='core.division')),
|
||||
('home_stadium', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='home_teams', to='core.stadium')),
|
||||
('sport', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='core.sport')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Team',
|
||||
'verbose_name_plural': 'Teams',
|
||||
'ordering': ['sport', 'city', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalTeamAlias',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||
('alias', models.CharField(help_text='The alias text to match against', max_length=200)),
|
||||
('alias_type', models.CharField(choices=[('full_name', 'Full Name'), ('city_name', 'City + Name'), ('abbreviation', 'Abbreviation'), ('nickname', 'Nickname'), ('historical', 'Historical Name')], default='full_name', max_length=20)),
|
||||
('valid_from', models.DateField(blank=True, help_text='Date from which this alias is valid (inclusive)', null=True)),
|
||||
('valid_until', models.DateField(blank=True, help_text='Date until which this alias is valid (inclusive)', null=True)),
|
||||
('is_primary', models.BooleanField(default=False, help_text='Whether this is a primary/preferred alias')),
|
||||
('source', models.CharField(blank=True, help_text='Source of this alias (e.g., ESPN, Basketball-Reference)', max_length=200)),
|
||||
('notes', models.TextField(blank=True, help_text='Notes about this alias (e.g., relocation details)')),
|
||||
('created_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('updated_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('team', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.team')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Team Alias',
|
||||
'verbose_name_plural': 'historical Team Aliases',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalGame',
|
||||
fields=[
|
||||
('id', models.CharField(db_index=True, help_text='Canonical ID (e.g., game_nba_2025_20251022_bos_lal)', max_length=100)),
|
||||
('season', models.PositiveSmallIntegerField(help_text='Season start year (e.g., 2025 for 2025-26 season)')),
|
||||
('game_date', models.DateTimeField(help_text='Game date and time (UTC)')),
|
||||
('game_number', models.PositiveSmallIntegerField(blank=True, help_text='Game number for doubleheaders (1 or 2)', null=True)),
|
||||
('home_score', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||
('away_score', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('in_progress', 'In Progress'), ('final', 'Final'), ('postponed', 'Postponed'), ('cancelled', 'Cancelled'), ('suspended', 'Suspended')], default='scheduled', max_length=20)),
|
||||
('is_neutral_site', models.BooleanField(default=False, help_text='Whether game is at neutral site')),
|
||||
('is_playoff', models.BooleanField(default=False, help_text='Whether this is a playoff game')),
|
||||
('playoff_round', models.CharField(blank=True, help_text='Playoff round (e.g., Finals, Conference Finals)', max_length=50)),
|
||||
('raw_home_team', models.CharField(blank=True, help_text='Original scraped home team name', max_length=200)),
|
||||
('raw_away_team', models.CharField(blank=True, help_text='Original scraped away team name', max_length=200)),
|
||||
('raw_stadium', models.CharField(blank=True, help_text='Original scraped stadium name', max_length=200)),
|
||||
('source_url', models.URLField(blank=True, help_text='URL where game was scraped from')),
|
||||
('created_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('updated_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('sport', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.sport')),
|
||||
('stadium', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.stadium')),
|
||||
('away_team', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.team')),
|
||||
('home_team', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.team')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Game',
|
||||
'verbose_name_plural': 'historical Games',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='conference',
|
||||
unique_together={('sport', 'name')},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StadiumAlias',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('alias', models.CharField(help_text='The alias text to match against', max_length=200)),
|
||||
('alias_type', models.CharField(choices=[('official', 'Official Name'), ('former', 'Former Name'), ('nickname', 'Nickname'), ('abbreviation', 'Abbreviation')], default='official', max_length=20)),
|
||||
('valid_from', models.DateField(blank=True, help_text='Date from which this alias is valid (inclusive)', null=True)),
|
||||
('valid_until', models.DateField(blank=True, help_text='Date until which this alias is valid (inclusive)', null=True)),
|
||||
('is_primary', models.BooleanField(default=False, help_text='Whether this is the current/primary name')),
|
||||
('source', models.CharField(blank=True, help_text='Source of this alias', max_length=200)),
|
||||
('notes', models.TextField(blank=True, help_text='Notes about this alias (e.g., naming rights deal)')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('stadium', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='core.stadium')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Stadium Alias',
|
||||
'verbose_name_plural': 'Stadium Aliases',
|
||||
'ordering': ['stadium', '-valid_from'],
|
||||
'indexes': [models.Index(fields=['alias'], name='core_stadiu_alias_7984d4_idx'), models.Index(fields=['stadium', 'valid_from', 'valid_until'], name='core_stadiu_stadium_d38e1b_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Game',
|
||||
fields=[
|
||||
('id', models.CharField(help_text='Canonical ID (e.g., game_nba_2025_20251022_bos_lal)', max_length=100, primary_key=True, serialize=False)),
|
||||
('season', models.PositiveSmallIntegerField(help_text='Season start year (e.g., 2025 for 2025-26 season)')),
|
||||
('game_date', models.DateTimeField(help_text='Game date and time (UTC)')),
|
||||
('game_number', models.PositiveSmallIntegerField(blank=True, help_text='Game number for doubleheaders (1 or 2)', null=True)),
|
||||
('home_score', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||
('away_score', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('in_progress', 'In Progress'), ('final', 'Final'), ('postponed', 'Postponed'), ('cancelled', 'Cancelled'), ('suspended', 'Suspended')], default='scheduled', max_length=20)),
|
||||
('is_neutral_site', models.BooleanField(default=False, help_text='Whether game is at neutral site')),
|
||||
('is_playoff', models.BooleanField(default=False, help_text='Whether this is a playoff game')),
|
||||
('playoff_round', models.CharField(blank=True, help_text='Playoff round (e.g., Finals, Conference Finals)', max_length=50)),
|
||||
('raw_home_team', models.CharField(blank=True, help_text='Original scraped home team name', max_length=200)),
|
||||
('raw_away_team', models.CharField(blank=True, help_text='Original scraped away team name', max_length=200)),
|
||||
('raw_stadium', models.CharField(blank=True, help_text='Original scraped stadium name', max_length=200)),
|
||||
('source_url', models.URLField(blank=True, help_text='URL where game was scraped from')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('sport', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='games', to='core.sport')),
|
||||
('stadium', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='games', to='core.stadium')),
|
||||
('away_team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='away_games', to='core.team')),
|
||||
('home_team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='home_games', to='core.team')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Game',
|
||||
'verbose_name_plural': 'Games',
|
||||
'ordering': ['-game_date', 'sport'],
|
||||
'indexes': [models.Index(fields=['sport', 'season'], name='core_game_sport_i_67c5c8_idx'), models.Index(fields=['sport', 'game_date'], name='core_game_sport_i_db4971_idx'), models.Index(fields=['home_team', 'season'], name='core_game_home_te_9b45c7_idx'), models.Index(fields=['away_team', 'season'], name='core_game_away_te_c8e42f_idx'), models.Index(fields=['status'], name='core_game_status_249a25_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamAlias',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('alias', models.CharField(help_text='The alias text to match against', max_length=200)),
|
||||
('alias_type', models.CharField(choices=[('full_name', 'Full Name'), ('city_name', 'City + Name'), ('abbreviation', 'Abbreviation'), ('nickname', 'Nickname'), ('historical', 'Historical Name')], default='full_name', max_length=20)),
|
||||
('valid_from', models.DateField(blank=True, help_text='Date from which this alias is valid (inclusive)', null=True)),
|
||||
('valid_until', models.DateField(blank=True, help_text='Date until which this alias is valid (inclusive)', null=True)),
|
||||
('is_primary', models.BooleanField(default=False, help_text='Whether this is a primary/preferred alias')),
|
||||
('source', models.CharField(blank=True, help_text='Source of this alias (e.g., ESPN, Basketball-Reference)', max_length=200)),
|
||||
('notes', models.TextField(blank=True, help_text='Notes about this alias (e.g., relocation details)')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='core.team')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Team Alias',
|
||||
'verbose_name_plural': 'Team Aliases',
|
||||
'ordering': ['team', '-valid_from'],
|
||||
'indexes': [models.Index(fields=['alias'], name='core_teamal_alias_a89339_idx'), models.Index(fields=['team', 'valid_from', 'valid_until'], name='core_teamal_team_id_e29cea_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
53
core/migrations/0002_conference_division_canonical_id.py
Executable file
53
core/migrations/0002_conference_division_canonical_id.py
Executable file
@@ -0,0 +1,53 @@
|
||||
# Generated manually
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='conference',
|
||||
name='canonical_id',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text='Canonical ID from bootstrap JSON (e.g., nba_eastern)',
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='division',
|
||||
name='canonical_id',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text='Canonical ID from bootstrap JSON (e.g., nba_southeast)',
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalconference',
|
||||
name='canonical_id',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text='Canonical ID from bootstrap JSON (e.g., nba_eastern)',
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicaldivision',
|
||||
name='canonical_id',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text='Canonical ID from bootstrap JSON (e.g., nba_southeast)',
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
]
|
||||
21
core/migrations/0003_sport_icon_name_color_hex.py
Normal file
21
core/migrations/0003_sport_icon_name_color_hex.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_conference_division_canonical_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sport',
|
||||
name='icon_name',
|
||||
field=models.CharField(blank=True, help_text='SF Symbol name (e.g., baseball.fill, basketball.fill)', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sport',
|
||||
name='color_hex',
|
||||
field=models.CharField(blank=True, help_text='Brand color hex (e.g., #CE1141)', max_length=10),
|
||||
),
|
||||
]
|
||||
0
core/migrations/__init__.py
Executable file
0
core/migrations/__init__.py
Executable file
Reference in New Issue
Block a user