awesome_nested_set 2.1.6 → 3.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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