Fix #214: ChangeProposal (propose-then-confirm)
Implements non-negotiable #1: the AI assistant never writes autonomously. Every assistant/contributor "write" emits a ChangeProposal — a structured diff a human approves, edits, or rejects. Design: docs/design/change-proposal.md. Structural guarantee: a proposal's operations reach the DB ONLY via change_proposal_service.apply(), which requires the actor be an editor and dispatches each op through the normal editing services (person/name/event/ relationship/source/citation create/update/delete) — so every change passes the privacy engine and is audited as the approving human. propose() only inserts a pending row; it performs no domain mutation. Model providers stay read-only, so no model response can mutate tree data. - ChangeProposal model + migration (status pending|applied|rejected, origin assistant|contributor, JSONB operations, reviewer + apply_error). - Service: propose / list / get / apply (with optional edited ops) / reject / delete; a dispatcher mapping ops → editing services. v1 applies ops in order, not cross-op transactional (single-op is atomic; documented). - API /trees/{id}/proposals + a frontend review page (approve/reject; editor- gated) and sidebar entry. Tests: proposal doesn't apply until approved; reject doesn't apply; non-editor member can see but not apply; multi-op; approve-with-edits; apply-error keeps it pending. Full suite 87 passed; single alembic head. Closes #214 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { api } from "@/lib/api/client";
|
||||
import type { components } from "@/lib/api/schema";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
type Proposal = components["schemas"]["ChangeProposalRead"];
|
||||
type Op = { op: string; entity_type: string; entity_id?: string | null; payload?: Record<string, unknown> };
|
||||
|
||||
const STATUS_STYLE: Record<string, string> = {
|
||||
pending: "border-bronze/40 text-bronze",
|
||||
applied: "border-green-600/40 text-green-700 dark:text-green-400",
|
||||
rejected: "border-[var(--border)] text-[var(--muted)]",
|
||||
};
|
||||
|
||||
export default function ProposalsPage() {
|
||||
const { id: treeId } = useParams<{ id: string }>();
|
||||
const [proposals, setProposals] = useState<Proposal[]>([]);
|
||||
const [meRole, setMeRole] = useState<string | null>(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const [props, me, members] = await Promise.all([
|
||||
api.GET("/api/v1/trees/{tree_id}/proposals", { params: { path: { tree_id: treeId } } }),
|
||||
api.GET("/api/v1/users/me"),
|
||||
api.GET("/api/v1/trees/{tree_id}/members", { params: { path: { tree_id: treeId } } }),
|
||||
]);
|
||||
const myId = me.data?.id;
|
||||
setMeRole((members.data ?? []).find((m) => m.user_id === myId)?.role ?? null);
|
||||
setProposals(props.data ?? []);
|
||||
setReady(true);
|
||||
}, [treeId]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const canReview = meRole === "owner" || meRole === "editor";
|
||||
|
||||
async function act(pid: string, action: "apply" | "reject") {
|
||||
setBusy(pid);
|
||||
const { error, response } = await api.POST(
|
||||
`/api/v1/trees/{tree_id}/proposals/{proposal_id}/${action}` as "/api/v1/trees/{tree_id}/proposals/{proposal_id}/apply",
|
||||
{ params: { path: { tree_id: treeId, proposal_id: pid } } },
|
||||
);
|
||||
setBusy(null);
|
||||
if (error) {
|
||||
// 409 = couldn't apply (e.g. references a missing record); reload shows apply_error.
|
||||
if (response.status !== 409) alert("Action failed.");
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
if (!ready) return <p className="text-[var(--muted)]">Loading…</p>;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Change proposals</h1>
|
||||
<p className="mt-1 text-sm text-[var(--muted)]">
|
||||
Suggested edits (from the assistant or contributors). Nothing changes until you approve —
|
||||
approving applies it as your own edit, through the normal checks and audit log.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{proposals.length === 0 ? (
|
||||
<p className="text-[var(--muted)]">No proposals.</p>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{proposals.map((p) => {
|
||||
const ops = (p.operations as Op[]) ?? [];
|
||||
return (
|
||||
<li key={p.id}>
|
||||
<Card>
|
||||
<CardContent className="space-y-3 p-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium">{p.summary}</div>
|
||||
<div className="text-xs text-[var(--muted)]">
|
||||
{p.origin} · {ops.length} change{ops.length === 1 ? "" : "s"}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`shrink-0 rounded-full border px-2.5 py-0.5 text-xs ${
|
||||
STATUS_STYLE[p.status] ?? ""
|
||||
}`}
|
||||
>
|
||||
{p.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{p.rationale && <p className="text-sm text-[var(--muted)]">{p.rationale}</p>}
|
||||
|
||||
<ul className="space-y-1 text-sm">
|
||||
{ops.map((o, i) => (
|
||||
<li key={i} className="rounded-md bg-bronze/[0.05] px-3 py-1.5">
|
||||
<span className="font-medium capitalize">{o.op}</span>{" "}
|
||||
<span className="text-[var(--muted)]">{o.entity_type}</span>
|
||||
{o.payload && Object.keys(o.payload).length > 0 && (
|
||||
<span className="text-[var(--muted)]">
|
||||
{" "}
|
||||
·{" "}
|
||||
{Object.entries(o.payload)
|
||||
.map(([k, v]) => `${k}: ${String(v)}`)
|
||||
.join(", ")}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{p.apply_error && (
|
||||
<p className="rounded-md border border-red-600/40 bg-red-600/[0.06] px-3 py-2 text-sm text-red-700 dark:text-red-400">
|
||||
Couldn’t apply: {p.apply_error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{p.status === "pending" && canReview && (
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" disabled={busy === p.id} onClick={() => act(p.id, "apply")}>
|
||||
Approve & apply
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={busy === p.id}
|
||||
onClick={() => act(p.id, "reject")}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{p.status === "pending" && !canReview && (
|
||||
<p className="text-xs text-[var(--muted)]">Only an editor can approve this.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Archive,
|
||||
ArrowDownUp,
|
||||
BookText,
|
||||
ClipboardCheck,
|
||||
Compass,
|
||||
FolderTree,
|
||||
Image as ImageIcon,
|
||||
@@ -138,6 +139,12 @@ export function AppSidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||
icon={Sparkles}
|
||||
active={pathname.startsWith(`/trees/${treeId}/cleanup`)}
|
||||
/>
|
||||
<Item
|
||||
href={`/trees/${treeId}/proposals`}
|
||||
label="Proposals"
|
||||
icon={ClipboardCheck}
|
||||
active={pathname.startsWith(`/trees/${treeId}/proposals`)}
|
||||
/>
|
||||
<Item
|
||||
href={`/trees/${treeId}/members`}
|
||||
label="Members"
|
||||
|
||||
Vendored
+352
@@ -961,6 +961,76 @@ export interface paths {
|
||||
patch: operations["update_member_api_v1_trees__tree_id__members__membership_id__patch"];
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/trees/{tree_id}/proposals": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** List Proposals */
|
||||
get: operations["list_proposals_api_v1_trees__tree_id__proposals_get"];
|
||||
put?: never;
|
||||
/** Create Proposal */
|
||||
post: operations["create_proposal_api_v1_trees__tree_id__proposals_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/trees/{tree_id}/proposals/{proposal_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get Proposal */
|
||||
get: operations["get_proposal_api_v1_trees__tree_id__proposals__proposal_id__get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
/** Delete Proposal */
|
||||
delete: operations["delete_proposal_api_v1_trees__tree_id__proposals__proposal_id__delete"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/trees/{tree_id}/proposals/{proposal_id}/apply": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Apply Proposal */
|
||||
post: operations["apply_proposal_api_v1_trees__tree_id__proposals__proposal_id__apply_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/trees/{tree_id}/proposals/{proposal_id}/reject": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Reject Proposal */
|
||||
post: operations["reject_proposal_api_v1_trees__tree_id__proposals__proposal_id__reject_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
@@ -1013,6 +1083,63 @@ export interface components {
|
||||
/** Source Id */
|
||||
source_id?: string | null;
|
||||
};
|
||||
/** ChangeProposalCreate */
|
||||
ChangeProposalCreate: {
|
||||
/** Summary */
|
||||
summary: string;
|
||||
/** Rationale */
|
||||
rationale?: string | null;
|
||||
/** @default contributor */
|
||||
origin?: components["schemas"]["ChangeProposalOrigin"];
|
||||
/** Operations */
|
||||
operations: components["schemas"]["ProposalOperation"][];
|
||||
};
|
||||
/**
|
||||
* ChangeProposalOrigin
|
||||
* @enum {string}
|
||||
*/
|
||||
ChangeProposalOrigin: "assistant" | "contributor";
|
||||
/** ChangeProposalRead */
|
||||
ChangeProposalRead: {
|
||||
/**
|
||||
* Id
|
||||
* Format: uuid
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Tree Id
|
||||
* Format: uuid
|
||||
*/
|
||||
tree_id: string;
|
||||
status: components["schemas"]["ChangeProposalStatus"];
|
||||
origin: components["schemas"]["ChangeProposalOrigin"];
|
||||
/** Created By User Id */
|
||||
created_by_user_id: string | null;
|
||||
/** Summary */
|
||||
summary: string;
|
||||
/** Rationale */
|
||||
rationale: string | null;
|
||||
/** Operations */
|
||||
operations: unknown[];
|
||||
/** Reviewed By User Id */
|
||||
reviewed_by_user_id: string | null;
|
||||
/** Reviewed At */
|
||||
reviewed_at: string | null;
|
||||
/** Review Note */
|
||||
review_note: string | null;
|
||||
/** Apply Error */
|
||||
apply_error: string | null;
|
||||
/**
|
||||
* Created At
|
||||
* Format: date-time
|
||||
*/
|
||||
created_at: string;
|
||||
};
|
||||
/**
|
||||
* ChangeProposalStatus
|
||||
* @enum {string}
|
||||
*/
|
||||
ChangeProposalStatus: "pending" | "applied" | "rejected";
|
||||
/**
|
||||
* CitationConfidence
|
||||
* @enum {string}
|
||||
@@ -1560,6 +1687,29 @@ export interface components {
|
||||
/** Notes */
|
||||
notes?: string | null;
|
||||
};
|
||||
/** ProposalOperation */
|
||||
ProposalOperation: {
|
||||
/** Op */
|
||||
op: string;
|
||||
/** Entity Type */
|
||||
entity_type: string;
|
||||
/** Entity Id */
|
||||
entity_id?: string | null;
|
||||
/**
|
||||
* Payload
|
||||
* @default {}
|
||||
*/
|
||||
payload?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
/** ProposalReview */
|
||||
ProposalReview: {
|
||||
/** Note */
|
||||
note?: string | null;
|
||||
/** Operations */
|
||||
operations?: components["schemas"]["ProposalOperation"][] | null;
|
||||
};
|
||||
/**
|
||||
* PublicTreeRead
|
||||
* @description Tree projection for the public surface — deliberately omits owner_id so a
|
||||
@@ -4299,4 +4449,206 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
list_proposals_api_v1_trees__tree_id__proposals_get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
status?: components["schemas"]["ChangeProposalStatus"] | null;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
tree_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ChangeProposalRead"][];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
create_proposal_api_v1_trees__tree_id__proposals_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
tree_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ChangeProposalCreate"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ChangeProposalRead"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
get_proposal_api_v1_trees__tree_id__proposals__proposal_id__get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
tree_id: string;
|
||||
proposal_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ChangeProposalRead"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
delete_proposal_api_v1_trees__tree_id__proposals__proposal_id__delete: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
tree_id: string;
|
||||
proposal_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
apply_proposal_api_v1_trees__tree_id__proposals__proposal_id__apply_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
tree_id: string;
|
||||
proposal_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProposalReview"] | null;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ChangeProposalRead"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
reject_proposal_api_v1_trees__tree_id__proposals__proposal_id__reject_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
tree_id: string;
|
||||
proposal_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProposalReview"] | null;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ChangeProposalRead"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3742,6 +3742,357 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/trees/{tree_id}/proposals": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"proposals"
|
||||
],
|
||||
"summary": "List Proposals",
|
||||
"operationId": "list_proposals_api_v1_trees__tree_id__proposals_get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tree_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Tree Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ChangeProposalStatus"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Status"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ChangeProposalRead"
|
||||
},
|
||||
"title": "Response List Proposals Api V1 Trees Tree Id Proposals Get"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"proposals"
|
||||
],
|
||||
"summary": "Create Proposal",
|
||||
"operationId": "create_proposal_api_v1_trees__tree_id__proposals_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tree_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Tree Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChangeProposalCreate"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChangeProposalRead"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/trees/{tree_id}/proposals/{proposal_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"proposals"
|
||||
],
|
||||
"summary": "Get Proposal",
|
||||
"operationId": "get_proposal_api_v1_trees__tree_id__proposals__proposal_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tree_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Tree Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "proposal_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Proposal Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChangeProposalRead"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"proposals"
|
||||
],
|
||||
"summary": "Delete Proposal",
|
||||
"operationId": "delete_proposal_api_v1_trees__tree_id__proposals__proposal_id__delete",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tree_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Tree Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "proposal_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Proposal Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Successful Response"
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/trees/{tree_id}/proposals/{proposal_id}/apply": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"proposals"
|
||||
],
|
||||
"summary": "Apply Proposal",
|
||||
"operationId": "apply_proposal_api_v1_trees__tree_id__proposals__proposal_id__apply_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tree_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Tree Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "proposal_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Proposal Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ProposalReview"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Data"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChangeProposalRead"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/trees/{tree_id}/proposals/{proposal_id}/reject": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"proposals"
|
||||
],
|
||||
"summary": "Reject Proposal",
|
||||
"operationId": "reject_proposal_api_v1_trees__tree_id__proposals__proposal_id__reject_post",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tree_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Tree Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "proposal_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Proposal Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ProposalReview"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Data"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChangeProposalRead"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -3886,6 +4237,179 @@
|
||||
],
|
||||
"title": "Body_upload_media_api_v1_trees__tree_id__media_post"
|
||||
},
|
||||
"ChangeProposalCreate": {
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"title": "Summary"
|
||||
},
|
||||
"rationale": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Rationale"
|
||||
},
|
||||
"origin": {
|
||||
"$ref": "#/components/schemas/ChangeProposalOrigin",
|
||||
"default": "contributor"
|
||||
},
|
||||
"operations": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ProposalOperation"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Operations"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"summary",
|
||||
"operations"
|
||||
],
|
||||
"title": "ChangeProposalCreate"
|
||||
},
|
||||
"ChangeProposalOrigin": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"assistant",
|
||||
"contributor"
|
||||
],
|
||||
"title": "ChangeProposalOrigin"
|
||||
},
|
||||
"ChangeProposalRead": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Id"
|
||||
},
|
||||
"tree_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Tree Id"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/ChangeProposalStatus"
|
||||
},
|
||||
"origin": {
|
||||
"$ref": "#/components/schemas/ChangeProposalOrigin"
|
||||
},
|
||||
"created_by_user_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Created By User Id"
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"title": "Summary"
|
||||
},
|
||||
"rationale": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Rationale"
|
||||
},
|
||||
"operations": {
|
||||
"items": {},
|
||||
"type": "array",
|
||||
"title": "Operations"
|
||||
},
|
||||
"reviewed_by_user_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Reviewed By User Id"
|
||||
},
|
||||
"reviewed_at": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Reviewed At"
|
||||
},
|
||||
"review_note": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Review Note"
|
||||
},
|
||||
"apply_error": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Apply Error"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Created At"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"tree_id",
|
||||
"status",
|
||||
"origin",
|
||||
"created_by_user_id",
|
||||
"summary",
|
||||
"rationale",
|
||||
"operations",
|
||||
"reviewed_by_user_id",
|
||||
"reviewed_at",
|
||||
"review_note",
|
||||
"apply_error",
|
||||
"created_at"
|
||||
],
|
||||
"title": "ChangeProposalRead"
|
||||
},
|
||||
"ChangeProposalStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pending",
|
||||
"applied",
|
||||
"rejected"
|
||||
],
|
||||
"title": "ChangeProposalStatus"
|
||||
},
|
||||
"CitationConfidence": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -5662,6 +6186,73 @@
|
||||
"type": "object",
|
||||
"title": "PersonUpdate"
|
||||
},
|
||||
"ProposalOperation": {
|
||||
"properties": {
|
||||
"op": {
|
||||
"type": "string",
|
||||
"title": "Op"
|
||||
},
|
||||
"entity_type": {
|
||||
"type": "string",
|
||||
"title": "Entity Type"
|
||||
},
|
||||
"entity_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Entity Id"
|
||||
},
|
||||
"payload": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
"title": "Payload",
|
||||
"default": {}
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"op",
|
||||
"entity_type"
|
||||
],
|
||||
"title": "ProposalOperation"
|
||||
},
|
||||
"ProposalReview": {
|
||||
"properties": {
|
||||
"note": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Note"
|
||||
},
|
||||
"operations": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ProposalOperation"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Operations"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "ProposalReview"
|
||||
},
|
||||
"PublicTreeRead": {
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
Reference in New Issue
Block a user