- Add generic OIDC/SSO support (configurable via env vars) - Admin can enable SSO-only mode (disables password login) - SSO callback auto-creates and verifies users - Login page shows SSO button when configured, hides password form in SSO-only mode - Fix reminders: skip course quizzes and deleted quizzes - Don't create reminders for course quiz attempts - Add user reminder opt-out toggle in Settings > Notifications - Scheduler checks user opt-out before sending emails Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
93 lines
3 KiB
Python
93 lines
3 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 or quiz.deleted_at or quiz.course_id:
|
|
reminder.is_active = False
|
|
continue
|
|
|
|
# Check if user has opted out of reminders
|
|
try:
|
|
import redis as redis_lib, json as _json
|
|
from app.config import settings as cfg
|
|
r = redis_lib.from_url(cfg.REDIS_URL, decode_responses=True, socket_connect_timeout=1)
|
|
user_settings = _json.loads(r.get(f"user_settings:{user.id}") or "{}")
|
|
if user_settings.get("reminders_disabled"):
|
|
continue
|
|
except Exception:
|
|
pass
|
|
|
|
# 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()
|