- FastAPI backend with JWT auth, roles (admin/moderator/user) - PDF upload (up to 500MB) with streaming, PyMuPDF text extraction - ChromaDB vectorization per page with metadata - LiteLLM AI question extraction from PDF (not generation) - Image extraction from PDF pages, graceful fallback - Quiz modes: timed (countdown timer) + learning (answers shown inline) - Page-by-page question navigation with dot navigator - TTS endpoint using LiteLLM (Google Vertex / OpenAI voices) - Admin dashboard: AI model management per task, user role management - Moderator role: upload PDFs, create sections, generate quizzes - Spaced repetition reminders via SMTP email (SM-2 intervals) - APScheduler daily reminder jobs - Celery + Redis for background PDF processing - React frontend with all pages - Docker Compose deployment (nginx + backend + celery + redis) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
82 lines
2.5 KiB
Python
82 lines
2.5 KiB
Python
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
|
|
|
|
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:
|
|
reminder.is_active = False
|
|
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()
|