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:
Trey t
2026-02-19 14:04:27 -06:00
parent 4353d5943c
commit 63acf7accb
114 changed files with 13070 additions and 887 deletions

162
core/resources.py Normal file
View File

@@ -0,0 +1,162 @@
"""Import/Export resources for core models."""
from import_export import resources, fields
from import_export.widgets import ForeignKeyWidget
from .models import Sport, Conference, Division, Team, Stadium, Game, TeamAlias, StadiumAlias
class SportResource(resources.ModelResource):
class Meta:
model = Sport
import_id_fields = ['code']
fields = [
'code', 'name', 'short_name', 'season_type',
'season_start_month', 'season_end_month',
'expected_game_count', 'is_active',
]
export_order = fields
class ConferenceResource(resources.ModelResource):
sport = fields.Field(
column_name='sport',
attribute='sport',
widget=ForeignKeyWidget(Sport, 'code')
)
class Meta:
model = Conference
import_id_fields = ['sport', 'name']
fields = ['sport', 'canonical_id', 'name', 'short_name', 'order']
export_order = fields
class DivisionResource(resources.ModelResource):
conference = fields.Field(
column_name='conference',
attribute='conference',
widget=ForeignKeyWidget(Conference, 'name')
)
sport = fields.Field(attribute='conference__sport__code', readonly=True)
class Meta:
model = Division
import_id_fields = ['conference', 'name']
fields = ['sport', 'conference', 'canonical_id', 'name', 'short_name', 'order']
export_order = fields
class TeamResource(resources.ModelResource):
sport = fields.Field(
column_name='sport',
attribute='sport',
widget=ForeignKeyWidget(Sport, 'code')
)
division = fields.Field(
column_name='division',
attribute='division',
widget=ForeignKeyWidget(Division, 'name')
)
home_stadium = fields.Field(
column_name='home_stadium',
attribute='home_stadium',
widget=ForeignKeyWidget(Stadium, 'name')
)
class Meta:
model = Team
import_id_fields = ['id']
fields = [
'id', 'sport', 'division', 'city', 'name', 'full_name',
'abbreviation', 'primary_color', 'secondary_color',
'logo_url', 'home_stadium', 'is_active',
]
export_order = fields
class StadiumResource(resources.ModelResource):
sport = fields.Field(
column_name='sport',
attribute='sport',
widget=ForeignKeyWidget(Sport, 'code')
)
class Meta:
model = Stadium
import_id_fields = ['id']
fields = [
'id', 'sport', 'name', 'city', 'state', 'country',
'latitude', 'longitude', 'timezone', 'capacity',
'surface', 'roof_type', 'opened_year', 'image_url',
]
export_order = fields
class GameResource(resources.ModelResource):
sport = fields.Field(
column_name='sport',
attribute='sport',
widget=ForeignKeyWidget(Sport, 'code')
)
home_team = fields.Field(
column_name='home_team',
attribute='home_team',
widget=ForeignKeyWidget(Team, 'abbreviation')
)
away_team = fields.Field(
column_name='away_team',
attribute='away_team',
widget=ForeignKeyWidget(Team, 'abbreviation')
)
stadium = fields.Field(
column_name='stadium',
attribute='stadium',
widget=ForeignKeyWidget(Stadium, 'name')
)
class Meta:
model = Game
import_id_fields = ['id']
fields = [
'id', 'sport', 'season', 'home_team', 'away_team',
'stadium', 'game_date', 'game_number', 'status',
'home_score', 'away_score', 'is_playoff', 'playoff_round',
'is_neutral_site', 'source_url',
]
export_order = fields
class TeamAliasResource(resources.ModelResource):
team = fields.Field(
column_name='team',
attribute='team',
widget=ForeignKeyWidget(Team, 'abbreviation')
)
sport = fields.Field(attribute='team__sport__code', readonly=True)
class Meta:
model = TeamAlias
import_id_fields = ['team', 'alias']
fields = [
'sport', 'team', 'alias', 'alias_type',
'valid_from', 'valid_until', 'is_primary', 'source', 'notes',
]
export_order = fields
class StadiumAliasResource(resources.ModelResource):
stadium = fields.Field(
column_name='stadium',
attribute='stadium',
widget=ForeignKeyWidget(Stadium, 'name')
)
sport = fields.Field(attribute='stadium__sport__code', readonly=True)
class Meta:
model = StadiumAlias
import_id_fields = ['stadium', 'alias']
fields = [
'sport', 'stadium', 'alias', 'alias_type',
'valid_from', 'valid_until', 'is_primary', 'source', 'notes',
]
export_order = fields