summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-08-07 06:15:14 +0200
committererdgeist <erdgeist@erdgeist.org>2026-08-07 06:15:14 +0200
commitf4ddfff03ca9f25d50f39a1971877362d85eb9cb (patch)
tree31e6774fd4cf2b579a9622277be150417a2fa48e /app/models
parent02a4ea750428aa1a9c9e7f2680553c0ce4ef1fec (diff)
Move the pending address from the node onto the draft
Diffstat (limited to 'app/models')
-rw-r--r--app/models/node.rb51
-rw-r--r--app/models/page.rb28
2 files changed, 50 insertions, 29 deletions
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