"""Achievement evaluation and awarding logic""" from datetime import datetime, timedelta from sqlalchemy.orm import Session from sqlalchemy import func, distinct from app.models import ( User, Achievement, UserAchievement, QuizAttempt, LoginEvent, Enrollment, ModuleProgress, Course, Module, ProgressStatus, ) def check_and_award_achievements(db: Session, user_id: int): """Check and award all eligible achievements for a user""" user = db.get(User, user_id) if not user: return # Get all achievements all_achievements = db.query(Achievement).all() existing_user_achievements = { ua.achievement_id for ua in db.query(UserAchievement).filter(UserAchievement.user_id == user_id).all() } for achievement in all_achievements: if achievement.id in existing_user_achievements: continue earned = False if achievement.code == "FIRST_COURSE_COMPLETE": # Check if user has completed any course completed_count = ( db.query(func.count(distinct(ModuleProgress.module_id))) .join(Module, Module.id == ModuleProgress.module_id) .filter( ModuleProgress.user_id == user_id, ModuleProgress.status == ProgressStatus.completed, ) .scalar() ) # Check if all modules in at least one course are completed user_courses = ( db.query(Course.id) .join(Enrollment, Enrollment.course_id == Course.id) .filter(Enrollment.user_id == user_id) .all() ) for (course_id,) in user_courses: total_modules = db.query(func.count(Module.id)).filter(Module.course_id == course_id).scalar() completed_modules = ( db.query(func.count(Module.id)) .join(ModuleProgress, ModuleProgress.module_id == Module.id) .filter( Module.course_id == course_id, ModuleProgress.user_id == user_id, ModuleProgress.status == ProgressStatus.completed, ) .scalar() ) if total_modules > 0 and completed_modules >= total_modules: earned = True break elif achievement.code == "PERFECT_FINAL": # Check if any quiz attempt has score 100 perfect = ( db.query(QuizAttempt) .filter(QuizAttempt.user_id == user_id, QuizAttempt.score == 100.0) .first() ) earned = perfect is not None elif achievement.code == "THREE_PERFECTS_ROW": # Check last 3 attempts for consecutive 100s attempts = ( db.query(QuizAttempt) .filter(QuizAttempt.user_id == user_id) .order_by(QuizAttempt.submitted_at.desc()) .limit(3) .all() ) if len(attempts) >= 3 and all(a.score == 100.0 for a in attempts): earned = True elif achievement.code == "FAST_FINISHER": # Check if any course was completed within 24h of enrollment user_enrollments = ( db.query(Enrollment) .filter(Enrollment.user_id == user_id) .all() ) for enrollment in user_enrollments: total_modules = ( db.query(func.count(Module.id)) .filter(Module.course_id == enrollment.course_id) .scalar() ) completed_modules = ( db.query(ModuleProgress) .join(Module, Module.id == ModuleProgress.module_id) .filter( Module.course_id == enrollment.course_id, ModuleProgress.user_id == user_id, ModuleProgress.status == ProgressStatus.completed, ) .all() ) if total_modules > 0 and len(completed_modules) >= total_modules: # Check if last completion was within 24h of enrollment last_completion = max(m.completed_at for m in completed_modules if m.completed_at) if last_completion and (last_completion - enrollment.enrolled_at) <= timedelta(hours=24): earned = True break elif achievement.code == "CONSISTENT_LEARNER": # Check if user has logged in on 5 different days distinct_days = ( db.query(func.date(LoginEvent.logged_in_at)) .filter(LoginEvent.user_id == user_id) .distinct() .count() ) earned = distinct_days >= 5 if earned: db.add(UserAchievement(user_id=user_id, achievement_id=achievement.id)) db.commit()