From 932d4a4be40587fa6d489eca16a1ef6d6f2936d1 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Thu, 23 Jul 2026 12:24:52 +0200 Subject: Witness asset uploads and out-of-band attaches in the action log --- app/controllers/assets_controller.rb | 5 +++++ app/helpers/node_actions_helper.rb | 15 +++++++++++++++ app/models/node.rb | 8 ++++++++ app/models/node_action.rb | 11 +++++++++++ config/locales/de.yml | 3 +++ config/locales/en.yml | 3 +++ test/controllers/assets_controller_test.rb | 10 ++++++++++ test/models/node_attach_asset_test.rb | 19 +++++++++++++++++++ 8 files changed, 74 insertions(+) diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index 8df4c94d..7edd9c05 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -50,6 +50,11 @@ class AssetsController < ApplicationController respond_to do |format| if @asset.save flash[:notice] = 'Asset was successfully created.' + NodeAction.record!(:participants => [@asset], :user => current_user, + :action => "asset_create", + :asset_name => @asset.name, + :content_type => @asset.upload_content_type, + :path => @asset.upload.url.sub(/\?\d+$/, "")) attach_to(attach_node) if attach_node format.html { redirect_to(@asset) } format.xml { render :xml => @asset, :status => :created, :location => @asset } diff --git a/app/helpers/node_actions_helper.rb b/app/helpers/node_actions_helper.rb index 02d1ba83..ed8d0407 100644 --- a/app/helpers/node_actions_helper.rb +++ b/app/helpers/node_actions_helper.rb @@ -13,6 +13,9 @@ module NodeActionsHelper "destroy" => "trash-x", "discard_autosave" => "eraser", "destroy_draft" => "eraser", + "asset_create" => "upload", + "asset_attach" => "paperclip", + "asset_destroy" => "file-x" }.freeze def verb_icon action @@ -181,6 +184,18 @@ module NodeActionsHelper :path => h(action.metadata["path"])).html_safe end + def summarize_asset_create action + t("node_actions.asset_create", :actor => actor_ref(action), + :asset => h(action.metadata["asset_name"].presence || action.metadata["path"])).html_safe + end + + def summarize_asset_attach action + m = action.metadata + key = m["headline"] ? "node_actions.asset_attach_headline" : "node_actions.asset_attach" + t(key, :actor => actor_ref(action), :subject => subject_ref(action), + :asset => h(m["asset_name"].presence || m["path"])).html_safe + end + def summarize_asset_destroy action m = action.metadata parts = [t("node_actions.asset_destroy", :actor => actor_ref(action), diff --git a/app/models/node.rb b/app/models/node.rb index 7a93e799..0a9cd2d1 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -501,6 +501,14 @@ class Node < ApplicationRecord to_attach.each do |row| row.related_assets.create!(:asset => asset, :headline => headline_state == :set) end + + if to_attach.any? + metadata = { :asset_name => asset.name, + :path => asset.upload.url.sub(/\?\d+$/, "") } + metadata[:headline] = true if headline_state == :set + NodeAction.record!(:node => self, :participants => [self, asset], + :user => user, :action => "asset_attach", **metadata) + end end { :attached => to_attach.size, diff --git a/app/models/node_action.rb b/app/models/node_action.rb index 9ed0b628..aa52f489 100644 --- a/app/models/node_action.rb +++ b/app/models/node_action.rb @@ -69,6 +69,17 @@ class NodeAction < ApplicationRecord # "path" -- final path, flat string (create-symmetric) # "destroyed_descendants" -- integer, only when positive; one entry # at the root, per the subtree rule. + # + # "asset_create" (witnessed upload; participants: the asset alone): + # "asset_name", "content_type", "path" -- flat strings + # + # "asset_attach" (out-of-band attach via Node#attach_asset!; written + # only when at least one new join was created, per the tandem rule -- + # in-editor curation stays draft-scoped and surfaces at publish. + # participants: the node (primary) and the asset): + # "asset_name", "path" -- flat strings + # "headline" -- boolean, only when set by this attach + # # "asset_destroy" (witnessed asset deletion; always written, even for # unattached assets -- the files were publicly reachable; node column # nil, subjects via participants: the asset plus every then-attached diff --git a/config/locales/de.yml b/config/locales/de.yml index d6241d51..fff74799 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -116,6 +116,9 @@ de: revision_created: "angelegt am %{date} von %{actor}" revision_published: "veröffentlicht am %{date} von %{actor}" revision_restored: "wiederhergestellt am %{date} von %{actor}" + asset_create: "%{actor} hat das Asset „%{asset}“ hochgeladen" + asset_attach: "%{actor} hat „%{asset}“ an %{subject} angehängt" + asset_attach_headline: "%{actor} hat „%{asset}“ als Aufmacher an %{subject} angehängt" asset_destroy: "%{actor} hat das Asset „%{asset}“ gelöscht" asset_destroy_detached: "— entfernt von %{paths}" asset_destroy_headlines: "(war Aufmacher von %{paths})" diff --git a/config/locales/en.yml b/config/locales/en.yml index bd59915b..fc9f5d13 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -71,6 +71,9 @@ en: asset_destroy: "%{actor} destroyed asset “%{asset}”" asset_destroy_detached: "— detached from %{paths}" asset_destroy_headlines: "(was the headline of %{paths})" + asset_create: "%{actor} uploaded asset “%{asset}”" + asset_attach: "%{actor} attached “%{asset}” to %{subject}" + asset_attach_headline: "%{actor} attached “%{asset}” to %{subject} as its headline" open_gallery: "Open gallery" asset_licenses: diff --git a/test/controllers/assets_controller_test.rb b/test/controllers/assets_controller_test.rb index 0e251aad..4be2e8a1 100644 --- a/test/controllers/assets_controller_test.rb +++ b/test/controllers/assets_controller_test.rb @@ -133,6 +133,16 @@ class AssetsControllerTest < ActionController::TestCase assert_equal node_path(node), flash[:headline_kept_path] end + test "create with node_id writes an asset_create and an asset_attach entry" do + node = Node.root.children.create!(:slug => "asset_log_pair") + assert_difference 'NodeAction.where(:action => "asset_create").count' do + assert_difference 'NodeAction.where(:action => "asset_attach").count' do + post :create, params: { asset: { name: 'Logged twice' }, node_id: node.id } + end + end + assert_equal users(:quentin), NodeAction.last.user + end + # --- edit --- test "get edit" do diff --git a/test/models/node_attach_asset_test.rb b/test/models/node_attach_asset_test.rb index cb5b60f5..2df2cfbb 100644 --- a/test/models/node_attach_asset_test.rb +++ b/test/models/node_attach_asset_test.rb @@ -95,6 +95,25 @@ class NodeAttachAssetTest < ActiveSupport::TestCase assert_raises(ActiveRecord::RecordInvalid) { @node.attach_asset!(@image, :user => @user) } end + test "attaching writes an asset_attach entry with node and asset participants" do + result = @node.attach_asset!(@image, :user => @user, :headline => true) + assert_equal :set, result[:headline] + + action = NodeAction.where(:action => "asset_attach").last + assert_equal @node, action.node + subjects = action.action_participants.map { |p| [p.subject_type, p.subject_id] } + assert_includes subjects, ["Node", @node.id] + assert_includes subjects, ["Asset", @image.id] + assert action.metadata["headline"] + end + + test "a fully redundant attach writes no entry" do + @node.attach_asset!(@image, :user => @user) + assert_no_difference 'NodeAction.count' do + @node.attach_asset!(@image, :user => @user) + end + end + private def create_image_asset -- cgit v1.3