gapguide-api / apps /roles /views.py
arifRB's picture
Deploy GapGuide backend (Docker)
ffd36e0 verified
Raw
History Blame Contribute Delete
7.03 kB
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import generics, permissions, status, viewsets
from rest_framework.response import Response
from rest_framework.views import APIView
from django.db import transaction
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import FilterSet
from .models import Role, RoleSkill, UserTargetRole
from .serializers import (
RoleAdminSerializer,
RoleDetailSerializer,
RoleListSerializer,
RoleSkillAdminSerializer,
UserTargetRoleSerializer,
)
class RoleFilterSet(FilterSet):
class Meta:
model = Role
fields = {
'industry': ['exact'],
}
class RoleListView(generics.ListAPIView):
# Intentionally unpaginated: the role catalog is a small, curated set
# (~10 seeded, expected ceiling ~50). A plain list keeps the frontend
# dropdown trivial. Revisit if the catalog ever grows past a few hundred.
queryset = Role.objects.filter(is_active=True)
serializer_class = RoleListSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_class = RoleFilterSet
pagination_class = None
class RoleDetailView(generics.RetrieveAPIView):
queryset = Role.objects.filter(is_active=True)
serializer_class = RoleDetailSerializer
permission_classes = [permissions.IsAuthenticated]
class UserTargetRoleView(APIView):
serializer_class = UserTargetRoleSerializer
permission_classes = [permissions.IsAuthenticated]
@extend_schema(
responses={
200: UserTargetRoleSerializer,
404: OpenApiResponse(description='No active target role.'),
},
)
def get(self, request):
try:
target_role = UserTargetRole.objects.get(user=request.user, is_active=True)
serializer = UserTargetRoleSerializer(target_role, context={'request': request})
return Response(serializer.data)
except UserTargetRole.DoesNotExist:
return Response(
{"detail": "No active target role."},
status=status.HTTP_404_NOT_FOUND
)
@extend_schema(
request=UserTargetRoleSerializer,
responses=UserTargetRoleSerializer,
)
@transaction.atomic
def post(self, request):
in_serializer = UserTargetRoleSerializer(
data=request.data, context={'request': request},
)
in_serializer.is_valid(raise_exception=True)
role_id = in_serializer.validated_data['role_id']
try:
role = Role.objects.get(id=role_id, is_active=True)
except Role.DoesNotExist:
# The role was concurrently deactivated between validate_role_id and
# here. 400 (not 404): the request was well-formed but the resource
# state changed; key on role_id to match the validation-time shape.
return Response(
{'role_id': ['Role not found or inactive.']},
status=status.HTTP_400_BAD_REQUEST,
)
# Lock any existing active rows for this user so concurrent POSTs
# serialize on the row and the partial unique constraint holds.
existing = list(
UserTargetRole.objects
.select_for_update()
.filter(user=request.user, is_active=True)
)
for row in existing:
if row.role_id == role.id:
# Same role reselected — upsert semantics: refresh selected_at
# so the frontend sees the re-activation event.
row.selected_at = timezone.now()
row.save(update_fields=['selected_at'])
serializer = UserTargetRoleSerializer(row, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
UserTargetRole.objects.filter(pk__in=[r.pk for r in existing]).update(is_active=False)
target_role = UserTargetRole.objects.create(
user=request.user,
role=role,
is_active=True,
)
serializer = UserTargetRoleSerializer(target_role, context={'request': request})
return Response(serializer.data, status=status.HTTP_201_CREATED)
@extend_schema(
request=None,
responses={
204: OpenApiResponse(description='Active target role deactivated.'),
404: OpenApiResponse(description='No active target role to deactivate.'),
},
)
@transaction.atomic
def delete(self, request):
try:
target_role = UserTargetRole.objects.get(user=request.user, is_active=True)
target_role.is_active = False
target_role.save()
return Response(status=status.HTTP_204_NO_CONTENT)
except UserTargetRole.DoesNotExist:
return Response(
{"detail": "No active target role to deactivate."},
status=status.HTTP_404_NOT_FOUND
)
class RoleAdminViewSet(viewsets.ModelViewSet):
"""Admin-only CRUD on Role.
List includes inactive roles (admins need to see them). DELETE is a soft
delete — sets is_active=False — per the spec in CLAUDE.md (roles are
never hard-deleted because historical UserTargetRole rows still reference
them).
"""
queryset = Role.objects.all().order_by('role_name')
serializer_class = RoleAdminSerializer
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
pagination_class = None
def destroy(self, request, *args, **kwargs):
role = self.get_object()
role.is_active = False
role.save(update_fields=['is_active'])
return Response(status=status.HTTP_204_NO_CONTENT)
class RoleSkillAdminViewSet(viewsets.ModelViewSet):
"""Admin-only CRUD on a Role's required-skill rows.
Accepts a `?role=<id>` query param so the frontend role edit dialog can
fetch only the RoleSkills for the role it is editing.
"""
queryset = RoleSkill.objects.all().select_related('skill', 'role')
serializer_class = RoleSkillAdminSerializer
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
pagination_class = None
def get_queryset(self):
qs = super().get_queryset()
role_id = self.request.query_params.get('role')
if role_id:
# Guard non-numeric input: filtering on a non-int pk raises a
# ValueError at query prep (→ 500). isascii() also rejects Unicode
# digits (e.g. '²') that isdigit() accepts but int() can't parse.
qs = qs.filter(role_id=role_id) if (
str(role_id).isascii() and str(role_id).isdigit()
) else qs.none()
return qs