summaryrefslogtreecommitdiff
path: root/app/models/node.rb
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/node.rb')
-rw-r--r--app/models/node.rb226
1 files changed, 203 insertions, 23 deletions
diff --git a/app/models/node.rb b/app/models/node.rb
index 92ecc12..73eefd1 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -1,13 +1,14 @@
1class Node < ApplicationRecord 1class Node < ApplicationRecord
2 # Mixins and Plugins 2 # Mixins and Plugins
3 acts_as_nested_set 3 include NestedTree
4 4
5 # Associations 5 # Associations
6 has_many :pages, -> { order("revision ASC") }, :dependent => :destroy 6 has_many :pages, -> { order("revision ASC") }, :dependent => :destroy
7 belongs_to :head, :class_name => "Page", :foreign_key => :head_id, :dependent => :destroy, optional: true 7 belongs_to :head, :class_name => "Page", :foreign_key => :head_id, :dependent => :destroy, optional: true
8 belongs_to :draft, :class_name => "Page", :foreign_key => :draft_id, :dependent => :destroy, optional: true 8 belongs_to :draft, :class_name => "Page", :foreign_key => :draft_id, :dependent => :destroy, optional: true
9 belongs_to :autosave, :class_name => "Page", :foreign_key => :autosave_id, :dependent => :destroy, optional: true
9 has_many :permissions, :dependent => :destroy 10 has_many :permissions, :dependent => :destroy
10 has_one :event, :dependent => :destroy 11 has_many :events, :dependent => :destroy
11 belongs_to :lock_owner, :class_name => "User", :foreign_key => :locking_user_id, optional: true 12 belongs_to :lock_owner, :class_name => "User", :foreign_key => :locking_user_id, optional: true
12 13
13 # Callbacks 14 # Callbacks
@@ -16,21 +17,11 @@ class Node < ApplicationRecord
16 after_save :update_unique_names_of_children 17 after_save :update_unique_names_of_children
17 18
18 # Validations 19 # Validations
19 validates_length_of :slug, :within => 1..255, :unless => -> { parent_id.nil? } 20 validates_length_of :slug, :within => 1..255, :unless => -> { parent_id.nil? || slug.blank? }
20 validates_presence_of :slug, :unless => -> { parent_id.nil? } 21 validates_presence_of :slug, :unless => -> { parent_id.nil? }
21 validates_uniqueness_of :slug, :scope => :parent_id, :unless => -> { parent_id.nil? } 22 validates_uniqueness_of :slug, :scope => :parent_id, :unless => -> { parent_id.nil? }
22 validates_presence_of :parent_id, :unless => -> { Node.root.nil? } 23 validates_presence_of :parent_id, :unless => -> { Node.root.nil? }
23 24
24 validate :borders # This should never ever happen.
25
26 # Index for Fulltext Search
27 # define_index do
28 # indexes head.translations.title
29 # indexes slug
30 # indexes unique_name
31 # indexes head.translations.body
32 # end
33
34 # Class methods 25 # Class methods
35 26
36 # Returns a page for a given node. If no revision is supplied, it returns 27 # Returns a page for a given node. If no revision is supplied, it returns
@@ -59,6 +50,101 @@ class Node < ApplicationRecord
59 50
60 # Instance Methods 51 # Instance Methods
61 52
53 # Acquires (or reaffirms) the editing lock without creating a draft or
54 # an autosave -- both are now deferred until there is real content to
55 # hold.
56 def lock_for_editing! current_user
57 if self.lock_owner.nil? || self.lock_owner == current_user
58 lock_for! current_user
59 if self.draft
60 self.draft.user = current_user if self.draft.user.nil?
61 self.draft.editor = current_user
62 self.draft.save!
63 end
64 self
65 else
66 raise(
67 LockedByAnotherUser,
68 "Page is locked by another user who is working on it! " \
69 "Last modification: #{(self.autosave || self.draft || self.head).updated_at.to_fs(:db)}"
70 )
71 end
72 end
73
74 # Creates or updates the autosave buffer from the given attributes.
75 # Autosave rows are never associated to the node via node_id -- they
76 # must never appear in self.pages / the revisions list, which is the
77 # whole reason autosave exists as a separate, unversioned layer.
78 def autosave! attributes, current_user
79 assert_locked_by! current_user
80
81 unless self.autosave
82 self.autosave = Page.create!(:editor => current_user)
83 self.autosave.clone_attributes_from(self.draft || self.head) if self.draft || self.head
84 self.save!
85 end
86 self.autosave.assign_attributes(attributes)
87 self.autosave.save!
88 self.autosave
89 end
90
91 # Promotes the current autosave into the draft (creating the draft if
92 # none exists yet) and destroys the autosave afterward. This is what
93 # the explicit "Save" action does; it never creates a new revision --
94 # same as any other in-place draft edit. The new draft is created via
95 # self.pages.create! rather than by repointing the autosave's own
96 # node_id, because acts_as_list assigns the revision number at create
97 # time, scoped to node_id -- a page created with node_id nil and
98 # reassigned afterward would carry a wrong or missing revision number.
99 def save_draft! current_user
100 assert_locked_by! current_user
101 return unless self.autosave
102
103 if self.draft
104 preserved_published_at = self.draft.published_at
105 self.draft.clone_attributes_from self.autosave
106 self.draft.published_at = preserved_published_at
107 self.draft.user_id = self.autosave.user_id if self.autosave.user_id
108 self.draft.editor = current_user
109 self.draft.save!
110 else
111 empty_page = self.pages.create!
112 empty_page.clone_attributes_from self.autosave
113 empty_page.user = self.autosave.user_id ? self.autosave.user : (self.head ? self.head.user : current_user)
114 empty_page.editor = current_user
115 empty_page.published_at = self.head.published_at if self.head
116 empty_page.save!
117 self.draft = empty_page
118 self.save!
119 end
120
121 self.autosave.destroy
122 self.autosave_id = nil
123 self.save!
124 self.draft.reload
125 end
126
127 def resolve_page_reference ref
128 case ref.to_s
129 when "head" then head
130 when "draft" then draft
131 when "autosave" then autosave
132 else pages.find_by_revision(ref)
133 end
134 end
135
136 # Which layer-pairs are meaningful to compare right now, given this
137 # node's actual state. Head vs autosave only shows up when no draft
138 # sits between them -- with a draft present, autosave is compared
139 # against the draft, never past it straight to head.
140 def available_layer_pairs
141 pairs = []
142 pairs << [:head, :draft] if head && draft
143 pairs << [:draft, :autosave] if draft && autosave
144 pairs << [:head, :autosave] if head && autosave && !draft
145 pairs
146 end
147
62 def find_or_create_draft current_user 148 def find_or_create_draft current_user
63 self.wipe_draft! 149 self.wipe_draft!
64 if draft && self.lock_owner == current_user 150 if draft && self.lock_owner == current_user
@@ -94,6 +180,36 @@ class Node < ApplicationRecord
94 self.draft.reload 180 self.draft.reload
95 end 181 end
96 182
183 # Discards exactly the topmost non-empty layer -- autosave if present,
184 # else draft -- and reveals whatever's beneath it. Releases the lock
185 # only once nothing is left to protect (no draft survives); leaves it
186 # alone whenever a draft remains, since #edit still has real content
187 # open.
188 def revert! current_user
189 assert_locked_by! current_user
190
191 if self.autosave
192 self.autosave.destroy
193 self.autosave_id = nil
194 self.save!
195 elsif self.draft && self.head
196 self.draft.destroy
197 self.draft_id = nil
198 self.save!
199 end
200
201 self.unlock! unless self.draft
202 self.reload
203 end
204
205 def staged_slug=(value)
206 if head.blank?
207 self.slug = value
208 else
209 super
210 end
211 end
212
97 def publish_draft! 213 def publish_draft!
98 # Return nil if nothing to publish and no staged changes 214 # Return nil if nothing to publish and no staged changes
99 return nil unless self.draft || staged_slug || staged_parent_id 215 return nil unless self.draft || staged_slug || staged_parent_id
@@ -112,6 +228,11 @@ class Node < ApplicationRecord
112 228
113 if staged_parent_id && (staged_parent_id != parent_id) 229 if staged_parent_id && (staged_parent_id != parent_id)
114 new_parent = Node.find(staged_parent_id) 230 new_parent = Node.find(staged_parent_id)
231
232 if new_parent == self || self.descendants.include?(new_parent)
233 raise ActiveRecord::RecordInvalid.new(self), "Cannot move a node under itself or one of its own descendants"
234 end
235
115 self.staged_parent_id = nil 236 self.staged_parent_id = nil
116 self.save! 237 self.save!
117 self.move_to_child_of(new_parent) 238 self.move_to_child_of(new_parent)
@@ -128,11 +249,22 @@ class Node < ApplicationRecord
128 self 249 self
129 end 250 end
130 251
131 # removes a draft and the lock if it is older than a day and still 252 # Releases whatever's stale and abandoned; never anything actively in
132 # identical to head 253 # use. Three independent cases share one rule -- nothing is touched
254 # unless it's been sitting untouched for over a day:
255 # - a lock held with no draft or autosave at all (the editor opened
256 # the page and never actually wrote anything)
257 # - a fresh autosave older than a day, never promoted to a draft
258 # - a draft older than a day that's still identical to head
133 def wipe_draft! 259 def wipe_draft!
260 return if self.autosave && self.autosave.updated_at > 1.day.ago
261
134 unless self.draft 262 unless self.draft
263 return if self.autosave.nil? && self.locked? && self.updated_at > 1.day.ago
264 self.autosave&.destroy
265 self.autosave_id = nil
135 self.unlock! 266 self.unlock!
267 self.save!
136 return 268 return
137 end 269 end
138 return unless self.head 270 return unless self.head
@@ -203,6 +335,10 @@ class Node < ApplicationRecord
203 unique_path.length == 3 && unique_path[0] == "updates" 335 unique_path.length == 3 && unique_path[0] == "updates"
204 end 336 end
205 337
338 def editable_page
339 autosave || draft || head
340 end
341
206 # Returns immutable node id for all new nodes so that the atom feed entry ids 342 # Returns immutable node id for all new nodes so that the atom feed entry ids
207 # stay the same eventhough the slug or positions changes. 343 # stay the same eventhough the slug or positions changes.
208 # Can be removed after a year or so ;) 344 # Can be removed after a year or so ;)
@@ -220,12 +356,59 @@ class Node < ApplicationRecord
220 .distinct 356 .distinct
221 end 357 end
222 358
359 # This one is for admin-only views, where finding a draft is the point.
360 # Substring match on whichever of head/draft is present.
361 def self.editor_search(term)
362 words = term.to_s.split(/\s+/).reject(&:blank?)
363 return none if words.empty?
364
365 conditions = []
366 binds = {}
367
368 words.each_with_index do |word, i|
369 key = "term#{i}"
370 binds[key.to_sym] = "%#{sanitize_sql_like(word)}%"
371 conditions << "(head_translations.title ILIKE :#{key} OR head_translations.abstract ILIKE :#{key} " \
372 "OR draft_translations.title ILIKE :#{key} OR draft_translations.abstract ILIKE :#{key})"
373 end
374
375 joins("LEFT JOIN page_translations head_translations ON head_translations.page_id = nodes.head_id")
376 .joins("LEFT JOIN page_translations draft_translations ON draft_translations.page_id = nodes.draft_id")
377 .where(conditions.join(" AND "), binds)
378 .distinct
379 end
380
381 def self.drafts_and_autosaves(current_user_id: nil)
382 scope = where("draft_id IS NOT NULL OR autosave_id IS NOT NULL")
383 return scope.order("updated_at DESC") unless current_user_id
384
385 scope.order(
386 Arel.sql(sanitize_sql_array(["CASE WHEN locking_user_id = ? THEN 0 ELSE 1 END, updated_at DESC", current_user_id]))
387 )
388 end
389
390 def self.recently_changed
391 includes(:head).where(
392 "pages.updated_at < ? AND pages.updated_at > ? AND nodes.parent_id IS NOT NULL",
393 Time.now, Time.now - 14.days
394 ).order("pages.updated_at desc").references(:head)
395 end
396
223 protected 397 protected
224 def lock_for! current_user 398 def lock_for! current_user
225 self.lock_owner = current_user 399 self.lock_owner = current_user
226 self.save 400 self.save
227 end 401 end
228 402
403 def assert_locked_by! current_user
404 return if self.lock_owner == current_user
405 raise(
406 LockedByAnotherUser,
407 "Page is locked by another user who is working on it! " \
408 "Last modification: #{(self.autosave || self.draft || self.head).updated_at.to_fs(:db)}"
409 )
410 end
411
229 # Creates an empty page and associates it to the given node. This means 412 # Creates an empty page and associates it to the given node. This means
230 # freshly created node has an empty draft. A user can create nodes as he 413 # freshly created node has an empty draft. A user can create nodes as he
231 # wants to which will not appear on the public page until the author edits 414 # wants to which will not appear on the public page until the author edits
@@ -246,10 +429,13 @@ class Node < ApplicationRecord
246 # Watch out recursion ahead! update_unique_name itself triggers this 429 # Watch out recursion ahead! update_unique_name itself triggers this
247 # after_save callback which invokes update_unique_name on its children. 430 # after_save callback which invokes update_unique_name on its children.
248 # Hopefully until no childrens occur 431 # Hopefully until no childrens occur
432 #
433 # Queries parent_id directly rather than the NestedTree#children
434 # association out of habit from the old awesome_nested_set-avoidance
435 # workaround - no longer strictly necessary now that children is
436 # equally safe, but left as-is since it already works correctly.
249 def update_unique_names_of_children 437 def update_unique_names_of_children
250 unless root? 438 unless root?
251 # Use parent_id-based traversal instead of lft/rgt descendants
252 # due to awesome_nested_set not refreshing parent lft/rgt in memory
253 Node.where(:parent_id => self.id).each do |child| 439 Node.where(:parent_id => self.id).each do |child|
254 child.reload 440 child.reload
255 child.update_unique_name 441 child.update_unique_name
@@ -257,10 +443,4 @@ class Node < ApplicationRecord
257 end 443 end
258 end 444 end
259 end 445 end
260
261 def borders
262 if lft && rgt && (lft > rgt)
263 errors.add("Fuck!. lft should never be smaller than rgt")
264 end
265 end
266end 446end