From d56155231814633e04f856d22646fea24ef97011 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 15 Jul 2026 01:41:34 +0200 Subject: Add NodeAction: an append-only log of who did what to a node node_id/page_id/user_id are lookup and ordering only -- all three nullify on delete, so an entry outlives its actor and its subject. Everything that must survive those deletions lives in a mandatory metadata jsonb written once at creation: the actor's username, the node's human-readable name (pinned to the default locale), and action-specific extras such as publish's title from/to. NodeAction.record! is the single constructor, so every entry gets the same baseline metadata without each call site re-implementing it. occurred_at is one field for live and backfilled entries alike; inferred_from distinguishes them -- nil means witnessed at the moment it happened, populated names how a backfilled entry was estimated. Instrumented so far: publish (crediting the actual publisher, threaded through from the controller -- previously nobody had the act of publishing recorded anywhere), revert's discard_autosave and destroy_draft branches, and translation destroy. publish_draft! now runs in a transaction so the promotion and its log entry land together. The remaining verbs follow once this mechanism has proven itself. --- app/models/node.rb | 65 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 28 deletions(-) (limited to 'app/models/node.rb') diff --git a/app/models/node.rb b/app/models/node.rb index 311c5c0..b41df06 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -170,10 +170,12 @@ class Node < ApplicationRecord self.autosave.destroy self.autosave_id = nil self.save! + NodeAction.record!(:node => self, :user => current_user, :action => "discard_autosave") elsif self.draft && self.head self.draft.destroy self.draft_id = nil self.save! + NodeAction.record!(:node => self, :user => current_user, :action => "destroy_draft") end self.unlock! unless self.draft @@ -188,43 +190,50 @@ class Node < ApplicationRecord end end - def publish_draft! + def publish_draft! current_user = nil # Return nil if nothing to publish and no staged changes return nil unless self.draft || staged_slug || staged_parent_id - if self.draft - self.head = self.draft - self.head.published_at ||= Time.now - self.head.save! - self.draft = nil - end + ActiveRecord::Base.transaction do + if self.draft + previous_title = self.head ? Globalize.with_locale(I18n.default_locale) { self.head.title } : nil + self.head = self.draft + self.head.published_at ||= Time.now + self.head.save! + self.draft = nil + + new_title = Globalize.with_locale(I18n.default_locale) { self.head.title } + NodeAction.record!(:node => self, :page => self.head, :user => current_user, + :action => "publish", :from => previous_title, :to => new_title) + end - if staged_slug && (staged_slug != slug) - self.slug = staged_slug - self.staged_slug = nil - end + if staged_slug && (staged_slug != slug) + self.slug = staged_slug + self.staged_slug = nil + end - if staged_parent_id && (staged_parent_id != parent_id) - new_parent = Node.find(staged_parent_id) + if staged_parent_id && (staged_parent_id != parent_id) + new_parent = Node.find(staged_parent_id) - if new_parent == self || self.descendants.include?(new_parent) - raise ActiveRecord::RecordInvalid.new(self), "Cannot move a node under itself or one of its own descendants" - end + if new_parent == self || self.descendants.include?(new_parent) + raise ActiveRecord::RecordInvalid.new(self), "Cannot move a node under itself or one of its own descendants" + end - self.staged_parent_id = nil - self.save! - self.move_to_child_of(new_parent) - else - unless self.save - raise ActiveRecord::RecordInvalid.new(self) + self.staged_parent_id = nil + self.save! + self.move_to_child_of(new_parent) + else + unless self.save + raise ActiveRecord::RecordInvalid.new(self) + end end - end - self.reload - self.update_unique_name - self.send(:update_unique_names_of_children) - self.unlock! - self + self.reload + self.update_unique_name + self.send(:update_unique_names_of_children) + self.unlock! + self + end end def restore_revision! revision -- cgit v1.3