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:
119
notifications/admin.py
Normal file
119
notifications/admin.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from simple_history.admin import SimpleHistoryAdmin
|
||||
|
||||
from .models import EmailConfiguration, EmailLog
|
||||
|
||||
|
||||
@admin.register(EmailConfiguration)
|
||||
class EmailConfigurationAdmin(SimpleHistoryAdmin):
|
||||
list_display = [
|
||||
'name',
|
||||
'is_enabled_badge',
|
||||
'recipient_count',
|
||||
'notify_on_scrape_complete',
|
||||
'notify_on_scrape_failure',
|
||||
'notify_on_sync_failure',
|
||||
]
|
||||
list_filter = ['is_enabled']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
fieldsets = [
|
||||
(None, {
|
||||
'fields': ['name', 'is_enabled']
|
||||
}),
|
||||
('Recipients', {
|
||||
'fields': ['recipient_emails']
|
||||
}),
|
||||
('Scraper Notifications', {
|
||||
'fields': [
|
||||
'notify_on_scrape_complete',
|
||||
'notify_on_scrape_failure',
|
||||
'notify_on_new_reviews',
|
||||
]
|
||||
}),
|
||||
('CloudKit Sync Notifications', {
|
||||
'fields': [
|
||||
'notify_on_sync_complete',
|
||||
'notify_on_sync_failure',
|
||||
]
|
||||
}),
|
||||
('Thresholds', {
|
||||
'fields': ['min_games_for_notification']
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ['created_at', 'updated_at'],
|
||||
'classes': ['collapse']
|
||||
}),
|
||||
]
|
||||
|
||||
actions = ['send_test_email']
|
||||
|
||||
def is_enabled_badge(self, obj):
|
||||
if obj.is_enabled:
|
||||
return format_html('<span style="color: green;">● Enabled</span>')
|
||||
return format_html('<span style="color: gray;">○ Disabled</span>')
|
||||
is_enabled_badge.short_description = 'Status'
|
||||
|
||||
def recipient_count(self, obj):
|
||||
return len(obj.get_recipients())
|
||||
recipient_count.short_description = 'Recipients'
|
||||
|
||||
@admin.action(description='Send test email')
|
||||
def send_test_email(self, request, queryset):
|
||||
from notifications.tasks import send_test_notification
|
||||
for config in queryset:
|
||||
send_test_notification.delay(config.id)
|
||||
self.message_user(request, f'Test emails queued for {queryset.count()} configurations.')
|
||||
|
||||
|
||||
@admin.register(EmailLog)
|
||||
class EmailLogAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'subject',
|
||||
'status_badge',
|
||||
'recipients_display',
|
||||
'created_at',
|
||||
]
|
||||
list_filter = ['status', 'created_at']
|
||||
search_fields = ['subject', 'recipients']
|
||||
date_hierarchy = 'created_at'
|
||||
ordering = ['-created_at']
|
||||
readonly_fields = [
|
||||
'configuration',
|
||||
'subject',
|
||||
'recipients',
|
||||
'body_preview',
|
||||
'status',
|
||||
'error_message',
|
||||
'scrape_job',
|
||||
'sync_job',
|
||||
'created_at',
|
||||
]
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def status_badge(self, obj):
|
||||
colors = {
|
||||
'sent': '#5cb85c',
|
||||
'failed': '#d9534f',
|
||||
}
|
||||
color = colors.get(obj.status, '#999')
|
||||
return format_html(
|
||||
'<span style="background-color: {}; color: white; padding: 3px 8px; '
|
||||
'border-radius: 3px; font-size: 11px;">{}</span>',
|
||||
color,
|
||||
obj.status.upper()
|
||||
)
|
||||
status_badge.short_description = 'Status'
|
||||
|
||||
def recipients_display(self, obj):
|
||||
recipients = obj.recipients.split(',')
|
||||
if len(recipients) > 2:
|
||||
return f"{recipients[0]}, +{len(recipients)-1} more"
|
||||
return obj.recipients
|
||||
recipients_display.short_description = 'Recipients'
|
||||
Reference in New Issue
Block a user