175 lines
6.1 KiB
Python
175 lines
6.1 KiB
Python
"""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,
|
|
)
|