awesome_nested_set 2.1.6 → 3.0.0.rc.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.
@@ -0,0 +1,72 @@
1
+ # Mixed into both classes and instances to provide easy access to the column names
2
+ module CollectiveIdea #:nodoc:
3
+ module Acts #:nodoc:
4
+ module NestedSet #:nodoc:
5
+ module Columns
6
+ def left_column_name
7
+ acts_as_nested_set_options[:left_column]
8
+ end
9
+
10
+ def right_column_name
11
+ acts_as_nested_set_options[:right_column]
12
+ end
13
+
14
+ def depth_column_name
15
+ acts_as_nested_set_options[:depth_column]
16
+ end
17
+
18
+ def parent_column_name
19
+ acts_as_nested_set_options[:parent_column]
20
+ end
21
+
22
+ def order_column
23
+ acts_as_nested_set_options[:order_column] || left_column_name
24
+ end
25
+
26
+ def scope_column_names
27
+ Array(acts_as_nested_set_options[:scope])
28
+ end
29
+
30
+ def quoted_left_column_name
31
+ ActiveRecord::Base.connection.quote_column_name(left_column_name)
32
+ end
33
+
34
+ def quoted_right_column_name
35
+ ActiveRecord::Base.connection.quote_column_name(right_column_name)
36
+ end
37
+
38
+ def quoted_depth_column_name
39
+ ActiveRecord::Base.connection.quote_column_name(depth_column_name)
40
+ end
41
+
42
+ def quoted_parent_column_name
43
+ ActiveRecord::Base.connection.quote_column_name(parent_column_name)
44
+ end
45
+
46
+ def quoted_scope_column_names
47
+ scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
48
+ end
49
+
50
+ def quoted_order_column_name
51
+ ActiveRecord::Base.connection.quote_column_name(order_column)
52
+ end
53
+
54
+ def quoted_primary_key_column_full_name
55
+ "#{quoted_table_name}.#{ActiveRecord::Base.connection.quote_column_name('id')}"
56
+ end
57
+
58
+ def quoted_left_column_full_name
59
+ "#{quoted_table_name}.#{quoted_left_column_name}"
60
+ end
61
+
62
+ def quoted_right_column_full_name
63
+ "#{quoted_table_name}.#{quoted_right_column_name}"
64
+ end
65
+
66
+ def quoted_parent_column_full_name
67
+ "#{quoted_table_name}.#{quoted_parent_column_name}"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -38,51 +38,6 @@ module CollectiveIdea #:nodoc:
38
38
  end
39
39
  result
40
40
  end
41
-
42
- # Returns options for select as nested_set_options, sorted by an specific column
43
- # It requires passing a string with the name of the column to sort the set with
44
- # You can exclude some items from the tree.
45
- # You can pass a block receiving an item and returning the string displayed in the select.
46
- #
47
- # == Params
48
- # * +class_or_item+ - Class name or top level times
49
- # * +:column+ - Column to sort the set (this will sort each children for all root elements)
50
- # * +mover+ - The item that is being move, used to exlude impossible moves
51
- # * +&block+ - a block that will be used to display: { |item| ... item.name }
52
- #
53
- # == Usage
54
- #
55
- # <%= f.select :parent_id, nested_set_options(Category, :sort_by_this_column, @category) {|i|
56
- # "#{'–' * i.level} #{i.name}"
57
- # }) %>
58
- #
59
- def sorted_nested_set_options(class_or_item, order, mover = nil)
60
- if class_or_item.is_a? Array
61
- items = class_or_item.reject { |e| !e.root? }
62
- else
63
- class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
64
- items = Array(class_or_item)
65
- end
66
- result = []
67
- children = []
68
- items.each do |root|
69
- root.class.associate_parents(root.self_and_descendants).map do |i|
70
- if mover.nil? || mover.new_record? || mover.move_possible?(i)
71
- if !i.leaf?
72
- children.sort_by! &order
73
- children.each { |c| result << [yield(c), c.id] }
74
- children = []
75
- result << [yield(i), i.id]
76
- else
77
- children << i
78
- end
79
- end
80
- end.compact
81
- end
82
- children.sort_by! &order
83
- children.each { |c| result << [yield(c), c.id] }
84
- result
85
- end
86
41
  end
87
42
  end
88
43
  end
