FEAT - TASKER: internal adapter для Codex Agent Gateway
This commit is contained in:
parent
533f8c6356
commit
2ae353c8d5
|
|
@ -0,0 +1,641 @@
|
|||
from html import escape
|
||||
|
||||
from django.db import transaction
|
||||
from django.http import JsonResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from plane.app.realtime.issue_events import publish_issue_event_on_commit
|
||||
from plane.authentication.views.nodedc_logout import is_internal_logout_request_authorized
|
||||
from plane.authentication.views.nodedc_workspace_adapter import parse_json_body
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueAssignee,
|
||||
IssueComment,
|
||||
IssueLabel,
|
||||
Label,
|
||||
Project,
|
||||
ProjectMember,
|
||||
State,
|
||||
User,
|
||||
WorkspaceMember,
|
||||
)
|
||||
|
||||
|
||||
NODEDC_STRUCTURED_BLOCKS_KEY = "nodedc_structured_blocks"
|
||||
AGENT_EMAIL_DOMAIN = "agents.nodedc.local"
|
||||
AGENT_BOT_TYPE = "nodedc_codex_agent"
|
||||
ALLOWED_PRIORITIES = ["none", "low", "medium", "high", "urgent"]
|
||||
|
||||
|
||||
def unauthorized_response():
|
||||
return JsonResponse({"ok": False, "error": "internal_access_unauthorized"}, status=401)
|
||||
|
||||
|
||||
def invalid_json_response():
|
||||
return JsonResponse({"ok": False, "error": "invalid_json"}, status=400)
|
||||
|
||||
|
||||
def validation_error(error, status=400):
|
||||
return JsonResponse({"ok": False, "error": error}, status=status)
|
||||
|
||||
|
||||
def serialize_project(project):
|
||||
return {
|
||||
"id": str(project.id),
|
||||
"workspace_slug": project.workspace.slug,
|
||||
"name": project.name,
|
||||
"identifier": project.identifier,
|
||||
}
|
||||
|
||||
|
||||
def serialize_state(state):
|
||||
return {
|
||||
"id": str(state.id),
|
||||
"name": state.name,
|
||||
"color": state.color,
|
||||
"group": state.group,
|
||||
"sequence": state.sequence,
|
||||
"default": state.default,
|
||||
}
|
||||
|
||||
|
||||
def serialize_label(label):
|
||||
return {
|
||||
"id": str(label.id),
|
||||
"name": label.name,
|
||||
"color": label.color,
|
||||
}
|
||||
|
||||
|
||||
def serialize_member(project_member):
|
||||
return {
|
||||
"id": str(project_member.member_id),
|
||||
"project_member_id": str(project_member.id),
|
||||
"display_name": project_member.member.display_name,
|
||||
"email": project_member.member.email,
|
||||
"role": project_member.role,
|
||||
}
|
||||
|
||||
|
||||
def serialize_issue(issue):
|
||||
return {
|
||||
"id": str(issue.id),
|
||||
"project_id": str(issue.project_id),
|
||||
"workspace_slug": issue.workspace.slug,
|
||||
"title": issue.name,
|
||||
"description": issue.description_html,
|
||||
"priority": issue.priority,
|
||||
"state_id": str(issue.state_id) if issue.state_id else None,
|
||||
"sequence_id": issue.sequence_id,
|
||||
"detail_layout": issue.detail_layout or {},
|
||||
"label_ids": [
|
||||
str(label_id)
|
||||
for label_id in IssueLabel.objects.filter(
|
||||
issue=issue,
|
||||
deleted_at__isnull=True,
|
||||
).values_list("label_id", flat=True)
|
||||
],
|
||||
"assignee_ids": [
|
||||
str(assignee_id)
|
||||
for assignee_id in IssueAssignee.objects.filter(
|
||||
issue=issue,
|
||||
deleted_at__isnull=True,
|
||||
).values_list("assignee_id", flat=True)
|
||||
],
|
||||
"created_by": str(issue.created_by_id) if issue.created_by_id else None,
|
||||
"updated_by": str(issue.updated_by_id) if issue.updated_by_id else None,
|
||||
"created_at": issue.created_at.isoformat(),
|
||||
"updated_at": issue.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def serialize_comment(comment):
|
||||
return {
|
||||
"id": str(comment.id),
|
||||
"issue_id": str(comment.issue_id),
|
||||
"body": comment.comment_html,
|
||||
"actor_id": str(comment.actor_id) if comment.actor_id else None,
|
||||
"created_at": comment.created_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def resolve_project(project_id, workspace_slug=None):
|
||||
queryset = Project.objects.filter(
|
||||
id=project_id,
|
||||
deleted_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
).select_related("workspace")
|
||||
|
||||
if workspace_slug:
|
||||
queryset = queryset.filter(workspace__slug=workspace_slug)
|
||||
|
||||
return queryset.first()
|
||||
|
||||
|
||||
def resolve_issue(project_id, issue_id, workspace_slug=None):
|
||||
project = resolve_project(project_id, workspace_slug)
|
||||
if project is None:
|
||||
return None, None
|
||||
|
||||
issue = (
|
||||
Issue.issue_objects.filter(project=project, id=issue_id)
|
||||
.select_related("workspace", "project", "state")
|
||||
.first()
|
||||
)
|
||||
return project, issue
|
||||
|
||||
|
||||
def get_agent_header(request, key):
|
||||
return request.headers.get(key, "").strip()
|
||||
|
||||
|
||||
def get_agent_identity(request):
|
||||
agent_id = get_agent_header(request, "X-NODEDC-Agent-Id")
|
||||
owner_user_id = get_agent_header(request, "X-NODEDC-Agent-Owner-User-Id")
|
||||
token_id = get_agent_header(request, "X-NODEDC-Agent-Token-Id")
|
||||
|
||||
if not agent_id or not owner_user_id or not token_id:
|
||||
return None
|
||||
|
||||
return {
|
||||
"agent_id": agent_id,
|
||||
"owner_user_id": owner_user_id,
|
||||
"token_id": token_id,
|
||||
}
|
||||
|
||||
|
||||
def ensure_agent_actor(request, workspace, project=None):
|
||||
identity = get_agent_identity(request)
|
||||
if identity is None:
|
||||
return None
|
||||
|
||||
agent_id = identity["agent_id"]
|
||||
short_id = agent_id.replace("-", "")[:12]
|
||||
email = f"agent+{agent_id}@{AGENT_EMAIL_DOMAIN}"
|
||||
username = f"nodedc_agent_{short_id}"
|
||||
display_name = f"Codex Agent {short_id}"
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
email=email,
|
||||
defaults={
|
||||
"username": username,
|
||||
"display_name": display_name,
|
||||
"first_name": "Codex",
|
||||
"last_name": f"Agent {short_id}",
|
||||
"is_bot": True,
|
||||
"bot_type": AGENT_BOT_TYPE,
|
||||
"is_active": True,
|
||||
},
|
||||
)
|
||||
|
||||
update_fields = []
|
||||
if not user.is_bot:
|
||||
user.is_bot = True
|
||||
update_fields.append("is_bot")
|
||||
if user.bot_type != AGENT_BOT_TYPE:
|
||||
user.bot_type = AGENT_BOT_TYPE
|
||||
update_fields.append("bot_type")
|
||||
if not user.is_active:
|
||||
user.is_active = True
|
||||
update_fields.append("is_active")
|
||||
if user.display_name != display_name:
|
||||
user.display_name = display_name
|
||||
update_fields.append("display_name")
|
||||
|
||||
if update_fields:
|
||||
update_fields.append("updated_at")
|
||||
user.save(update_fields=update_fields)
|
||||
|
||||
WorkspaceMember.objects.get_or_create(
|
||||
workspace=workspace,
|
||||
member=user,
|
||||
defaults={
|
||||
"role": 15,
|
||||
"company_role": "codex-agent",
|
||||
"is_active": True,
|
||||
"is_banned": False,
|
||||
},
|
||||
)
|
||||
|
||||
if project is not None:
|
||||
ProjectMember.objects.get_or_create(
|
||||
workspace=workspace,
|
||||
project=project,
|
||||
member=user,
|
||||
defaults={
|
||||
"role": 15,
|
||||
"is_active": True,
|
||||
},
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def validate_internal_request(request):
|
||||
if not is_internal_logout_request_authorized(request):
|
||||
return unauthorized_response()
|
||||
|
||||
if get_agent_identity(request) is None:
|
||||
return validation_error("missing_agent_headers", status=400)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def html_from_text(value):
|
||||
text = value.strip() if isinstance(value, str) else ""
|
||||
return f"<p>{escape(text)}</p>" if text else "<p></p>"
|
||||
|
||||
|
||||
def merge_structured_blocks(detail_layout, structured_blocks):
|
||||
if structured_blocks is None:
|
||||
return detail_layout or {}
|
||||
|
||||
if not isinstance(structured_blocks, list):
|
||||
return None
|
||||
|
||||
updated_layout = dict(detail_layout or {})
|
||||
updated_layout[NODEDC_STRUCTURED_BLOCKS_KEY] = structured_blocks
|
||||
return updated_layout
|
||||
|
||||
|
||||
def update_issue_labels(issue, label_ids):
|
||||
labels = list(Label.objects.filter(id__in=label_ids, project=issue.project, deleted_at__isnull=True))
|
||||
if len(labels) != len(set(label_ids)):
|
||||
return False
|
||||
|
||||
IssueLabel.objects.filter(issue=issue, deleted_at__isnull=True).delete()
|
||||
for label in labels:
|
||||
IssueLabel.objects.create(issue=issue, label=label, project=issue.project, workspace=issue.workspace)
|
||||
return True
|
||||
|
||||
|
||||
def update_issue_assignees(issue, member_ids):
|
||||
project_members = list(
|
||||
ProjectMember.objects.filter(
|
||||
project=issue.project,
|
||||
member_id__in=member_ids,
|
||||
is_active=True,
|
||||
deleted_at__isnull=True,
|
||||
).select_related("member")
|
||||
)
|
||||
if len(project_members) != len(set(member_ids)):
|
||||
return False
|
||||
|
||||
IssueAssignee.objects.filter(issue=issue, deleted_at__isnull=True).delete()
|
||||
for project_member in project_members:
|
||||
IssueAssignee.objects.create(
|
||||
issue=issue,
|
||||
assignee=project_member.member,
|
||||
project=issue.project,
|
||||
workspace=issue.workspace,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCAgentProjectResolveEndpoint(View):
|
||||
def post(self, request):
|
||||
error_response = validate_internal_request(request)
|
||||
if error_response is not None:
|
||||
return error_response
|
||||
|
||||
payload = parse_json_body(request)
|
||||
if payload is None:
|
||||
return invalid_json_response()
|
||||
|
||||
grants = payload.get("grants")
|
||||
if not isinstance(grants, list):
|
||||
return validation_error("grants_required")
|
||||
|
||||
project_filters = []
|
||||
workspace_slugs = []
|
||||
for grant in grants:
|
||||
if not isinstance(grant, dict):
|
||||
continue
|
||||
workspace_slug = grant.get("workspace_slug")
|
||||
project_id = grant.get("project_id")
|
||||
if not isinstance(workspace_slug, str) or not workspace_slug:
|
||||
continue
|
||||
if isinstance(project_id, str) and project_id:
|
||||
project_filters.append((workspace_slug, project_id))
|
||||
else:
|
||||
workspace_slugs.append(workspace_slug)
|
||||
|
||||
projects = []
|
||||
if workspace_slugs:
|
||||
projects.extend(
|
||||
Project.objects.filter(
|
||||
workspace__slug__in=workspace_slugs,
|
||||
deleted_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
).select_related("workspace")
|
||||
)
|
||||
for workspace_slug, project_id in project_filters:
|
||||
project = resolve_project(project_id, workspace_slug)
|
||||
if project is not None:
|
||||
projects.append(project)
|
||||
|
||||
unique_projects = {str(project.id): project for project in projects}
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
"projects": [serialize_project(project) for project in unique_projects.values()],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCAgentProjectContextEndpoint(View):
|
||||
def get(self, request, project_id):
|
||||
error_response = validate_internal_request(request)
|
||||
if error_response is not None:
|
||||
return error_response
|
||||
|
||||
project = resolve_project(project_id, request.GET.get("workspace_slug"))
|
||||
if project is None:
|
||||
return validation_error("project_not_found", status=404)
|
||||
|
||||
states = State.objects.filter(project=project, deleted_at__isnull=True).order_by("sequence")
|
||||
labels = Label.objects.filter(project=project, deleted_at__isnull=True).order_by("sort_order")
|
||||
members = (
|
||||
ProjectMember.objects.filter(project=project, is_active=True, deleted_at__isnull=True, member__is_bot=False)
|
||||
.select_related("member")
|
||||
.order_by("member__display_name")
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
"project": serialize_project(project),
|
||||
"states": [serialize_state(state) for state in states],
|
||||
"labels": [serialize_label(label) for label in labels],
|
||||
"members": [serialize_member(member) for member in members],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCAgentIssueListEndpoint(View):
|
||||
def get(self, request):
|
||||
error_response = validate_internal_request(request)
|
||||
if error_response is not None:
|
||||
return error_response
|
||||
|
||||
project_id = request.GET.get("project_id")
|
||||
if not project_id:
|
||||
return validation_error("project_id_required")
|
||||
|
||||
project = resolve_project(project_id, request.GET.get("workspace_slug"))
|
||||
if project is None:
|
||||
return validation_error("project_not_found", status=404)
|
||||
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(project=project)
|
||||
.select_related("workspace", "project", "state")
|
||||
.order_by("-updated_at")
|
||||
)
|
||||
query = request.GET.get("query")
|
||||
if query:
|
||||
queryset = queryset.filter(name__icontains=query)
|
||||
|
||||
return JsonResponse({"ok": True, "issues": [serialize_issue(issue) for issue in queryset[:100]]})
|
||||
|
||||
def post(self, request):
|
||||
error_response = validate_internal_request(request)
|
||||
if error_response is not None:
|
||||
return error_response
|
||||
|
||||
payload = parse_json_body(request)
|
||||
if payload is None:
|
||||
return invalid_json_response()
|
||||
|
||||
project = resolve_project(payload.get("project_id"), payload.get("workspace_slug"))
|
||||
if project is None:
|
||||
return validation_error("project_not_found", status=404)
|
||||
|
||||
title = payload.get("title")
|
||||
if not isinstance(title, str) or not title.strip():
|
||||
return validation_error("title_required")
|
||||
|
||||
priority = payload.get("priority") or "none"
|
||||
if priority not in ALLOWED_PRIORITIES:
|
||||
return validation_error("invalid_priority")
|
||||
|
||||
detail_layout = merge_structured_blocks({}, payload.get("structured_blocks"))
|
||||
if detail_layout is None:
|
||||
return validation_error("invalid_structured_blocks")
|
||||
|
||||
with transaction.atomic():
|
||||
actor = ensure_agent_actor(request, project.workspace, project)
|
||||
if actor is None:
|
||||
return validation_error("missing_agent_headers")
|
||||
|
||||
issue = Issue(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
name=title.strip(),
|
||||
description_html=html_from_text(payload.get("description")),
|
||||
priority=priority,
|
||||
detail_layout=detail_layout,
|
||||
external_source=AGENT_BOT_TYPE,
|
||||
external_id=get_agent_header(request, "X-NODEDC-Agent-Id"),
|
||||
)
|
||||
issue.save(created_by_id=actor.id)
|
||||
|
||||
publish_issue_event_on_commit("issue.created", issue, actor_id=actor.id, changed_fields=payload.keys())
|
||||
return JsonResponse({"ok": True, "issue": serialize_issue(issue)}, status=201)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCAgentIssueUpdateEndpoint(View):
|
||||
def patch(self, request, issue_id):
|
||||
error_response = validate_internal_request(request)
|
||||
if error_response is not None:
|
||||
return error_response
|
||||
|
||||
payload = parse_json_body(request)
|
||||
if payload is None:
|
||||
return invalid_json_response()
|
||||
|
||||
project, issue = resolve_issue(payload.get("project_id"), issue_id, payload.get("workspace_slug"))
|
||||
if project is None:
|
||||
return validation_error("project_not_found", status=404)
|
||||
if issue is None:
|
||||
return validation_error("issue_not_found", status=404)
|
||||
|
||||
detail_layout = (
|
||||
merge_structured_blocks(issue.detail_layout, payload.get("structured_blocks"))
|
||||
if "structured_blocks" in payload
|
||||
else issue.detail_layout
|
||||
)
|
||||
if detail_layout is None:
|
||||
return validation_error("invalid_structured_blocks")
|
||||
|
||||
update_fields = []
|
||||
if isinstance(payload.get("title"), str) and payload["title"].strip():
|
||||
issue.name = payload["title"].strip()
|
||||
update_fields.append("name")
|
||||
if "description" in payload:
|
||||
issue.description_html = html_from_text(payload.get("description"))
|
||||
update_fields.append("description_html")
|
||||
if payload.get("priority") in ALLOWED_PRIORITIES:
|
||||
issue.priority = payload["priority"]
|
||||
update_fields.append("priority")
|
||||
elif "priority" in payload:
|
||||
return validation_error("invalid_priority")
|
||||
if "structured_blocks" in payload:
|
||||
issue.detail_layout = detail_layout
|
||||
update_fields.append("detail_layout")
|
||||
|
||||
if update_fields:
|
||||
actor = ensure_agent_actor(request, project.workspace, project)
|
||||
if actor is None:
|
||||
return validation_error("missing_agent_headers")
|
||||
issue.updated_by = actor
|
||||
update_fields.extend(["updated_by", "updated_at"])
|
||||
issue.save(update_fields=update_fields, disable_auto_set_user=True)
|
||||
publish_issue_event_on_commit("issue.updated", issue, actor_id=actor.id, changed_fields=update_fields)
|
||||
|
||||
return JsonResponse({"ok": True, "issue": serialize_issue(issue)})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCAgentIssueMoveEndpoint(View):
|
||||
def post(self, request, issue_id):
|
||||
error_response = validate_internal_request(request)
|
||||
if error_response is not None:
|
||||
return error_response
|
||||
|
||||
payload = parse_json_body(request)
|
||||
if payload is None:
|
||||
return invalid_json_response()
|
||||
|
||||
project, issue = resolve_issue(payload.get("project_id"), issue_id, payload.get("workspace_slug"))
|
||||
if project is None:
|
||||
return validation_error("project_not_found", status=404)
|
||||
if issue is None:
|
||||
return validation_error("issue_not_found", status=404)
|
||||
|
||||
state = State.objects.filter(project=project, id=payload.get("state_id"), deleted_at__isnull=True).first()
|
||||
if state is None:
|
||||
return validation_error("state_not_found", status=404)
|
||||
|
||||
actor = ensure_agent_actor(request, project.workspace, project)
|
||||
if actor is None:
|
||||
return validation_error("missing_agent_headers")
|
||||
|
||||
issue.state = state
|
||||
issue.updated_by = actor
|
||||
if state.group == "completed":
|
||||
issue.completed_at = timezone.now()
|
||||
else:
|
||||
issue.completed_at = None
|
||||
issue.save(update_fields=["state", "updated_by", "completed_at", "updated_at"], disable_auto_set_user=True)
|
||||
publish_issue_event_on_commit("issue.updated", issue, actor_id=actor.id, changed_fields=["state"])
|
||||
return JsonResponse({"ok": True, "issue": serialize_issue(issue)})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCAgentIssueCommentEndpoint(View):
|
||||
def post(self, request, issue_id):
|
||||
error_response = validate_internal_request(request)
|
||||
if error_response is not None:
|
||||
return error_response
|
||||
|
||||
payload = parse_json_body(request)
|
||||
if payload is None:
|
||||
return invalid_json_response()
|
||||
|
||||
project, issue = resolve_issue(payload.get("project_id"), issue_id, payload.get("workspace_slug"))
|
||||
if project is None:
|
||||
return validation_error("project_not_found", status=404)
|
||||
if issue is None:
|
||||
return validation_error("issue_not_found", status=404)
|
||||
|
||||
body = payload.get("body")
|
||||
if not isinstance(body, str) or not body.strip():
|
||||
return validation_error("body_required")
|
||||
|
||||
actor = ensure_agent_actor(request, project.workspace, project)
|
||||
if actor is None:
|
||||
return validation_error("missing_agent_headers")
|
||||
|
||||
comment = IssueComment(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
issue=issue,
|
||||
actor=actor,
|
||||
comment_html=html_from_text(body),
|
||||
comment_json={},
|
||||
)
|
||||
comment.save(created_by_id=actor.id)
|
||||
return JsonResponse({"ok": True, "comment": serialize_comment(comment)}, status=201)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCAgentIssueLabelsEndpoint(View):
|
||||
def put(self, request, issue_id):
|
||||
error_response = validate_internal_request(request)
|
||||
if error_response is not None:
|
||||
return error_response
|
||||
|
||||
payload = parse_json_body(request)
|
||||
if payload is None:
|
||||
return invalid_json_response()
|
||||
|
||||
project, issue = resolve_issue(payload.get("project_id"), issue_id, payload.get("workspace_slug"))
|
||||
if project is None:
|
||||
return validation_error("project_not_found", status=404)
|
||||
if issue is None:
|
||||
return validation_error("issue_not_found", status=404)
|
||||
|
||||
label_ids = payload.get("label_ids")
|
||||
if not isinstance(label_ids, list):
|
||||
return validation_error("label_ids_required")
|
||||
|
||||
if not update_issue_labels(issue, label_ids):
|
||||
return validation_error("label_not_found", status=404)
|
||||
|
||||
actor = ensure_agent_actor(request, project.workspace, project)
|
||||
if actor is not None:
|
||||
issue.updated_by = actor
|
||||
issue.save(update_fields=["updated_by", "updated_at"], disable_auto_set_user=True)
|
||||
publish_issue_event_on_commit("issue.updated", issue, actor_id=actor.id, changed_fields=["labels"])
|
||||
|
||||
return JsonResponse({"ok": True, "issue": serialize_issue(issue)})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCAgentIssueAssigneesEndpoint(View):
|
||||
def put(self, request, issue_id):
|
||||
error_response = validate_internal_request(request)
|
||||
if error_response is not None:
|
||||
return error_response
|
||||
|
||||
payload = parse_json_body(request)
|
||||
if payload is None:
|
||||
return invalid_json_response()
|
||||
|
||||
project, issue = resolve_issue(payload.get("project_id"), issue_id, payload.get("workspace_slug"))
|
||||
if project is None:
|
||||
return validation_error("project_not_found", status=404)
|
||||
if issue is None:
|
||||
return validation_error("issue_not_found", status=404)
|
||||
|
||||
member_ids = payload.get("member_ids")
|
||||
if not isinstance(member_ids, list):
|
||||
return validation_error("member_ids_required")
|
||||
|
||||
if not update_issue_assignees(issue, member_ids):
|
||||
return validation_error("member_not_found", status=404)
|
||||
|
||||
actor = ensure_agent_actor(request, project.workspace, project)
|
||||
if actor is not None:
|
||||
issue.updated_by = actor
|
||||
issue.save(update_fields=["updated_by", "updated_at"], disable_auto_set_user=True)
|
||||
publish_issue_event_on_commit("issue.updated", issue, actor_id=actor.id, changed_fields=["assignees"])
|
||||
|
||||
return JsonResponse({"ok": True, "issue": serialize_issue(issue)})
|
||||
|
|
@ -15,6 +15,16 @@ from plane.authentication.views.nodedc_logout import (
|
|||
NodeDCFrontChannelLogoutEndpoint,
|
||||
NodeDCInternalSessionLogoutEndpoint,
|
||||
)
|
||||
from plane.authentication.views.nodedc_agent_adapter import (
|
||||
NodeDCAgentIssueAssigneesEndpoint,
|
||||
NodeDCAgentIssueCommentEndpoint,
|
||||
NodeDCAgentIssueLabelsEndpoint,
|
||||
NodeDCAgentIssueListEndpoint,
|
||||
NodeDCAgentIssueMoveEndpoint,
|
||||
NodeDCAgentIssueUpdateEndpoint,
|
||||
NodeDCAgentProjectContextEndpoint,
|
||||
NodeDCAgentProjectResolveEndpoint,
|
||||
)
|
||||
from plane.authentication.views.nodedc_workspace_adapter import (
|
||||
NodeDCInternalProjectMembershipEnsureEndpoint,
|
||||
NodeDCInternalProjectMembershipRemoveEndpoint,
|
||||
|
|
@ -74,6 +84,46 @@ urlpatterns = [
|
|||
NodeDCInternalProjectMembershipRemoveEndpoint.as_view(),
|
||||
name="nodedc-internal-project-membership-remove",
|
||||
),
|
||||
path(
|
||||
"api/internal/nodedc/agent/projects/resolve",
|
||||
NodeDCAgentProjectResolveEndpoint.as_view(),
|
||||
name="nodedc-agent-project-resolve",
|
||||
),
|
||||
path(
|
||||
"api/internal/nodedc/agent/projects/<uuid:project_id>/context",
|
||||
NodeDCAgentProjectContextEndpoint.as_view(),
|
||||
name="nodedc-agent-project-context",
|
||||
),
|
||||
path(
|
||||
"api/internal/nodedc/agent/issues",
|
||||
NodeDCAgentIssueListEndpoint.as_view(),
|
||||
name="nodedc-agent-issue-list",
|
||||
),
|
||||
path(
|
||||
"api/internal/nodedc/agent/issues/<uuid:issue_id>",
|
||||
NodeDCAgentIssueUpdateEndpoint.as_view(),
|
||||
name="nodedc-agent-issue-update",
|
||||
),
|
||||
path(
|
||||
"api/internal/nodedc/agent/issues/<uuid:issue_id>/move",
|
||||
NodeDCAgentIssueMoveEndpoint.as_view(),
|
||||
name="nodedc-agent-issue-move",
|
||||
),
|
||||
path(
|
||||
"api/internal/nodedc/agent/issues/<uuid:issue_id>/comments",
|
||||
NodeDCAgentIssueCommentEndpoint.as_view(),
|
||||
name="nodedc-agent-issue-comment",
|
||||
),
|
||||
path(
|
||||
"api/internal/nodedc/agent/issues/<uuid:issue_id>/labels",
|
||||
NodeDCAgentIssueLabelsEndpoint.as_view(),
|
||||
name="nodedc-agent-issue-labels",
|
||||
),
|
||||
path(
|
||||
"api/internal/nodedc/agent/issues/<uuid:issue_id>/assignees",
|
||||
NodeDCAgentIssueAssigneesEndpoint.as_view(),
|
||||
name="nodedc-agent-issue-assignees",
|
||||
),
|
||||
path("api/", include("plane.app.urls")),
|
||||
path("api/public/", include("plane.space.urls")),
|
||||
path("api/instances/", include("plane.license.urls")),
|
||||
|
|
|
|||
Loading…
Reference in New Issue