Mercurial > hg > soundsoftware-site
comparison .svn/pristine/fd/fd672f720e53aab0a0184fd9c83dcdbc26d28825.svn-base @ 1298:4f746d8966dd redmine_2.3_integration
Merge from redmine-2.3 branch to create new branch redmine-2.3-integration
author | Chris Cannam |
---|---|
date | Fri, 14 Jun 2013 09:28:30 +0100 |
parents | 622f24f53b42 |
children |
comparison
equal
deleted
inserted
replaced
1297:0a574315af3e | 1298:4f746d8966dd |
---|---|
1 module CollectiveIdea #:nodoc: | |
2 module Acts #:nodoc: | |
3 module NestedSet #:nodoc: | |
4 | |
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement | |
6 # an _ordered_ tree, with the added feature that you can select the children and all of their | |
7 # descendants with a single query. The drawback is that insertion or move need some complex | |
8 # sql queries. But everything is done here by this module! | |
9 # | |
10 # Nested sets are appropriate each time you want either an orderd tree (menus, | |
11 # commercial categories) or an efficient way of querying big trees (threaded posts). | |
12 # | |
13 # == API | |
14 # | |
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one | |
16 # by another easier. | |
17 # | |
18 # item.children.create(:name => "child1") | |
19 # | |
20 | |
21 # Configuration options are: | |
22 # | |
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id) | |
24 # * +:left_column+ - column name for left boundry data, default "lft" | |
25 # * +:right_column+ - column name for right boundry data, default "rgt" | |
26 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id" | |
27 # (if it hasn't been already) and use that as the foreign key restriction. You | |
28 # can also pass an array to scope by multiple attributes. | |
29 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt> | |
30 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the | |
31 # child objects are destroyed alongside this object by calling their destroy | |
32 # method. If set to :delete_all (default), all the child objects are deleted | |
33 # without calling their destroy method. | |
34 # * +:counter_cache+ adds a counter cache for the number of children. | |
35 # defaults to false. | |
36 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt> | |
37 # | |
38 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and | |
39 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added | |
40 # to acts_as_nested_set models | |
41 def acts_as_nested_set(options = {}) | |
42 options = { | |
43 :parent_column => 'parent_id', | |
44 :left_column => 'lft', | |
45 :right_column => 'rgt', | |
46 :dependent => :delete_all, # or :destroy | |
47 :counter_cache => false, | |
48 :order => 'id' | |
49 }.merge(options) | |
50 | |
51 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/ | |
52 options[:scope] = "#{options[:scope]}_id".intern | |
53 end | |
54 | |
55 class_attribute :acts_as_nested_set_options | |
56 self.acts_as_nested_set_options = options | |
57 | |
58 include CollectiveIdea::Acts::NestedSet::Model | |
59 include Columns | |
60 extend Columns | |
61 | |
62 belongs_to :parent, :class_name => self.base_class.to_s, | |
63 :foreign_key => parent_column_name, | |
64 :counter_cache => options[:counter_cache], | |
65 :inverse_of => :children | |
66 has_many :children, :class_name => self.base_class.to_s, | |
67 :foreign_key => parent_column_name, :order => left_column_name, | |
68 :inverse_of => :parent, | |
69 :before_add => options[:before_add], | |
70 :after_add => options[:after_add], | |
71 :before_remove => options[:before_remove], | |
72 :after_remove => options[:after_remove] | |
73 | |
74 attr_accessor :skip_before_destroy | |
75 | |
76 before_create :set_default_left_and_right | |
77 before_save :store_new_parent | |
78 after_save :move_to_new_parent | |
79 before_destroy :destroy_descendants | |
80 | |
81 # no assignment to structure fields | |
82 [left_column_name, right_column_name].each do |column| | |
83 module_eval <<-"end_eval", __FILE__, __LINE__ | |
84 def #{column}=(x) | |
85 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead." | |
86 end | |
87 end_eval | |
88 end | |
89 | |
90 define_model_callbacks :move | |
91 end | |
92 | |
93 module Model | |
94 extend ActiveSupport::Concern | |
95 | |
96 module ClassMethods | |
97 # Returns the first root | |
98 def root | |
99 roots.first | |
100 end | |
101 | |
102 def roots | |
103 where(parent_column_name => nil).order(quoted_left_column_name) | |
104 end | |
105 | |
106 def leaves | |
107 where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name) | |
108 end | |
109 | |
110 def valid? | |
111 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid? | |
112 end | |
113 | |
114 def left_and_rights_valid? | |
115 joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " + | |
116 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}"). | |
117 where( | |
118 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " + | |
119 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " + | |
120 "#{quoted_table_name}.#{quoted_left_column_name} >= " + | |
121 "#{quoted_table_name}.#{quoted_right_column_name} OR " + | |
122 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " + | |
123 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " + | |
124 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))" | |
125 ).count == 0 | |
126 end | |
127 | |
128 def no_duplicates_for_columns? | |
129 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c| | |
130 connection.quote_column_name(c) | |
131 end.push(nil).join(", ") | |
132 [quoted_left_column_name, quoted_right_column_name].all? do |column| | |
133 # No duplicates | |
134 select("#{scope_string}#{column}, COUNT(#{column})"). | |
135 group("#{scope_string}#{column}"). | |
136 having("COUNT(#{column}) > 1"). | |
137 first.nil? | |
138 end | |
139 end | |
140 | |
141 # Wrapper for each_root_valid? that can deal with scope. | |
142 def all_roots_valid? | |
143 if acts_as_nested_set_options[:scope] | |
144 roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots| | |
145 each_root_valid?(grouped_roots) | |
146 end | |
147 else | |
148 each_root_valid?(roots) | |
149 end | |
150 end | |
151 | |
152 def each_root_valid?(roots_to_validate) | |
153 left = right = 0 | |
154 roots_to_validate.all? do |root| | |
155 (root.left > left && root.right > right).tap do | |
156 left = root.left | |
157 right = root.right | |
158 end | |
159 end | |
160 end | |
161 | |
162 # Rebuilds the left & rights if unset or invalid. | |
163 # Also very useful for converting from acts_as_tree. | |
164 def rebuild!(validate_nodes = true) | |
165 # Don't rebuild a valid tree. | |
166 return true if valid? | |
167 | |
168 scope = lambda{|node|} | |
169 if acts_as_nested_set_options[:scope] | |
170 scope = lambda{|node| | |
171 scope_column_names.inject(""){|str, column_name| | |
172 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} " | |
173 } | |
174 } | |
175 end | |
176 indices = {} | |
177 | |
178 set_left_and_rights = lambda do |node| | |
179 # set left | |
180 node[left_column_name] = indices[scope.call(node)] += 1 | |
181 # find | |
182 where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).order(acts_as_nested_set_options[:order]).each{|n| set_left_and_rights.call(n) } | |
183 # set right | |
184 node[right_column_name] = indices[scope.call(node)] += 1 | |
185 node.save!(:validate => validate_nodes) | |
186 end | |
187 | |
188 # Find root node(s) | |
189 root_nodes = where("#{quoted_parent_column_name} IS NULL").order(acts_as_nested_set_options[:order]).each do |root_node| | |
190 # setup index for this scope | |
191 indices[scope.call(root_node)] ||= 0 | |
192 set_left_and_rights.call(root_node) | |
193 end | |
194 end | |
195 | |
196 # Iterates over tree elements and determines the current level in the tree. | |
197 # Only accepts default ordering, odering by an other column than lft | |
198 # does not work. This method is much more efficent than calling level | |
199 # because it doesn't require any additional database queries. | |
200 # | |
201 # Example: | |
202 # Category.each_with_level(Category.root.self_and_descendants) do |o, level| | |
203 # | |
204 def each_with_level(objects) | |
205 path = [nil] | |
206 objects.each do |o| | |
207 if o.parent_id != path.last | |
208 # we are on a new level, did we decent or ascent? | |
209 if path.include?(o.parent_id) | |
210 # remove wrong wrong tailing paths elements | |
211 path.pop while path.last != o.parent_id | |
212 else | |
213 path << o.parent_id | |
214 end | |
215 end | |
216 yield(o, path.length - 1) | |
217 end | |
218 end | |
219 end | |
220 | |
221 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder. | |
222 # | |
223 # category.self_and_descendants.count | |
224 # category.ancestors.find(:all, :conditions => "name like '%foo%'") | |
225 | |
226 # Value of the parent column | |
227 def parent_id | |
228 self[parent_column_name] | |
229 end | |
230 | |
231 # Value of the left column | |
232 def left | |
233 self[left_column_name] | |
234 end | |
235 | |
236 # Value of the right column | |
237 def right | |
238 self[right_column_name] | |
239 end | |
240 | |
241 # Returns true if this is a root node. | |
242 def root? | |
243 parent_id.nil? | |
244 end | |
245 | |
246 def leaf? | |
247 new_record? || (right - left == 1) | |
248 end | |
249 | |
250 # Returns true is this is a child node | |
251 def child? | |
252 !parent_id.nil? | |
253 end | |
254 | |
255 # Returns root | |
256 def root | |
257 self_and_ancestors.where(parent_column_name => nil).first | |
258 end | |
259 | |
260 # Returns the array of all parents and self | |
261 def self_and_ancestors | |
262 nested_set_scope.where([ | |
263 "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right | |
264 ]) | |
265 end | |
266 | |
267 # Returns an array of all parents | |
268 def ancestors | |
269 without_self self_and_ancestors | |
270 end | |
271 | |
272 # Returns the array of all children of the parent, including self | |
273 def self_and_siblings | |
274 nested_set_scope.where(parent_column_name => parent_id) | |
275 end | |
276 | |
277 # Returns the array of all children of the parent, except self | |
278 def siblings | |
279 without_self self_and_siblings | |
280 end | |
281 | |
282 # Returns a set of all of its nested children which do not have children | |
283 def leaves | |
284 descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1") | |
285 end | |
286 | |
287 # Returns the level of this object in the tree | |
288 # root level is 0 | |
289 def level | |
290 parent_id.nil? ? 0 : ancestors.count | |
291 end | |
292 | |
293 # Returns a set of itself and all of its nested children | |
294 def self_and_descendants | |
295 nested_set_scope.where([ | |
296 "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right | |
297 ]) | |
298 end | |
299 | |
300 # Returns a set of all of its children and nested children | |
301 def descendants | |
302 without_self self_and_descendants | |
303 end | |
304 | |
305 def is_descendant_of?(other) | |
306 other.left < self.left && self.left < other.right && same_scope?(other) | |
307 end | |
308 | |
309 def is_or_is_descendant_of?(other) | |
310 other.left <= self.left && self.left < other.right && same_scope?(other) | |
311 end | |
312 | |
313 def is_ancestor_of?(other) | |
314 self.left < other.left && other.left < self.right && same_scope?(other) | |
315 end | |
316 | |
317 def is_or_is_ancestor_of?(other) | |
318 self.left <= other.left && other.left < self.right && same_scope?(other) | |
319 end | |
320 | |
321 # Check if other model is in the same scope | |
322 def same_scope?(other) | |
323 Array(acts_as_nested_set_options[:scope]).all? do |attr| | |
324 self.send(attr) == other.send(attr) | |
325 end | |
326 end | |
327 | |
328 # Find the first sibling to the left | |
329 def left_sibling | |
330 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]). | |
331 order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last | |
332 end | |
333 | |
334 # Find the first sibling to the right | |
335 def right_sibling | |
336 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first | |
337 end | |
338 | |
339 # Shorthand method for finding the left sibling and moving to the left of it. | |
340 def move_left | |
341 move_to_left_of left_sibling | |
342 end | |
343 | |
344 # Shorthand method for finding the right sibling and moving to the right of it. | |
345 def move_right | |
346 move_to_right_of right_sibling | |
347 end | |
348 | |
349 # Move the node to the left of another node (you can pass id only) | |
350 def move_to_left_of(node) | |
351 move_to node, :left | |
352 end | |
353 | |
354 # Move the node to the left of another node (you can pass id only) | |
355 def move_to_right_of(node) | |
356 move_to node, :right | |
357 end | |
358 | |
359 # Move the node to the child of another node (you can pass id only) | |
360 def move_to_child_of(node) | |
361 move_to node, :child | |
362 end | |
363 | |
364 # Move the node to root nodes | |
365 def move_to_root | |
366 move_to nil, :root | |
367 end | |
368 | |
369 def move_possible?(target) | |
370 self != target && # Can't target self | |
371 same_scope?(target) && # can't be in different scopes | |
372 # !(left..right).include?(target.left..target.right) # this needs tested more | |
373 # detect impossible move | |
374 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right)) | |
375 end | |
376 | |
377 def to_text | |
378 self_and_descendants.map do |node| | |
379 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})" | |
380 end.join("\n") | |
381 end | |
382 | |
383 protected | |
384 | |
385 def without_self(scope) | |
386 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self]) | |
387 end | |
388 | |
389 # All nested set queries should use this nested_set_scope, which performs finds on | |
390 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set | |
391 # declaration. | |
392 def nested_set_scope(options = {}) | |
393 options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options) | |
394 scopes = Array(acts_as_nested_set_options[:scope]) | |
395 options[:conditions] = scopes.inject({}) do |conditions,attr| | |
396 conditions.merge attr => self[attr] | |
397 end unless scopes.empty? | |
398 self.class.base_class.scoped options | |
399 end | |
400 | |
401 def store_new_parent | |
402 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false | |
403 true # force callback to return true | |
404 end | |
405 | |
406 def move_to_new_parent | |
407 if @move_to_new_parent_id.nil? | |
408 move_to_root | |
409 elsif @move_to_new_parent_id | |
410 move_to_child_of(@move_to_new_parent_id) | |
411 end | |
412 end | |
413 | |
414 # on creation, set automatically lft and rgt to the end of the tree | |
415 def set_default_left_and_right | |
416 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").limit(1).lock(true).first | |
417 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0 | |
418 # adds the new node to the right of all existing nodes | |
419 self[left_column_name] = maxright + 1 | |
420 self[right_column_name] = maxright + 2 | |
421 end | |
422 | |
423 def in_tenacious_transaction(&block) | |
424 retry_count = 0 | |
425 begin | |
426 transaction(&block) | |
427 rescue ActiveRecord::StatementInvalid => error | |
428 raise unless connection.open_transactions.zero? | |
429 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/ | |
430 raise unless retry_count < 10 | |
431 retry_count += 1 | |
432 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction" | |
433 sleep(rand(retry_count)*0.1) # Aloha protocol | |
434 retry | |
435 end | |
436 end | |
437 | |
438 # Prunes a branch off of the tree, shifting all of the elements on the right | |
439 # back to the left so the counts still work. | |
440 def destroy_descendants | |
441 return if right.nil? || left.nil? || skip_before_destroy | |
442 | |
443 in_tenacious_transaction do | |
444 reload_nested_set | |
445 # select the rows in the model that extend past the deletion point and apply a lock | |
446 self.class.base_class. | |
447 select("id"). | |
448 where("#{quoted_left_column_name} >= ?", left). | |
449 lock(true). | |
450 all | |
451 | |
452 if acts_as_nested_set_options[:dependent] == :destroy | |
453 descendants.each do |model| | |
454 model.skip_before_destroy = true | |
455 model.destroy | |
456 end | |
457 else | |
458 nested_set_scope.delete_all( | |
459 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", | |
460 left, right] | |
461 ) | |
462 end | |
463 | |
464 # update lefts and rights for remaining nodes | |
465 diff = right - left + 1 | |
466 nested_set_scope.update_all( | |
467 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff], | |
468 ["#{quoted_left_column_name} > ?", right] | |
469 ) | |
470 nested_set_scope.update_all( | |
471 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff], | |
472 ["#{quoted_right_column_name} > ?", right] | |
473 ) | |
474 | |
475 reload | |
476 # Don't allow multiple calls to destroy to corrupt the set | |
477 self.skip_before_destroy = true | |
478 end | |
479 end | |
480 | |
481 # reload left, right, and parent | |
482 def reload_nested_set | |
483 reload( | |
484 :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}", | |
485 :lock => true | |
486 ) | |
487 end | |
488 | |
489 def move_to(target, position) | |
490 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record? | |
491 run_callbacks :move do | |
492 in_tenacious_transaction do | |
493 if target.is_a? self.class.base_class | |
494 target.reload_nested_set | |
495 elsif position != :root | |
496 # load object if node is not an object | |
497 target = nested_set_scope.find(target) | |
498 end | |
499 self.reload_nested_set | |
500 | |
501 unless position == :root || move_possible?(target) | |
502 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree." | |
503 end | |
504 | |
505 bound = case position | |
506 when :child; target[right_column_name] | |
507 when :left; target[left_column_name] | |
508 when :right; target[right_column_name] + 1 | |
509 when :root; 1 | |
510 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)." | |
511 end | |
512 | |
513 if bound > self[right_column_name] | |
514 bound = bound - 1 | |
515 other_bound = self[right_column_name] + 1 | |
516 else | |
517 other_bound = self[left_column_name] - 1 | |
518 end | |
519 | |
520 # there would be no change | |
521 return if bound == self[right_column_name] || bound == self[left_column_name] | |
522 | |
523 # we have defined the boundaries of two non-overlapping intervals, | |
524 # so sorting puts both the intervals and their boundaries in order | |
525 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort | |
526 | |
527 # select the rows in the model between a and d, and apply a lock | |
528 self.class.base_class.select('id').lock(true).where( | |
529 ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}] | |
530 ) | |
531 | |
532 new_parent = case position | |
533 when :child; target.id | |
534 when :root; nil | |
535 else target[parent_column_name] | |
536 end | |
537 | |
538 self.nested_set_scope.update_all([ | |
539 "#{quoted_left_column_name} = CASE " + | |
540 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " + | |
541 "THEN #{quoted_left_column_name} + :d - :b " + | |
542 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " + | |
543 "THEN #{quoted_left_column_name} + :a - :c " + | |
544 "ELSE #{quoted_left_column_name} END, " + | |
545 "#{quoted_right_column_name} = CASE " + | |
546 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " + | |
547 "THEN #{quoted_right_column_name} + :d - :b " + | |
548 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " + | |
549 "THEN #{quoted_right_column_name} + :a - :c " + | |
550 "ELSE #{quoted_right_column_name} END, " + | |
551 "#{quoted_parent_column_name} = CASE " + | |
552 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " + | |
553 "ELSE #{quoted_parent_column_name} END", | |
554 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent} | |
555 ]) | |
556 end | |
557 target.reload_nested_set if target | |
558 self.reload_nested_set | |
559 end | |
560 end | |
561 | |
562 end | |
563 | |
564 # Mixed into both classes and instances to provide easy access to the column names | |
565 module Columns | |
566 def left_column_name | |
567 acts_as_nested_set_options[:left_column] | |
568 end | |
569 | |
570 def right_column_name | |
571 acts_as_nested_set_options[:right_column] | |
572 end | |
573 | |
574 def parent_column_name | |
575 acts_as_nested_set_options[:parent_column] | |
576 end | |
577 | |
578 def scope_column_names | |
579 Array(acts_as_nested_set_options[:scope]) | |
580 end | |
581 | |
582 def quoted_left_column_name | |
583 connection.quote_column_name(left_column_name) | |
584 end | |
585 | |
586 def quoted_right_column_name | |
587 connection.quote_column_name(right_column_name) | |
588 end | |
589 | |
590 def quoted_parent_column_name | |
591 connection.quote_column_name(parent_column_name) | |
592 end | |
593 | |
594 def quoted_scope_column_names | |
595 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) } | |
596 end | |
597 end | |
598 | |
599 end | |
600 end | |
601 end |