from __future__ import annotations import json from datetime import datetime from typing import List, Optional from sqlalchemy import func from sqlalchemy.orm import Session from app.models.achievement import Achievement, AchievementType, UserAchievement from app.models.progress import ProgressStatus, UserProgress from app.schemas.achievement import AchievementCreate, AchievementUpdate, UserAchievementCreate from app.services.user import user as user_service from app.utils.db import CRUDBase class CRUDAchievement(CRUDBase[Achievement, AchievementCreate, AchievementUpdate]): def get_by_type( self, db: Session, *, achievement_type: AchievementType, skip: int = 0, limit: int = 100 ) -> List[Achievement]: """ Get achievements by type. """ return ( db.query(Achievement) .filter(Achievement.type == achievement_type) .offset(skip) .limit(limit) .all() ) def get_active(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[Achievement]: """ Get active achievements. """ return ( db.query(Achievement) .filter(Achievement.is_active == True) .offset(skip) .limit(limit) .all() ) class CRUDUserAchievement(CRUDBase[UserAchievement, UserAchievementCreate, UserAchievementCreate]): def get_by_user( self, db: Session, *, user_id: int, skip: int = 0, limit: int = 100 ) -> List[UserAchievement]: """ Get a user's achievements. """ return ( db.query(UserAchievement) .filter(UserAchievement.user_id == user_id) .offset(skip) .limit(limit) .all() ) def get_by_user_achievement( self, db: Session, *, user_id: int, achievement_id: int ) -> Optional[UserAchievement]: """ Get a specific user achievement. """ return ( db.query(UserAchievement) .filter( UserAchievement.user_id == user_id, UserAchievement.achievement_id == achievement_id ) .first() ) def award_achievement( self, db: Session, *, user_id: int, achievement_id: int ) -> UserAchievement: """ Award an achievement to a user. """ # Check if user already has this achievement existing = self.get_by_user_achievement(db, user_id=user_id, achievement_id=achievement_id) if existing: return existing # Get the achievement to award points achievement = db.query(Achievement).filter(Achievement.id == achievement_id).first() # Create new user achievement user_achievement = UserAchievement( user_id=user_id, achievement_id=achievement_id, earned_at=datetime.utcnow() ) db.add(user_achievement) # Award points to user if achievement and achievement.points > 0: user_service.add_points(db, user_id=user_id, points=achievement.points) db.commit() db.refresh(user_achievement) return user_achievement def check_achievements(self, db: Session, *, user_id: int) -> List[UserAchievement]: """ Check and award achievements based on user's progress. """ # Get all active achievements active_achievements = db.query(Achievement).filter(Achievement.is_active == True).all() # Get user's current achievements user_achievements = ( db.query(UserAchievement.achievement_id) .filter(UserAchievement.user_id == user_id) .all() ) already_earned = [ua[0] for ua in user_achievements] # Get user data for achievement criteria user = user_service.get(db, id=user_id) # Counters for various achievement criteria completed_lessons_count = ( db.query(func.count(UserProgress.id)) .filter( UserProgress.user_id == user_id, UserProgress.status == ProgressStatus.COMPLETED ) .scalar() or 0 ) # List to store newly awarded achievements newly_awarded = [] # Check each achievement for achievement in active_achievements: # Skip if already earned if achievement.id in already_earned: continue # Parse criteria try: criteria = json.loads(achievement.criteria) except json.JSONDecodeError: # Skip if criteria is invalid JSON continue # Check different types of achievements awarded = False if achievement.type == AchievementType.COMPLETION: # Completion achievements (e.g., complete X lessons) if criteria.get("type") == "lessons_completed" and "count" in criteria: if completed_lessons_count >= criteria["count"]: awarded = True elif achievement.type == AchievementType.MILESTONE: # Milestone achievements (e.g., reach level X, earn Y points) if criteria.get("type") == "reach_level" and "level" in criteria: if user.level >= criteria["level"]: awarded = True elif criteria.get("type") == "earn_points" and "points" in criteria: if user.points >= criteria["points"]: awarded = True # Award the achievement if criteria met if awarded: user_achievement = self.award_achievement( db, user_id=user_id, achievement_id=achievement.id ) newly_awarded.append(user_achievement) return newly_awarded achievement = CRUDAchievement(Achievement) user_achievement = CRUDUserAchievement(UserAchievement)