"""SkillViewSet — CRUD навыков сотрудников.""" from __future__ import annotations from django.db import transaction from rest_framework import filters, viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from employees.models import EmployeeSkillProgress, Skill from employees.services.skill_progress import skill_queryset_with_rarity from ...permissions import AdminOrActionOrModelPerms from ..serializers import SkillSerializer from ._helpers import Employee class SkillViewSet(viewsets.ModelViewSet): """ /api/v1/skills/ - GET list/retrieve — IsAuthenticated - POST (create) — IsAuthenticated - PATCH/PUT — staff/superuser ИЛИ perm employees.change_skill - DELETE — staff/superuser ИЛИ perm employees.delete_skill Экшены: - POST /skills/bulk_create - POST /skills/merge """ queryset = skill_queryset_with_rarity().order_by("name") serializer_class = SkillSerializer filter_backends = [filters.SearchFilter, filters.OrderingFilter] search_fields = ["name"] ordering_fields = ["name", "id"] ordering = ["name"] pagination_class = None required_perms_by_action = { "merge": "employees.change_skill", } def get_queryset(self): queryset = super().get_queryset() if self.action in {"list", "create", "bulk_create"}: return queryset.filter(is_secret=False) return queryset def get_permissions(self): if self.action in ("create", "bulk_create", "list", "retrieve"): return [IsAuthenticated()] return [IsAuthenticated(), AdminOrActionOrModelPerms()] @action(detail=False, methods=["post"]) def bulk_create(self, request): names = request.data.get("names") if not isinstance(names, list) or not names: return Response( {"detail": "Поле 'names' должно быть непустым списком строк"}, status=400, ) cleaned = [] seen = set() for n in names: if not isinstance(n, str): continue s = n.strip() if not s: continue key = s.lower() if key in seen: continue seen.add(key) cleaned.append(s) existing = set( Skill.objects.filter(name__in=cleaned).values_list("name", flat=True) ) to_create = [Skill(name=n) for n in cleaned if n not in existing] Skill.objects.bulk_create(to_create, ignore_conflicts=True) created = Skill.objects.filter( name__in=[n for n in cleaned if n not in existing] ).order_by("name") return Response( { "created_count": created.count(), "created": SkillSerializer(created, many=True).data, }, status=201, ) @action(detail=False, methods=["post"]) @transaction.atomic def merge(self, request): """Заменяет source на target. Опционально переносит всех сотрудников. """ sid = request.data.get("source_id") tid = request.data.get("target_id") reassign = bool(request.data.get("reassign", True)) delete_source = bool(request.data.get("delete_source", True)) if not sid or not tid: return Response({"detail": "source_id и target_id обязательны"}, status=400) try: sid = int(sid) tid = int(tid) except (TypeError, ValueError): return Response( {"detail": "source_id и target_id должны быть числами"}, status=400, ) if sid == tid: return Response( {"detail": "source_id и target_id не должны совпадать"}, status=400, ) try: source = Skill.objects.get(pk=sid) target = Skill.objects.get(pk=tid) except Skill.DoesNotExist: return Response({"detail": "Skill не найден"}, status=404) if source.is_secret or target.is_secret: return Response( {"detail": "Секретные навыки нельзя объединять вручную"}, status=400, ) moved_count = 0 if reassign: qs = Employee.objects.filter(skills=source).only("id").distinct() for emp in qs: emp.skills.add(target) source_progress = EmployeeSkillProgress.objects.filter( employee=emp, skill=source, ).first() target_progress = EmployeeSkillProgress.objects.filter( employee=emp, skill=target, ).first() if source_progress and target_progress: source_progress.experience_events.update(progress=target_progress) target_progress.experience += source_progress.experience target_progress.is_active = True target_progress.save( update_fields=["experience", "is_active", "updated_at"] ) source_progress.delete() elif source_progress: source_progress.skill = target source_progress.is_active = True source_progress.save( update_fields=["skill", "is_active", "updated_at"] ) emp.skills.remove(source) moved_count += 1 if delete_source: source.delete() return Response( { "ok": True, "source_id": sid, "target_id": tid, "reassigned_employees": moved_count, "source_deleted": bool(delete_source), }, status=200, )