loyal_awesome_nested_set 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.
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+ module CollectiveIdea #:nodoc:
3
+ module Acts #:nodoc:
4
+ module NestedSet #:nodoc:
5
+ # This module provides some helpers for the model classes using acts_as_nested_set.
6
+ # It is included by default in all views.
7
+ #
8
+ module Helper
9
+ # Returns options for select.
10
+ # You can exclude some items from the tree.
11
+ # You can pass a block receiving an item and returning the string displayed in the select.
12
+ #
13
+ # == Params
14
+ # * +class_or_item+ - Class name or top level times
15
+ # * +mover+ - The item that is being move, used to exlude impossible moves
16
+ # * +&block+ - a block that will be used to display: { |item| ... item.name }
17
+ #
18
+ # == Usage
19
+ #
20
+ # <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
21
+ # "#{'–' * i.level} #{i.name}"
22
+ # }) %>
23
+ #
24
+ def nested_set_options(class_or_item, mover = nil)
25
+ if class_or_item.is_a? Array
26
+ items = class_or_item.reject { |e| !e.root? }
27
+ else
28
+ class_or_item = class_or_item.roots if class_or_item.respond_to?(:scoped)
29
+ items = Array(class_or_item)
30
+ end
31
+ result = []
32
+ items.each do |root|
33
+ result += root.class.associate_parents(root.self_and_descendants).map do |i|
34
+ if mover.nil? || mover.new_record? || mover.move_possible?(i)
35
+ [yield(i), i.id]
36
+ end
37
+ end.compact
38
+ end
39
+ result
40
+ end
41
+ end
42
+ end
43
+ end
44
+ 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
@@ -0,0 +1,58 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ module Model
5
+ module Prunable
6
+
7
+ # Prunes a branch off of the tree, shifting all of the elements on the right
8
+ # back to the left so the counts still work.
9
+ def destroy_descendants
10
+ return if right.nil? || left.nil? || skip_before_destroy
11
+
12
+ in_tenacious_transaction do
13
+ reload_nested_set
14
+ # select the rows in the model that extend past the deletion point and apply a lock
15
+ nested_set_scope.right_of(left).select(id).lock(true)
16
+
17
+ destroy_or_delete_descendants
18
+
19
+ # update lefts and rights for remaining nodes
20
+ update_siblings_for_remaining_nodes
21
+
22
+ # Don't allow multiple calls to destroy to corrupt the set
23
+ self.skip_before_destroy = true
24
+ end
25
+ end
26
+
27
+ def destroy_or_delete_descendants
28
+ if acts_as_nested_set_options[:dependent] == :destroy
29
+ descendants.each do |model|
30
+ model.skip_before_destroy = true
31
+ model.destroy
32
+ end
33
+ else
34
+ descendants.delete_all
35
+ end
36
+ end
37
+
38
+ def update_siblings_for_remaining_nodes
39
+ update_siblings(:left)
40
+ update_siblings(:right)
41
+ end
42
+
43
+ def update_siblings(direction)
44
+ full_column_name = send("quoted_#{direction}_column_full_name")
45
+ column_name = send("quoted_#{direction}_column_name")
46
+
47
+ nested_set_scope.where(["#{full_column_name} > ?", right]).
48
+ update_all(["#{column_name} = (#{column_name} - ?)", diff])
49
+ end
50
+
51
+ def diff
52
+ right - left + 1
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end