diff options
| -rw-r--r-- | app/controllers/assets_controller.rb | 7 | ||||
| -rw-r--r-- | app/helpers/node_actions_helper.rb | 1 | ||||
| -rw-r--r-- | app/models/node.rb | 107 | ||||
| -rw-r--r-- | app/views/layouts/_flash.html.erb | 5 | ||||
| -rw-r--r-- | config/locales/de.yml | 6 | ||||
| -rw-r--r-- | config/locales/en.yml | 4 | ||||
| -rw-r--r-- | test/controllers/assets_controller_test.rb | 34 | ||||
| -rw-r--r-- | test/controllers/content_controller_test.rb | 2 | ||||
| -rw-r--r-- | test/models/node_attach_asset_test.rb | 83 |
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 |
| 126 | end | 131 | end |
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 |
