diff options
| -rw-r--r-- | app/models/node.rb | 57 | ||||
| -rw-r--r-- | test/models/node_attach_asset_test.rb | 107 |
2 files changed, 164 insertions, 0 deletions
diff --git a/app/models/node.rb b/app/models/node.rb index 746b7c7..a5a40d3 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -442,6 +442,63 @@ class Node < ApplicationRecord | |||
| 442 | end | 442 | end |
| 443 | end | 443 | end |
| 444 | 444 | ||
| 445 | # Attaches an asset to every current lifecycle row -- head, draft and | ||
| 446 | # autosave -- that does not already carry it. Attachments are page- | ||
| 447 | # scoped content (RelatedAsset belongs_to :page; drafts and autosaves | ||
| 448 | # are wholesale clones), so attaching to a single layer is how an | ||
| 449 | # attachment gets silently lost when another layer replaces it at | ||
| 450 | # publish or save. This is the out-of-band counterpart to the | ||
| 451 | # in-editor attach UI; it refuses when someone else holds the editing | ||
| 452 | # lock. Attaching to a head row changes the public page immediately, | ||
| 453 | # by design -- same reasoning as formalizing an already-existing | ||
| 454 | # editorial link. | ||
| 455 | # | ||
| 456 | # headline is a node-level decision: the flag is set on the newly | ||
| 457 | # created joins only when no current row has a headline yet and the | ||
| 458 | # asset is eligible; otherwise the asset is attached plain and the | ||
| 459 | # result says why, so the caller can point the editor at the star in | ||
| 460 | # the editor instead. | ||
| 461 | # | ||
| 462 | # Returns { :attached => n, :already => n, | ||
| 463 | # :headline => nil | :set | :kept_existing | :not_eligible } | ||
| 464 | def attach_asset! asset, user:, headline: false | ||
| 465 | if in_trash? || trash_node? | ||
| 466 | raise ActiveRecord::RecordInvalid.new(self), "Cannot attach assets to a node in the Trash" | ||
| 467 | end | ||
| 468 | |||
| 469 | if lock_owner && lock_owner != user | ||
| 470 | raise( | ||
| 471 | LockedByAnotherUser, | ||
| 472 | "Page is locked by another user who is working on it! " \ | ||
| 473 | "Last modification: #{(self.autosave || self.draft || self.head).updated_at.to_fs(:db)}" | ||
| 474 | ) | ||
| 475 | end | ||
| 476 | |||
| 477 | rows = [head, draft, autosave].compact | ||
| 478 | to_attach = rows.reject { |row| row.related_assets.exists?(:asset_id => asset.id) } | ||
| 479 | |||
| 480 | headline_state = | ||
| 481 | if headline && to_attach.any? | ||
| 482 | if !(asset.image? || asset.pdf?) | ||
| 483 | :not_eligible | ||
| 484 | elsif rows.any? { |row| row.headline_asset.present? } | ||
| 485 | :kept_existing | ||
| 486 | else | ||
| 487 | :set | ||
| 488 | end | ||
| 489 | end | ||
| 490 | |||
| 491 | ActiveRecord::Base.transaction do | ||
| 492 | to_attach.each do |row| | ||
| 493 | row.related_assets.create!(:asset => asset, :headline => headline_state == :set) | ||
| 494 | end | ||
| 495 | end | ||
| 496 | |||
| 497 | { :attached => to_attach.size, | ||
| 498 | :already => rows.size - to_attach.size, | ||
| 499 | :headline => headline_state } | ||
| 500 | end | ||
| 501 | |||
| 445 | def title | 502 | def title |
| 446 | editable_page&.title | 503 | editable_page&.title |
| 447 | end | 504 | end |
diff --git a/test/models/node_attach_asset_test.rb b/test/models/node_attach_asset_test.rb new file mode 100644 index 0000000..cb5b60f --- /dev/null +++ b/test/models/node_attach_asset_test.rb | |||
| @@ -0,0 +1,107 @@ | |||
| 1 | require "test_helper" | ||
| 2 | |||
| 3 | class NodeAttachAssetTest < ActiveSupport::TestCase | ||
| 4 | |||
| 5 | def setup | ||
| 6 | @user = users(:quentin) | ||
| 7 | @other = users(:aaron) | ||
| 8 | @node = Node.root.children.create!(:slug => "attach_asset_test") | ||
| 9 | @image = create_image_asset | ||
| 10 | end | ||
| 11 | |||
| 12 | test "attaches to a draft-only node" do | ||
| 13 | result = @node.attach_asset!(@image, :user => @user) | ||
| 14 | assert_equal 1, result[:attached] | ||
| 15 | assert_includes @node.draft.assets, @image | ||
| 16 | end | ||
| 17 | |||
| 18 | test "attaches to head when no draft is pending" do | ||
| 19 | @node.publish_draft!(@user) | ||
| 20 | result = @node.attach_asset!(@image, :user => @user) | ||
| 21 | assert_equal 1, result[:attached] | ||
| 22 | assert_includes @node.head.assets, @image | ||
| 23 | end | ||
| 24 | |||
| 25 | test "attaches to head and pending draft alike" do | ||
| 26 | @node.publish_draft!(@user) | ||
| 27 | @node.lock_for_editing!(@user) | ||
| 28 | @node.create_new_draft(@user) | ||
| 29 | result = @node.attach_asset!(@image, :user => @user) | ||
| 30 | assert_equal 2, result[:attached] | ||
| 31 | assert_includes @node.head.assets, @image | ||
| 32 | assert_includes @node.draft.assets, @image | ||
| 33 | end | ||
| 34 | |||
| 35 | test "attaches to all three lifecycle rows" do | ||
| 36 | @node.publish_draft!(@user) | ||
| 37 | @node.lock_for_editing!(@user) | ||
| 38 | @node.create_new_draft(@user) | ||
| 39 | @node.autosave!({ :title => "wip" }, @user) | ||
| 40 | result = @node.attach_asset!(@image, :user => @user) | ||
| 41 | assert_equal 3, result[:attached] | ||
| 42 | [@node.head, @node.draft, @node.autosave].each do |row| | ||
| 43 | assert_includes row.assets, @image | ||
| 44 | end | ||
| 45 | end | ||
| 46 | |||
| 47 | test "skips rows that already carry the asset" do | ||
| 48 | @node.draft.related_assets.create!(:asset => @image) | ||
| 49 | @node.publish_draft!(@user) | ||
| 50 | @node.lock_for_editing!(@user) | ||
| 51 | @node.create_new_draft(@user) | ||
| 52 | result = @node.attach_asset!(@image, :user => @user) | ||
| 53 | 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 | ||
| 56 | end | ||
| 57 | |||
| 58 | test "refuses when another user holds the lock and writes nothing" do | ||
| 59 | @node.lock_for_editing!(@other) | ||
| 60 | assert_raises(LockedByAnotherUser) { @node.attach_asset!(@image, :user => @user) } | ||
| 61 | assert_empty @node.draft.assets.reload | ||
| 62 | end | ||
| 63 | |||
| 64 | test "proceeds when the attaching user holds the lock" do | ||
| 65 | @node.lock_for_editing!(@user) | ||
| 66 | result = @node.attach_asset!(@image, :user => @user) | ||
| 67 | assert_equal 1, result[:attached] | ||
| 68 | end | ||
| 69 | |||
| 70 | test "sets the headline when none exists" do | ||
| 71 | result = @node.attach_asset!(@image, :user => @user, :headline => true) | ||
| 72 | assert_equal :set, result[:headline] | ||
| 73 | assert_equal @image, @node.draft.headline_asset | ||
| 74 | end | ||
| 75 | |||
| 76 | test "keeps an existing headline and reports it" do | ||
| 77 | incumbent = create_image_asset | ||
| 78 | @node.draft.related_assets.create!(:asset => incumbent, :headline => true) | ||
| 79 | result = @node.attach_asset!(@image, :user => @user, :headline => true) | ||
| 80 | assert_equal :kept_existing, result[:headline] | ||
| 81 | assert_equal incumbent, @node.draft.reload.headline_asset | ||
| 82 | assert_includes @node.draft.assets, @image | ||
| 83 | end | ||
| 84 | |||
| 85 | test "declines the headline flag for ineligible asset types" do | ||
| 86 | plain = create_plain_asset | ||
| 87 | result = @node.attach_asset!(plain, :user => @user, :headline => true) | ||
| 88 | assert_equal :not_eligible, result[:headline] | ||
| 89 | assert_includes @node.draft.assets.reload, plain | ||
| 90 | assert_nil @node.draft.headline_asset | ||
| 91 | end | ||
| 92 | |||
| 93 | test "refuses nodes in the Trash" do | ||
| 94 | @node.trash!(@user) | ||
| 95 | assert_raises(ActiveRecord::RecordInvalid) { @node.attach_asset!(@image, :user => @user) } | ||
| 96 | end | ||
| 97 | |||
| 98 | private | ||
| 99 | |||
| 100 | def create_image_asset | ||
| 101 | Asset.create!(:name => "attach test image", :upload_content_type => "image/png") | ||
| 102 | end | ||
| 103 | |||
| 104 | def create_plain_asset | ||
| 105 | Asset.create!(:name => "attach test note", :upload_content_type => "text/plain") | ||
| 106 | end | ||
| 107 | end | ||
