diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-08-07 06:15:14 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-08-07 06:15:14 +0200 |
| commit | f4ddfff03ca9f25d50f39a1971877362d85eb9cb (patch) | |
| tree | 31e6774fd4cf2b579a9622277be150417a2fa48e | |
| parent | 02a4ea750428aa1a9c9e7f2680553c0ce4ef1fec (diff) | |
Move the pending address from the node onto the draft
| -rw-r--r-- | app/controllers/nodes_controller.rb | 19 | ||||
| -rw-r--r-- | app/models/node.rb | 51 | ||||
| -rw-r--r-- | app/models/page.rb | 28 | ||||
| -rw-r--r-- | app/views/nodes/edit.html.erb | 12 | ||||
| -rw-r--r-- | app/views/nodes/show.html.erb | 2 | ||||
| -rw-r--r-- | config/locales/de.yml | 8 | ||||
| -rw-r--r-- | config/locales/en.yml | 8 | ||||
| -rw-r--r-- | db/migrate/20260807010722_add_address_to_pages.rb | 6 | ||||
| -rw-r--r-- | lib/tasks/pages.rake | 33 | ||||
| -rw-r--r-- | public/javascripts/admin_search.js | 2 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 4 | ||||
| -rw-r--r-- | test/controllers/nodes_controller_test.rb | 51 | ||||
| -rw-r--r-- | test/models/node_test.rb | 96 | ||||
| -rw-r--r-- | test/models/node_trash_test.rb | 10 |
14 files changed, 233 insertions, 97 deletions
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 22606674..3009e25a 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb | |||
| @@ -90,7 +90,6 @@ class NodesController < ApplicationController | |||
| 90 | end | 90 | end |
| 91 | 91 | ||
| 92 | def update | 92 | def update |
| 93 | @node.update(node_update_params) | ||
| 94 | @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user ) | 93 | @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user ) |
| 95 | @node.save_draft!(current_user) | 94 | @node.save_draft!(current_user) |
| 96 | 95 | ||
| @@ -123,7 +122,6 @@ class NodesController < ApplicationController | |||
| 123 | end | 122 | end |
| 124 | 123 | ||
| 125 | def autosave | 124 | def autosave |
| 126 | @node.update(node_update_params) | ||
| 127 | @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user ) | 125 | @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user ) |
| 128 | head :ok | 126 | head :ok |
| 129 | rescue LockedByAnotherUser => e | 127 | rescue LockedByAnotherUser => e |
| @@ -167,13 +165,13 @@ class NodesController < ApplicationController | |||
| 167 | end | 165 | end |
| 168 | 166 | ||
| 169 | def restore_from_trash | 167 | def restore_from_trash |
| 170 | parent = Node.find(params[:parent_id]) | 168 | if params[:parent_id].present? && @node.draft |
| 171 | @node.restore_from_trash!(parent, current_user) | 169 | @node.draft.update!(:parent_node_id => params[:parent_id]) |
| 170 | end | ||
| 171 | |||
| 172 | @node.restore_from_trash!(current_user) | ||
| 172 | flash[:notice] = t("flash.nodes.restored") | 173 | flash[:notice] = t("flash.nodes.restored") |
| 173 | redirect_to node_path(@node) | 174 | redirect_to node_path(@node) |
| 174 | rescue ActiveRecord::RecordNotFound | ||
| 175 | flash[:error] = t("flash.nodes.restore_target_missing") | ||
| 176 | redirect_to node_path(@node) | ||
| 177 | rescue ActiveRecord::RecordInvalid => e | 175 | rescue ActiveRecord::RecordInvalid => e |
| 178 | flash[:error] = e.record.errors.full_messages.to_sentence | 176 | flash[:error] = e.record.errors.full_messages.to_sentence |
| 179 | redirect_to node_path(@node) | 177 | redirect_to node_path(@node) |
| @@ -281,12 +279,9 @@ class NodesController < ApplicationController | |||
| 281 | params.fetch(:node, {}).permit(:slug, :parent_id) | 279 | params.fetch(:node, {}).permit(:slug, :parent_id) |
| 282 | end | 280 | end |
| 283 | 281 | ||
| 284 | def node_update_params | ||
| 285 | params.fetch(:node, {}).permit(:staged_slug, :staged_parent_id) | ||
| 286 | end | ||
| 287 | |||
| 288 | def page_params | 282 | def page_params |
| 289 | params.fetch(:page, {}).permit(:title, :abstract, :body, :template_name, :published_at, :user_id) | 283 | params.fetch(:page, {}).permit(:title, :abstract, :body, :template_name, |
| 284 | :published_at, :user_id, :slug, :parent_node_id) | ||
| 290 | end | 285 | end |
| 291 | 286 | ||
| 292 | def find_node | 287 | def find_node |
diff --git a/app/models/node.rb b/app/models/node.rb index 4b7c9772..5c28a786 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -128,6 +128,7 @@ class Node < ApplicationRecord | |||
| 128 | 128 | ||
| 129 | def autosave! attributes, current_user | 129 | def autosave! attributes, current_user |
| 130 | ensure_autosave!(current_user) | 130 | ensure_autosave!(current_user) |
| 131 | attributes = attributes.except(:slug, "slug") if attributes[:slug].blank? && attributes["slug"].blank? | ||
| 131 | self.autosave.assign_attributes(attributes) | 132 | self.autosave.assign_attributes(attributes) |
| 132 | self.autosave.save! | 133 | self.autosave.save! |
| 133 | self.autosave | 134 | self.autosave |
| @@ -227,17 +228,9 @@ class Node < ApplicationRecord | |||
| 227 | self.reload | 228 | self.reload |
| 228 | end | 229 | end |
| 229 | 230 | ||
| 230 | def staged_slug=(value) | ||
| 231 | if head.blank? | ||
| 232 | self.slug = value | ||
| 233 | else | ||
| 234 | super | ||
| 235 | end | ||
| 236 | end | ||
| 237 | |||
| 238 | def publish_draft! current_user = nil | 231 | def publish_draft! current_user = nil |
| 239 | # Return nil if nothing to publish and no staged changes | 232 | # Return nil if nothing to publish |
| 240 | return nil unless self.draft || staged_slug || staged_parent_id | 233 | return nil unless self.draft |
| 241 | 234 | ||
| 242 | guard_live_change!(current_user, :target_path => prospective_unique_name) | 235 | guard_live_change!(current_user, :target_path => prospective_unique_name) |
| 243 | 236 | ||
| @@ -263,26 +256,28 @@ class Node < ApplicationRecord | |||
| 263 | **NodeAction.head_diff(outgoing_head, self.head)) | 256 | **NodeAction.head_diff(outgoing_head, self.head)) |
| 264 | end | 257 | end |
| 265 | 258 | ||
| 266 | if staged_slug && (staged_slug != slug) | 259 | if self.head.slug.present? && self.head.slug != slug |
| 267 | self.slug = staged_slug | 260 | self.slug = self.head.slug |
| 268 | self.staged_slug = nil | ||
| 269 | end | 261 | end |
| 270 | 262 | ||
| 271 | if staged_parent_id && (staged_parent_id != parent_id) | 263 | if self.head.parent_node_id && self.head.parent_node_id != parent_id |
| 272 | new_parent = Node.find(staged_parent_id) | 264 | new_parent = Node.find_by(:id => self.head.parent_node_id) |
| 273 | 265 | ||
| 274 | if new_parent == self || self.descendants.include?(new_parent) | 266 | unless new_parent |
| 267 | errors.add(:base, :move_target_missing) | ||
| 268 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 269 | end | ||
| 270 | |||
| 271 | if new_parent == self || self.descendants.include?(new_parent) || | ||
| 272 | new_parent.trash_node? || new_parent.in_trash? | ||
| 275 | errors.add(:base, :move_under_self) | 273 | errors.add(:base, :move_under_self) |
| 276 | raise ActiveRecord::RecordInvalid.new(self) | 274 | raise ActiveRecord::RecordInvalid.new(self) |
| 277 | end | 275 | end |
| 278 | 276 | ||
| 279 | self.staged_parent_id = nil | ||
| 280 | self.save! | 277 | self.save! |
| 281 | self.move_to_child_of(new_parent) | 278 | self.move_to_child_of(new_parent) |
| 282 | else | 279 | else |
| 283 | unless self.save | 280 | raise ActiveRecord::RecordInvalid.new(self) unless self.save |
| 284 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 285 | end | ||
| 286 | end | 281 | end |
| 287 | 282 | ||
| 288 | self.reload | 283 | self.reload |
| @@ -369,9 +364,12 @@ class Node < ApplicationRecord | |||
| 369 | # subtree comes back exactly as it sits in the Trash: all drafts, | 364 | # subtree comes back exactly as it sits in the Trash: all drafts, |
| 370 | # nothing published. Republication is a separate, witnessed act | 365 | # nothing published. Republication is a separate, witnessed act |
| 371 | # per node. | 366 | # per node. |
| 372 | def restore_from_trash! new_parent, current_user = nil | 367 | def restore_from_trash! current_user = nil |
| 373 | return nil unless in_trash? | 368 | return nil unless in_trash? |
| 374 | 369 | ||
| 370 | target = (draft || head)&.parent_node_id | ||
| 371 | new_parent = target ? Node.find_by(:id => target) : nil | ||
| 372 | |||
| 375 | if new_parent.nil? || new_parent == self || descendants.include?(new_parent) || | 373 | if new_parent.nil? || new_parent == self || descendants.include?(new_parent) || |
| 376 | new_parent.trash_node? || new_parent.in_trash? | 374 | new_parent.trash_node? || new_parent.in_trash? |
| 377 | errors.add(:base, :restore_target_invalid) | 375 | errors.add(:base, :restore_target_invalid) |
| @@ -593,11 +591,9 @@ class Node < ApplicationRecord | |||
| 593 | root? || self.class.restricted_path?(unique_name) | 591 | root? || self.class.restricted_path?(unique_name) |
| 594 | end | 592 | end |
| 595 | 593 | ||
| 594 | # Falls back to the live address when the draft records none. | ||
| 596 | def prospective_unique_name | 595 | def prospective_unique_name |
| 597 | target_parent = staged_parent_id ? Node.find_by(:id => staged_parent_id) : parent | 596 | (draft || head)&.prospective_unique_name || unique_name |
| 598 | return nil unless target_parent | ||
| 599 | |||
| 600 | [target_parent.unique_name.presence, staged_slug.presence || slug].compact.join("/") | ||
| 601 | end | 597 | end |
| 602 | 598 | ||
| 603 | # Returns immutable node id for all new nodes so that the atom feed entry ids | 599 | # Returns immutable node id for all new nodes so that the atom feed entry ids |
| @@ -702,7 +698,7 @@ class Node < ApplicationRecord | |||
| 702 | # that draft and publishes it. | 698 | # that draft and publishes it. |
| 703 | def initialize_empty_page | 699 | def initialize_empty_page |
| 704 | if self.pages.empty? | 700 | if self.pages.empty? |
| 705 | self.draft = self.pages.create! | 701 | self.draft = self.pages.create!(:slug => self.slug, :parent_node_id => self.parent_id) |
| 706 | self.save | 702 | self.save |
| 707 | end | 703 | end |
| 708 | end | 704 | end |
| @@ -744,14 +740,11 @@ class Node < ApplicationRecord | |||
| 744 | def reserved_slug_stays_reserved | 740 | def reserved_slug_stays_reserved |
| 745 | if parent&.root? && !trash_node_already_me? | 741 | if parent&.root? && !trash_node_already_me? |
| 746 | errors.add(:slug, :reserved_for_trash) if slug == CccConventions::TRASH_SLUG | 742 | errors.add(:slug, :reserved_for_trash) if slug == CccConventions::TRASH_SLUG |
| 747 | errors.add(:staged_slug, :reserved_for_trash) if staged_slug == CccConventions::TRASH_SLUG | ||
| 748 | end | 743 | end |
| 749 | 744 | ||
| 750 | if persisted? && slug_was == CccConventions::TRASH_SLUG && Node.find(id).trash_node? | 745 | if persisted? && slug_was == CccConventions::TRASH_SLUG && Node.find(id).trash_node? |
| 751 | errors.add(:slug, :trash_immutable) if slug_changed? | 746 | errors.add(:slug, :trash_immutable) if slug_changed? |
| 752 | errors.add(:parent_id, :trash_immutable) if parent_id_changed? | 747 | errors.add(:parent_id, :trash_immutable) if parent_id_changed? |
| 753 | errors.add(:staged_slug, :trash_must_be_empty) if staged_slug.present? | ||
| 754 | errors.add(:staged_parent_id, :trash_must_be_empty) if staged_parent_id.present? | ||
| 755 | end | 748 | end |
| 756 | end | 749 | end |
| 757 | 750 | ||
diff --git a/app/models/page.rb b/app/models/page.rb index 8313b1d4..4635f1b4 100644 --- a/app/models/page.rb +++ b/app/models/page.rb | |||
| @@ -20,9 +20,13 @@ class Page < ApplicationRecord | |||
| 20 | :inclusion => { :in => ->(_) { Page.custom_templates } }, | 20 | :inclusion => { :in => ->(_) { Page.custom_templates } }, |
| 21 | :allow_blank => true, | 21 | :allow_blank => true, |
| 22 | :if => :template_name_changed? | 22 | :if => :template_name_changed? |
| 23 | validates_format_of :slug, :with => /\A[A-Za-z0-9][A-Za-z0-9_-]*\z/, | ||
| 24 | :unless => -> { slug.blank? } | ||
| 25 | validate :page_slug_not_reserved | ||
| 23 | 26 | ||
| 24 | # Associations | 27 | # Associations |
| 25 | belongs_to :node, optional: true | 28 | belongs_to :node, optional: true |
| 29 | belongs_to :parent_node, :class_name => "Node", :optional => true | ||
| 26 | belongs_to :user, optional: true | 30 | belongs_to :user, optional: true |
| 27 | belongs_to :editor, :class_name => "User", optional: true | 31 | belongs_to :editor, :class_name => "User", optional: true |
| 28 | has_many :related_assets, :dependent => :destroy | 32 | has_many :related_assets, :dependent => :destroy |
| @@ -202,6 +206,8 @@ class Page < ApplicationRecord | |||
| 202 | page.translations.reload | 206 | page.translations.reload |
| 203 | 207 | ||
| 204 | # Clone untranslated attributes | 208 | # Clone untranslated attributes |
| 209 | self.slug = page.slug | ||
| 210 | self.parent_node_id = page.parent_node_id | ||
| 205 | self.tag_list = page.tag_list | 211 | self.tag_list = page.tag_list |
| 206 | self.template_name ||= page.template_name | 212 | self.template_name ||= page.template_name |
| 207 | self.published_at = page.published_at | 213 | self.published_at = page.published_at |
| @@ -292,6 +298,20 @@ class Page < ApplicationRecord | |||
| 292 | published_at.nil? ? true : published_at < Time.now | 298 | published_at.nil? ? true : published_at < Time.now |
| 293 | end | 299 | end |
| 294 | 300 | ||
| 301 | # The address this page will have once published. | ||
| 302 | def prospective_unique_name | ||
| 303 | return nil if parent_node_id.nil? | ||
| 304 | |||
| 305 | parent = Node.find_by(:id => parent_node_id) | ||
| 306 | return nil unless parent | ||
| 307 | |||
| 308 | [parent.unique_name.presence, slug].compact.join("/") | ||
| 309 | end | ||
| 310 | |||
| 311 | def parent_node_missing? | ||
| 312 | parent_node_id.present? && !Node.exists?(:id => parent_node_id) | ||
| 313 | end | ||
| 314 | |||
| 295 | def effective_lang | 315 | def effective_lang |
| 296 | if translated_locales.empty? | 316 | if translated_locales.empty? |
| 297 | return 'de' | 317 | return 'de' |
| @@ -333,6 +353,14 @@ class Page < ApplicationRecord | |||
| 333 | end | 353 | end |
| 334 | end | 354 | end |
| 335 | 355 | ||
| 356 | def page_slug_not_reserved | ||
| 357 | return unless slug == CccConventions::TRASH_SLUG | ||
| 358 | return if node&.trash_node? | ||
| 359 | return unless parent_node_id && Node.find_by(:id => parent_node_id)&.root? | ||
| 360 | |||
| 361 | errors.add(:slug, :reserved_for_trash) | ||
| 362 | end | ||
| 363 | |||
| 336 | # Installs (or re-installs) the trigger that keeps page_translations' | 364 | # Installs (or re-installs) the trigger that keeps page_translations' |
| 337 | # search_vector in sync. Idempotent, safe to call on every boot. | 365 | # search_vector in sync. Idempotent, safe to call on every boot. |
| 338 | # search_vector is populated by a raw Postgres trigger, not anything | 366 | # search_vector is populated by a raw Postgres trigger, not anything |
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index c153c241..ae206578 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb | |||
| @@ -96,20 +96,20 @@ | |||
| 96 | <div id="metadata"> | 96 | <div id="metadata"> |
| 97 | <div class="layout_row_label"><%= t(".slug") %></div> | 97 | <div class="layout_row_label"><%= t(".slug") %></div> |
| 98 | <div class="layout_row_content"> | 98 | <div class="layout_row_content"> |
| 99 | <%= f.text_field( | 99 | <%= d.text_field( |
| 100 | :staged_slug, :value => @node.staged_slug || @node.slug | 100 | :slug, :value => @page.slug || @node.slug |
| 101 | ) | 101 | ) |
| 102 | %> | 102 | %> |
| 103 | </div> | 103 | </div> |
| 104 | 104 | ||
| 105 | <div class="layout_row_label"><%= t(".parent") %></div> | 105 | <div class="layout_row_label"><%= t(".parent") %></div> |
| 106 | <div class="layout_row_content"> | 106 | <div class="layout_row_content"> |
| 107 | <%= text_field_tag :move_to_search_term, @node.parent.title rescue "" %> | 107 | <%= text_field_tag :move_to_search_term, (Node.find_by(:id => @page.parent_node_id) || @node.parent)&.title %> |
| 108 | <p class="field_hint"><%= t(".parent_hint") %></p> | 108 | <p class="field_hint"><%= t(".parent_hint") %></p> |
| 109 | <div id="move_to_search_results" class="search_results"></div> | 109 | <div id="move_to_search_results" class="search_results"></div> |
| 110 | <%= f.hidden_field( | 110 | <%= d.hidden_field( |
| 111 | :staged_parent_id, | 111 | :parent_node_id, |
| 112 | :value => @node.staged_parent_id || @node.parent_id | 112 | :value => @page.parent_node_id || @node.parent_id |
| 113 | ) | 113 | ) |
| 114 | %> | 114 | %> |
| 115 | </div> | 115 | </div> |
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 105e37ea..3b2d28d9 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb | |||
| @@ -105,7 +105,7 @@ | |||
| 105 | <% end %> | 105 | <% end %> |
| 106 | <div class="info_item"> | 106 | <div class="info_item"> |
| 107 | <span class="info_label"><%= t(".restore_to") %></span> | 107 | <span class="info_label"><%= t(".restore_to") %></span> |
| 108 | <% suggestion = @node.suggested_restore_parent %> | 108 | <% suggestion = @node.draft&.parent_node || @node.suggested_restore_parent %> |
| 109 | <%= form_tag restore_from_trash_node_path(@node), :method => :put, :class => "aligned_action_row" do %> | 109 | <%= form_tag restore_from_trash_node_path(@node), :method => :put, :class => "aligned_action_row" do %> |
| 110 | <div class="restore_picker"> | 110 | <div class="restore_picker"> |
| 111 | <%= text_field_tag :restore_search_term, | 111 | <%= text_field_tag :restore_search_term, |
diff --git a/config/locales/de.yml b/config/locales/de.yml index 85cc91c5..6068d4c4 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -112,9 +112,7 @@ de: | |||
| 112 | title: "Titel" | 112 | title: "Titel" |
| 113 | node: | 113 | node: |
| 114 | slug: "Slug" | 114 | slug: "Slug" |
| 115 | staged_slug: "Vorgemerkter Slug" | ||
| 116 | parent_id: "Eltern-Node" | 115 | parent_id: "Eltern-Node" |
| 117 | staged_parent_id: "Vorgemerkter Eltern-Node" | ||
| 118 | head_id: "Head" | 116 | head_id: "Head" |
| 119 | related_asset: | 117 | related_asset: |
| 120 | headline: "Aufmacher" | 118 | headline: "Aufmacher" |
| @@ -137,11 +135,16 @@ de: | |||
| 137 | trash_undeletable: "Der Papierkorb-Node kann nicht gelöscht werden" | 135 | trash_undeletable: "Der Papierkorb-Node kann nicht gelöscht werden" |
| 138 | publish_in_trash: "Ein Node im Papierkorb kann nicht veröffentlicht werden" | 136 | publish_in_trash: "Ein Node im Papierkorb kann nicht veröffentlicht werden" |
| 139 | move_under_self: "Ein Node kann nicht unter sich selbst oder einen seiner Nachfahren verschoben werden" | 137 | move_under_self: "Ein Node kann nicht unter sich selbst oder einen seiner Nachfahren verschoben werden" |
| 138 | move_target_missing: "Die Seite, unter die verschoben werden sollte, gibt es nicht mehr." | ||
| 140 | trash_the_trash: "Der Papierkorb-Node selbst kann nicht in den Papierkorb verschoben werden" | 139 | trash_the_trash: "Der Papierkorb-Node selbst kann nicht in den Papierkorb verschoben werden" |
| 141 | restore_target_invalid: "Das Wiederherstellungsziel muss ein lebender Node sein" | 140 | restore_target_invalid: "Das Wiederherstellungsziel muss ein lebender Node sein" |
| 142 | destroy_outside_trash: "Nodes können nur aus dem Papierkorb gelöscht werden" | 141 | destroy_outside_trash: "Nodes können nur aus dem Papierkorb gelöscht werden" |
| 143 | attach_in_trash: "An einen Node im Papierkorb können keine Assets angehängt werden" | 142 | attach_in_trash: "An einen Node im Papierkorb können keine Assets angehängt werden" |
| 144 | not_permitted: "In diesem Bereich dürfen nur Mitglieder der Redaktion veröffentlichte Inhalte ändern" | 143 | not_permitted: "In diesem Bereich dürfen nur Mitglieder der Redaktion veröffentlichte Inhalte ändern" |
| 144 | page: | ||
| 145 | attributes: | ||
| 146 | slug: | ||
| 147 | reserved_for_trash: "ist für den Papierkorb reserviert" | ||
| 145 | user: | 148 | user: |
| 146 | attributes: | 149 | attributes: |
| 147 | roles: | 150 | roles: |
| @@ -676,7 +679,6 @@ de: | |||
| 676 | trashed: "Seite wurde in den Papierkorb verschoben" | 679 | trashed: "Seite wurde in den Papierkorb verschoben" |
| 677 | already_trashed: "Seite ist bereits im Papierkorb" | 680 | already_trashed: "Seite ist bereits im Papierkorb" |
| 678 | restored: "Seite wurde aus dem Papierkorb wiederhergestellt" | 681 | restored: "Seite wurde aus dem Papierkorb wiederhergestellt" |
| 679 | restore_target_missing: "Wiederherstellungsziel nicht gefunden" | ||
| 680 | deleted: "Seite wurde endgültig gelöscht" | 682 | deleted: "Seite wurde endgültig gelöscht" |
| 681 | published: "Entwurf wurde veröffentlicht" | 683 | published: "Entwurf wurde veröffentlicht" |
| 682 | unlocked: "Node entsperrt" | 684 | unlocked: "Node entsperrt" |
diff --git a/config/locales/en.yml b/config/locales/en.yml index 6cff43f0..524d07d0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -63,9 +63,7 @@ en: | |||
| 63 | title: "Title" | 63 | title: "Title" |
| 64 | node: | 64 | node: |
| 65 | slug: "Slug" | 65 | slug: "Slug" |
| 66 | staged_slug: "Staged slug" | ||
| 67 | parent_id: "Parent node" | 66 | parent_id: "Parent node" |
| 68 | staged_parent_id: "Staged parent node" | ||
| 69 | head_id: "Head" | 67 | head_id: "Head" |
| 70 | related_asset: | 68 | related_asset: |
| 71 | headline: "Headline" | 69 | headline: "Headline" |
| @@ -83,11 +81,16 @@ en: | |||
| 83 | trash_undeletable: "The Trash node cannot be destroyed" | 81 | trash_undeletable: "The Trash node cannot be destroyed" |
| 84 | publish_in_trash: "Cannot publish a node in the Trash" | 82 | publish_in_trash: "Cannot publish a node in the Trash" |
| 85 | move_under_self: "Cannot move a node under itself or one of its own descendants" | 83 | move_under_self: "Cannot move a node under itself or one of its own descendants" |
| 84 | move_target_missing: "The page this was to be moved under no longer exists." | ||
| 86 | trash_the_trash: "The Trash node itself cannot be trashed" | 85 | trash_the_trash: "The Trash node itself cannot be trashed" |
| 87 | restore_target_invalid: "Restore target must be a living node" | 86 | restore_target_invalid: "Restore target must be a living node" |
| 88 | destroy_outside_trash: "Nodes are only destroyed from the Trash" | 87 | destroy_outside_trash: "Nodes are only destroyed from the Trash" |
| 89 | 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" |
| 90 | 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 | page: | ||
| 91 | attributes: | ||
| 92 | slug: | ||
| 93 | reserved_for_trash: "is reserved for the Trash" | ||
| 91 | user: | 94 | user: |
| 92 | attributes: | 95 | attributes: |
| 93 | roles: | 96 | roles: |
| @@ -639,7 +642,6 @@ en: | |||
| 639 | trashed: "Page has been moved to the Trash" | 642 | trashed: "Page has been moved to the Trash" |
| 640 | already_trashed: "Page is already in the Trash" | 643 | already_trashed: "Page is already in the Trash" |
| 641 | restored: "Page has been restored from the Trash" | 644 | restored: "Page has been restored from the Trash" |
| 642 | restore_target_missing: "Restore target not found" | ||
| 643 | deleted: "Page has been permanently deleted" | 645 | deleted: "Page has been permanently deleted" |
| 644 | published: "Draft has been published" | 646 | published: "Draft has been published" |
| 645 | unlocked: "Node unlocked" | 647 | unlocked: "Node unlocked" |
diff --git a/db/migrate/20260807010722_add_address_to_pages.rb b/db/migrate/20260807010722_add_address_to_pages.rb new file mode 100644 index 00000000..12cf89bd --- /dev/null +++ b/db/migrate/20260807010722_add_address_to_pages.rb | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | class AddAddressToPages < ActiveRecord::Migration[8.1] | ||
| 2 | def change | ||
| 3 | add_column :pages, :slug, :string | ||
| 4 | add_column :pages, :parent_node_id, :integer | ||
| 5 | end | ||
| 6 | end | ||
diff --git a/lib/tasks/pages.rake b/lib/tasks/pages.rake new file mode 100644 index 00000000..0cfbc5dc --- /dev/null +++ b/lib/tasks/pages.rake | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | namespace :pages do | ||
| 2 | desc "Backfill pages.slug and pages.parent_node_id from each page's " \ | ||
| 3 | "node. Historical accuracy is not attempted. Every revision gets " \ | ||
| 4 | "the node's current address, which is right for head and draft and " \ | ||
| 5 | "harmless for older revisions, and avoids nil checks everywhere. " \ | ||
| 6 | "Dry run unless WRITE=1." | ||
| 7 | task :backfill_address => :environment do | ||
| 8 | write = ENV["WRITE"] == "1" | ||
| 9 | puts "DRY RUN -- nothing written. Re-run with WRITE=1." unless write | ||
| 10 | |||
| 11 | touched = 0 | ||
| 12 | Node.find_each do |node| | ||
| 13 | scope = node.pages.where("slug IS DISTINCT FROM :s OR parent_node_id IS DISTINCT FROM :p", | ||
| 14 | :s => node.slug, :p => node.parent_id) | ||
| 15 | count = scope.count | ||
| 16 | next if count.zero? | ||
| 17 | |||
| 18 | scope.update_all(:slug => node.slug, :parent_node_id => node.parent_id) if write | ||
| 19 | touched += count | ||
| 20 | end | ||
| 21 | |||
| 22 | # Autosaves carry no node_id -- has_many :pages does not cover them. | ||
| 23 | Node.where.not(:autosave_id => nil).includes(:autosave).find_each do |node| | ||
| 24 | a = node.autosave | ||
| 25 | next if a.slug == node.slug && a.parent_node_id == node.parent_id | ||
| 26 | |||
| 27 | a.update_columns(:slug => node.slug, :parent_node_id => node.parent_id) if write | ||
| 28 | touched += 1 | ||
| 29 | end | ||
| 30 | |||
| 31 | puts "#{write ? "updated" : "would update"} #{touched} pages" | ||
| 32 | end | ||
| 33 | end | ||
diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index 37b696e4..852f5339 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js | |||
| @@ -257,7 +257,7 @@ move_to_search = { | |||
| 257 | showRestricted: true, | 257 | showRestricted: true, |
| 258 | onSelect: function(node) { | 258 | onSelect: function(node) { |
| 259 | $("#move_to_search_term").val(node.title); | 259 | $("#move_to_search_term").val(node.title); |
| 260 | $("#node_staged_parent_id").val(node.node_id); | 260 | $("#page_parent_node_id").val(node.node_id); |
| 261 | } | 261 | } |
| 262 | }); | 262 | }); |
| 263 | } | 263 | } |
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 08a19a79..889a5729 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css | |||
| @@ -1566,7 +1566,7 @@ form.button_to button[type="submit"] { | |||
| 1566 | } | 1566 | } |
| 1567 | 1567 | ||
| 1568 | input[type=text]#tag_list, | 1568 | input[type=text]#tag_list, |
| 1569 | input[type=text]#node_staged_slug, | 1569 | input[type=text]#page_slug, |
| 1570 | input#move_to_search_term { | 1570 | input#move_to_search_term { |
| 1571 | padding: 5px; | 1571 | padding: 5px; |
| 1572 | } | 1572 | } |
| @@ -1644,7 +1644,7 @@ input[type=text]#page_title { | |||
| 1644 | } | 1644 | } |
| 1645 | 1645 | ||
| 1646 | input#tag_list, | 1646 | input#tag_list, |
| 1647 | input#node_staged_slug, | 1647 | input#page_slug, |
| 1648 | input#move_to_search_term { | 1648 | input#move_to_search_term { |
| 1649 | box-sizing: border-box; | 1649 | box-sizing: border-box; |
| 1650 | width: 100%; | 1650 | width: 100%; |
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb index 745ae4e0..c7caeed1 100644 --- a/test/controllers/nodes_controller_test.rb +++ b/test/controllers/nodes_controller_test.rb | |||
| @@ -193,7 +193,8 @@ class NodesControllerTest < ActionController::TestCase | |||
| 193 | test "publish draft with staged_slug unqueal slug" do | 193 | test "publish draft with staged_slug unqueal slug" do |
| 194 | login_as :quentin | 194 | login_as :quentin |
| 195 | 195 | ||
| 196 | test_node = Node.root.children.create! :slug => "test_node", :staged_slug => "peter_pan" | 196 | test_node = Node.root.children.create!(:slug => "test_node") |
| 197 | test_node.draft.update!(:slug => "peter_pan") | ||
| 197 | 198 | ||
| 198 | put :publish, params: { :id => test_node.id } | 199 | put :publish, params: { :id => test_node.id } |
| 199 | 200 | ||
| @@ -205,7 +206,8 @@ class NodesControllerTest < ActionController::TestCase | |||
| 205 | test "publish draft with staged_slug with more levels of nodes" do | 206 | test "publish draft with staged_slug with more levels of nodes" do |
| 206 | login_as :quentin | 207 | login_as :quentin |
| 207 | 208 | ||
| 208 | test_node = Node.root.children.create! :slug => "test_node", :staged_slug => "peter_pan" | 209 | test_node = Node.root.children.create!(:slug => "test_node") |
| 210 | test_node.draft.update!(:slug => "peter_pan") | ||
| 209 | test_node2 = test_node.children.create! :slug => "test_node2" | 211 | test_node2 = test_node.children.create! :slug => "test_node2" |
| 210 | 212 | ||
| 211 | put :publish, params: { :id => test_node.id } | 213 | put :publish, params: { :id => test_node.id } |
| @@ -215,12 +217,13 @@ class NodesControllerTest < ActionController::TestCase | |||
| 215 | assert_equal "peter_pan", test_node.unique_name | 217 | assert_equal "peter_pan", test_node.unique_name |
| 216 | end | 218 | end |
| 217 | 219 | ||
| 218 | test "publish draft with staged_parent_id" do | 220 | test "publish draft with a moved parent" do |
| 219 | login_as :quentin | 221 | login_as :quentin |
| 220 | 222 | ||
| 221 | parent = Node.root.children.create! :slug => "parent" | 223 | parent = Node.root.children.create!(:slug => "parent") |
| 222 | test_node = Node.root.children.create! :slug => "test_node", :staged_parent_id => parent.id | 224 | test_node = Node.root.children.create!(:slug => "test_node") |
| 223 | test_node2 = test_node.children.create! :slug => "test_node2" | 225 | test_node.draft.update!(:parent_node_id => parent.id) |
| 226 | test_node2 = test_node.children.create!(:slug => "test_node2") | ||
| 224 | 227 | ||
| 225 | put :publish, params: { :id => test_node.id } | 228 | put :publish, params: { :id => test_node.id } |
| 226 | 229 | ||
| @@ -229,18 +232,13 @@ class NodesControllerTest < ActionController::TestCase | |||
| 229 | assert_equal "parent/test_node/test_node2", test_node2.unique_name | 232 | assert_equal "parent/test_node/test_node2", test_node2.unique_name |
| 230 | end | 233 | end |
| 231 | 234 | ||
| 232 | test "publish draft with staged_parent_id and staged_slug" do | 235 | test "publish draft with a moved parent and a renamed slug" do |
| 233 | login_as :quentin | 236 | login_as :quentin |
| 234 | 237 | ||
| 235 | parent = Node.root.children.create! :slug => "parent" | 238 | parent = Node.root.children.create!(:slug => "parent") |
| 236 | 239 | test_node = Node.root.children.create!(:slug => "test_node") | |
| 237 | test_node = Node.root.children.create!( | 240 | test_node.draft.update!(:parent_node_id => parent.id, :slug => "peter_pan") |
| 238 | :slug => "test_node", | 241 | test_node2 = test_node.children.create!(:slug => "test_node2") |
| 239 | :staged_parent_id => parent.id, | ||
| 240 | :staged_slug => "peter_pan" | ||
| 241 | ) | ||
| 242 | |||
| 243 | test_node2 = test_node.children.create! :slug => "test_node2" | ||
| 244 | 242 | ||
| 245 | put :publish, params: { :id => test_node.id } | 243 | put :publish, params: { :id => test_node.id } |
| 246 | 244 | ||
| @@ -293,7 +291,7 @@ class NodesControllerTest < ActionController::TestCase | |||
| 293 | 291 | ||
| 294 | other_node = Node.root.children.create( :slug => "other" ) | 292 | other_node = Node.root.children.create( :slug => "other" ) |
| 295 | 293 | ||
| 296 | node.staged_parent_id = other_node.id | 294 | node.draft.update!(:parent_node_id => other_node.id) |
| 297 | node.publish_draft! | 295 | node.publish_draft! |
| 298 | 296 | ||
| 299 | assert Node.valid? | 297 | assert Node.valid? |
| @@ -711,18 +709,33 @@ class NodesControllerTest < ActionController::TestCase | |||
| 711 | assert flash[:error].present? | 709 | assert flash[:error].present? |
| 712 | end | 710 | end |
| 713 | 711 | ||
| 714 | test "restore_from_trash reparents to the given parent" do | 712 | test "restore_from_trash reparents to the 'old' parent" do |
| 715 | login_as :quentin | 713 | login_as :quentin |
| 716 | node = Node.root.children.create!(:slug => "restore_me") | 714 | node = Node.root.children.create!(:slug => "restore_me") |
| 717 | node.trash!(users(:quentin)) | 715 | node.trash!(users(:quentin)) |
| 718 | target = Node.root.children.create!(:slug => "restore_home") | 716 | target = Node.root.children.create!(:slug => "restore_home") |
| 719 | 717 | ||
| 720 | put :restore_from_trash, params: { :id => node.id, :parent_id => target.id } | 718 | node.reload.draft.update!(:parent_node_id => target.id) |
| 719 | put :restore_from_trash, params: { :locale => "de", :id => node.id } | ||
| 721 | 720 | ||
| 722 | assert_redirected_to node_path(node) | 721 | assert_redirected_to node_path(node) |
| 723 | assert_equal target, node.reload.parent | 722 | assert_equal target, node.reload.parent |
| 724 | end | 723 | end |
| 725 | 724 | ||
| 725 | test "restore_from_trash follows an explicitly chosen parent" do | ||
| 726 | login_as :quentin | ||
| 727 | node = Node.root.children.create!(:slug => "restore_pick") | ||
| 728 | node.trash!(users(:quentin)) | ||
| 729 | chosen = Node.root.children.create!(:slug => "chosen_home") | ||
| 730 | |||
| 731 | put :restore_from_trash, params: { :locale => "de", :id => node.id, | ||
| 732 | :parent_id => chosen.id } | ||
| 733 | |||
| 734 | assert_equal chosen, node.reload.parent | ||
| 735 | assert_equal chosen.id, node.draft.parent_node_id, | ||
| 736 | "the choice is recorded on the draft, not applied behind its back" | ||
| 737 | end | ||
| 738 | |||
| 726 | test "destroy refuses a node outside the Trash" do | 739 | test "destroy refuses a node outside the Trash" do |
| 727 | login_as :quentin | 740 | login_as :quentin |
| 728 | node = Node.root.children.create!(:slug => "not_deletable_here") | 741 | node = Node.root.children.create!(:slug => "not_deletable_here") |
diff --git a/test/models/node_test.rb b/test/models/node_test.rb index c7fee58e..df1e96cb 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb | |||
| @@ -488,7 +488,7 @@ class NodeTest < ActiveSupport::TestCase | |||
| 488 | a = Node.root.children.create!(:slug => "cycle_guard_a") | 488 | a = Node.root.children.create!(:slug => "cycle_guard_a") |
| 489 | b = a.children.create!(:slug => "cycle_guard_b") | 489 | b = a.children.create!(:slug => "cycle_guard_b") |
| 490 | 490 | ||
| 491 | a.staged_parent_id = b.id | 491 | a.draft.update!(:parent_node_id => b.id) |
| 492 | 492 | ||
| 493 | assert_raises(ActiveRecord::RecordInvalid) { a.publish_draft! } | 493 | assert_raises(ActiveRecord::RecordInvalid) { a.publish_draft! } |
| 494 | 494 | ||
| @@ -702,16 +702,15 @@ class NodeTest < ActiveSupport::TestCase | |||
| 702 | assert_equal "New Title", action.metadata.dig("title", "to") | 702 | assert_equal "New Title", action.metadata.dig("title", "to") |
| 703 | end | 703 | end |
| 704 | 704 | ||
| 705 | test "publishing a staged slug change logs a move with the path pair" do | 705 | test "publishing a slug change logs a move with the path pair" do |
| 706 | node = create_node_with_published_page | 706 | node = create_node_with_published_page |
| 707 | path_before = node.unique_name | 707 | path_before = node.unique_name |
| 708 | node.staged_slug = "moved-#{node.slug}" | 708 | find_or_create_draft(node, @user1) |
| 709 | node.save! | 709 | node.draft.update!(:slug => "moved-#{node.slug}") |
| 710 | publish_count_before = NodeAction.where(:action => "publish").count | ||
| 711 | 710 | ||
| 712 | node.publish_draft!(@user1) | 711 | node.publish_draft!(@user1) |
| 713 | |||
| 714 | node.reload | 712 | node.reload |
| 713 | |||
| 715 | assert_not_equal path_before, node.unique_name | 714 | assert_not_equal path_before, node.unique_name |
| 716 | 715 | ||
| 717 | action = NodeAction.where(:action => "move").last | 716 | action = NodeAction.where(:action => "move").last |
| @@ -719,16 +718,12 @@ class NodeTest < ActiveSupport::TestCase | |||
| 719 | assert_equal @user1, action.user | 718 | assert_equal @user1, action.user |
| 720 | assert_equal path_before, action.metadata.dig("path", "from") | 719 | assert_equal path_before, action.metadata.dig("path", "from") |
| 721 | assert_equal node.unique_name, action.metadata.dig("path", "to") | 720 | assert_equal node.unique_name, action.metadata.dig("path", "to") |
| 722 | |||
| 723 | # No draft was pending: path change alone must not fabricate a publish. | ||
| 724 | assert_equal publish_count_before, NodeAction.where(:action => "publish").count | ||
| 725 | end | 721 | end |
| 726 | 722 | ||
| 727 | test "publishing a draft together with a staged move logs two entries" do | 723 | test "publishing a draft together with a move logs two entries" do |
| 728 | node = create_node_with_published_page | 724 | node = create_node_with_published_page |
| 729 | find_or_create_draft(node, @user1) | 725 | find_or_create_draft(node, @user1) |
| 730 | node.staged_slug = "relocated-#{node.slug}" | 726 | node.draft.update!(:slug => "relocated-#{node.slug}") |
| 731 | node.save! | ||
| 732 | 727 | ||
| 733 | assert_difference "NodeAction.count", 2 do | 728 | assert_difference "NodeAction.count", 2 do |
| 734 | node.publish_draft!(@user1) | 729 | node.publish_draft!(@user1) |
| @@ -890,7 +885,8 @@ class NodeTest < ActiveSupport::TestCase | |||
| 890 | Node.trash | 885 | Node.trash |
| 891 | 886 | ||
| 892 | assert_not Node.root.children.build(:slug => CccConventions::TRASH_SLUG).valid? | 887 | assert_not Node.root.children.build(:slug => CccConventions::TRASH_SLUG).valid? |
| 893 | assert_not Node.root.children.build(:slug => "fine", :staged_slug => CccConventions::TRASH_SLUG).valid? | 888 | page = Page.new(:slug => CccConventions::TRASH_SLUG, :parent_node_id => Node.root.id) |
| 889 | assert_not page.valid? | ||
| 894 | assert Node.trash.children.create!(:slug => "sub").children.build(:slug => CccConventions::TRASH_SLUG).valid? | 890 | assert Node.trash.children.create!(:slug => "sub").children.build(:slug => CccConventions::TRASH_SLUG).valid? |
| 895 | end | 891 | end |
| 896 | 892 | ||
| @@ -1012,7 +1008,7 @@ class NodeTest < ActiveSupport::TestCase | |||
| 1012 | year = updates.children.create!(:slug => "2026") | 1008 | year = updates.children.create!(:slug => "2026") |
| 1013 | node = Node.root.children.create!(:slug => "outside-post") | 1009 | node = Node.root.children.create!(:slug => "outside-post") |
| 1014 | node.reload.draft.update!(:title => "Entwurf") | 1010 | node.reload.draft.update!(:title => "Entwurf") |
| 1015 | node.update!(:staged_parent_id => year.id) | 1011 | node.draft.update!(:parent_node_id => year.id) |
| 1016 | 1012 | ||
| 1017 | assert_raises(ActiveRecord::RecordInvalid) { node.publish_draft!(editor) } | 1013 | assert_raises(ActiveRecord::RecordInvalid) { node.publish_draft!(editor) } |
| 1018 | assert_nil node.reload.head | 1014 | assert_nil node.reload.head |
| @@ -1025,7 +1021,7 @@ class NodeTest < ActiveSupport::TestCase | |||
| 1025 | club = Node.root.children.create!(:slug => "club") | 1021 | club = Node.root.children.create!(:slug => "club") |
| 1026 | node = Node.root.children.create!(:slug => "movable-post") | 1022 | node = Node.root.children.create!(:slug => "movable-post") |
| 1027 | node.reload.draft.update!(:title => "Entwurf") | 1023 | node.reload.draft.update!(:title => "Entwurf") |
| 1028 | node.update!(:staged_parent_id => club.id) | 1024 | node.draft.update!(:parent_node_id => club.id) |
| 1029 | 1025 | ||
| 1030 | node.publish_draft!(editor) | 1026 | node.publish_draft!(editor) |
| 1031 | assert_equal club.id, node.reload.parent_id | 1027 | assert_equal club.id, node.reload.parent_id |
| @@ -1036,7 +1032,7 @@ class NodeTest < ActiveSupport::TestCase | |||
| 1036 | :password => "secret", :password_confirmation => "secret") | 1032 | :password => "secret", :password_confirmation => "secret") |
| 1037 | node = Node.root.children.create!(:slug => "harmless") | 1033 | node = Node.root.children.create!(:slug => "harmless") |
| 1038 | node.reload.draft.update!(:title => "Entwurf") | 1034 | node.reload.draft.update!(:title => "Entwurf") |
| 1039 | node.update!(:staged_slug => "updates") | 1035 | node.draft.update!(:slug => "updates") |
| 1040 | 1036 | ||
| 1041 | assert_raises(ActiveRecord::RecordInvalid) { node.publish_draft!(editor) } | 1037 | assert_raises(ActiveRecord::RecordInvalid) { node.publish_draft!(editor) } |
| 1042 | assert_nil node.reload.head | 1038 | assert_nil node.reload.head |
| @@ -1057,7 +1053,9 @@ class NodeTest < ActiveSupport::TestCase | |||
| 1057 | node = Node.root.children.create!(:slug => "restorable") | 1053 | node = Node.root.children.create!(:slug => "restorable") |
| 1058 | node.reload.trash! | 1054 | node.reload.trash! |
| 1059 | 1055 | ||
| 1060 | assert_raises(ActiveRecord::RecordInvalid) { node.restore_from_trash!(updates, editor) } | 1056 | node.reload.draft.update!(:parent_node_id => updates.id) |
| 1057 | |||
| 1058 | assert_raises(ActiveRecord::RecordInvalid) { node.restore_from_trash!(editor) } | ||
| 1061 | assert node.reload.in_trash? | 1059 | assert node.reload.in_trash? |
| 1062 | end | 1060 | end |
| 1063 | 1061 | ||
| @@ -1068,7 +1066,8 @@ class NodeTest < ActiveSupport::TestCase | |||
| 1068 | node = Node.root.children.create!(:slug => "restorable_free") | 1066 | node = Node.root.children.create!(:slug => "restorable_free") |
| 1069 | node.reload.trash! | 1067 | node.reload.trash! |
| 1070 | 1068 | ||
| 1071 | node.restore_from_trash!(club, editor) | 1069 | node.reload.draft.update!(:parent_node_id => club.id) |
| 1070 | node.restore_from_trash!(editor) | ||
| 1072 | assert_not node.reload.in_trash? | 1071 | assert_not node.reload.in_trash? |
| 1073 | assert_equal club.id, node.parent_id | 1072 | assert_equal club.id, node.parent_id |
| 1074 | end | 1073 | end |
| @@ -1101,4 +1100,65 @@ class NodeTest < ActiveSupport::TestCase | |||
| 1101 | node.update_external_url!("https://example.org", users(:quentin)) | 1100 | node.update_external_url!("https://example.org", users(:quentin)) |
| 1102 | end | 1101 | end |
| 1103 | end | 1102 | end |
| 1103 | |||
| 1104 | test "a new node's draft carries the node's address" do | ||
| 1105 | parent = Node.root.children.create!(:slug => "addr_parent") | ||
| 1106 | node = parent.children.create!(:slug => "addr_child") | ||
| 1107 | |||
| 1108 | assert_equal "addr_child", node.draft.slug | ||
| 1109 | assert_equal parent.id, node.draft.parent_node_id | ||
| 1110 | end | ||
| 1111 | |||
| 1112 | test "an autosave inherits the draft's address" do | ||
| 1113 | node = Node.root.children.create!(:slug => "addr_autosave") | ||
| 1114 | node.draft.update!(:slug => "renamed") | ||
| 1115 | node.lock_for_editing!(users(:quentin)) | ||
| 1116 | node.autosave!({ :title => "x" }, users(:quentin)) | ||
| 1117 | |||
| 1118 | assert_equal "renamed", node.autosave.slug | ||
| 1119 | end | ||
| 1120 | |||
| 1121 | test "a blank slug in an autosave leaves the address unchanged" do | ||
| 1122 | node = Node.root.children.create!(:slug => "addr_blank") | ||
| 1123 | node.lock_for_editing!(users(:quentin)) | ||
| 1124 | node.autosave!({ :slug => "", :title => "x" }, users(:quentin)) | ||
| 1125 | |||
| 1126 | assert_equal "addr_blank", node.autosave.slug | ||
| 1127 | end | ||
| 1128 | |||
| 1129 | test "a node with no draft has nothing to publish" do | ||
| 1130 | node = create_node_with_published_page | ||
| 1131 | assert_nil node.draft | ||
| 1132 | |||
| 1133 | assert_nil node.publish_draft!(@user1) | ||
| 1134 | end | ||
| 1135 | |||
| 1136 | test "prospective_unique_name reports the draft's intended address" do | ||
| 1137 | parent = Node.root.children.create!(:slug => "prospective_parent") | ||
| 1138 | node = Node.root.children.create!(:slug => "prospective_child") | ||
| 1139 | node.draft.update!(:parent_node_id => parent.id) | ||
| 1140 | |||
| 1141 | assert_equal "prospective_parent/prospective_child", | ||
| 1142 | node.prospective_unique_name | ||
| 1143 | end | ||
| 1144 | |||
| 1145 | test "prospective_unique_name falls back to the live address when the parent is gone" do | ||
| 1146 | parent = Node.root.children.create!(:slug => "doomed_parent") | ||
| 1147 | node = Node.root.children.create!(:slug => "orphan_child") | ||
| 1148 | node.draft.update!(:parent_node_id => parent.id) | ||
| 1149 | parent.destroy! | ||
| 1150 | |||
| 1151 | assert node.reload.draft.parent_node_missing? | ||
| 1152 | assert_equal "orphan_child", node.prospective_unique_name | ||
| 1153 | end | ||
| 1154 | |||
| 1155 | test "publishing a draft whose parent is gone is refused" do | ||
| 1156 | parent = Node.root.children.create!(:slug => "doomed_parent_two") | ||
| 1157 | node = Node.root.children.create!(:slug => "orphan_child_two") | ||
| 1158 | node.draft.update!(:parent_node_id => parent.id) | ||
| 1159 | parent.destroy! | ||
| 1160 | |||
| 1161 | assert_raises(ActiveRecord::RecordInvalid) { node.reload.publish_draft! } | ||
| 1162 | assert_nil node.reload.head | ||
| 1163 | end | ||
| 1104 | end | 1164 | end |
diff --git a/test/models/node_trash_test.rb b/test/models/node_trash_test.rb index 3947f20e..a4774c58 100644 --- a/test/models/node_trash_test.rb +++ b/test/models/node_trash_test.rb | |||
| @@ -76,7 +76,8 @@ class NodeTrashTest < ActiveSupport::TestCase | |||
| 76 | node.trash!(@user1) | 76 | node.trash!(@user1) |
| 77 | target = Node.root.children.create!(:slug => "restore_target") | 77 | target = Node.root.children.create!(:slug => "restore_target") |
| 78 | 78 | ||
| 79 | node.reload.restore_from_trash!(target, @user1) | 79 | node.reload.draft.update!(:parent_node_id => target.id) |
| 80 | node.reload.restore_from_trash!(@user1) | ||
| 80 | node.reload | 81 | node.reload |
| 81 | 82 | ||
| 82 | assert_equal target, node.parent | 83 | assert_equal target, node.parent |
| @@ -91,8 +92,11 @@ class NodeTrashTest < ActiveSupport::TestCase | |||
| 91 | node.trash!(@user1) | 92 | node.trash!(@user1) |
| 92 | other_trashed = Node.trash.children.create!(:slug => "also_trashed") | 93 | other_trashed = Node.trash.children.create!(:slug => "also_trashed") |
| 93 | 94 | ||
| 94 | assert_raises(ActiveRecord::RecordInvalid) { node.reload.restore_from_trash!(Node.trash, @user1) } | 95 | node.reload.draft.update!(:parent_node_id => Node.trash.id) |
| 95 | assert_raises(ActiveRecord::RecordInvalid) { node.reload.restore_from_trash!(other_trashed, @user1) } | 96 | assert_raises(ActiveRecord::RecordInvalid) { node.reload.restore_from_trash!(@user1) } |
| 97 | |||
| 98 | node.reload.draft.update!(:parent_node_id => other_trashed.id) | ||
| 99 | assert_raises(ActiveRecord::RecordInvalid) { node.reload.restore_from_trash!(@user1) } | ||
| 96 | end | 100 | end |
| 97 | 101 | ||
| 98 | test "destroy_from_trash! refuses nodes outside the Trash" do | 102 | test "destroy_from_trash! refuses nodes outside the Trash" do |
