Implement plans
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
@@ -11,12 +12,15 @@ from django.db.models import Count
|
||||
from django.urls import reverse
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import timezone
|
||||
from django.views import View
|
||||
|
||||
from core.clients.transport import send_message_raw
|
||||
from core.models import (
|
||||
AnswerSuggestionEvent,
|
||||
ChatTaskSource,
|
||||
CodexPermissionRequest,
|
||||
CodexRun,
|
||||
DerivedTask,
|
||||
DerivedTaskEvent,
|
||||
ExternalSyncEvent,
|
||||
@@ -30,6 +34,7 @@ from core.models import (
|
||||
Chat,
|
||||
ExternalChatLink,
|
||||
)
|
||||
from core.tasks.codex_support import resolve_external_chat_id
|
||||
from core.tasks.providers import get_provider
|
||||
|
||||
SAFE_TASK_FLAGS_DEFAULTS = {
|
||||
@@ -268,11 +273,47 @@ def _creator_label_for_message(user, service: str, message) -> str:
|
||||
|
||||
def _apply_task_creator_labels(user, task_rows):
|
||||
rows = list(task_rows or [])
|
||||
person_identifier_cache: dict[tuple[str, str], PersonIdentifier | None] = {}
|
||||
|
||||
def _resolve_person_identifier(service_key: str, sender_identifier: str):
|
||||
key = (str(service_key or "").strip().lower(), str(sender_identifier or "").strip())
|
||||
if key in person_identifier_cache:
|
||||
return person_identifier_cache[key]
|
||||
variants = _person_identifier_scope_variants(key[0], key[1])
|
||||
row = (
|
||||
PersonIdentifier.objects.filter(
|
||||
user=user,
|
||||
service=key[0],
|
||||
identifier__in=variants or [key[1]],
|
||||
)
|
||||
.select_related("person")
|
||||
.first()
|
||||
)
|
||||
person_identifier_cache[key] = row
|
||||
return row
|
||||
|
||||
for row in rows:
|
||||
origin = getattr(row, "origin_message", None)
|
||||
service_key = str(getattr(row, "source_service", "") or "").strip().lower()
|
||||
sender_identifier = str(getattr(origin, "sender_uuid", "") or "").strip()
|
||||
row.creator_label = _creator_label_for_message(user, service_key, origin)
|
||||
row.creator_identifier = str(getattr(origin, "sender_uuid", "") or "").strip()
|
||||
row.creator_identifier = sender_identifier
|
||||
row.creator_compose_href = ""
|
||||
if sender_identifier and service_key:
|
||||
person_identifier = _resolve_person_identifier(service_key, sender_identifier)
|
||||
compose_service = service_key
|
||||
compose_identifier = sender_identifier
|
||||
compose_person_id = ""
|
||||
if person_identifier is not None:
|
||||
compose_identifier = str(getattr(person_identifier, "identifier", "") or "").strip() or sender_identifier
|
||||
compose_person_id = str(getattr(person_identifier, "person_id", "") or "")
|
||||
query = {
|
||||
"service": compose_service,
|
||||
"identifier": compose_identifier,
|
||||
}
|
||||
if compose_person_id:
|
||||
query["person"] = compose_person_id
|
||||
row.creator_compose_href = f"{reverse('compose_page')}?{urlencode(query)}"
|
||||
return rows
|
||||
|
||||
|
||||
@@ -283,6 +324,96 @@ def _provider_row_map(user):
|
||||
}
|
||||
|
||||
|
||||
def _codex_settings_with_defaults(raw: dict | None) -> dict:
|
||||
row = dict(raw or {})
|
||||
timeout_raw = str(row.get("timeout_seconds") or "60").strip()
|
||||
try:
|
||||
timeout_seconds = max(1, int(timeout_raw))
|
||||
except Exception:
|
||||
timeout_seconds = 60
|
||||
return {
|
||||
"command": str(row.get("command") or "codex").strip() or "codex",
|
||||
"workspace_root": str(row.get("workspace_root") or "").strip(),
|
||||
"default_profile": str(row.get("default_profile") or "").strip(),
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"chat_link_mode": "task-sync",
|
||||
"instance_label": str(row.get("instance_label") or "default").strip() or "default",
|
||||
"approver_service": str(row.get("approver_service") or "").strip().lower(),
|
||||
"approver_identifier": str(row.get("approver_identifier") or "").strip(),
|
||||
"approver_mode": "channel",
|
||||
}
|
||||
|
||||
|
||||
def _enqueue_codex_task_submission(
|
||||
*,
|
||||
user,
|
||||
task: DerivedTask,
|
||||
source_service: str,
|
||||
source_channel: str,
|
||||
mode: str = "default",
|
||||
command_text: str = "",
|
||||
source_message=None,
|
||||
) -> CodexRun:
|
||||
external_chat_id = resolve_external_chat_id(
|
||||
user=user,
|
||||
provider="codex_cli",
|
||||
service=source_service,
|
||||
channel=source_channel,
|
||||
)
|
||||
provider_payload = {
|
||||
"task_id": str(task.id),
|
||||
"reference_code": str(task.reference_code or ""),
|
||||
"title": str(task.title or ""),
|
||||
"external_key": str(task.external_key or ""),
|
||||
"project_name": str(getattr(task.project, "name", "") or ""),
|
||||
"epic_name": str(getattr(task.epic, "name", "") or ""),
|
||||
"source_service": str(source_service or ""),
|
||||
"source_channel": str(source_channel or ""),
|
||||
"external_chat_id": external_chat_id,
|
||||
"origin_message_id": str(getattr(task, "origin_message_id", "") or ""),
|
||||
"trigger_message_id": str(getattr(source_message, "id", "") or ""),
|
||||
"mode": str(mode or "default"),
|
||||
}
|
||||
if command_text:
|
||||
provider_payload["command_text"] = str(command_text)
|
||||
run = CodexRun.objects.create(
|
||||
user=user,
|
||||
task=task,
|
||||
source_message=source_message,
|
||||
project=task.project,
|
||||
epic=task.epic,
|
||||
source_service=str(source_service or ""),
|
||||
source_channel=str(source_channel or ""),
|
||||
external_chat_id=external_chat_id,
|
||||
status="queued",
|
||||
request_payload={"action": "append_update", "provider_payload": dict(provider_payload)},
|
||||
result_payload={},
|
||||
error="",
|
||||
)
|
||||
provider_payload["codex_run_id"] = str(run.id)
|
||||
run.request_payload = {"action": "append_update", "provider_payload": dict(provider_payload)}
|
||||
run.save(update_fields=["request_payload", "updated_at"])
|
||||
idempotency_key = (
|
||||
f"codex_submit:{task.id}:{mode}:{hashlib.sha1(str(command_text or '').encode('utf-8')).hexdigest()[:10]}:{run.id}"
|
||||
)
|
||||
ExternalSyncEvent.objects.update_or_create(
|
||||
idempotency_key=idempotency_key,
|
||||
defaults={
|
||||
"user": user,
|
||||
"task": task,
|
||||
"task_event": None,
|
||||
"provider": "codex_cli",
|
||||
"status": "pending",
|
||||
"payload": {
|
||||
"action": "append_update",
|
||||
"provider_payload": dict(provider_payload),
|
||||
},
|
||||
"error": "",
|
||||
},
|
||||
)
|
||||
return run
|
||||
|
||||
|
||||
def _normalize_channel_identifier(service: str, identifier: str) -> str:
|
||||
service_key = str(service or "").strip().lower()
|
||||
value = str(identifier or "").strip()
|
||||
@@ -337,6 +468,41 @@ def _upsert_group_source(*, user, service: str, channel_identifier: str, project
|
||||
return source
|
||||
|
||||
|
||||
def _notify_epic_created_in_project_chats(*, project: TaskProject, epic: TaskEpic) -> None:
|
||||
rows = (
|
||||
ChatTaskSource.objects.filter(project=project, enabled=True)
|
||||
.order_by("service", "channel_identifier")
|
||||
.values_list("service", "channel_identifier")
|
||||
)
|
||||
seen: set[tuple[str, str]] = set()
|
||||
for service, channel_identifier in rows:
|
||||
svc = str(service or "").strip().lower()
|
||||
chan = str(channel_identifier or "").strip()
|
||||
if not svc or not chan:
|
||||
continue
|
||||
key = (svc, chan)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
try:
|
||||
async_to_sync(send_message_raw)(
|
||||
svc,
|
||||
chan,
|
||||
text=(
|
||||
f"[epic] Created '{epic.name}' in project '{project.name}'.\n"
|
||||
"WhatsApp usage:\n"
|
||||
"- create epic: epic: <Epic name> (or .epic <Epic name>)\n"
|
||||
"- add task to epic: task: <description> [epic:<Epic name>]\n"
|
||||
"- list tasks: .l list tasks\n"
|
||||
"- undo latest task: .undo"
|
||||
),
|
||||
attachments=[],
|
||||
metadata={"origin": "task_epic_announce"},
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
def _person_identifier_scope_variants(service: str, identifier: str) -> list[str]:
|
||||
service_key = str(service or "").strip().lower()
|
||||
raw_identifier = str(identifier or "").strip()
|
||||
@@ -690,6 +856,7 @@ class TaskProjectDetail(LoginRequiredMixin, View):
|
||||
return redirect("tasks_project", project_id=str(project.id))
|
||||
if created:
|
||||
messages.success(request, f"Created epic '{epic.name}'.")
|
||||
_notify_epic_created_in_project_chats(project=project, epic=epic)
|
||||
else:
|
||||
messages.info(request, f"Epic '{epic.name}' already exists.")
|
||||
return redirect("tasks_project", project_id=str(project.id))
|
||||
@@ -701,6 +868,28 @@ class TaskProjectDetail(LoginRequiredMixin, View):
|
||||
messages.success(request, f"Deleted epic '{deleted_name}'.")
|
||||
return redirect("tasks_project", project_id=str(project.id))
|
||||
|
||||
if action == "task_set_epic":
|
||||
task = get_object_or_404(
|
||||
DerivedTask,
|
||||
id=request.POST.get("task_id"),
|
||||
user=request.user,
|
||||
project=project,
|
||||
)
|
||||
epic_id = str(request.POST.get("epic_id") or "").strip()
|
||||
epic = None
|
||||
if epic_id:
|
||||
epic = get_object_or_404(TaskEpic, id=epic_id, project=project)
|
||||
task.epic = epic
|
||||
task.save(update_fields=["epic"])
|
||||
if epic is None:
|
||||
messages.success(request, f"Cleared epic for task #{task.reference_code}.")
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
f"Assigned task #{task.reference_code} to epic '{epic.name}'.",
|
||||
)
|
||||
return redirect("tasks_project", project_id=str(project.id))
|
||||
|
||||
if action == "project_delete":
|
||||
deleted_name = str(project.name or "").strip() or "Project"
|
||||
project.delete()
|
||||
@@ -862,6 +1051,10 @@ class TaskDetail(LoginRequiredMixin, View):
|
||||
getattr(task, "origin_message", None),
|
||||
)
|
||||
sync_events = task.external_sync_events.order_by("-created_at")
|
||||
codex_runs = task.codex_runs.select_related("source_message").order_by("-created_at")
|
||||
permission_requests = CodexPermissionRequest.objects.filter(codex_run__task=task).select_related(
|
||||
"codex_run", "external_sync_event"
|
||||
).order_by("-requested_at")
|
||||
return render(
|
||||
request,
|
||||
self.template_name,
|
||||
@@ -869,6 +1062,8 @@ class TaskDetail(LoginRequiredMixin, View):
|
||||
"task": task,
|
||||
"events": events,
|
||||
"sync_events": sync_events,
|
||||
"codex_runs": codex_runs,
|
||||
"permission_requests": permission_requests,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -895,8 +1090,39 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
row.allowed_prefixes_csv = ",".join(row.settings_effective["allowed_prefixes"])
|
||||
provider_map = _provider_row_map(request.user)
|
||||
codex_cfg = provider_map.get("codex_cli")
|
||||
codex_settings = dict(getattr(codex_cfg, "settings", {}) or {})
|
||||
codex_settings = _codex_settings_with_defaults(dict(getattr(codex_cfg, "settings", {}) or {}))
|
||||
mock_cfg = provider_map.get("mock")
|
||||
codex_provider = get_provider("codex_cli")
|
||||
codex_healthcheck = codex_provider.healthcheck(codex_settings) if codex_cfg else None
|
||||
codex_queue_counts = {
|
||||
"pending": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="codex_cli", status="pending"
|
||||
).count(),
|
||||
"waiting_approval": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="codex_cli", status="waiting_approval"
|
||||
).count(),
|
||||
"failed": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="codex_cli", status="failed"
|
||||
).count(),
|
||||
"ok": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="codex_cli", status="ok"
|
||||
).count(),
|
||||
}
|
||||
codex_recent_runs = CodexRun.objects.filter(user=request.user).order_by("-created_at")[:10]
|
||||
latest_worker_event = (
|
||||
ExternalSyncEvent.objects.filter(
|
||||
user=request.user,
|
||||
provider="codex_cli",
|
||||
)
|
||||
.filter(status__in=["ok", "failed", "waiting_approval", "retrying"])
|
||||
.order_by("-updated_at")
|
||||
.first()
|
||||
)
|
||||
worker_heartbeat_at = getattr(latest_worker_event, "updated_at", None)
|
||||
worker_heartbeat_age = ""
|
||||
if worker_heartbeat_at is not None:
|
||||
delta_seconds = max(0, int((timezone.now() - worker_heartbeat_at).total_seconds()))
|
||||
worker_heartbeat_age = f"{delta_seconds}s ago"
|
||||
external_chat_links = list(
|
||||
ExternalChatLink.objects.filter(user=request.user).select_related(
|
||||
"person", "person_identifier"
|
||||
@@ -932,6 +1158,19 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
"default_profile": str(codex_settings.get("default_profile") or ""),
|
||||
"timeout_seconds": int(codex_settings.get("timeout_seconds") or 60),
|
||||
"chat_link_mode": str(codex_settings.get("chat_link_mode") or "task-sync"),
|
||||
"instance_label": str(codex_settings.get("instance_label") or "default"),
|
||||
"approver_service": str(codex_settings.get("approver_service") or ""),
|
||||
"approver_identifier": str(codex_settings.get("approver_identifier") or ""),
|
||||
"approver_mode": "channel",
|
||||
},
|
||||
"codex_compact_summary": {
|
||||
"healthcheck_ok": bool(getattr(codex_healthcheck, "ok", False)),
|
||||
"healthcheck_error": str(getattr(codex_healthcheck, "error", "") or ""),
|
||||
"healthcheck_payload": dict(getattr(codex_healthcheck, "payload", {}) or {}),
|
||||
"worker_heartbeat_at": worker_heartbeat_at,
|
||||
"worker_heartbeat_age": worker_heartbeat_age,
|
||||
"queue_counts": codex_queue_counts,
|
||||
"recent_runs": codex_recent_runs,
|
||||
},
|
||||
"person_identifiers": person_identifiers,
|
||||
"external_link_person_identifiers": external_link_person_identifiers,
|
||||
@@ -961,12 +1200,13 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
|
||||
if action == "epic_create":
|
||||
project = get_object_or_404(TaskProject, id=request.POST.get("project_id"), user=request.user)
|
||||
TaskEpic.objects.create(
|
||||
epic = TaskEpic.objects.create(
|
||||
project=project,
|
||||
name=str(request.POST.get("name") or "Epic").strip() or "Epic",
|
||||
external_key=str(request.POST.get("external_key") or "").strip(),
|
||||
active=bool(request.POST.get("active") or "1"),
|
||||
)
|
||||
_notify_epic_created_in_project_chats(project=project, epic=epic)
|
||||
return _settings_redirect(request)
|
||||
|
||||
if action == "source_create":
|
||||
@@ -1063,18 +1303,18 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
row.enabled = bool(request.POST.get("enabled"))
|
||||
settings_payload = dict(row.settings or {})
|
||||
if provider == "codex_cli":
|
||||
timeout_raw = str(request.POST.get("timeout_seconds") or "60").strip()
|
||||
try:
|
||||
timeout_value = max(1, int(timeout_raw))
|
||||
except Exception:
|
||||
timeout_value = 60
|
||||
settings_payload = {
|
||||
"command": str(request.POST.get("command") or "codex").strip() or "codex",
|
||||
"workspace_root": str(request.POST.get("workspace_root") or "").strip(),
|
||||
"default_profile": str(request.POST.get("default_profile") or "").strip(),
|
||||
"timeout_seconds": timeout_value,
|
||||
"chat_link_mode": "task-sync",
|
||||
}
|
||||
settings_payload = _codex_settings_with_defaults(
|
||||
{
|
||||
"command": request.POST.get("command"),
|
||||
"workspace_root": request.POST.get("workspace_root"),
|
||||
"default_profile": request.POST.get("default_profile"),
|
||||
"timeout_seconds": request.POST.get("timeout_seconds"),
|
||||
"instance_label": request.POST.get("instance_label"),
|
||||
"approver_service": request.POST.get("approver_service"),
|
||||
"approver_identifier": request.POST.get("approver_identifier"),
|
||||
"approver_mode": "channel",
|
||||
}
|
||||
)
|
||||
row.settings = settings_payload
|
||||
row.save(update_fields=["enabled", "settings", "updated_at"])
|
||||
return _settings_redirect(request)
|
||||
@@ -1159,6 +1399,196 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
return _settings_redirect(request)
|
||||
|
||||
|
||||
class TaskCodexSubmit(LoginRequiredMixin, View):
|
||||
def post(self, request):
|
||||
task_id = str(request.POST.get("task_id") or "").strip()
|
||||
next_url = str(request.POST.get("next") or reverse("tasks_hub")).strip()
|
||||
task = get_object_or_404(
|
||||
DerivedTask.objects.select_related("project", "epic", "origin_message"),
|
||||
id=task_id,
|
||||
user=request.user,
|
||||
)
|
||||
cfg = TaskProviderConfig.objects.filter(
|
||||
user=request.user,
|
||||
provider="codex_cli",
|
||||
enabled=True,
|
||||
).first()
|
||||
if cfg is None:
|
||||
messages.error(
|
||||
request,
|
||||
"Codex provider is disabled. Enable it in Task Settings first.",
|
||||
)
|
||||
return redirect(next_url)
|
||||
run = _enqueue_codex_task_submission(
|
||||
user=request.user,
|
||||
task=task,
|
||||
source_service=str(task.source_service or ""),
|
||||
source_channel=str(task.source_channel or ""),
|
||||
mode="default",
|
||||
source_message=getattr(task, "origin_message", None),
|
||||
)
|
||||
messages.success(request, f"Sent task #{task.reference_code} to Codex (run {run.id}).")
|
||||
return redirect(next_url)
|
||||
|
||||
|
||||
class CodexSettingsPage(LoginRequiredMixin, View):
|
||||
template_name = "pages/codex-settings.html"
|
||||
|
||||
def _context(self, request):
|
||||
cfg = TaskProviderConfig.objects.filter(user=request.user, provider="codex_cli").first()
|
||||
settings_payload = _codex_settings_with_defaults(dict(getattr(cfg, "settings", {}) or {}))
|
||||
provider = get_provider("codex_cli")
|
||||
health = provider.healthcheck(settings_payload) if cfg else None
|
||||
|
||||
status_filter = str(request.GET.get("status") or "").strip().lower()
|
||||
service_filter = str(request.GET.get("service") or "").strip().lower()
|
||||
channel_filter = str(request.GET.get("channel") or "").strip()
|
||||
project_filter = str(request.GET.get("project") or "").strip()
|
||||
date_from = str(request.GET.get("date_from") or "").strip()
|
||||
|
||||
runs = CodexRun.objects.filter(user=request.user).select_related("task", "project", "epic").order_by("-created_at")
|
||||
if status_filter:
|
||||
runs = runs.filter(status=status_filter)
|
||||
if service_filter:
|
||||
runs = runs.filter(source_service=service_filter)
|
||||
if channel_filter:
|
||||
runs = runs.filter(source_channel=channel_filter)
|
||||
if project_filter:
|
||||
runs = runs.filter(project_id=project_filter)
|
||||
if date_from:
|
||||
runs = runs.filter(created_at__date__gte=date_from)
|
||||
runs = runs[:200]
|
||||
|
||||
queue_counts = {
|
||||
"pending": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="codex_cli", status="pending"
|
||||
).count(),
|
||||
"waiting_approval": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="codex_cli", status="waiting_approval"
|
||||
).count(),
|
||||
"failed": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="codex_cli", status="failed"
|
||||
).count(),
|
||||
"ok": ExternalSyncEvent.objects.filter(
|
||||
user=request.user, provider="codex_cli", status="ok"
|
||||
).count(),
|
||||
}
|
||||
permission_requests = (
|
||||
CodexPermissionRequest.objects.filter(user=request.user)
|
||||
.select_related("codex_run", "codex_run__task", "external_sync_event")
|
||||
.order_by("-requested_at")[:200]
|
||||
)
|
||||
return {
|
||||
"provider_config": cfg,
|
||||
"provider_settings": settings_payload,
|
||||
"health": health,
|
||||
"runs": runs,
|
||||
"queue_counts": queue_counts,
|
||||
"permission_requests": permission_requests,
|
||||
"projects": TaskProject.objects.filter(user=request.user).order_by("name"),
|
||||
"filters": {
|
||||
"status": status_filter,
|
||||
"service": service_filter,
|
||||
"channel": channel_filter,
|
||||
"project": project_filter,
|
||||
"date_from": date_from,
|
||||
},
|
||||
}
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name, self._context(request))
|
||||
|
||||
|
||||
class CodexApprovalAction(LoginRequiredMixin, View):
|
||||
def post(self, request):
|
||||
request_id = str(request.POST.get("request_id") or "").strip()
|
||||
decision = str(request.POST.get("decision") or "").strip().lower()
|
||||
row = get_object_or_404(
|
||||
CodexPermissionRequest.objects.select_related("codex_run", "external_sync_event"),
|
||||
id=request_id,
|
||||
user=request.user,
|
||||
)
|
||||
if row.status != "pending":
|
||||
return redirect("codex_settings")
|
||||
now = timezone.now()
|
||||
if decision == "approve":
|
||||
row.status = "approved"
|
||||
row.resolved_at = now
|
||||
row.resolved_by_identifier = "settings_ui"
|
||||
row.resolution_note = "approved via settings ui"
|
||||
row.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"resolved_at",
|
||||
"resolved_by_identifier",
|
||||
"resolution_note",
|
||||
]
|
||||
)
|
||||
run = row.codex_run
|
||||
run.status = "approved_waiting_resume"
|
||||
run.error = ""
|
||||
run.save(update_fields=["status", "error", "updated_at"])
|
||||
provider_payload = dict(run.request_payload.get("provider_payload") or {})
|
||||
provider_payload.update(
|
||||
{
|
||||
"mode": "approval_response",
|
||||
"approval_key": row.approval_key,
|
||||
"resume_payload": dict(row.resume_payload or {}),
|
||||
"codex_run_id": str(run.id),
|
||||
}
|
||||
)
|
||||
ExternalSyncEvent.objects.update_or_create(
|
||||
idempotency_key=f"codex_approval:{row.approval_key}:approved",
|
||||
defaults={
|
||||
"user": request.user,
|
||||
"task": run.task,
|
||||
"task_event": run.derived_task_event,
|
||||
"provider": "codex_cli",
|
||||
"status": "pending",
|
||||
"payload": {"action": "append_update", "provider_payload": provider_payload},
|
||||
"error": "",
|
||||
},
|
||||
)
|
||||
messages.success(request, f"Approved {row.approval_key}. Resume event queued.")
|
||||
return redirect("codex_settings")
|
||||
|
||||
row.status = "denied"
|
||||
row.resolved_at = now
|
||||
row.resolved_by_identifier = "settings_ui"
|
||||
row.resolution_note = "denied via settings ui"
|
||||
row.save(update_fields=["status", "resolved_at", "resolved_by_identifier", "resolution_note"])
|
||||
run = row.codex_run
|
||||
run.status = "denied"
|
||||
run.error = "approval_denied"
|
||||
run.save(update_fields=["status", "error", "updated_at"])
|
||||
ExternalSyncEvent.objects.update_or_create(
|
||||
idempotency_key=f"codex_approval:{row.approval_key}:denied",
|
||||
defaults={
|
||||
"user": request.user,
|
||||
"task": run.task,
|
||||
"task_event": run.derived_task_event,
|
||||
"provider": "codex_cli",
|
||||
"status": "failed",
|
||||
"payload": {
|
||||
"action": "append_update",
|
||||
"provider_payload": {
|
||||
"mode": "approval_response",
|
||||
"approval_key": row.approval_key,
|
||||
"codex_run_id": str(run.id),
|
||||
},
|
||||
},
|
||||
"error": "approval_denied",
|
||||
},
|
||||
)
|
||||
if row.external_sync_event_id:
|
||||
ExternalSyncEvent.objects.filter(id=row.external_sync_event_id).update(
|
||||
status="failed",
|
||||
error="approval_denied",
|
||||
)
|
||||
messages.warning(request, f"Denied {row.approval_key}.")
|
||||
return redirect("codex_settings")
|
||||
|
||||
|
||||
class AnswerSuggestionSend(LoginRequiredMixin, View):
|
||||
def post(self, request):
|
||||
event = get_object_or_404(
|
||||
|
||||
Reference in New Issue
Block a user