summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/helpers/node_actions_helper.rb17
-rw-r--r--app/models/asset.rb45
-rw-r--r--app/models/node_action.rb23
-rw-r--r--config/locales/de.yml7
-rw-r--r--config/locales/en.yml7
-rw-r--r--test/models/asset_destroy_test.rb49
6 files changed, 42 insertions, 106 deletions
diff --git a/app/helpers/node_actions_helper.rb b/app/helpers/node_actions_helper.rb
index 960e28b3..09de4250 100644
--- a/app/helpers/node_actions_helper.rb
+++ b/app/helpers/node_actions_helper.rb
@@ -330,22 +330,9 @@ module NodeActionsHelper
330 :asset => asset_ref(action)).html_safe 330 :asset => asset_ref(action)).html_safe
331 end 331 end
332 332
333 def summarize_asset_attach action
334 m = action.metadata
335 key = m["headline"] ? "node_actions.asset_attach_headline" : "node_actions.asset_attach"
336 t(key, :actor => actor_ref(action), :subject => subject_ref(action),
337 :asset => asset_ref(action)).html_safe
338 end
339
340 def summarize_asset_destroy action 333 def summarize_asset_destroy action
341 m = action.metadata 334 t("node_actions.asset_destroy", :actor => actor_ref(action),
342 parts = [t("node_actions.asset_destroy", :actor => actor_ref(action), 335 :asset => asset_ref(action)).html_safe
343 :asset => asset_ref(action))]
344 parts << t("node_actions.asset_destroy_detached",
345 :paths => h(Array(m["detached_from"]).join(", "))) if m["detached_from"].present?
346 parts << t("node_actions.asset_destroy_headlines",
347 :paths => h(Array(m["headline_removed_from"]).join(", "))) if m["headline_removed_from"].present?
348 safe_join(parts, " ")
349 end 336 end
350 337
351 def summarize_otp_enroll action 338 def summarize_otp_enroll action
diff --git a/app/models/asset.rb b/app/models/asset.rb
index 4d43b18f..08ef5a78 100644
--- a/app/models/asset.rb
+++ b/app/models/asset.rb
@@ -41,44 +41,23 @@ class Asset < ApplicationRecord
41 :ids => page_ids).distinct 41 :ids => page_ids).distinct
42 end 42 end
43 43
44 # An asset's reach is the reach of the pages carrying it: destroying one 44 # Witnessed destruction, refused while the asset is attached to any
45 # removes it from every live page at once, so a single restricted 45 # current row. Detaching stays an in-editor act, so nothing
46 # attachment makes the destruction a restricted act. 46 # destroyed here is carried by a page. The entry is still warranted:
47 def restricted? 47 # the original and its variants are reachable under /system/uploads
48 attached_nodes.any?(&:restricted?) 48 # until the row and its files die.
49 end
50
51 # Witnessed destruction. Destroying an asset is a public-facing act
52 # even when unattached. The original and its variants are publicly
53 # reachable under /system/uploads, so an entry is always written,
54 # before the row and its files die. Every currently-attached node
55 # participates so its zoomed history shows the loss; the asset itself
56 # participates as the first non-Node subject (its participant row
57 # dangles after destroy, by design, the name lives on in metadata).
58 def destroy_witnessed! user: 49 def destroy_witnessed! user:
59 if user && !user.may_change_live?(self) 50 if attached_nodes.any?
60 errors.add(:base, :not_permitted) 51 errors.add(:base, :destroy_while_attached)
61 raise ActiveRecord::RecordInvalid.new(self) 52 raise ActiveRecord::RecordInvalid.new(self)
62 end 53 end
63 54
64 ActiveRecord::Base.transaction do 55 ActiveRecord::Base.transaction do
65 affected = attached_nodes.to_a 56 NodeAction.record!(:participants => [self], :user => user,
66 headline_losses = affected.select do |node| 57 :action => "asset_destroy",
67 [node.head, node.draft, node.autosave].compact.any? do |row| 58 :asset_name => name,
68 row.related_assets.exists?(:asset_id => id, :headline => true) 59 :content_type => upload_content_type,
69 end 60 :path => upload.url.sub(/\?\d+$/, ""))
70 end
71
72 metadata = {
73 :asset_name => name,
74 :content_type => upload_content_type,
75 :path => upload.url.sub(/\?\d+$/, ""),
76 }
77 metadata[:detached_from] = affected.map(&:unique_name) if affected.any?
78 metadata[:headline_removed_from] = headline_losses.map(&:unique_name) if headline_losses.any?
79
80 NodeAction.record!(:participants => [self] + affected, :user => user,
81 :action => "asset_destroy", **metadata)
82 destroy! 61 destroy!
83 end 62 end
84 end 63 end
diff --git a/app/models/node_action.rb b/app/models/node_action.rb
index 1e64861e..aab1d238 100644
--- a/app/models/node_action.rb
+++ b/app/models/node_action.rb
@@ -82,22 +82,13 @@ class NodeAction < ApplicationRecord
82 # "asset_create" (witnessed upload; participants: the asset alone): 82 # "asset_create" (witnessed upload; participants: the asset alone):
83 # "asset_name", "content_type", "path" -- flat strings 83 # "asset_name", "content_type", "path" -- flat strings
84 # 84 #
85 # "asset_attach" (out-of-band attach via Node#attach_asset!; written 85 # "asset_destroy" (witnessed asset deletion; the files were publicly
86 # only when at least one new join was created, per the tandem rule -- 86 # reachable, so an entry is always written. Destruction is refused
87 # in-editor curation stays draft-scoped and surfaces at publish. 87 # while the asset is attached to any current row, so no node is ever
88 # participants: the node (primary) and the asset): 88 # affected: node column nil, the asset the sole participant):
89 # "asset_name", "path" -- flat strings 89 # "asset_name" -- flat string
90 # "headline" -- boolean, only when set by this attach 90 # "content_type" -- flat string
91 # 91 # "path" -- public original path, flat string
92 # "asset_destroy" (witnessed asset deletion; always written, even for
93 # unattached assets -- the files were publicly reachable; node column
94 # nil, subjects via participants: the asset plus every then-attached
95 # node):
96 # "asset_name" -- flat string
97 # "content_type" -- flat string
98 # "path" -- public original path, flat string
99 # "detached_from" -- array of unique_names, only when any
100 # "headline_removed_from" -- array of unique_names, only when any
101 # 92 #
102 # "otp_enroll" / "otp_disable" / "otp_reset" (second-factor 93 # "otp_enroll" / "otp_disable" / "otp_reset" (second-factor
103 # lifecycle) and "user_create" / "user_deactivate" / 94 # lifecycle) and "user_create" / "user_deactivate" /
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 58367aaf..92a46768 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -155,7 +155,7 @@ de:
155 asset: 155 asset:
156 attributes: 156 attributes:
157 base: 157 base:
158 not_permitted: "Dieses Asset ist an geschützte Seiten angehängt; nur die Redaktion darf es löschen" 158 destroy_while_attached: "Ein Asset, das noch an Seiten angehängt ist, kann nicht gelöscht werden. Bitte entferne es zuerst im Editor"
159 upload: 159 upload:
160 unreadable_image: "konnte nicht als Bild gelesen werden. Möglicherweise ist sie beschädigt oder hat ein anderes Format, als der Name vermuten lässt." 160 unreadable_image: "konnte nicht als Bild gelesen werden. Möglicherweise ist sie beschädigt oder hat ein anderes Format, als der Name vermuten lässt."
161 event: 161 event:
@@ -218,11 +218,7 @@ de:
218 revision_restored: "wiederhergestellt am %{date} von %{actor}" 218 revision_restored: "wiederhergestellt am %{date} von %{actor}"
219 asset_history: "Chronik" 219 asset_history: "Chronik"
220 asset_create: "%{actor} hat das Asset „%{asset}“ hochgeladen" 220 asset_create: "%{actor} hat das Asset „%{asset}“ hochgeladen"
221 asset_attach: "%{actor} hat „%{asset}“ an %{subject} angehängt"
222 asset_attach_headline: "%{actor} hat „%{asset}“ als Aufmacher an %{subject} angehängt"
223 asset_destroy: "%{actor} hat das Asset „%{asset}“ gelöscht" 221 asset_destroy: "%{actor} hat das Asset „%{asset}“ gelöscht"
224 asset_destroy_detached: "— entfernt von %{paths}"
225 asset_destroy_headlines: "(war Aufmacher von %{paths})"
226 detail_assets_added: "Anhänge hinzugefügt: %{names}" 222 detail_assets_added: "Anhänge hinzugefügt: %{names}"
227 detail_assets_removed: "Anhänge entfernt: %{names}" 223 detail_assets_removed: "Anhänge entfernt: %{names}"
228 assets_reordered: "Anhänge umsortiert" 224 assets_reordered: "Anhänge umsortiert"
@@ -814,6 +810,7 @@ de:
814 locked_prefix: "Die Seite ist von %{login} gesperrt —" 810 locked_prefix: "Die Seite ist von %{login} gesperrt —"
815 locked_link: "dort entsperren" 811 locked_link: "dort entsperren"
816 locked_suffix: ", dann von der Asset-Seite aus erneut anhängen." 812 locked_suffix: ", dann von der Asset-Seite aus erneut anhängen."
813 resolve_link: "Seite öffnen, um den Bearbeitungskonflikt zu lösen"
817 headline_prefix: "Der bestehende Aufmacher der Seite wurde behalten —" 814 headline_prefix: "Der bestehende Aufmacher der Seite wurde behalten —"
818 headline_link: "dort ändern" 815 headline_link: "dort ändern"
819 headline_suffix: ", falls gewünscht." 816 headline_suffix: ", falls gewünscht."
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4bdca31b..458a4069 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -106,7 +106,7 @@ en:
106 asset: 106 asset:
107 attributes: 107 attributes:
108 base: 108 base:
109 not_permitted: "Only Redaktion members may change assets related to nodes in this section" 109 destroy_while_attached: "Cannot destroy an asset that is still attached to pages; remove it in each page's editor first"
110 upload: 110 upload:
111 unreadable_image: "could not be read as an image. It may be damaged, or not the format its name suggests." 111 unreadable_image: "could not be read as an image. It may be damaged, or not the format its name suggests."
112 event: 112 event:
@@ -170,11 +170,7 @@ en:
170 revision_restored: "restored %{date} by %{actor}" 170 revision_restored: "restored %{date} by %{actor}"
171 asset_history: "History" 171 asset_history: "History"
172 asset_destroy: "%{actor} destroyed asset “%{asset}”" 172 asset_destroy: "%{actor} destroyed asset “%{asset}”"
173 asset_destroy_detached: "— detached from %{paths}"
174 asset_destroy_headlines: "(was the headline of %{paths})"
175 asset_create: "%{actor} uploaded asset “%{asset}”" 173 asset_create: "%{actor} uploaded asset “%{asset}”"
176 asset_attach: "%{actor} attached “%{asset}” to %{subject}"
177 asset_attach_headline: "%{actor} attached “%{asset}” to %{subject} as its headline"
178 detail_assets_added: "attachments added: %{names}" 174 detail_assets_added: "attachments added: %{names}"
179 detail_assets_removed: "attachments removed: %{names}" 175 detail_assets_removed: "attachments removed: %{names}"
180 assets_reordered: "attachments reordered" 176 assets_reordered: "attachments reordered"
@@ -766,6 +762,7 @@ en:
766 locked_prefix: "The page is locked by %{login} —" 762 locked_prefix: "The page is locked by %{login} —"
767 locked_link: "unlock it there" 763 locked_link: "unlock it there"
768 locked_suffix: " first, then attach from this asset's page." 764 locked_suffix: " first, then attach from this asset's page."
765 resolve_link: "Open the page to resolve this"
769 headline_prefix: "The page's existing headline was kept —" 766 headline_prefix: "The page's existing headline was kept —"
770 headline_link: "change it there" 767 headline_link: "change it there"
771 headline_suffix: " if needed." 768 headline_suffix: " if needed."
diff --git a/test/models/asset_destroy_test.rb b/test/models/asset_destroy_test.rb
index 2f38d692..54512b16 100644
--- a/test/models/asset_destroy_test.rb
+++ b/test/models/asset_destroy_test.rb
@@ -8,29 +8,6 @@ class AssetDestroyTest < ActiveSupport::TestCase
8 :upload_content_type => "image/png") 8 :upload_content_type => "image/png")
9 end 9 end
10 10
11 test "destroying an attached asset logs nodes and asset as participants" do
12 node = Node.root.children.create!(:slug => "asset_destroy_attached")
13 node.attach_asset!(@asset, :user => @user)
14
15 @asset.destroy_witnessed!(:user => @user)
16
17 action = NodeAction.where(:action => "asset_destroy").last
18 subjects = action.action_participants.map { |p| [p.subject_type, p.subject_id] }
19 assert_includes subjects, ["Asset", @asset.id]
20 assert_includes subjects, ["Node", node.id]
21 assert_equal [node.unique_name], action.metadata["detached_from"]
22 end
23
24 test "records which nodes lost their headline" do
25 node = Node.root.children.create!(:slug => "asset_destroy_headline")
26 node.attach_asset!(@asset, :user => @user, :headline => true)
27
28 @asset.destroy_witnessed!(:user => @user)
29
30 action = NodeAction.where(:action => "asset_destroy").last
31 assert_equal [node.unique_name], action.metadata["headline_removed_from"]
32 end
33
34 test "an unattached asset is still witnessed" do 11 test "an unattached asset is still witnessed" do
35 @asset.destroy_witnessed!(:user => @user) 12 @asset.destroy_witnessed!(:user => @user)
36 13
@@ -49,16 +26,24 @@ class AssetDestroyTest < ActiveSupport::TestCase
49 assert_nil action.action_participants.first.subject 26 assert_nil action.action_participants.first.subject
50 end 27 end
51 28
52 test "destroying an asset attached to a restricted node needs the redaktion role" do 29 test "destruction is refused while the asset is attached" do
53 editor = User.create!(:login => "asset_gate", :email => "ag@example.com", 30 node = Node.root.children.create!(:slug => "asset_destroy_attached")
54 :password => "secret", :password_confirmation => "secret") 31 node.attach_asset!(@asset, :user => @user)
55 updates = Node.root.children.create!(:slug => "updates") 32
56 node = updates.children.create!(:slug => "gated-attachment") 33 assert_raises(ActiveRecord::RecordInvalid) { @asset.destroy_witnessed!(:user => @user) }
57 node.reload.attach_asset!(@asset, :user => nil) 34 assert Asset.exists?(@asset.id)
35 assert_equal 0, NodeAction.where(:action => "asset_destroy").count
36 end
37
38 test "an attachment on a draft alone is enough to refuse" do
39 node = Node.root.children.create!(:slug => "asset_destroy_draft_only")
40 node.attach_asset!(@asset, :user => @user)
41 node.publish_draft!(@user)
42 node.lock_for_editing!(@user)
43 node.create_new_draft(@user)
44 node.head.related_assets.destroy_all
58 45
59 error = assert_raises(ActiveRecord::RecordInvalid) { @asset.destroy_witnessed!(:user => editor) } 46 assert_raises(ActiveRecord::RecordInvalid) { @asset.destroy_witnessed!(:user => @user) }
60 assert_includes error.message,
61 I18n.t("activerecord.errors.models.asset.attributes.base.not_permitted")
62 assert Asset.exists?(@asset.id) 47 assert Asset.exists?(@asset.id)
63 end 48 end
64end 49end