177 lines
6.7 KiB
Python
177 lines
6.7 KiB
Python
from django.db import IntegrityError, transaction
|
|
from django.utils import timezone
|
|
from ninja import Router
|
|
from ninja.errors import HttpError
|
|
|
|
from apps.accounts.api import require_permission
|
|
from apps.audit.models import OperationAuditLog
|
|
from apps.skills.models import SkillDefinition, SkillVersion
|
|
from apps.skills.schemas import (
|
|
SkillCreateIn,
|
|
SkillDefinitionOut,
|
|
SkillDefinitionUpdateIn,
|
|
SkillDetailOut,
|
|
SkillDraftUpdateIn,
|
|
SkillVersionOut,
|
|
)
|
|
from apps.skills.services import SkillValidationError, ensure_draft, validate_skill_version
|
|
|
|
router = Router(tags=["skills"])
|
|
|
|
|
|
def serialize_definition(definition: SkillDefinition, detailed: bool = False) -> dict:
|
|
draft = definition.versions.filter(status="draft").first()
|
|
payload = {
|
|
"id": definition.id,
|
|
"code": definition.code,
|
|
"name": definition.name,
|
|
"description": definition.description,
|
|
"active_version": definition.active_version.version
|
|
if definition.active_version_id
|
|
else None,
|
|
"draft_version": draft.version if draft else None,
|
|
"updated_at": definition.updated_at,
|
|
}
|
|
if detailed:
|
|
payload["versions"] = list(definition.versions.all())
|
|
return payload
|
|
|
|
|
|
def audit(user, action: str, definition: SkillDefinition, snapshot: dict) -> None:
|
|
OperationAuditLog.objects.create(
|
|
user=user,
|
|
module="skills",
|
|
action=action,
|
|
target_type="skill",
|
|
target_id=str(definition.id),
|
|
request_snapshot=snapshot,
|
|
result="success",
|
|
)
|
|
|
|
|
|
@router.get("", response=list[SkillDefinitionOut])
|
|
def list_skills(request):
|
|
require_permission(request, "skill.manage")
|
|
queryset = SkillDefinition.objects.select_related("active_version").prefetch_related("versions")
|
|
return [serialize_definition(item) for item in queryset]
|
|
|
|
|
|
@router.post("", response=SkillDetailOut)
|
|
def create_skill(request, payload: SkillCreateIn):
|
|
require_permission(request, "skill.manage")
|
|
try:
|
|
with transaction.atomic():
|
|
definition = SkillDefinition.objects.create(
|
|
code=payload.code,
|
|
name=payload.name,
|
|
description=payload.description,
|
|
created_by=request.user,
|
|
)
|
|
SkillVersion.objects.create(
|
|
definition=definition,
|
|
version=1,
|
|
status="draft",
|
|
prompt_template=payload.prompt_template,
|
|
input_schema=payload.input_schema,
|
|
output_schema=payload.output_schema,
|
|
example_input=payload.example_input,
|
|
created_by=request.user,
|
|
)
|
|
except IntegrityError as error:
|
|
raise HttpError(400, "Skill code already exists") from error
|
|
audit(request.user, "create", definition, {"code": definition.code})
|
|
return serialize_definition(definition, detailed=True)
|
|
|
|
|
|
@router.get("/{skill_id}", response=SkillDetailOut)
|
|
def get_skill(request, skill_id: int):
|
|
require_permission(request, "skill.manage")
|
|
definition = SkillDefinition.objects.select_related("active_version").get(id=skill_id)
|
|
return serialize_definition(definition, detailed=True)
|
|
|
|
|
|
@router.patch("/{skill_id}", response=SkillDetailOut)
|
|
def update_skill(request, skill_id: int, payload: SkillDefinitionUpdateIn):
|
|
require_permission(request, "skill.manage")
|
|
definition = SkillDefinition.objects.select_related("active_version").get(id=skill_id)
|
|
values = payload.dict(exclude_unset=True)
|
|
for field, value in values.items():
|
|
setattr(definition, field, value)
|
|
definition.save()
|
|
audit(request.user, "update", definition, {"fields": sorted(values)})
|
|
return serialize_definition(definition, detailed=True)
|
|
|
|
|
|
@router.post("/{skill_id}/draft", response=SkillVersionOut)
|
|
def create_skill_draft(request, skill_id: int):
|
|
require_permission(request, "skill.manage")
|
|
definition = SkillDefinition.objects.select_related("active_version").get(id=skill_id)
|
|
return ensure_draft(definition, request.user)
|
|
|
|
|
|
@router.get("/{skill_id}/draft", response=SkillVersionOut)
|
|
def get_skill_draft(request, skill_id: int):
|
|
require_permission(request, "skill.manage")
|
|
return SkillVersion.objects.get(definition_id=skill_id, status="draft")
|
|
|
|
|
|
@router.patch("/{skill_id}/draft", response=SkillVersionOut)
|
|
def update_skill_draft(request, skill_id: int, payload: SkillDraftUpdateIn):
|
|
require_permission(request, "skill.manage")
|
|
definition = SkillDefinition.objects.select_related("active_version").get(id=skill_id)
|
|
draft = ensure_draft(definition, request.user)
|
|
values = payload.dict(exclude_unset=True)
|
|
for field, value in values.items():
|
|
setattr(draft, field, value)
|
|
try:
|
|
validate_skill_version(draft)
|
|
except SkillValidationError as error:
|
|
raise HttpError(400, str(error)) from error
|
|
draft.save()
|
|
audit(request.user, "update_draft", definition, {"version": draft.version})
|
|
return draft
|
|
|
|
|
|
@router.get("/{skill_id}/versions", response=list[SkillVersionOut])
|
|
def list_skill_versions(request, skill_id: int):
|
|
require_permission(request, "skill.manage")
|
|
return SkillVersion.objects.filter(definition_id=skill_id)
|
|
|
|
|
|
@router.post("/{skill_id}/publish", response=SkillVersionOut)
|
|
def publish_skill(request, skill_id: int):
|
|
require_permission(request, "skill.manage")
|
|
with transaction.atomic():
|
|
definition = SkillDefinition.objects.select_for_update().get(id=skill_id)
|
|
draft = SkillVersion.objects.select_for_update().get(
|
|
definition=definition,
|
|
status="draft",
|
|
)
|
|
try:
|
|
validate_skill_version(draft)
|
|
except SkillValidationError as error:
|
|
raise HttpError(400, str(error)) from error
|
|
draft.status = "published"
|
|
draft.published_by = request.user
|
|
draft.published_at = timezone.now()
|
|
draft.save(update_fields=["status", "published_by", "published_at", "updated_at"])
|
|
definition.active_version = draft
|
|
definition.save(update_fields=["active_version", "updated_at"])
|
|
audit(request.user, "publish", definition, {"version": draft.version})
|
|
return draft
|
|
|
|
|
|
@router.post("/{skill_id}/rollback/{version}", response=SkillDetailOut)
|
|
def rollback_skill(request, skill_id: int, version: int):
|
|
require_permission(request, "skill.manage")
|
|
definition = SkillDefinition.objects.select_related("active_version").get(id=skill_id)
|
|
target = SkillVersion.objects.get(
|
|
definition=definition,
|
|
version=version,
|
|
status="published",
|
|
)
|
|
definition.active_version = target
|
|
definition.save(update_fields=["active_version", "updated_at"])
|
|
audit(request.user, "rollback", definition, {"version": target.version})
|
|
return serialize_definition(definition, detailed=True)
|