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>
90 lines
2.7 KiB
Python
90 lines
2.7 KiB
Python
from django.contrib import admin
|
|
from django.utils.html import format_html
|
|
from import_export.admin import ImportExportMixin
|
|
from simple_history.admin import SimpleHistoryAdmin
|
|
|
|
from core.models import Stadium, StadiumAlias
|
|
from core.resources import StadiumResource
|
|
|
|
|
|
class StadiumAliasInline(admin.TabularInline):
|
|
model = StadiumAlias
|
|
extra = 0
|
|
fields = ['alias', 'alias_type', 'valid_from', 'valid_until', 'is_primary']
|
|
ordering = ['-valid_from']
|
|
|
|
|
|
@admin.register(Stadium)
|
|
class StadiumAdmin(ImportExportMixin, SimpleHistoryAdmin):
|
|
resource_class = StadiumResource
|
|
list_display = [
|
|
'name',
|
|
'sport',
|
|
'location_display',
|
|
'capacity_display',
|
|
'surface',
|
|
'roof_type',
|
|
'opened_year',
|
|
'home_team_count',
|
|
'alias_count',
|
|
]
|
|
list_filter = ['sport', 'country', 'surface', 'roof_type']
|
|
search_fields = ['id', 'name', 'city', 'state']
|
|
ordering = ['sport', 'city', 'name']
|
|
readonly_fields = ['id', 'created_at', 'updated_at', 'map_link']
|
|
inlines = [StadiumAliasInline]
|
|
|
|
fieldsets = [
|
|
(None, {
|
|
'fields': ['id', 'sport', 'name']
|
|
}),
|
|
('Location', {
|
|
'fields': ['city', 'state', 'country', 'timezone']
|
|
}),
|
|
('Coordinates', {
|
|
'fields': ['latitude', 'longitude', 'map_link']
|
|
}),
|
|
('Venue Details', {
|
|
'fields': ['capacity', 'surface', 'roof_type', 'opened_year']
|
|
}),
|
|
('Media', {
|
|
'fields': ['image_url']
|
|
}),
|
|
('Metadata', {
|
|
'fields': ['created_at', 'updated_at'],
|
|
'classes': ['collapse']
|
|
}),
|
|
]
|
|
|
|
def location_display(self, obj):
|
|
return obj.location
|
|
location_display.short_description = 'Location'
|
|
|
|
def capacity_display(self, obj):
|
|
if obj.capacity:
|
|
return f"{obj.capacity:,}"
|
|
return '-'
|
|
capacity_display.short_description = 'Capacity'
|
|
|
|
def home_team_count(self, obj):
|
|
return obj.home_teams.count()
|
|
home_team_count.short_description = 'Teams'
|
|
|
|
def alias_count(self, obj):
|
|
return obj.aliases.count()
|
|
alias_count.short_description = 'Aliases'
|
|
|
|
def map_link(self, obj):
|
|
if obj.latitude and obj.longitude:
|
|
return format_html(
|
|
'<a href="https://www.google.com/maps?q={},{}" target="_blank">View on Google Maps</a>',
|
|
obj.latitude, obj.longitude
|
|
)
|
|
return '-'
|
|
map_link.short_description = 'Map'
|
|
|
|
def get_search_results(self, request, queryset, search_term):
|
|
"""Enable autocomplete search."""
|
|
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
|
|
return queryset, use_distinct
|