FEATURE: CRUD access for ai-artifacts (admin only) (#34193)

- GET /admin/plugins/discourse-ai/ai-artifacts.json — list (paginated,
per_page capped at 100)
- GET /admin/plugins/discourse-ai/ai-artifacts/:id.json — show
- POST /admin/plugins/discourse-ai/ai-artifacts.json — create
- PUT /admin/plugins/discourse-ai/ai-artifacts/:id.json — update
- DELETE /admin/plugins/discourse-ai/ai-artifacts/:id.json — destroy

Admin-only, JSON responses.
This commit is contained in:
Sam
2025-08-14 12:17:20 +10:00
committed by GitHub
parent aad0dddad5
commit d2cd9462ba
4 changed files with 218 additions and 0 deletions
@@ -0,0 +1,85 @@
# frozen_string_literal: true
module DiscourseAi
module Admin
class AiArtifactsController < ::Admin::AdminController
requires_plugin ::DiscourseAi::PLUGIN_NAME
PER_PAGE_MAX = 100
before_action :find_artifact, only: %i[show update destroy]
def index
page = params[:page].to_i
page = 1 if page < 1
per_page = params[:per_page].to_i
per_page = 50 if per_page < 1
per_page = PER_PAGE_MAX if per_page > PER_PAGE_MAX
base = AiArtifact.all
total = base.count
artifacts = base.order(created_at: :desc).offset((page - 1) * per_page).limit(per_page)
render json: {
artifacts: serialize_data(artifacts, AiArtifactSerializer),
meta: {
total: total,
page: page,
per_page: per_page,
has_more: total > page * per_page,
},
}
end
def show
render_serialized(@artifact, AiArtifactSerializer)
end
def create
artifact = AiArtifact.new(artifact_params)
if artifact.save
render_serialized(artifact, AiArtifactSerializer, status: :created)
else
render_json_error artifact
end
end
def update
if @artifact.update(artifact_params)
render_serialized(@artifact, AiArtifactSerializer)
else
render_json_error @artifact
end
end
def destroy
if @artifact.destroy
head :no_content
else
render_json_error @artifact
end
end
private
def find_artifact
@artifact = AiArtifact.find(params[:id])
end
def artifact_params
params.require(:ai_artifact).permit(
:user_id,
:post_id,
:name,
:html,
:css,
:js,
metadata: {
},
)
end
end
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AiArtifactSerializer < ApplicationSerializer
attributes :id, :user_id, :post_id, :name, :html, :css, :js, :metadata, :created_at, :updated_at
self.root = "ai_artifact"
end
+8
View File
@@ -70,6 +70,14 @@ Discourse::Application.routes.draw do
:constraints => StaffConstraint.new
scope "/admin/plugins/discourse-ai", constraints: AdminConstraint.new do
resources :ai_artifacts,
only: %i[index show create update destroy],
path: "ai-artifacts",
controller: "discourse_ai/admin/ai_artifacts",
defaults: {
format: :json,
}
resources :ai_personas,
only: %i[index new create edit update destroy],
path: "ai-personas",
@@ -0,0 +1,118 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::Admin::AiArtifactsController, type: :request do
fab!(:admin)
fab!(:user)
fab!(:target_post) { Fabricate(:post) }
before do
enable_current_plugin
sign_in(admin)
end
describe "GET #index" do
fab!(:artifacts) { Fabricate.times(3, :ai_artifact) }
it "returns paginated list with meta" do
get "/admin/plugins/discourse-ai/ai-artifacts.json", params: { page: 1, per_page: 2 }
expect(response).to be_successful
json = response.parsed_body
expect(json["artifacts"]).to be_an(Array)
expect(json["artifacts"].length).to eq(2)
expect(json["meta"]).to include("total", "page", "per_page", "has_more")
expect(json["meta"]["total"]).to eq(AiArtifact.count)
expect(json["meta"]["page"]).to eq(1)
expect(json["meta"]["per_page"]).to eq(2)
expect(json["meta"]["has_more"]).to eq(true)
end
it "clamps per_page to max" do
get "/admin/plugins/discourse-ai/ai-artifacts.json", params: { per_page: 5000 }
expect(response).to be_successful
expect(response.parsed_body.dig("meta", "per_page")).to eq(100)
end
end
describe "GET #show" do
fab!(:artifact) { Fabricate(:ai_artifact) }
it "returns a single artifact" do
get "/admin/plugins/discourse-ai/ai-artifacts/#{artifact.id}.json"
expect(response).to be_successful
json = response.parsed_body
expect(json.dig("ai_artifact", "id")).to eq(artifact.id)
expect(json.dig("ai_artifact", "name")).to eq(artifact.name)
end
end
describe "POST #create" do
it "creates an artifact" do
params = {
ai_artifact: {
user_id: admin.id,
post_id: target_post.id,
name: "Admin Created",
html: "<div>hello</div>",
css: ".x { color: red; }",
js: "console.log('x')",
metadata: {
public: false,
},
},
}
expect {
post "/admin/plugins/discourse-ai/ai-artifacts.json",
params: params.to_json,
headers: {
"CONTENT_TYPE" => "application/json",
}
}.to change(AiArtifact, :count).by(1)
expect(response).to have_http_status(:created)
expect(response.parsed_body.dig("ai_artifact", "name")).to eq("Admin Created")
end
end
describe "PUT #update" do
fab!(:artifact) { Fabricate(:ai_artifact) }
it "updates fields" do
put "/admin/plugins/discourse-ai/ai-artifacts/#{artifact.id}.json",
params: { ai_artifact: { name: "Updated Name", metadata: { public: true } } }.to_json,
headers: {
"CONTENT_TYPE" => "application/json",
}
expect(response).to be_successful
artifact.reload
expect(artifact.name).to eq("Updated Name")
expect(artifact.metadata["public"]).to eq(true)
end
end
describe "DELETE #destroy" do
fab!(:artifact) { Fabricate(:ai_artifact) }
it "removes the artifact" do
expect { delete "/admin/plugins/discourse-ai/ai-artifacts/#{artifact.id}.json" }.to change(
AiArtifact,
:count,
).by(-1)
expect(response).to have_http_status(:no_content)
end
end
context "when not admin" do
before { sign_in(user) }
it "blocks access" do
get "/admin/plugins/discourse-ai/ai-artifacts.json"
expect(response.status).to eq(404)
end
end
end