import asyncio import logging from datetime import datetime from apscheduler.schedulers.background import BackgroundScheduler from app.database import SessionLocal from app.models.reminder import ReminderSchedule from app.models.user import User from app.models.quiz import Quiz from app.models.attempt import QuizAttempt logger = logging.getLogger(__name__) scheduler = BackgroundScheduler() def check_and_send_reminders(): """Check for due reminders and send emails.""" db = SessionLocal() try: due_reminders = db.query(ReminderSchedule).filter( ReminderSchedule.is_active == True, ReminderSchedule.next_reminder_at <= datetime.utcnow(), ).all() if not due_reminders: return logger.info(f"Found {len(due_reminders)} due reminders") for reminder in due_reminders: user = db.query(User).filter(User.id == reminder.user_id).first() quiz = db.query(Quiz).filter(Quiz.id == reminder.quiz_id).first() if not user or not quiz or quiz.deleted_at or quiz.course_id: reminder.is_active = False continue # Deactivate if the user has no completed attempts left for this quiz # (e.g., they deleted their attempts — spaced repetition no longer applies). has_completed_attempt = db.query(QuizAttempt.id).filter( QuizAttempt.user_id == user.id, QuizAttempt.quiz_id == quiz.id, QuizAttempt.completed_at.isnot(None), ).first() is not None if not has_completed_attempt: reminder.is_active = False continue # Respect the user's opt-out preference (canonical source: Postgres). if user.reminders_disabled: continue # Send email asynchronously try: from app.services.email_service import send_reminder_email loop = asyncio.new_event_loop() loop.run_until_complete( send_reminder_email( email=user.email, user_name=user.name, quiz_title=quiz.title, score=reminder.performance_score, next_date=reminder.next_reminder_at.strftime("%Y-%m-%d"), ) ) loop.close() except Exception as e: logger.error(f"Failed to send reminder {reminder.id}: {e}") # Schedule next reminder from datetime import timedelta reminder.next_reminder_at = datetime.utcnow() + timedelta(days=reminder.interval_days) db.commit() except Exception as e: logger.exception(f"Scheduler error: {e}") finally: db.close() def start_scheduler(): """Start the APScheduler with daily reminder check.""" scheduler.add_job( check_and_send_reminders, "interval", hours=24, id="reminder_check", replace_existing=True, ) scheduler.start() logger.info("Scheduler started — reminder check runs every 24 hours") def stop_scheduler(): if scheduler.running: scheduler.shutdown()