Files
SportstimeAPI/notifications/tasks.py
Trey t 63acf7accb 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>
2026-02-19 14:04:27 -06:00

241 lines
7.4 KiB
Python

import logging
from celery import shared_task
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.conf import settings
logger = logging.getLogger('notifications')
@shared_task
def send_scrape_notification(job_id: int):
"""
Send email notification after scraper job.
"""
from scraper.models import ScrapeJob, ManualReviewItem
from notifications.models import EmailConfiguration, EmailLog
try:
job = ScrapeJob.objects.select_related('config__sport').get(id=job_id)
except ScrapeJob.DoesNotExist:
logger.error(f"ScrapeJob {job_id} not found")
return
# Get email configuration
config = EmailConfiguration.objects.filter(is_enabled=True).first()
if not config:
logger.info("No email configuration enabled")
return
# Check if we should send based on configuration
if job.status == 'completed' and not config.notify_on_scrape_complete:
return
if job.status == 'failed' and not config.notify_on_scrape_failure:
return
# Check minimum games threshold
total_changes = job.games_new + job.games_updated
if job.status == 'completed' and total_changes < config.min_games_for_notification:
logger.info(f"Skipping notification: {total_changes} changes below threshold {config.min_games_for_notification}")
return
# Get review items if configured
review_items = []
if config.notify_on_new_reviews and job.review_items_created > 0:
review_items = list(
ManualReviewItem.objects.filter(job=job)
.values('raw_value', 'item_type', 'suggested_id', 'confidence', 'reason')[:10]
)
# Build context
context = {
'job': job,
'sport': job.config.sport,
'season_display': job.config.sport.get_season_display(job.config.season),
'review_items': review_items,
'suggested_actions': get_suggested_actions(job),
}
# Render email
subject = f"[SportsTime] {job.config.sport.short_name} Scraper: {job.status.upper()}"
if job.status == 'completed':
subject = f"[SportsTime] {job.config.sport.short_name}: {job.games_new} new, {job.games_updated} updated"
html_body = render_to_string('notifications/emails/scrape_report.html', context)
text_body = render_to_string('notifications/emails/scrape_report.txt', context)
# Send email
recipients = config.get_recipients()
try:
send_mail(
subject=subject,
message=text_body,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=recipients,
html_message=html_body,
fail_silently=False,
)
# Log success
EmailLog.objects.create(
configuration=config,
subject=subject,
recipients=','.join(recipients),
body_preview=text_body[:500],
status='sent',
scrape_job=job,
)
logger.info(f"Sent scrape notification for job {job_id}")
except Exception as e:
# Log failure
EmailLog.objects.create(
configuration=config,
subject=subject,
recipients=','.join(recipients),
body_preview=text_body[:500],
status='failed',
error_message=str(e),
scrape_job=job,
)
logger.error(f"Failed to send scrape notification: {e}")
@shared_task
def send_sync_notification(job_id: int):
"""
Send email notification after CloudKit sync.
"""
from cloudkit.models import CloudKitSyncJob
from notifications.models import EmailConfiguration, EmailLog
try:
job = CloudKitSyncJob.objects.select_related('configuration').get(id=job_id)
except CloudKitSyncJob.DoesNotExist:
logger.error(f"CloudKitSyncJob {job_id} not found")
return
# Get email configuration
config = EmailConfiguration.objects.filter(is_enabled=True).first()
if not config:
return
# Check if we should send
if job.status == 'completed' and not config.notify_on_sync_complete:
return
if job.status == 'failed' and not config.notify_on_sync_failure:
return
# Build email
subject = f"[SportsTime] CloudKit Sync: {job.status.upper()}"
if job.status == 'completed':
subject = f"[SportsTime] CloudKit Sync: {job.records_synced} records"
context = {
'job': job,
}
html_body = render_to_string('notifications/emails/sync_report.html', context)
text_body = render_to_string('notifications/emails/sync_report.txt', context)
recipients = config.get_recipients()
try:
send_mail(
subject=subject,
message=text_body,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=recipients,
html_message=html_body,
fail_silently=False,
)
EmailLog.objects.create(
configuration=config,
subject=subject,
recipients=','.join(recipients),
body_preview=text_body[:500],
status='sent',
sync_job=job,
)
except Exception as e:
EmailLog.objects.create(
configuration=config,
subject=subject,
recipients=','.join(recipients),
body_preview=text_body[:500],
status='failed',
error_message=str(e),
sync_job=job,
)
logger.error(f"Failed to send sync notification: {e}")
@shared_task
def send_test_notification(config_id: int):
"""
Send a test notification email.
"""
from notifications.models import EmailConfiguration, EmailLog
try:
config = EmailConfiguration.objects.get(id=config_id)
except EmailConfiguration.DoesNotExist:
return
subject = "[SportsTime] Test Notification"
body = "This is a test notification from SportsTime.\n\nIf you received this, email notifications are working correctly."
recipients = config.get_recipients()
try:
send_mail(
subject=subject,
message=body,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=recipients,
fail_silently=False,
)
EmailLog.objects.create(
configuration=config,
subject=subject,
recipients=','.join(recipients),
body_preview=body,
status='sent',
)
logger.info(f"Sent test notification to {recipients}")
except Exception as e:
EmailLog.objects.create(
configuration=config,
subject=subject,
recipients=','.join(recipients),
body_preview=body,
status='failed',
error_message=str(e),
)
logger.error(f"Failed to send test notification: {e}")
def get_suggested_actions(job):
"""
Generate suggested actions based on job results.
"""
actions = []
if job.review_items_created > 0:
actions.append(f"Review {job.review_items_created} items in the review queue")
if job.games_errors > 0:
actions.append(f"Investigate {job.games_errors} game processing errors")
if job.status == 'failed':
actions.append("Check scraper logs for error details")
actions.append("Verify data source availability")
if job.games_found == 0 and job.status == 'completed':
actions.append("Verify scraper configuration and season dates")
return actions