@@ -0,0 +1,29 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ class Iterator
5
+ attr_reader :objects
6
+
7
+ def initialize(objects)
8
+ @objects = objects
9
+ end
10
+
11
+ def each_with_level
12
+ path = [nil]
13
+ objects.each do |o|
14
+ if o.parent_id != path.last
15
+ # we are on a new level, did we descend or ascend?
16
+ if path.include?(o.parent_id)
17
+ # remove wrong tailing paths elements
18
+ path.pop while path.last != o.parent_id
19
+ else
20
+ path << o.parent_id
21
+ end
22
+ end
23
+ yield(o, path.length - 1)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,212 @@
1
+ require 'awesome_nested_set/model/prunable'
2
+ require 'awesome_nested_set/model/movable'
3
+ require 'awesome_nested_set/model/transactable'
4
+ require 'awesome_nested_set/model/relatable'
5
+ require 'awesome_nested_set/model/rebuildable'
6
+ require 'awesome_nested_set/model/validatable'
7
+ require 'awesome_nested_set/iterator'
8
+
9
+ module CollectiveIdea #:nodoc:
10
+ module Acts #:nodoc:
11
+ module NestedSet #:nodoc:
12
+
13
+ module Model
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ delegate :quoted_table_name, :arel_table, :to => self
18
+ extend Validatable
19
+ extend Rebuildable
20
+ include Movable
21
+ include Prunable
22
+ include Relatable
23
+ include Transactable
24
+ end
25
+
26
+ module ClassMethods
27
+ def associate_parents(objects)
28
+ return objects unless objects.all? {|o| o.respond_to?(:association)}
29
+
30
+ id_indexed = objects.index_by(&:id)
31
+ objects.each do |object|
32
+ association = object.association(:parent)
33
+ parent = id_indexed[object.parent_id]
34
+
35
+ if !association.loaded? && parent
36
+ association.target = parent
37
+ association.set_inverse_instance(parent)
38
+ end
39
+ end
40
+ end
41
+
42
+ def children_of(parent_id)
43
+ where arel_table[parent_column_name].eq(parent_id)
44
+ end
45
+
46
+ # Iterates over tree elements and determines the current level in the tree.
47
+ # Only accepts default ordering, odering by an other column than lft
48
+ # does not work. This method is much more efficent than calling level
49
+ # because it doesn't require any additional database queries.
50
+ #
51
+ # Example:
52
+ # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
53
+ #
54
+ def each_with_level(objects, &block)
55
+ Iterator.new(objects).each_with_level(&block)
56
+ end
57
+
58
+ def leaves
59
+ nested_set_scope.where "#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1"
60
+ end
61
+
62
+ def left_of(node)
63
+ where arel_table[left_column_name].lt(node)
64
+ end
65
+
66
+ def left_of_right_side(node)
67
+ where arel_table[right_column_name].lteq(node)
68
+ end
69
+
70
+ def right_of(node)
71
+ where arel_table[left_column_name].gteq(node)
72
+ end
73
+
74
+ def nested_set_scope(options = {})
75
+ options = {:order => quoted_order_column_name}.merge(options)
76
+
77
+ where(options[:conditions]).order(options.delete(:order))
78
+ end
79
+
80
+ def primary_key_scope(id)
81
+ where arel_table[primary_key].eq(id)
82
+ end
83
+
84
+ def root
85
+ roots.first
86
+ end
87
+
88
+ def roots
89
+ nested_set_scope.children_of nil
90
+ end
91
+ end # end class methods
92
+
93
+ # 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.
94
+ #
95
+ # category.self_and_descendants.count
96
+ # category.ancestors.find(:all, :conditions => "name like '%foo%'")
97
+ # Value of the parent column
98
+ def parent_id(target = self)
99
+ target[parent_column_name]
100
+ end
101
+
102
+ # Value of the left column
103
+ def left(target = self)
104
+ target[left_column_name]
105
+ end
106
+
107
+ # Value of the right column
108
+ def right(target = self)
109
+ target[right_column_name]
110
+ end
111
+
112
+ # Returns true if this is a root node.
113
+ def root?
114
+ parent_id.nil?
115
+ end
116
+
117
+ # Returns true is this is a child node
118
+ def child?
119
+ !root?
120
+ end
121
+
122
+ # Returns true if this is the end of a branch.
123
+ def leaf?
124
+ persisted? && right.to_i - left.to_i == 1
125
+ end
126
+
127
+ # All nested set queries should use this nested_set_scope, which
128
+ # performs finds on the base ActiveRecord class, using the :scope
129
+ # declared in the acts_as_nested_set declaration.
130
+ def nested_set_scope(options = {})
131
+ if (scopes = Array(acts_as_nested_set_options[:scope])).any?
132
+ options[:conditions] = scopes.inject({}) do |conditions,attr|
133
+ conditions.merge attr => self[attr]
134
+ end
135
+ end
136
+
137
+ self.class.nested_set_scope options
138
+ end
139
+
140
+ def to_text
141
+ self_and_descendants.map do |node|
142
+ "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
143
+ end.join("\n")
144
+ end
145
+
146
+ protected
147
+
148
+ def without_self(scope)
149
+ return scope if new_record?
150
+ scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
151
+ end
152
+
153
+ def store_new_parent
154
+ @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
155
+ true # force callback to return true
156
+ end
157
+
158
+ def has_depth_column?
159
+ nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
160
+ end
161
+
162
+ def right_most_node
163
+ @right_most_node ||= self.class.base_class.unscoped.nested_set_scope(
164
+ :order => "#{quoted_right_column_full_name} desc"
165
+ ).first
166
+ end
167
+
168
+ def right_most_bound
169
+ @right_most_bound ||= begin
170
+ return 0 if right_most_node.nil?
171
+
172
+ right_most_node.lock!
173
+ right_most_node[right_column_name] || 0
174
+ end
175
+ end
176
+
177
+ def set_depth!
178
+ return unless has_depth_column?
179
+
180
+ in_tenacious_transaction do
181
+ reload
182
+ nested_set_scope.primary_key_scope(id).
183
+ update_all(["#{quoted_depth_column_name} = ?", level])
184
+ end
185
+ self[depth_column_name] = self.level
186
+ end
187
+
188
+ def set_default_left_and_right
189
+ # adds the new node to the right of all existing nodes
190
+ self[left_column_name] = right_most_bound + 1
191
+ self[right_column_name] = right_most_bound + 2
192
+ end
193
+
194
+ # reload left, right, and parent
195
+ def reload_nested_set
196
+ reload(
197
+ :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
198
+ :lock => true
199
+ )
200
+ end
201
+
202
+ def reload_target(target)
203
+ if target.is_a? self.class.base_class
204
+ target.reload
205
+ else
206
+ nested_set_scope.find(target)
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,137 @@
1
+ require 'awesome_nested_set/move'
2
+
3
+ module CollectiveIdea #:nodoc:
4
+ module Acts #:nodoc:
5
+ module NestedSet #:nodoc:
6
+ module Model
7
+ module Movable
8
+
9
+ def move_possible?(target)
10
+ self != target && # Can't target self
11
+ same_scope?(target) && # can't be in different scopes
12
+ # detect impossible move
13
+ within_bounds?(target.left, target.left) &&
14
+ within_bounds?(target.right, target.right)
15
+ end
16
+
17
+ # Shorthand method for finding the left sibling and moving to the left of it.
18
+ def move_left
19
+ move_to_left_of left_sibling
20
+ end
21
+
22
+ # Shorthand method for finding the right sibling and moving to the right of it.
23
+ def move_right
24
+ move_to_right_of right_sibling
25
+ end
26
+
27
+ # Move the node to the left of another node
28
+ def move_to_left_of(node)
29
+ move_to node, :left
30
+ end
31
+
32
+ # Move the node to the left of another node
33
+ def move_to_right_of(node)
34
+ move_to node, :right
35
+ end
36
+
37
+ # Move the node to the child of another node
38
+ def move_to_child_of(node)
39
+ move_to node, :child
40
+ end
41
+
42
+ # Move the node to the child of another node with specify index
43
+ def move_to_child_with_index(node, index)
44
+ if node.children.empty?
45
+ move_to_child_of(node)
46
+ elsif node.children.count == index
47
+ move_to_right_of(node.children.last)
48
+ else
49
+ move_to_left_of(node.children[index])
50
+ end
51
+ end
52
+
53
+ # Move the node to root nodes
54
+ def move_to_root
55
+ move_to_right_of(root)
56
+ end
57
+
58
+ # Order children in a nested set by an attribute
59
+ # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
60
+ # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
61
+ def move_to_ordered_child_of(parent, order_attribute, ascending = true)
62
+ self.move_to_root and return unless parent
63
+
64
+ left_neighbor = find_left_neighbor(parent, order_attribute, ascending)
65
+ self.move_to_child_of(parent)
66
+
67
+ return unless parent.children.many?
68
+
69
+ if left_neighbor
70
+ self.move_to_right_of(left_neighbor)
71
+ else # Self is the left most node.
72
+ self.move_to_left_of(parent.children[0])
73
+ end
74
+ end
75
+
76
+ # Find the node immediately to the left of this node.
77
+ def find_left_neighbor(parent, order_attribute, ascending)
78
+ left = nil
79
+ parent.children.each do |n|
80
+ if ascending
81
+ left = n if n.send(order_attribute) < self.send(order_attribute)
82
+ else
83
+ left = n if n.send(order_attribute) > self.send(order_attribute)
84
+ end
85
+ end
86
+ left
87
+ end
88
+
89
+ def move_to(target, position)
90
+ prevent_unpersisted_move
91
+
92
+ run_callbacks :move do
93
+ in_tenacious_transaction do
94
+ target = reload_target(target)
95
+ self.reload_nested_set
96
+
97
+ Move.new(target, position, self).move
98
+ end
99
+ after_move_to(target, position)
100
+ end
101
+ end
102
+
103
+ protected
104
+
105
+ def after_move_to(target, position)
106
+ target.reload_nested_set if target
107
+ self.set_depth!
108
+ self.descendants.each(&:save)
109
+ self.reload_nested_set
110
+ end
111
+
112
+ def move_to_new_parent
113
+ if @move_to_new_parent_id.nil?
114
+ move_to_root
115
+ elsif @move_to_new_parent_id
116
+ move_to_child_of(@move_to_new_parent_id)
117
+ end
118
+ end
119
+
120
+ def out_of_bounds?(left_bound, right_bound)
121
+ left <= left_bound && right >= right_bound
122
+ end
123
+
124
+ def prevent_unpersisted_move
125
+ if self.new_record?
126
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node"
127
+ end
128
+ end
129
+
130
+ def within_bounds?(left_bound, right_bound)
131
+ !out_of_bounds?(left_bound, right_bound)
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end