from django.shortcuts import get_object_or_404 from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.throttling import ScopedRateThrottle from rest_framework.views import APIView from .models import Character, LeaderboardEntry, StreakRecord, UserHearts, UserStats class GuestAwareReadThrottle(ScopedRateThrottle): """همون الگوی GuestAwareContentThrottle: کاربرِ مهمان (هر deviceId یک pk تازه) با IP کلید می‌خوره تا چرخشِ حساب سقفِ read رو دور نزنه.""" def get_cache_key(self, request, view): user = request.user if user and user.is_authenticated and getattr(user, 'is_guest', False): ident = f'guest-ip:{self.get_ident(request)}' elif user and user.is_authenticated: ident = user.pk else: ident = self.get_ident(request) return self.cache_format % {'scope': self.scope, 'ident': ident} from .serializers import ( CharacterListResponseSerializer, LeaderboardResponseSerializer, SelectCharacterRequestSerializer, SelectCharacterResponseSerializer, StreakCalendarResponseSerializer, UserHeartsResponseSerializer, UserSummaryResponseSerializer, ) class UserSummaryView(APIView): """GET /api/user/summary — پروفایل + آمار + استریک (ادغام‌شده)""" permission_classes = [IsAuthenticated] throttle_scope = 'read' throttle_classes = [GuestAwareReadThrottle] @extend_schema(responses=UserSummaryResponseSerializer, tags=['Progress & Gamification (progress)']) def get(self, request): user = request.user # accounts.User، از طریق TadrisinoTokenAuthentication stats = UserStats.objects.filter(user_id=user.id).first() streak = StreakRecord.objects.filter(user_id=user.id).first() data = { 'userId': str(user.id), 'name': user.name, 'avatar': user.avatar, 'gradeId': user.grade_id, 'xp': stats.xp if stats else 0, 'coins': stats.coins if stats else 0, 'streak': { 'current': streak.current_streak if streak else 0, 'longest': streak.longest_streak if streak else 0, }, # اپ subscription هنوز در این فاز پیاده‌سازی نشده؛ فعلاً null. 'subscription': None, } return Response(UserSummaryResponseSerializer(data).data) class StreakCalendarView(APIView): """GET /api/user/streak/calendar — تقویم کامل روزهای فعال""" permission_classes = [IsAuthenticated] throttle_scope = 'read' throttle_classes = [GuestAwareReadThrottle] @extend_schema(responses=StreakCalendarResponseSerializer, tags=['Progress & Gamification (progress)']) def get(self, request): user = request.user streak = StreakRecord.objects.filter(user_id=user.id).first() data = { 'currentStreak': streak.current_streak if streak else 0, 'longestStreak': streak.longest_streak if streak else 0, 'activeDays': list(streak.active_days.values_list('date', flat=True)) if streak else [], } return Response(StreakCalendarResponseSerializer(data).data) class LeaderboardView(APIView): """GET /api/leaderboard?period= — رتبه‌بندی هفتگی/ماهانه""" throttle_scope = 'read' throttle_classes = [GuestAwareReadThrottle] @extend_schema( parameters=[OpenApiParameter('period', OpenApiTypes.STR, required=False, enum=['weekly', 'all_time'])], responses=LeaderboardResponseSerializer, tags=['Progress & Gamification (progress)'], ) def get(self, request): period = request.query_params.get('period', 'weekly') entries = LeaderboardEntry.objects.filter(period=period).order_by('rank')[:50] # نام کاربر از accounts.User خونده می‌شه (فقط برای نمایش، بدون FK واقعی). from accounts.models import User as AccountUser user_ids = [e.user_id for e in entries] names = {u.id: u.name for u in AccountUser.objects.filter(id__in=user_ids)} data = { 'period': period, 'entries': [ { 'rank': e.rank, 'userId': str(e.user_id), 'name': names.get(e.user_id, ''), 'xpInPeriod': e.xp_in_period, } for e in entries ], } return Response(LeaderboardResponseSerializer(data).data) class UserHeartsView(APIView): """GET /api/user/hearts — وضعیت فعلی جان‌ها (هارت)""" permission_classes = [IsAuthenticated] @extend_schema(responses=UserHeartsResponseSerializer, tags=['Progress & Gamification (progress)']) def get(self, request): from subscription.services import has_unlimited_hearts user = request.user hearts, _ = UserHearts.objects.get_or_create(user_id=user.id) if hearts.apply_regen(): hearts.save(update_fields=['current_hearts', 'next_refill_at']) unlimited = has_unlimited_hearts(user.id) data = { 'currentHearts': hearts.max_hearts if unlimited else hearts.current_hearts, 'maxHearts': hearts.max_hearts, 'nextRefillAt': None if unlimited else hearts.next_refill_at, 'isUnlimited': unlimited, } return Response(UserHeartsResponseSerializer(data).data) class CharacterListView(APIView): """ GET /api/characters — کاتالوگِ کاراکترهای گیمیفیکیشن (مثلِ ماسکات‌های Duolingo)، به‌همراه این‌که کدوم برای کاربرِ فعلی باز شده (unlock_xp <= XP کاربر) و کدوم انتخاب‌شده‌ست. """ permission_classes = [IsAuthenticated] throttle_scope = 'read' throttle_classes = [GuestAwareReadThrottle] @extend_schema(responses=CharacterListResponseSerializer, tags=['Progress & Gamification (progress)']) def get(self, request): stats = UserStats.objects.filter(user_id=request.user.id).first() xp = stats.xp if stats else 0 selected_id = stats.selected_character_id if stats else None data = { 'characters': [ { 'characterId': str(c.id), 'slug': c.slug, 'name': c.name, 'icon': c.icon, 'unlockXp': c.unlock_xp, 'unlocked': xp >= c.unlock_xp, 'selected': c.id == selected_id, } for c in Character.objects.all() ], } return Response(CharacterListResponseSerializer(data).data) class SelectCharacterView(APIView): """POST /api/characters/select — انتخابِ کاراکترِ فعال؛ فقط کاراکترِ باز‌شده قابلِ انتخابه.""" permission_classes = [IsAuthenticated] throttle_scope = 'user_profile_write' throttle_classes = [ScopedRateThrottle] @extend_schema( request=SelectCharacterRequestSerializer, responses=SelectCharacterResponseSerializer, tags=['Progress & Gamification (progress)'], ) def post(self, request): serializer = SelectCharacterRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) character = get_object_or_404(Character, pk=serializer.validated_data['characterId']) stats, _ = UserStats.objects.get_or_create(user_id=request.user.id) if stats.xp < character.unlock_xp: raise ValidationError('این کاراکتر هنوز باز نشده است.') stats.selected_character_id = character.id stats.save(update_fields=['selected_character_id']) data = {'characterId': str(character.id), 'selected': True} return Response(SelectCharacterResponseSerializer(data).data)