summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/assets_controller.rb7
-rw-r--r--app/helpers/node_actions_helper.rb1
-rw-r--r--app/models/node.rb107
-rw-r--r--app/views/layouts/_flash.html.erb5
-rw-r--r--config/locales/de.yml6
-rw-r--r--config/locales/en.yml4
-rw-r--r--test/controllers/assets_controller_test.rb34
-rw-r--r--test/controllers/content_controller_test.rb2
-rw-r--r--test/models/node_attach_asset_test.rb83
9 files changed, 149 insertions, 100 deletions
diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb
index f8a7d1e4..d86370f4 100644
--- a/app/controllers/assets_controller.rb
+++ b/app/controllers/assets_controller.rb
@@ -111,8 +111,10 @@ class AssetsController < ApplicationController
111 flash[:notice] = 111 flash[:notice] =
112 if result[:attached].zero? 112 if result[:attached].zero?
113 t("flash.assets.already_attached", :title => node.title) 113 t("flash.assets.already_attached", :title => node.title)
114 elsif result[:draft_created]
115 t("flash.assets.attached_new_draft", :title => node.title)
114 else 116 else
115 t("flash.assets.attached", :title => node.title) 117 t("flash.assets.attached_to_draft", :title => node.title)
116 end 118 end
117 case result[:headline] 119 case result[:headline]
118 when :set then flash[:notice] += " " + t("flash.common.now_headline") 120 when :set then flash[:notice] += " " + t("flash.common.now_headline")
@@ -122,5 +124,8 @@ class AssetsController < ApplicationController
122 rescue LockedByAnotherUser 124 rescue LockedByAnotherUser
123 flash[:locked_by] = node.lock_owner&.login 125 flash[:locked_by] = node.lock_owner&.login
124 flash[:locked_node_path] = node_path(node) 126 flash[:locked_node_path] = node_path(node)
127 rescue ActiveRecord::RecordInvalid => e
128 flash[:error] = e.record.errors.full_messages.to_sentence
129 flash[:resolve_node_path] = node_path(node)
125 end 130 end
126end 131end
diff --git a/app/helpers/node_actions_helper.rb b/app/helpers/node_actions_helper.rb
index f21fee61..960e28b3 100644
--- a/app/helpers/node_actions_helper.rb
+++ b/app/helpers/node_actions_helper.rb
@@ -14,7 +14,6 @@ module NodeActionsHelper
14 "discard_autosave" => "eraser", 14 "discard_autosave" => "eraser",
15 "destroy_draft" => "eraser", 15 "destroy_draft" => "eraser",
16 "asset_create" => "upload", 16 "asset_create" => "upload",
17 "asset_attach" => "paperclip",
18 "asset_destroy" => "file-x", 17 "asset_destroy" => "file-x",
19 "otp_enroll" => "shield-lock", 18 "otp_enroll" => "shield-lock",
20 "otp_disable" => "shield-off", 19 "otp_disable" => "shield-off",
diff --git a/app/models/node.rb b/app/models/node.rb
index f5b76b45..a401ed69 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -6,8 +6,8 @@ class Node < ApplicationRecord
6 has_many :pages, -> { order("revision ASC") }, :dependent => :destroy 6 has_many :pages, -> { order("revision ASC") }, :dependent => :destroy
7 7
8 # Entries where this node is the primary subject (fast-path column). 8 # Entries where this node is the primary subject (fast-path column).
9 # For a *complete* history -- including subtree trash/destroy entries 9 # For a *complete* history, including subtree trash/destroy entries
10 # recorded at an ancestor -- use participated_actions instead. 10 # recorded at an ancestor, use participated_actions instead.
11 has_many :node_actions, :dependent => :nullify 11 has_many :node_actions, :dependent => :nullify
12 has_many :action_participations, :class_name => "ActionParticipant", :as => :subject 12 has_many :action_participations, :class_name => "ActionParticipant", :as => :subject
13 has_many :participated_actions, :through => :action_participations, :source => :node_action 13 has_many :participated_actions, :through => :action_participations, :source => :node_action
@@ -15,7 +15,7 @@ class Node < ApplicationRecord
15 belongs_to :head, :class_name => "Page", :foreign_key => :head_id, optional: true 15 belongs_to :head, :class_name => "Page", :foreign_key => :head_id, optional: true
16 belongs_to :draft, :class_name => "Page", :foreign_key => :draft_id, optional: true 16 belongs_to :draft, :class_name => "Page", :foreign_key => :draft_id, optional: true
17 # Autosave pages carry no node_id, so has_many :pages does not cover 17 # Autosave pages carry no node_id, so has_many :pages does not cover
18 # them -- this dependent: :destroy is their only cleanup on node destroy. 18 # them. This dependent: :destroy is their only cleanup on node destroy.
19 belongs_to :autosave, :class_name => "Page", :foreign_key => :autosave_id, :dependent => :destroy, optional: true 19 belongs_to :autosave, :class_name => "Page", :foreign_key => :autosave_id, :dependent => :destroy, optional: true
20 20
21 has_many :events, :dependent => :destroy 21 has_many :events, :dependent => :destroy
@@ -44,8 +44,8 @@ class Node < ApplicationRecord
44 :if => :default_template_name_changed? 44 :if => :default_template_name_changed?
45 45
46 # Everything outside the Trash subtree, the Trash node included. 46 # Everything outside the Trash subtree, the Trash node included.
47 # Relies on unique_name being authoritative for tree position -- 47 # Relies on unique_name being authoritative for tree position.
48 # the same trust public routing places in it. 48 # The same trust public routing places in it.
49 scope :not_in_trash, -> { 49 scope :not_in_trash, -> {
50 where.not(:unique_name => CccConventions::TRASH_SLUG) 50 where.not(:unique_name => CccConventions::TRASH_SLUG)
51 .where("unique_name NOT LIKE ?", "#{CccConventions::TRASH_SLUG}/%") 51 .where("unique_name NOT LIKE ?", "#{CccConventions::TRASH_SLUG}/%")
@@ -93,7 +93,7 @@ class Node < ApplicationRecord
93 # Instance Methods 93 # Instance Methods
94 94
95 # Acquires (or reaffirms) the editing lock without creating a draft or 95 # Acquires (or reaffirms) the editing lock without creating a draft or
96 # an autosave -- both are now deferred until there is real content to 96 # an autosave, both are now deferred until there is real content to
97 # hold. 97 # hold.
98 def lock_for_editing! current_user 98 def lock_for_editing! current_user
99 if self.lock_owner.nil? || self.lock_owner == current_user 99 if self.lock_owner.nil? || self.lock_owner == current_user
@@ -133,11 +133,11 @@ class Node < ApplicationRecord
133 133
134 # Promotes the current autosave into the draft (creating the draft if 134 # Promotes the current autosave into the draft (creating the draft if
135 # none exists yet) and destroys the autosave afterward. This is what 135 # none exists yet) and destroys the autosave afterward. This is what
136 # the explicit "Save" action does; it never creates a new revision -- 136 # the explicit "Save" action does; it never creates a new revision,
137 # same as any other in-place draft edit. The new draft is created via 137 # same as any other in-place draft edit. The new draft is created via
138 # self.pages.create! rather than by repointing the autosave's own 138 # self.pages.create! rather than by repointing the autosave's own
139 # node_id, because acts_as_list assigns the revision number at create 139 # node_id, because acts_as_list assigns the revision number at create
140 # time, scoped to node_id -- a page created with node_id nil and 140 # time, scoped to node_id, a page created with node_id nil and
141 # reassigned afterward would carry a wrong or missing revision number. 141 # reassigned afterward would carry a wrong or missing revision number.
142 def save_draft! current_user 142 def save_draft! current_user
143 assert_locked_by! current_user 143 assert_locked_by! current_user
@@ -178,7 +178,7 @@ class Node < ApplicationRecord
178 178
179 # Which layer-pairs are meaningful to compare right now, given this 179 # Which layer-pairs are meaningful to compare right now, given this
180 # node's actual state. Head vs autosave only shows up when no draft 180 # node's actual state. Head vs autosave only shows up when no draft
181 # sits between them -- with a draft present, autosave is compared 181 # sits between them, with a draft present, autosave is compared
182 # against the draft, never past it straight to head. 182 # against the draft, never past it straight to head.
183 def available_layer_pairs 183 def available_layer_pairs
184 pairs = [] 184 pairs = []
@@ -201,8 +201,8 @@ class Node < ApplicationRecord
201 self.draft.reload 201 self.draft.reload
202 end 202 end
203 203
204 # Discards exactly the topmost non-empty layer -- autosave if present, 204 # Discards exactly the topmost non-empty layer: autosave if present,
205 # else draft -- and reveals whatever's beneath it. Releases the lock 205 # else draft, and reveals whatever's beneath it. Releases the lock
206 # only once nothing is left to protect (no draft survives); leaves it 206 # only once nothing is left to protect (no draft survives); leaves it
207 # alone whenever a draft remains, since #edit still has real content 207 # alone whenever a draft remains, since #edit still has real content
208 # open. 208 # open.
@@ -313,7 +313,7 @@ class Node < ApplicationRecord
313 # Moves this node and its subtree into the Trash. Demotes every head 313 # Moves this node and its subtree into the Trash. Demotes every head
314 # in the subtree first (aggregators and search operate on heads 314 # in the subtree first (aggregators and search operate on heads
315 # regardless of tree position); where a node has no draft, the former 315 # regardless of tree position); where a node has no draft, the former
316 # head becomes its draft so content stays editable and restorable -- 316 # head becomes its draft so content stays editable and restorable,
317 # otherwise the former head remains a plain revision. One log entry, 317 # otherwise the former head remains a plain revision. One log entry,
318 # at the root, carrying the leaving-public-view snapshot. 318 # at the root, carrying the leaving-public-view snapshot.
319 def trash! current_user = nil 319 def trash! current_user = nil
@@ -475,28 +475,17 @@ class Node < ApplicationRecord
475 end 475 end
476 end 476 end
477 477
478 # Attaches an asset to every current lifecycle row -- head, draft and 478 # Attaches an asset to the node's draft, creating one from the head if
479 # autosave -- that does not already carry it. Attachments are page- 479 # none exists.
480 # scoped content (RelatedAsset belongs_to :page; drafts and autosaves
481 # are wholesale clones), so attaching to a single layer is how an
482 # attachment gets silently lost when another layer replaces it at
483 # publish or save. This is the out-of-band counterpart to the
484 # in-editor attach UI; it refuses when someone else holds the editing
485 # lock. Attaching to a head row changes the public page immediately,
486 # by design -- same reasoning as formalizing an already-existing
487 # editorial link.
488 # 480 #
489 # headline is a node-level decision: the flag is set on the newly 481 # Refuses when an autosave exists: it predates this attach and would
490 # created joins only when no current row has a headline yet and the 482 # overwrite the draft at save_draft!, silently dropping the join.
491 # asset is eligible; otherwise the asset is attached plain and the 483 # Refuses when someone else holds the lock, so an editor's work is not
492 # result says why, so the caller can point the editor at the star in 484 # altered under them.
493 # the editor instead.
494 # 485 #
495 # Returns { :attached => n, :already => n, 486 # Returns { :attached => n, :already => n, :draft_created => bool,
496 # :headline => nil | :set | :kept_existing | :not_eligible } 487 # :headline => nil | :set | :kept_existing | :not_eligible }
497 def attach_asset! asset, user:, headline: false 488 def attach_asset! asset, user:, headline: false
498 guard_live_change!(user)
499
500 if in_trash? || trash_node? 489 if in_trash? || trash_node?
501 errors.add(:base, :attach_in_trash) 490 errors.add(:base, :attach_in_trash)
502 raise ActiveRecord::RecordInvalid.new(self) 491 raise ActiveRecord::RecordInvalid.new(self)
@@ -510,37 +499,43 @@ class Node < ApplicationRecord
510 ) 499 )
511 end 500 end
512 501
513 rows = [head, draft, autosave].compact 502 if autosave
514 to_attach = rows.reject { |row| row.related_assets.exists?(:asset_id => asset.id) } 503 errors.add(:base, :attach_with_autosave)
504 raise ActiveRecord::RecordInvalid.new(self)
505 end
515 506
516 headline_state = 507 source = draft || head
517 if headline && to_attach.any? 508 if source && source.related_assets.exists?(:asset_id => asset.id)
518 if !(asset.image? || asset.pdf?) 509 return { :attached => 0,
519 :not_eligible 510 :already => 1,
520 elsif rows.any? { |row| row.headline_asset.present? } 511 :draft_created => false,
521 :kept_existing 512 :headline => nil }
522 else 513 end
523 :set 514
524 end 515 draft_created = draft.nil?
525 end
526 516
527 ActiveRecord::Base.transaction do 517 ActiveRecord::Base.transaction do
528 to_attach.each do |row| 518 create_new_draft(user) if draft_created
529 row.related_assets.create!(:asset => asset, :headline => headline_state == :set) 519 page = draft.reload
530 end
531 520
532 if to_attach.any? 521 headline_state =
533 metadata = { :asset_name => asset.name, 522 if headline
534 :path => asset.upload.url.sub(/\?\d+$/, "") } 523 if !(asset.image? || asset.pdf?)
535 metadata[:headline] = true if headline_state == :set 524 :not_eligible
536 NodeAction.record!(:node => self, :participants => [self, asset], 525 elsif page.headline_asset.present?
537 :user => user, :action => "asset_attach", **metadata) 526 :kept_existing
538 end 527 else
539 end 528 :set
529 end
530 end
531
532 page.related_assets.create!(:asset => asset, :headline => headline_state == :set)
540 533
541 { :attached => to_attach.size, 534 { :attached => 1,
542 :already => rows.size - to_attach.size, 535 :already => 0,
543 :headline => headline_state } 536 :draft_created => draft_created,
537 :headline => headline_state }
538 end
544 end 539 end
545 540
546 def title 541 def title
diff --git a/app/views/layouts/_flash.html.erb b/app/views/layouts/_flash.html.erb
index b72c377f..2cc7a5ab 100644
--- a/app/views/layouts/_flash.html.erb
+++ b/app/views/layouts/_flash.html.erb
@@ -13,6 +13,11 @@
13 <%= link_to t(".locked_link"), flash[:locked_node_path] %><%= t(".locked_suffix") %> 13 <%= link_to t(".locked_link"), flash[:locked_node_path] %><%= t(".locked_suffix") %>
14 </span> 14 </span>
15 <% end %> 15 <% end %>
16 <% if flash[:resolve_node_path] %>
17 <span class="warning">
18 <%= link_to t(".resolve_link"), flash[:resolve_node_path] %>
19 </span>
20 <% end %>
16 <% if flash[:headline_kept_path] %> 21 <% if flash[:headline_kept_path] %>
17 <span class="warning"><%= t(".headline_prefix") %> 22 <span class="warning"><%= t(".headline_prefix") %>
18 <%= link_to t(".headline_link"), flash[:headline_kept_path] %><%= t(".headline_suffix") %> 23 <%= link_to t(".headline_link"), flash[:headline_kept_path] %><%= t(".headline_suffix") %>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index c6dc17b7..58367aaf 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -136,6 +136,7 @@ de:
136 destroy_outside_trash: "Nodes können nur aus dem Papierkorb gelöscht werden" 136 destroy_outside_trash: "Nodes können nur aus dem Papierkorb gelöscht werden"
137 attach_in_trash: "An einen Node im Papierkorb können keine Assets angehängt werden" 137 attach_in_trash: "An einen Node im Papierkorb können keine Assets angehängt werden"
138 not_permitted: "In diesem Bereich dürfen nur Mitglieder der Redaktion veröffentlichte Inhalte ändern" 138 not_permitted: "In diesem Bereich dürfen nur Mitglieder der Redaktion veröffentlichte Inhalte ändern"
139 attach_with_autosave: "An einen Node mit ungespeicherten Änderungen im Editor können keine Assets angehängt werden"
139 page: 140 page:
140 attributes: 141 attributes:
141 slug: 142 slug:
@@ -659,8 +660,9 @@ de:
659 assets: 660 assets:
660 created: "Asset wurde angelegt." 661 created: "Asset wurde angelegt."
661 updated: "Asset wurde aktualisiert." 662 updated: "Asset wurde aktualisiert."
662 attached: "Asset wurde angelegt und an „%{title}“ angehängt." 663 already_attached: "Asset wurde gespeichert. Es war bereits an „%{title}“ angehängt."
663 already_attached: "Asset gespeichert — es war bereits an „%{title}“ angehängt." 664 attached_to_draft: "Zum Entwurf von %{title} hinzugefügt. Zum Veröffentlichen den Entwurf freigeben."
665 attached_new_draft: "Entwurf von %{title} mit diesem Anhang angelegt. Zum Veröffentlichen den Entwurf freigeben."
664 events: 666 events:
665 created: "Termin wurde angelegt." 667 created: "Termin wurde angelegt."
666 updated: "Termin wurde aktualisiert." 668 updated: "Termin wurde aktualisiert."
diff --git a/config/locales/en.yml b/config/locales/en.yml
index bd339cc9..4bdca31b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -87,6 +87,7 @@ en:
87 destroy_outside_trash: "Nodes are only destroyed from the Trash" 87 destroy_outside_trash: "Nodes are only destroyed from the Trash"
88 attach_in_trash: "Cannot attach assets to a node in the Trash" 88 attach_in_trash: "Cannot attach assets to a node in the Trash"
89 not_permitted: "Only Redaktion members may change published content in this section" 89 not_permitted: "Only Redaktion members may change published content in this section"
90 attach_with_autosave: "Cannot attach assets to a node with unsaved changes in the editor"
90 page: 91 page:
91 attributes: 92 attributes:
92 slug: 93 slug:
@@ -627,8 +628,9 @@ en:
627 assets: 628 assets:
628 created: "Asset was successfully created." 629 created: "Asset was successfully created."
629 updated: "Asset was successfully updated." 630 updated: "Asset was successfully updated."
630 attached: "Asset was successfully created and attached to “%{title}”."
631 already_attached: "Asset saved — it was already attached to “%{title}”." 631 already_attached: "Asset saved — it was already attached to “%{title}”."
632 attached_to_draft: "Attached to the draft of %{title}. Publish it to make the change live."
633 attached_new_draft: "Created a draft of %{title} with this attachment. Publish it to make the change live."
632 events: 634 events:
633 created: "Event was successfully created." 635 created: "Event was successfully created."
634 updated: "Event was successfully updated." 636 updated: "Event was successfully updated."
diff --git a/test/controllers/assets_controller_test.rb b/test/controllers/assets_controller_test.rb
index 467e1a68..aa3e00e6 100644
--- a/test/controllers/assets_controller_test.rb
+++ b/test/controllers/assets_controller_test.rb
@@ -94,7 +94,7 @@ class AssetsControllerTest < ActionController::TestCase
94 94
95 # --- create with attach --- 95 # --- create with attach ---
96 96
97 test "create with node_id attaches the asset to the node's draft" do 97 test "create with node_id attaches the asset to an existing draft" do
98 node = Node.root.children.create!(:slug => "asset_attach_target") 98 node = Node.root.children.create!(:slug => "asset_attach_target")
99 99
100 post :create, params: { asset: { name: 'Attach me' }, node_id: node.id } 100 post :create, params: { asset: { name: 'Attach me' }, node_id: node.id }
@@ -102,7 +102,21 @@ class AssetsControllerTest < ActionController::TestCase
102 assert_response :redirect 102 assert_response :redirect
103 asset = Asset.last 103 asset = Asset.last
104 assert_includes node.draft.assets.reload, asset 104 assert_includes node.draft.assets.reload, asset
105 assert_equal I18n.t("flash.assets.attached", :title => node.title), flash[:notice] 105 assert_equal I18n.t("flash.assets.attached_to_draft", :title => node.title), flash[:notice]
106 end
107
108 test "create with node_id creates a draft when none is pending" do
109 node = Node.root.children.create!(:slug => "asset_attach_no_draft")
110 node.publish_draft!(users(:quentin))
111 assert_nil node.reload.draft
112
113 post :create, params: { asset: { name: 'Attach me too' }, node_id: node.id }
114
115 node.reload
116 assert_includes node.draft.assets.reload, Asset.last
117 assert_empty node.head.assets.reload
118 assert_equal users(:quentin), node.draft.editor
119 assert_equal I18n.t("flash.assets.attached_new_draft", :title => node.title), flash[:notice]
106 end 120 end
107 121
108 test "create against a foreign-locked node keeps the asset but refuses the attach" do 122 test "create against a foreign-locked node keeps the asset but refuses the attach" do
@@ -133,14 +147,16 @@ class AssetsControllerTest < ActionController::TestCase
133 assert_equal node_path(node), flash[:headline_kept_path] 147 assert_equal node_path(node), flash[:headline_kept_path]
134 end 148 end
135 149
136 test "create with node_id writes an asset_create and an asset_attach entry" do 150 test "create with node_id writes only an asset_create entry" do
137 node = Node.root.children.create!(:slug => "asset_log_pair") 151 node = Node.root.children.create!(:slug => "asset_log_single")
138 assert_difference 'NodeAction.where(:action => "asset_create").count' do 152
139 assert_difference 'NodeAction.where(:action => "asset_attach").count' do 153 assert_difference 'NodeAction.count', 1 do
140 post :create, params: { asset: { name: 'Logged twice' }, node_id: node.id } 154 post :create, params: { asset: { name: 'Logged once' }, node_id: node.id }
141 end
142 end 155 end
143 assert_equal users(:quentin), NodeAction.last.user 156
157 action = NodeAction.last
158 assert_equal "asset_create", action.action
159 assert_equal users(:quentin), action.user
144 end 160 end
145 161
146 # --- edit --- 162 # --- edit ---
diff --git a/test/controllers/content_controller_test.rb b/test/controllers/content_controller_test.rb
index 39fe276b..482c1ddf 100644
--- a/test/controllers/content_controller_test.rb
+++ b/test/controllers/content_controller_test.rb
@@ -170,6 +170,8 @@ class ContentControllerTest < ActionController::TestCase
170 :upload_content_type => "image/png", 170 :upload_content_type => "image/png",
171 :upload_updated_at => Time.at(1_700_000_000)) 171 :upload_updated_at => Time.at(1_700_000_000))
172 node.attach_asset!(asset, :user => @user1, :headline => true) 172 node.attach_asset!(asset, :user => @user1, :headline => true)
173 node.publish_draft!
174 node.reload
173 175
174 # has_variant? only tests File.exist?, so touching the path is enough 176 # has_variant? only tests File.exist?, so touching the path is enough
175 # and no ImageMagick runs in the suite. image/png takes .jpg for the 177 # and no ImageMagick runs in the suite. image/png takes .jpg for the
diff --git a/test/models/node_attach_asset_test.rb b/test/models/node_attach_asset_test.rb
index 2df2cfbb..9ab38158 100644
--- a/test/models/node_attach_asset_test.rb
+++ b/test/models/node_attach_asset_test.rb
@@ -9,49 +9,67 @@ class NodeAttachAssetTest < ActiveSupport::TestCase
9 @image = create_image_asset 9 @image = create_image_asset
10 end 10 end
11 11
12 test "attaches to a draft-only node" do 12 test "attaches to an existing draft" do
13 result = @node.attach_asset!(@image, :user => @user) 13 result = @node.attach_asset!(@image, :user => @user)
14 assert_equal 1, result[:attached] 14 assert_equal 1, result[:attached]
15 assert_not result[:draft_created]
15 assert_includes @node.draft.assets, @image 16 assert_includes @node.draft.assets, @image
16 end 17 end
17 18
18 test "attaches to head when no draft is pending" do 19 test "creates a draft when none is pending and leaves head untouched" do
19 @node.publish_draft!(@user) 20 @node.publish_draft!(@user)
20 result = @node.attach_asset!(@image, :user => @user) 21 result = @node.attach_asset!(@image, :user => @user)
21 assert_equal 1, result[:attached] 22 assert_equal 1, result[:attached]
22 assert_includes @node.head.assets, @image 23 assert result[:draft_created]
24 assert_includes @node.draft.assets, @image
25 assert_empty @node.head.assets.reload
23 end 26 end
24 27
25 test "attaches to head and pending draft alike" do 28 test "attaches to a pending draft and leaves head untouched" do
26 @node.publish_draft!(@user) 29 @node.publish_draft!(@user)
27 @node.lock_for_editing!(@user) 30 @node.lock_for_editing!(@user)
28 @node.create_new_draft(@user) 31 @node.create_new_draft(@user)
29 result = @node.attach_asset!(@image, :user => @user) 32 result = @node.attach_asset!(@image, :user => @user)
30 assert_equal 2, result[:attached] 33 assert_equal 1, result[:attached]
31 assert_includes @node.head.assets, @image 34 assert_not result[:draft_created]
32 assert_includes @node.draft.assets, @image 35 assert_includes @node.draft.assets, @image
36 assert_empty @node.head.assets.reload
33 end 37 end
34 38
35 test "attaches to all three lifecycle rows" do 39 test "refuses when an autosave exists and writes nothing" do
36 @node.publish_draft!(@user) 40 @node.publish_draft!(@user)
37 @node.lock_for_editing!(@user) 41 @node.lock_for_editing!(@user)
38 @node.create_new_draft(@user) 42 @node.create_new_draft(@user)
39 @node.autosave!({ :title => "wip" }, @user) 43 @node.autosave!({ :title => "wip" }, @user)
44 assert_raises(ActiveRecord::RecordInvalid) { @node.attach_asset!(@image, :user => @user) }
45 assert_empty @node.draft.assets.reload
46 assert_empty @node.autosave.assets.reload
47 end
48
49 test "reports an asset the draft already carries without duplicating it" do
50 @node.draft.related_assets.create!(:asset => @image)
40 result = @node.attach_asset!(@image, :user => @user) 51 result = @node.attach_asset!(@image, :user => @user)
41 assert_equal 3, result[:attached] 52 assert_equal 0, result[:attached]
42 [@node.head, @node.draft, @node.autosave].each do |row| 53 assert_equal 1, result[:already]
43 assert_includes row.assets, @image 54 assert_not result[:draft_created]
44 end 55 assert_equal 1, @node.draft.related_assets.where(:asset_id => @image.id).count
45 end 56 end
46 57
47 test "skips rows that already carry the asset" do 58 test "creates no draft when head already carries the asset" do
48 @node.draft.related_assets.create!(:asset => @image) 59 @node.draft.related_assets.create!(:asset => @image)
49 @node.publish_draft!(@user) 60 @node.publish_draft!(@user)
50 @node.lock_for_editing!(@user) 61 assert_nil @node.draft
51 @node.create_new_draft(@user) 62
63 result = @node.attach_asset!(@image, :user => @user)
64 assert_equal 0, result[:attached]
65 assert_not result[:draft_created]
66 assert_nil @node.reload.draft
67 end
68
69 test "attaching twice leaves one join row" do
70 @node.attach_asset!(@image, :user => @user)
52 result = @node.attach_asset!(@image, :user => @user) 71 result = @node.attach_asset!(@image, :user => @user)
53 assert_equal 0, result[:attached] 72 assert_equal 0, result[:attached]
54 assert_equal 2, result[:already]
55 assert_equal 1, @node.draft.related_assets.where(:asset_id => @image.id).count 73 assert_equal 1, @node.draft.related_assets.where(:asset_id => @image.id).count
56 end 74 end
57 75
@@ -82,6 +100,17 @@ class NodeAttachAssetTest < ActiveSupport::TestCase
82 assert_includes @node.draft.assets, @image 100 assert_includes @node.draft.assets, @image
83 end 101 end
84 102
103 test "keeps a headline the new draft inherited from head" do
104 incumbent = create_image_asset
105 @node.draft.related_assets.create!(:asset => incumbent, :headline => true)
106 @node.publish_draft!(@user)
107
108 result = @node.attach_asset!(@image, :user => @user, :headline => true)
109 assert result[:draft_created]
110 assert_equal :kept_existing, result[:headline]
111 assert_equal incumbent, @node.draft.reload.headline_asset
112 end
113
85 test "declines the headline flag for ineligible asset types" do 114 test "declines the headline flag for ineligible asset types" do
86 plain = create_plain_asset 115 plain = create_plain_asset
87 result = @node.attach_asset!(plain, :user => @user, :headline => true) 116 result = @node.attach_asset!(plain, :user => @user, :headline => true)
@@ -95,25 +124,19 @@ class NodeAttachAssetTest < ActiveSupport::TestCase
95 assert_raises(ActiveRecord::RecordInvalid) { @node.attach_asset!(@image, :user => @user) } 124 assert_raises(ActiveRecord::RecordInvalid) { @node.attach_asset!(@image, :user => @user) }
96 end 125 end
97 126
98 test "attaching writes an asset_attach entry with node and asset participants" do 127 test "attaching writes no log entry -- publish carries the witnessing" do
99 result = @node.attach_asset!(@image, :user => @user, :headline => true) 128 assert_no_difference "NodeAction.count" do
100 assert_equal :set, result[:headline]
101
102 action = NodeAction.where(:action => "asset_attach").last
103 assert_equal @node, action.node
104 subjects = action.action_participants.map { |p| [p.subject_type, p.subject_id] }
105 assert_includes subjects, ["Node", @node.id]
106 assert_includes subjects, ["Asset", @image.id]
107 assert action.metadata["headline"]
108 end
109
110 test "a fully redundant attach writes no entry" do
111 @node.attach_asset!(@image, :user => @user)
112 assert_no_difference 'NodeAction.count' do
113 @node.attach_asset!(@image, :user => @user) 129 @node.attach_asset!(@image, :user => @user)
114 end 130 end
115 end 131 end
116 132
133 test "attaching under a restricted surface needs no redaktion role" do
134 updates = Node.root.children.create!(:slug => "updates")
135 node = updates.children.create!(:slug => "gated-attachment")
136 result = node.reload.attach_asset!(@image, :user => @user)
137 assert_equal 1, result[:attached]
138 end
139
117 private 140 private
118 141
119 def create_image_asset 142 def create_image_asset