taxonomy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +28 -0
  3. data/Rakefile +31 -0
  4. data/lib/generators/taxonomy/migration/migration_generator.rb +39 -0
  5. data/lib/generators/taxonomy/migration/templates/active_record/migration.rb +36 -0
  6. data/lib/tasks/taxonomy_tasks.rake +4 -0
  7. data/lib/taxonomy.rb +25 -0
  8. data/lib/taxonomy/group_helper.rb +12 -0
  9. data/lib/taxonomy/has_tagger.rb +52 -0
  10. data/lib/taxonomy/has_taxonomy.rb +502 -0
  11. data/lib/taxonomy/tag.rb +485 -0
  12. data/lib/taxonomy/tag_list.rb +97 -0
  13. data/lib/taxonomy/tagging.rb +12 -0
  14. data/lib/taxonomy/tags_helper.rb +13 -0
  15. data/lib/taxonomy/version.rb +3 -0
  16. data/spec/dummy/Rakefile +7 -0
  17. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  18. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  19. data/spec/dummy/app/models/altered_inheriting_taggable_model.rb +3 -0
  20. data/spec/dummy/app/models/inheriting_taggable_model.rb +2 -0
  21. data/spec/dummy/app/models/other_taggable_model.rb +4 -0
  22. data/spec/dummy/app/models/post.rb +2 -0
  23. data/spec/dummy/app/models/taggable_model.rb +6 -0
  24. data/spec/dummy/app/models/taggable_user.rb +3 -0
  25. data/spec/dummy/app/models/treed_model.rb +3 -0
  26. data/spec/dummy/app/models/untaggable_model.rb +2 -0
  27. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  28. data/spec/dummy/config.ru +4 -0
  29. data/spec/dummy/config/application.rb +45 -0
  30. data/spec/dummy/config/boot.rb +10 -0
  31. data/spec/dummy/config/database.yml +19 -0
  32. data/spec/dummy/config/environment.rb +5 -0
  33. data/spec/dummy/config/environments/development.rb +30 -0
  34. data/spec/dummy/config/environments/production.rb +60 -0
  35. data/spec/dummy/config/environments/test.rb +39 -0
  36. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  37. data/spec/dummy/config/initializers/inflections.rb +10 -0
  38. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  39. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  40. data/spec/dummy/config/initializers/session_store.rb +8 -0
  41. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  42. data/spec/dummy/config/locales/en.yml +5 -0
  43. data/spec/dummy/config/routes.rb +58 -0
  44. data/spec/dummy/db/migrate/20111221004133_create_posts.rb +8 -0
  45. data/spec/dummy/db/migrate/20111221023928_taxonomy_migration.rb +35 -0
  46. data/spec/dummy/db/migrate/20111221024100_create_bulk.rb +18 -0
  47. data/spec/dummy/db/schema.rb +65 -0
  48. data/spec/dummy/db/test.sqlite3 +0 -0
  49. data/spec/dummy/log/test.log +100915 -0
  50. data/spec/dummy/public/404.html +26 -0
  51. data/spec/dummy/public/422.html +26 -0
  52. data/spec/dummy/public/500.html +26 -0
  53. data/spec/dummy/public/favicon.ico +0 -0
  54. data/spec/dummy/script/rails +6 -0
  55. data/spec/factories/posts.rb +6 -0
  56. data/spec/generators/taxonomy/migration/migration_generator_spec.rb +22 -0
  57. data/spec/models/post_spec.rb +5 -0
  58. data/spec/spec_helper.rb +30 -0
  59. data/spec/taxonomy/group_helper_spec.rb +21 -0
  60. data/spec/taxonomy/has_tagger_spec.rb +113 -0
  61. data/spec/taxonomy/has_taxonomy_spec.rb +226 -0
  62. data/spec/taxonomy/tag_list_spec.rb +70 -0
  63. data/spec/taxonomy/tag_spec.rb +462 -0
  64. data/spec/taxonomy/taggable_spec.rb +262 -0
  65. data/spec/taxonomy/tagger_spec.rb +40 -0
  66. data/spec/taxonomy/tagging_spec.rb +25 -0
  67. data/spec/taxonomy/tags_helper_spec.rb +29 -0
  68. metadata +225 -0
