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