lomda-hub/backend/app/achievements.py
2026-02-04 16:50:33 +02:00

138 lines
5.1 KiB
Python

"""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()