@@ -0,0 +1,485 @@
1
+ require 'iconv' # for slug generation, this should go away
2
+ class Tag < ActiveRecord::Base
3
+ attr_accessible :name, :context
4
+ attr_accessor :skip_before_destroy
5
+
6
+ ### ASSOCIATIONS:
7
+ has_many :taggings, :dependent => :destroy
8
+
9
+ ### VALIDATIONS:
10
+ validates :name, :presence => true, :uniqueness => {:scope => :context}
11
+ validates :slug, :presence => true, :uniqueness => {:scope => :context}
12
+
13
+ before_validation :permalize
14
+ before_validation :strip_name
15
+ before_create :set_default_left_and_right
16
+ before_save :store_new_parent
17
+ after_save :move_to_new_parent
18
+ before_destroy :destroy_descendants
19
+
20
+ belongs_to :parent, :class_name => self.base_class.to_s,
21
+ :foreign_key => Taxonomy.nested_set_options[:parent_column]
22
+ has_many :children, :class_name => self.base_class.to_s,
23
+ :foreign_key => Taxonomy.nested_set_options[:parent_column], :order => Taxonomy.nested_set_options[:left_column]
24
+
25
+ # no assignment to structure fields
26
+ [Taxonomy.nested_set_options[:left_column], Taxonomy.nested_set_options[:right_column]].each do |column|
27
+ module_eval <<-"end_eval", __FILE__, __LINE__
28
+ def #{column}=(x)
29
+ raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by nested set code, use move_to_* methods instead."
30
+ end
31
+ end_eval
32
+ end
33
+
34
+ define_callbacks("before_move", "after_move")
35
+
36
+ ### NESTED SCOPES
37
+
38
+ # calling parent_column_name in a where cause makes migrations explode in beta4, probably an AREL bug
39
+ # use :conditions for now
40
+ # scope :roots, where(parent_column_name => nil).order(quote_column_name(Taxonomy.nested_set_options[:left_column]))
41
+ scope :roots, where(Taxonomy.nested_set_options[:parent_column] => nil).order(Taxonomy.nested_set_options[:left_column])
42
+ scope :leaves, where("#{Taxonomy.nested_set_options[:right_column]} - #{Taxonomy.nested_set_options[:left_column]} = 1").order(Taxonomy.nested_set_options[:left_column])
43
+
44
+ ### TAG SCOPES:
45
+
46
+ scope :named, lambda { |context, name|
47
+ where("context = ? AND name LIKE ?", context, name)
48
+ }
49
+ scope :named_any, lambda { |context, list|
50
+ where(list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR ")).where(sanitize_sql(["(context = ?)", context]))
51
+ }
52
+ scope :named_like, lambda { |context, name|
53
+ where("name LIKE ?", "%#{name}%")
54
+ }
55
+ scope :named_like_any, lambda { |context, list|
56
+ where(list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR ")).where(sanitize_sql(["(context = ?)", context]))
57
+ }
58
+
59
+ ### CLASS METHODS:
60
+
61
+ def self.root
62
+ roots.first
63
+ end
64
+
65
+ def self.find_context_with_slug!(context, slug)
66
+ ret = self.where(:context => context, :slug => slug).first
67
+ raise ActiveRecord::RecordNotFound if ret.nil?
68
+ ret
69
+ end
70
+
71
+ def self.find_or_create_with_like_by_name(context, name)
72
+ named_like(context, name).first || create(:context => "#{context.singularize.to_s}", :name => name)
73
+ end
74
+
75
+ def self.find_or_create_all_with_like_by_name(context, *list)
76
+ list = [list].flatten
77
+
78
+ return [] if list.empty?
79
+
80
+ existing_tags = Tag.named_any(context, list).all
81
+ new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.downcase == name.downcase } }
82
+ created_tags = new_tag_names.map { |name| Tag.create(:context => "#{context.singularize.to_s}", :name => name) }
83
+
84
+ existing_tags + created_tags
85
+ end
86
+
87
+ def self.valid?
88
+ left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
89
+ end
90
+
91
+ def self.treed_taggings_for(context, options = {})
92
+ self.where("tags.context" => context.to_s.singularize)
93
+ end
94
+
95
+ def self.left_and_rights_valid?
96
+ self.base_class.joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
97
+ "#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} = parent.#{primary_key}").where(
98
+ "#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} IS NULL OR " +
99
+ "#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} IS NULL OR " +
100
+ "#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} >= " +
101
+ "#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} OR " +
102
+ "(#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} IS NOT NULL AND " +
103
+ "(#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} <= parent.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} OR " +
104
+ "#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} >= parent.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])}))"
105
+ ).count == 0
106
+ end
107
+
108
+ def self.no_duplicates_for_columns?
109
+ [connection.quote_column_name(Taxonomy.nested_set_options[:left_column]), connection.quote_column_name(Taxonomy.nested_set_options[:right_column])].all? do |column|
110
+ # No duplicates
111
+ self.base_class.select("#{column}, COUNT(#{column})").group("#{column} HAVING COUNT(#{column}) > 1").first.nil?
112
+ end
113
+ end
114
+
115
+ def self.all_roots_valid?
116
+ left = right = 0
117
+ roots.all? do |root|
118
+ g_returning(root.left > left && root.right > right) do
119
+ left = root.left
120
+ right = root.right
121
+ end
122
+ end
123
+ end
124
+
125
+ # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
126
+ def self.rebuild!
127
+ # Don't rebuild a valid tree.
128
+ return true if valid?
129
+
130
+ scope = lambda{|node|}
131
+ if Taxonomy.nested_set_options[:scope]
132
+ scope = lambda{|node|
133
+ scope_column_names.inject(""){|str, column_name|
134
+ str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
135
+ }
136
+ }
137
+ end
138
+ indices = {}
139
+
140
+ set_left_and_rights = lambda do |node|
141
+ # set left
142
+ node[Taxonomy.nested_set_options[:left_column]] = indices[scope.call(node)] += 1
143
+ # find
144
+ find(:all, :conditions => ["#{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} = ? #{scope.call(node)}", node], :order => "#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])}, #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])}, id").each{|n| set_left_and_rights.call(n) }
145
+ # set right
146
+ node[Taxonomy.nested_set_options[:right_column]] = indices[scope.call(node)] += 1
147
+ node.save!
148
+ end
149
+
150
+ # Find root node(s)
151
+ root_nodes = find(:all, :conditions => "#{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} IS NULL", :order => "#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])}, #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])}, id").each do |root_node|
152
+ # setup index for this scope
153
+ indices[scope.call(root_node)] ||= 0
154
+ set_left_and_rights.call(root_node)
155
+ end
156
+ end
157
+
158
+ ### INSTANCE METHODS:
159
+
160
+ def ==(object)
161
+ super || (object.is_a?(Tag) && name == object.name)
162
+ end
163
+
164
+ def to_s
165
+ name
166
+ end
167
+
168
+ def count
169
+ read_attribute(:count).to_i
170
+ end
171
+
172
+ ### INSTANCE METHODS FOR MOVING ITEMS IN NESTED SET
173
+
174
+ # Returns true if this is a root node
175
+ def root?
176
+ parent_id.nil?
177
+ end
178
+ # Value of the parent column
179
+ def parent_id
180
+ self[Taxonomy.nested_set_options[:parent_column]]
181
+ end
182
+
183
+ # Value of the left column
184
+ def left
185
+ self[Taxonomy.nested_set_options[:left_column]]
186
+ end
187
+
188
+ # Value of the right column
189
+ def right
190
+ self[Taxonomy.nested_set_options[:right_column]]
191
+ end
192
+
193
+ # Returns the level of this object in the tree
194
+ # root level is 0
195
+ def level
196
+ parent_id.nil? ? 0 : ancestors.count
197
+ end
198
+
199
+ def leaf?
200
+ !new_record? && right - left == 1
201
+ end
202
+
203
+ # Returns true is this is a child node
204
+ def child?
205
+ !parent_id.nil?
206
+ end
207
+
208
+ # Returns the array of all parents and self
209
+ def self_and_ancestors
210
+ self.reload
211
+ nested_set_scope.where("#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} <= ? AND #{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} >= ?", left, right)
212
+ end
213
+
214
+ # Returns a set of itself and all of its nested children
215
+ def self_and_descendants
216
+ self.reload
217
+ nested_set_scope.where("#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} >= ? AND #{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} <= ?", left, right)
218
+ end
219
+
220
+ # Returns the scope of all children of the parent, including self
221
+ def self_and_siblings
222
+ # Rails 3, but not really. scoped.where(Taxonomy.nested_set_options[:parent_column] => parent_id)
223
+ nested_set_scope.where(["#{Taxonomy.nested_set_options[:parent_column]} == #{parent_id}"])
224
+ end
225
+
226
+ # Check if other model is in the same scope
227
+ def same_scope?(other)
228
+ Array(Taxonomy.nested_set_options[:scope]).all? do |attr|
229
+ self.send(attr) == other.send(attr)
230
+ end
231
+ end
232
+
233
+ # Returns a set of all of its nested children which do not have children
234
+ def leaves
235
+ descendants.where "#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} - #{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} = 1"
236
+ end
237
+
238
+ # Returns a set of all of its children and nested children
239
+ def descendants
240
+ without_self(self_and_descendants)
241
+ end
242
+
243
+ # Returns an array of all parents
244
+ def ancestors
245
+ without_self(self_and_ancestors)
246
+ end
247
+
248
+ # Returns the array of all children of the parent, except self
249
+ def siblings
250
+ without_self(self_and_siblings)
251
+ end
252
+
253
+ # Move the node to the child of another node (you can pass id only)
254
+ def move_to_child_of(node)
255
+ move_to node, :child
256
+ end
257
+
258
+ # Find the first sibling to the left
259
+ def left_sibling
260
+ siblings.where("#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} < ?", left).order(
261
+ "#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} DESC").first
262
+ end
263
+
264
+ # Find the first sibling to the right
265
+ def right_sibling
266
+ siblings.where("#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} > ?", left).first
267
+ end
268
+
269
+ # Move the node to the left of another node (you can pass id only)
270
+ def move_to_left_of(node)
271
+ move_to node, :left
272
+ end
273
+ # Move the node to the left of another node (you can pass id only)
274
+ def move_to_right_of(node)
275
+ move_to node, :right
276
+ end
277
+ # Shorthand method for finding the left sibling and moving to the left of it.
278
+ def move_left
279
+ move_to_left_of left_sibling
280
+ end
281
+ # Shorthand method for finding the right sibling and moving to the right of it.
282
+ def move_right
283
+ move_to_right_of right_sibling
284
+ end
285
+
286
+ # Move the node to root nodes
287
+ def move_to_root
288
+ move_to nil, :root
289
+ end
290
+
291
+ def is_descendant_of?(other)
292
+ other.reload
293
+ other.left < self.left && self.left < other.right && same_scope?(other)
294
+ end
295
+
296
+ def is_or_is_descendant_of?(other)
297
+ other.reload
298
+ other.left <= self.left && self.left < other.right && same_scope?(other)
299
+ end
300
+
301
+ def is_ancestor_of?(other)
302
+ self.reload
303
+ self.left < other.left && other.left < self.right && same_scope?(other)
304
+ end
305
+
306
+ def is_or_is_ancestor_of?(other)
307
+ self.reload
308
+ self.left <= other.left && other.left < self.right && same_scope?(other)
309
+ end
310
+
311
+
312
+ def move_possible?(target)
313
+ self != target && # Can't target self
314
+ same_scope?(target) && # can't be in different scopes
315
+ # !(left..right).include?(target.left..target.right) # this needs tested more
316
+ # detect impossible move
317
+ !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
318
+ end
319
+
320
+ protected
321
+
322
+ def permalize
323
+ if (changed.include?(self.name) && !changed.include?(:slug)) || self.slug.nil? || self.slug.blank?
324
+ s = Iconv.iconv('ascii//ignore//translit', 'utf-8', self.name).to_s
325
+ s.gsub!(/\'/, '') # remove '
326
+ s.gsub!(/\W+/, ' ') # all non-word chars to spaces
327
+ s.strip! # ohh la la
328
+ s.downcase! #
329
+ s.gsub!(/\ +/, '-') # spaces to dashes, preferred separator char everywhere
330
+ # self.send("#{self.sluggable_conf[:slug_column]}=", s)
331
+ write_attribute(:slug, s)
332
+ end
333
+ end
334
+
335
+ def strip_name
336
+ self.name.strip! if self.name.present?
337
+ end
338
+
339
+ def without_self(s)
340
+ s.where("#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self)
341
+ end
342
+
343
+ # on creation, set automatically lft and rgt to the end of the tree
344
+ def set_default_left_and_right
345
+ maxright = nested_set_scope.maximum(Taxonomy.nested_set_options[:right_column]) || 0
346
+ # adds the new node to the right of all existing nodes
347
+ self[Taxonomy.nested_set_options[:left_column]] = maxright + 1
348
+ self[Taxonomy.nested_set_options[:right_column]] = maxright + 2
349
+ end
350
+
351
+ def store_new_parent
352
+ @move_to_new_parent_id = send("#{Taxonomy.nested_set_options[:parent_column]}_changed?") ? parent_id : false
353
+ true # force callback to return true
354
+ end
355
+
356
+ def move_to_new_parent
357
+ if @move_to_new_parent_id.nil?
358
+ move_to_root
359
+ elsif @move_to_new_parent_id
360
+ move_to_child_of(@move_to_new_parent_id)
361
+ end
362
+ end
363
+
364
+ # reload left, right, and parent
365
+ def reload_nested_set
366
+ reload(:select => "#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])}, " +
367
+ "#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])}, #{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])}")
368
+ end
369
+
370
+ # All nested set queries should use this nested_set_scope, which performs finds on
371
+ # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
372
+ # declaration.
373
+ def nested_set_scope
374
+ self.class.base_class.scoped.order(connection.quote_column_name(Taxonomy.nested_set_options[:left_column])) # options
375
+ end
376
+
377
+ # Prunes a branch off of the tree, shifting all of the elements on the right
378
+ # back to the left so the counts still work.
379
+ def destroy_descendants
380
+ return if right.nil? || left.nil? || skip_before_destroy
381
+
382
+ self.class.base_class.transaction do
383
+ if Taxonomy.nested_set_options[:dependent] == :destroy
384
+ descendants.each do |model|
385
+ model.skip_before_destroy = true
386
+ model.destroy
387
+ end
388
+ else
389
+ nested_set_scope.delete_all(
390
+ ["#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} > ? AND #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} < ?",
391
+ left, right]
392
+ )
393
+ end
394
+
395
+ # update lefts and rights for remaining nodes
396
+ diff = right - left + 1
397
+ nested_set_scope.update_all(
398
+ ["#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} = (#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} - ?)", diff],
399
+ ["#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} > ?", right]
400
+ )
401
+ nested_set_scope.update_all(
402
+ ["#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} = (#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} - ?)", diff],
403
+ ["#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} > ?", right]
404
+ )
405
+
406
+ # Don't allow multiple calls to destroy to corrupt the set
407
+ self.skip_before_destroy = true
408
+ end
409
+ end
410
+
411
+ def move_to(target, position)
412
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
413
+ return if run_callbacks(:before_move) == false
414
+ transaction do
415
+ if target.is_a? self.class.base_class
416
+ target.reload_nested_set
417
+ elsif position != :root
418
+ # load object if node is not an object
419
+ target = nested_set_scope.find(target)
420
+ end
421
+ self.reload_nested_set
422
+
423
+ unless position == :root || move_possible?(target)
424
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
425
+ end
426
+
427
+ bound = case position
428
+ when :child; target[Taxonomy.nested_set_options[:right_column]]
429
+ when :left; target[Taxonomy.nested_set_options[:left_column]]
430
+ when :right; target[Taxonomy.nested_set_options[:right_column]] + 1
431
+ when :root; 1
432
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
433
+ end
434
+
435
+ if bound > self[Taxonomy.nested_set_options[:right_column]]
436
+ bound = bound - 1
437
+ other_bound = self[Taxonomy.nested_set_options[:right_column]] + 1
438
+ else
439
+ other_bound = self[Taxonomy.nested_set_options[:left_column]] - 1
440
+ end
441
+
442
+ # there would be no change
443
+ return if bound == self[Taxonomy.nested_set_options[:right_column]] || bound == self[Taxonomy.nested_set_options[:left_column]]
444
+
445
+ # we have defined the boundaries of two non-overlapping intervals,
446
+ # so sorting puts both the intervals and their boundaries in order
447
+ a, b, c, d = [self[Taxonomy.nested_set_options[:left_column]], self[Taxonomy.nested_set_options[:right_column]], bound, other_bound].sort
448
+
449
+ new_parent = case position
450
+ when :child; target.id
451
+ when :root; nil
452
+ else target[Taxonomy.nested_set_options[:parent_column]]
453
+ end
454
+
455
+ self.class.base_class.update_all([
456
+ "#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} = CASE " +
457
+ "WHEN #{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} BETWEEN :a AND :b " +
458
+ "THEN #{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} + :d - :b " +
459
+ "WHEN #{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} BETWEEN :c AND :d " +
460
+ "THEN #{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} + :a - :c " +
461
+ "ELSE #{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} END, " +
462
+ "#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} = CASE " +
463
+ "WHEN #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} BETWEEN :a AND :b " +
464
+ "THEN #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} + :d - :b " +
465
+ "WHEN #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} BETWEEN :c AND :d " +
466
+ "THEN #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} + :a - :c " +
467
+ "ELSE #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} END, " +
468
+ "#{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} = CASE " +
469
+ "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
470
+ "ELSE #{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} END",
471
+ {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
472
+ ], nested_set_scope.where_values)
473
+ end
474
+ target.reload_nested_set if target
475
+ self.reload_nested_set
476
+ run_callbacks(:after_move)
477
+ end
478
+
479
+ private
480
+ def self.g_returning(value)
481
+ yield(value)
482
+ value
483
+ end
484
+
485
+ end