from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework import permissions, status from rest_framework.response import Response from rest_framework.views import APIView from apps.roles.models import Role, UserTargetRole from .services import ( DEFAULT_LIMIT_PER_SKILL, MAX_LIMIT_PER_SKILL, compute_gap_report, compute_recommendations, suggest_roles, ) _ROLE_ID_PARAM = OpenApiParameter( name='role_id', type=int, location=OpenApiParameter.QUERY, required=False, description='Role to analyze. Defaults to the user\'s active target role.', ) def _resolve_role(request): """Return (role, error_response). role_id query param overrides active target.""" role_id = ( request.query_params.get('role_id') or request.query_params.get('role') ) if role_id: try: role = Role.objects.get(id=role_id, is_active=True) except (Role.DoesNotExist, ValueError): return None, Response( {'detail': 'Role not found or inactive.'}, status=status.HTTP_404_NOT_FOUND, ) return role, None try: target = UserTargetRole.objects.select_related('role').get( user=request.user, is_active=True ) except UserTargetRole.DoesNotExist: return None, Response( {'detail': 'No active target role. Select a target role first.'}, status=status.HTTP_400_BAD_REQUEST, ) return target.role, None @extend_schema( parameters=[_ROLE_ID_PARAM], responses=OpenApiTypes.OBJECT, description='Weighted competency-fulfillment gap report for the user against a role.', ) class GapAnalysisView(APIView): permission_classes = [permissions.IsAuthenticated] def get(self, request): role, err = _resolve_role(request) if err is not None: return err report = compute_gap_report(request.user, role) return Response(report.to_dict(), status=status.HTTP_200_OK) @extend_schema( parameters=[ _ROLE_ID_PARAM, OpenApiParameter( name='limit', type=int, location=OpenApiParameter.QUERY, required=False, description=f'Resources returned per skill gap (default {DEFAULT_LIMIT_PER_SKILL}).', ), ], responses=OpenApiTypes.OBJECT, description='Ranked learning-resource recommendations per skill gap.', ) class RecommendationsView(APIView): permission_classes = [permissions.IsAuthenticated] def get(self, request): role, err = _resolve_role(request) if err is not None: return err limit_raw = request.query_params.get('limit') limit = DEFAULT_LIMIT_PER_SKILL if limit_raw is not None: try: limit = int(limit_raw) except ValueError: return Response( {'detail': 'limit must be a positive integer.'}, status=status.HTTP_400_BAD_REQUEST, ) if limit <= 0: return Response( {'detail': 'limit must be a positive integer.'}, status=status.HTTP_400_BAD_REQUEST, ) limit = min(limit, MAX_LIMIT_PER_SKILL) payload = compute_recommendations(request.user, role, limit_per_skill=limit) return Response(payload, status=status.HTTP_200_OK) @extend_schema( responses=OpenApiTypes.OBJECT, description=( 'Active roles ranked by how well the user\'s current skills fit them. ' 'Powers the "Recommended for you" section on the role-selection page.' ), ) class RoleSuggestionsView(APIView): permission_classes = [permissions.IsAuthenticated] def get(self, request): return Response(suggest_roles(request.user), status=status.HTTP_200_OK)