diff options
Diffstat (limited to 'app')
| -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 |
5 files changed, 64 insertions, 48 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, |
