julik-make_like_a_tree 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2009-01-25
2
+
3
+ * 1 major enhancement
4
+
5
+ * duh!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,7 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ init.rb
6
+ lib/make_like_a_tree.rb
7
+ test/test_ordered_tree.rb
data/README.txt ADDED
@@ -0,0 +1,87 @@
1
+ = make_like_a_tree
2
+
3
+ http://github.com/julik/make_like_a_tree
4
+
5
+ == DESCRIPTION:
6
+
7
+ Implement orderable trees in ActiveRecord using the nested set model, with multiple roots and scoping, and most importantly user-defined
8
+ ordering of subtrees. Fetches preordered trees in one go, updates are write-heavy.
9
+
10
+ This is a substantially butchered-up version/offspring of acts_as_threaded. The main additional perk is the ability
11
+ to reorder nodes, which are always fetched ordered. Example:
12
+
13
+ root = Folder.create! :name => "Main folder"
14
+ subfolder_1 = Folder.create! :name => "Subfolder", :parent_id => root.id
15
+ subfolder_2 = Folder.create! :name => "Another subfolder", :parent_id => root.id
16
+
17
+ subfolder_2.move_to_top # just like acts_as_list but nestedly awesome
18
+ root.all_children # => [subfolder_2, subfolder_1]
19
+
20
+ See the rdocs for examples the method names. It also inherits the awesome properties of acts_as_threaded, namely
21
+ materialized depth, root_id and parent_id values on each object which are updated when nodes get moved.
22
+
23
+ Thanks to the authors of acts_as_threaded, awesome_nested_set, better_nested_set and all the others for inspiration.
24
+
25
+
26
+ == FEATURES/PROBLEMS:
27
+
28
+ * Use create with parent_id set to the parent id (obvious, but somehow blocked in awesome_nested_set)
29
+ * Ugly SQL
30
+ * The node counts are currently not updated when a node is removed from a subtree and replanted elsewhere,
31
+ so you cannot rely on (right-left)/2 to get the child count
32
+ * You cannot replant a node by assigning a new parent_id, add_child needed instead
33
+ * The table needs to have proper defaults otherwise undefined behavior can happen. Otherwise demons
34
+ will fly out of your left nostril and make you rewrite the app in inline PHP.
35
+
36
+ == SYNOPSIS:
37
+
38
+ class NodeOfThatUbiquitousCms < ActiveRecord::Base
39
+ make_like_a_tree
40
+ end
41
+
42
+ == REQUIREMENTS:
43
+
44
+ Use the following migration (attention! dangerous defaults ahead!):
45
+
46
+ create_table :nodes do |t|
47
+ # Bookkeeping for threads
48
+ t.integer :root_id, :default => 0, :null => false
49
+ t.integer :parent_id, :default => 0, :null => false
50
+ t.integer :depth, :default => 0, :null => false
51
+ t.integer :lft, :default => 0, :null => false
52
+ t.integer :rgt, :default => 0, :null => false
53
+ end
54
+
55
+ == INSTALL:
56
+
57
+ Add a bare init file to your app and there:
58
+
59
+ require 'make_like_tree'
60
+ Julik::MakeLikeTree.bootstrap!
61
+
62
+ That way you can keep the plugin in gems. Or just vendorize it, it has a built-in init.rb
63
+
64
+ == LICENSE:
65
+
66
+ (The MIT License)
67
+
68
+ Copyright (c) 2009 Julik Tarkhanov <me@julik.nl>
69
+
70
+ Permission is hereby granted, free of charge, to any person obtaining
71
+ a copy of this software and associated documentation files (the
72
+ 'Software'), to deal in the Software without restriction, including
73
+ without limitation the rights to use, copy, modify, merge, publish,
74
+ distribute, sublicense, and/or sell copies of the Software, and to
75
+ permit persons to whom the Software is furnished to do so, subject to
76
+ the following conditions:
77
+
78
+ The above copyright notice and this permission notice shall be
79
+ included in all copies or substantial portions of the Software.
80
+
81
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
82
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
83
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
84
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
85
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
86
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
87
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/make_like_a_tree.rb'
6
+
7
+ # Disable spurious warnings when running tests, ActiveMagic cannot stand -w
8
+ Hoe::RUBY_FLAGS.replace ENV['RUBY_FLAGS'] || "-I#{%w(lib test).join(File::PATH_SEPARATOR)}" +
9
+ (Hoe::RUBY_DEBUG ? " #{RUBY_DEBUG}" : '')
10
+
11
+ Hoe.new('make_like_a_tree', Julik::MakeLikeTree::VERSION) do |p|
12
+ # p.rubyforge_name = 'OrderedTreex' # if different than lowercase project name
13
+ p.developer('Julik', 'me@julik.nl')
14
+ end
15
+
16
+ # vim: syntax=Ruby
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require File.dirname(__FILE__) + '/lib/make_like_a_tree'
2
+ Julik::MakeLikeTree.bootstrap!
@@ -0,0 +1,378 @@
1
+ module Julik
2
+ module MakeLikeTree
3
+ VERSION = '1.0.1'
4
+ def self.included(base) #:nodoc:
5
+ super
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ # Injects the module into ActiveRecord
10
+ def self.bootstrap!
11
+ ::ActiveRecord::Base.send :include, self
12
+ end
13
+
14
+ module ClassMethods
15
+ # An acts_as_threaded on steroids. Configuration options are:
16
+ #
17
+ # * +root_column+ - specifies the column name to use for identifying the root thread, default "root_id"
18
+ # * +parent_column+ - specifies the column name to use for keeping the position integer, default "parent_id"
19
+ # * +left_column+ - column name for left boundary data, default "lft"
20
+ # * +right_column+ - column name for right boundary data, default "rgt"
21
+ # * +depth+ - column name used to track the depth in the branch, default "depth"
22
+ # * +scope+ - adds an additional contraint on the threads when searching or updating
23
+ def make_like_a_tree(options = {})
24
+ configuration = { :root_column => "root_id", :parent_column => "parent_id", :left_column => "lft", :right_column => "rgt", :depth_column => 'depth', :scope => "(1=1)" }
25
+ configuration.update(options) if options.is_a?(Hash)
26
+ configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
27
+
28
+ if configuration[:scope].is_a?(Symbol)
29
+ scope_condition_method = %(
30
+ def scope_condition
31
+ if #{configuration[:scope].to_s}.nil?
32
+ "#{configuration[:scope].to_s} IS NULL"
33
+ else
34
+ "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
35
+ end
36
+ end
37
+ )
38
+ else
39
+ scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
40
+ end
41
+
42
+ after_create :apply_parenting_after_create
43
+
44
+ # before_update :register_parent_id_before_update, :unless => :new_record?
45
+ # after_update :replant_after_update
46
+
47
+ # TODO: refactor for class << self
48
+ class_eval <<-EOV
49
+ include Julik::MakeLikeTree::InstanceMethods
50
+
51
+ #{scope_condition_method}
52
+
53
+ def root_column() "#{configuration[:root_column]}" end
54
+ def parent_column() "#{configuration[:parent_column]}" end
55
+ def left_col_name() "#{configuration[:left_column]}" end
56
+ def right_col_name() "#{configuration[:right_column]}" end
57
+ def depth_column() "#{configuration[:depth_column]}" end
58
+
59
+ EOV
60
+ end
61
+ end
62
+
63
+ module InstanceMethods
64
+
65
+ # Move the item to a specific index within the range of it's siblings. Used to reorder lists.
66
+ # Will cause a cascading update on the neighbouring items and their children, but the update will be scoped
67
+ def move_to(idx)
68
+ transaction do
69
+ # Take a few shortcuts to avoid extra work
70
+ cur_idx = index_in_parent
71
+ return true if (cur_idx == idx)
72
+
73
+ range = siblings_and_self
74
+ return true if range.length == 1
75
+
76
+ cur_idx = range.index(self)
77
+ return true if cur_idx == idx
78
+
79
+ # Register starting and ending elements
80
+ start_left, end_right = range[0][left_col_name], range[-1][right_col_name]
81
+
82
+ old_range = range.dup
83
+
84
+ range.delete_at(cur_idx)
85
+ range.insert(idx, self)
86
+ range.compact! # If we inserted something outside of range and created empty slots
87
+
88
+ # Now remap segements
89
+ left_remaps, right_remaps = [], []
90
+
91
+ # Exhaust the range starting with the last element, determining a remap on the fly
92
+ while range.any?
93
+ e = range.pop
94
+
95
+ w = (e[right_col_name] - e[left_col_name])
96
+
97
+ # Determine by how many we need to shift the adjacent keys to put this item into place
98
+ offset_in_range = range.inject(0) do | sum, item_before |
99
+ sum + item_before[right_col_name] - item_before[left_col_name] + 1
100
+ end
101
+ shift = offset_in_range - e[left_col_name] + 1
102
+
103
+ unless shift.zero? # Optimize - do not move nodes that stay in the same place
104
+ condition_stmt = "#{left_col_name} >= #{e[left_col_name]} AND #{right_col_name} <= #{e[right_col_name]}"
105
+ value_stmt_left = "#{left_col_name} + #{shift}"
106
+ value_stmt_right = "#{right_col_name} + #{shift}"
107
+
108
+ left_remaps.unshift(
109
+ "WHEN (#{condition_stmt}) THEN (#{left_col_name} + #{shift})"
110
+ )
111
+ right_remaps.unshift(
112
+ "WHEN (#{condition_stmt}) THEN (#{right_col_name} + #{shift})"
113
+ )
114
+ end
115
+ end
116
+
117
+ # If we are not a root node, scope the changes to our subtree only - this will win us some less writes
118
+ update_condition = if root?
119
+ scope_condition
120
+ else
121
+ "#{scope_condition} AND #{root_column} = #{self[root_column]}"
122
+ end
123
+
124
+ self.class.update_all(
125
+ "#{left_col_name} = CASE #{left_remaps.join(' ')} ELSE #{left_col_name} END, " +
126
+ "#{right_col_name} = CASE #{right_remaps.join(' ')} ELSE #{right_col_name} END ",
127
+ update_condition
128
+ )
129
+ end
130
+ end
131
+
132
+ # Move the record down in the list (uses move_to)
133
+ def move_up
134
+ move_to(index_in_parent - 1)
135
+ end
136
+
137
+ # Move the record up in the list (uses move_to)
138
+ def move_down
139
+ move_to(index_in_parent + 1)
140
+ end
141
+
142
+ # Move the record to top of the list (uses move_to)
143
+ def move_to_top
144
+ move_to(0)
145
+ end
146
+
147
+ # Move the record to the bottom of the list (uses move_to)
148
+ def move_to_bottom
149
+ move_to(-1)
150
+ end
151
+
152
+ # Get the item index in parent. TODO: when the tree is balanced with no orphan counts, just use (rgt-lft)/2
153
+ def index_in_parent
154
+ # Fetch the item count of items that have the same root_id and the same parent_id and are lower than me on the indices
155
+ @index_in_parent ||= self.class.count_by_sql(
156
+ "SELECT COUNT(id) FROM #{self.class.table_name} WHERE " +
157
+ "#{right_col_name} < #{self[left_col_name]} AND #{parent_column} = #{self[parent_column]}"
158
+ )
159
+ end
160
+
161
+ # Override ActiveRecord::Base#reload to blow over all the memoized values
162
+ def reload(*any_arguments)
163
+ @index_in_parent, @is_root, @is_child, @old_parent_id, @rerooted = nil, nil, nil, nil, nil
164
+ super
165
+ end
166
+
167
+ # Returns true is this is a root thread.
168
+ def root?
169
+ @is_root ||= (self[root_column].to_i.zero? || (self[root_column] == self.id))
170
+ end
171
+
172
+ # Returns true is this is a child node
173
+ def child?
174
+ parent_id = self[parent_column]
175
+ @is_child ||= (!(parent_id.to_i.zero?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name]))
176
+ end
177
+
178
+ # Returns true if we have no idea what this is
179
+ def unknown?
180
+ !root? && !child?
181
+ end
182
+
183
+ # Used as an after_create callback to apply the parent_id assignment or create a root node
184
+ def apply_parenting_after_create
185
+ reload # Reload to bring in the id
186
+ assign_default_left_and_right
187
+ self.save
188
+ unless self[parent_column].to_i.zero?
189
+ # Load the parent
190
+ parent = self.class.find(self[parent_column])
191
+ parent.add_child self
192
+ end
193
+ true
194
+ end
195
+
196
+ # Place the item to the appropriate place as a root item
197
+ def assign_default_left_and_right(with_space_inside = 0)
198
+
199
+ # Instead of creating nodes with 0,0 make this a root node BY DEFAULT even if no children are specified
200
+ self[root_column] = self.id
201
+ last_root_node = self.class.find(:first, :conditions => "#{scope_condition} AND #{parent_column} = 0 AND id != #{self.id}",
202
+ :order => "#{right_col_name} DESC", :limit => 1)
203
+ offset = last_root_node ? last_root_node[right_col_name] : 0
204
+
205
+ self[left_col_name], self[right_col_name] = (offset+1), (offset + with_space_inside + 2)
206
+ end
207
+
208
+ # Shortcut for self[depth_column]
209
+ def level
210
+ self[depth_column]
211
+ end
212
+
213
+ # Adds a child to this object in the tree. If this object hasn't been initialized,
214
+ # it gets set up as a root node. Otherwise, this method will update all of the
215
+ # other elements in the tree and shift them to the right, keeping everything
216
+ # balanced.
217
+ def add_child(child)
218
+ child.reload # Pull in the id
219
+ k = self.class
220
+
221
+ new_left, new_right = determine_range_for_child(child)
222
+
223
+ move_by = new_left - child[left_col_name]
224
+ move_depth_by = (self[depth_column] + 1) - child[depth_column]
225
+
226
+ child_occupies = (new_right - new_left) + 1
227
+
228
+ transaction do
229
+ # bring the child and its grandchildren over
230
+ self.class.update_all(
231
+ "#{depth_column} = #{depth_column} + #{move_depth_by}," +
232
+ "#{root_column} = #{self[root_column]}," +
233
+ "#{left_col_name} = #{left_col_name} + #{move_by}," +
234
+ "#{right_col_name} = #{right_col_name} + #{move_by}",
235
+ "#{scope_condition} AND #{left_col_name} >= #{child[left_col_name]} AND #{right_col_name} <= #{child[right_col_name]}" +
236
+ " AND #{root_column} = #{child[root_column]} AND #{root_column} != 0"
237
+ )
238
+
239
+ # update parent_id on child ONLY
240
+ self.class.update_all(
241
+ "#{parent_column} = #{self.id}",
242
+ "id = #{child.id}"
243
+ )
244
+
245
+ # update myself and upstream to notify we are wider
246
+ self.class.update_all(
247
+ "#{right_col_name} = #{right_col_name} + #{child_occupies}",
248
+ "#{scope_condition} AND #{root_column} = #{self[root_column]} AND (#{depth_column} < #{self[depth_column]} OR id = #{self.id})"
249
+ )
250
+
251
+ # update items to my right AND downstream of them to notify them we are wider. Will shift root items to the right
252
+ self.class.update_all(
253
+ "#{left_col_name} = #{left_col_name} + #{child_occupies}, " +
254
+ "#{right_col_name} = #{right_col_name} + #{child_occupies}",
255
+ "#{depth_column} >= #{self[depth_column]} " +
256
+ "AND #{left_col_name} > #{self[right_col_name]}"
257
+ )
258
+ end
259
+ [self, child].map{|e| e.reload }
260
+ true
261
+ end
262
+
263
+ # Determine lft and rgt for a child item, taking into account the number of child and grandchild nodes it has.
264
+ # Normally you would not use this directly
265
+ def determine_range_for_child(child)
266
+ new_left = begin
267
+ right_bound_child = self.class.find(:first,
268
+ :conditions => "#{scope_condition} AND #{parent_column} = #{self.id} AND id != #{child.id}", :order => "#{right_col_name} DESC")
269
+ right_bound_child ? (right_bound_child[right_col_name] + 1) : (self[left_col_name] + 1)
270
+ end
271
+ new_right = new_left + (child[right_col_name] - child[left_col_name])
272
+ [new_left, new_right]
273
+ end
274
+
275
+ # Returns the number of children and grandchildren of this object
276
+ def child_count
277
+ return 0 unless might_have_children? # optimization shortcut
278
+ self.class.count_by_sql("SELECT COUNT(id) FROM #{self.class.table_name} WHERE #{conditions_for_all_children}")
279
+ end
280
+ alias_method :children_count, :child_count
281
+
282
+ # Shortcut to determine if our left and right values allow for possible children
283
+ def might_have_children?
284
+ (self[right_col_name] - self[left_col_name]) > 1
285
+ end
286
+
287
+ # Returns a set of itself and all of its nested children
288
+ def full_set
289
+ [self] + all_children
290
+ end
291
+ alias_method :all_children_and_self, :full_set
292
+
293
+ # Returns a set of all of its children and nested children
294
+ def all_children
295
+ return [] unless might_have_children? # optimization shortcut
296
+ self.class.find(:all, :conditions => conditions_for_all_children, :order => "#{left_col_name} ASC")
297
+ end
298
+
299
+ # Get conditions for direct and indirect children of this record
300
+ def conditions_for_all_children
301
+ "#{scope_condition} AND #{root_column} = #{self[root_column]} AND " +
302
+ "#{depth_column} > #{self[depth_column]} AND " +
303
+ "#{left_col_name} > #{self[left_col_name]} AND #{right_col_name} < #{self[right_col_name]}"
304
+ end
305
+
306
+ # Get conditions to find myself and my siblings
307
+ def conditions_for_self_and_siblings
308
+ "#{scope_condition} AND #{parent_column} = #{self[parent_column]}"
309
+ end
310
+
311
+ # Get immediate siblings, ordered
312
+ def siblings
313
+ self.class.find(:all, :conditions => "#{conditions_for_self_and_siblings} AND id != #{self.id}", :order => "#{left_col_name} ASC")
314
+ end
315
+
316
+ # Get myself and siblings, ordered
317
+ def siblings_and_self
318
+ self.class.find(:all, :conditions => conditions_for_self_and_siblings, :order => "#{left_col_name} ASC")
319
+ end
320
+
321
+ # Returns a set of only this entry's immediate children, also ordered by position
322
+ def direct_children
323
+ return [] unless might_have_children? # optimize!
324
+ self.class.find(:all,
325
+ :conditions => "#{scope_condition} AND #{parent_column} = #{self.id}",
326
+ :order => 'lft ASC'
327
+ )
328
+ end
329
+
330
+ # Make this item a root node
331
+ def promote_to_root
332
+ transaction do
333
+ my_width = child_count * 2
334
+
335
+ # Stash the values because assign_default_left_and_right reassigns them
336
+ old_left, old_right, old_root = self[left_col_name], self[right_col_name], self[root_column]
337
+ self[parent_column] = 0 # Signal the root node
338
+
339
+ assign_default_left_and_right(my_width)
340
+
341
+ move_by = self[left_col_name] - old_left
342
+ move_depth_by = self[depth_column]
343
+
344
+ # bring the child and its grandchildren over
345
+ self.class.update_all(
346
+ "#{depth_column} = #{depth_column} - #{move_depth_by}," +
347
+ "#{root_column} = #{self.id}," +
348
+ "#{left_col_name} = #{left_col_name} + #{move_by}," +
349
+ "#{right_col_name} = #{right_col_name} + #{move_by}",
350
+ "#{scope_condition} AND #{left_col_name} >= #{old_left} AND #{right_col_name} <= #{old_right}" +
351
+ " AND #{root_column} = #{old_root}"
352
+ )
353
+ self.reload
354
+ end
355
+ true
356
+ end
357
+
358
+ def register_parent_id_before_update
359
+ @old_parent_id = self.class.connection.select_value("SELECT #{parent_column} FROM #{self.class.table_name} WHERE id = #{self.id}")
360
+ true
361
+ end
362
+
363
+ def replant_after_update
364
+ if @old_parent_id.nil? || (@old_parent_id == self[parent_column])
365
+ return true
366
+ # If the new parent_id is nil, it means we are promoted to woot node
367
+ elsif self[parent_column].nil? || self[parent_column].zero?
368
+ promote_to_root
369
+ else
370
+ self.class.find(self[parent_column]).add_child(self)
371
+ end
372
+
373
+ true
374
+ end
375
+
376
+ end #InstanceMethods
377
+ end
378
+ end
@@ -0,0 +1,350 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'active_support'
4
+ require 'test/spec'
5
+
6
+ require File.dirname(__FILE__) + '/../init'
7
+
8
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :dbfile => ':memory:')
9
+ ActiveRecord::Migration.verbose = false
10
+ ActiveRecord::Schema.define do
11
+ create_table :nodes, :force => true do |t|
12
+
13
+ t.string :name, :null => false
14
+ t.integer :project_id
15
+
16
+ # Bookkeeping for threads
17
+ t.integer :root_id, :default => 0, :null => false
18
+ t.integer :parent_id, :default => 0, :null => false
19
+ t.integer :depth, :maxlength => 5, :default => 0, :null => false
20
+ t.integer :lft, :default => 0, :null => false
21
+ t.integer :rgt, :default => 0, :null => false
22
+ end
23
+
24
+ # Anonimous tables for anonimous classes
25
+ (1..20).each do | i |
26
+ create_table "an#{i}s", :force => true do # anis!
27
+ end
28
+ end
29
+ end
30
+
31
+ context "A Node used with OrderedTree should" do
32
+
33
+ class Node < ActiveRecord::Base
34
+ set_table_name "nodes"
35
+ make_like_a_tree :scope => :project
36
+ def _lr
37
+ [lft, rgt]
38
+ end
39
+ end
40
+
41
+ before do
42
+ Node.delete_all
43
+ end
44
+
45
+ specify "support full_set" do
46
+ folder1, folder2 = emit(:name => "One"), emit(:name => "Two")
47
+ three = emit(:name => "subfolder", :parent_id => folder1.id)
48
+
49
+ folder1.all_children_and_self.should.equal folder1.full_set
50
+ end
51
+
52
+ specify "return a proper scope condition" do
53
+ Node.new(:project_id => 1).scope_condition.should.equal "project_id = 1"
54
+ Node.new(:project_id => nil).scope_condition.should.equal "project_id IS NULL"
55
+ end
56
+
57
+ specify "return a bypass scope condition with no scope" do
58
+ class An2 < ActiveRecord::Base
59
+ make_like_a_tree
60
+ end
61
+ An2.new.scope_condition.should.equal "(1=1)"
62
+ end
63
+
64
+ specify "return a proper left and right column if they have been customized" do
65
+ class An1 < ActiveRecord::Base
66
+ make_like_a_tree :left_column => :foo, :right_column => :bar
67
+ end
68
+ An1.new.left_col_name.should.equal "foo"
69
+ An1.new.right_col_name.should.equal "bar"
70
+ end
71
+
72
+ specify "return a proper depth column if it has been customized" do
73
+ class An3 < ActiveRecord::Base
74
+ make_like_a_tree :depth_column => :niveau
75
+ end
76
+ An3.new.depth_column.should.equal "niveau"
77
+ end
78
+
79
+ specify "create root nodes with ordered left and right" do
80
+ groups = (0...2).map do | idx |
81
+ emit :name => "Group_#{idx}"
82
+ end
83
+ reload(groups)
84
+
85
+ groups[0]._lr.should.equal [1, 2]
86
+ groups[1]._lr.should.equal [3,4]
87
+ end
88
+
89
+ specify "create a good child node" do
90
+
91
+ root_node = emit :name => "Mother"
92
+ child_node = emit :name => "Daughter", :parent_id => root_node.id
93
+
94
+ reload(root_node, child_node)
95
+
96
+ root_node._lr.should.blaming("root node with one subset is 1,4").equal [1, 4]
97
+ child_node._lr.should.blaming("first in nested range is 2,3").equal [2, 3]
98
+ end
99
+
100
+ specify "create a number of good child nodes" do
101
+
102
+ root_node = emit :name => "Mother"
103
+ child_nodes = ["Daughter", "Brother"].map { |n| emit :name => n, :parent_id => root_node.id }
104
+
105
+ reload(root_node, child_nodes)
106
+
107
+ root_node._lr.should.blaming("extended range").equal [1, 6]
108
+ child_nodes[0]._lr.should.blaming("first in sequence is 2,3").equal [2, 3]
109
+ child_nodes[1]._lr.should.blaming("second in sequence is 4,5").equal [4, 5]
110
+
111
+ child_nodes.each do | cn |
112
+ cn.depth.should.blaming("depth increase").equal 1
113
+ cn.root_id.should.blaming("proper root assignment").equal root_node.id
114
+ cn.parent_id.should.blaming("parent assignment").equal root_node.id
115
+ end
116
+ end
117
+
118
+ specify "properly shift siblings to the right on child assignment to their left neighbour" do
119
+ root_node = emit :name => "Root one"
120
+
121
+ sub_node = emit :name => "Child 1", :parent_id => root_node.id
122
+ sub_node_sibling = emit :name => "Child 2", :parent_id => root_node.id
123
+
124
+ reload(sub_node_sibling)
125
+ sub_node_sibling._lr.should.equal [4,5]
126
+
127
+ # Now inject a child into sub_node
128
+ grandchild = emit :name => "Grandchild via Child 1", :parent_id => sub_node.id
129
+
130
+ reload(sub_node_sibling)
131
+ sub_node_sibling._lr.should.blaming("shifted right because a child was injected to the left of us").equal [6,7]
132
+
133
+ reload(root_node)
134
+ root_node._lr.should.blaming("increased range for the grandchild").equal [1,8]
135
+ end
136
+
137
+ specify "make nodes their own roots" do
138
+ a, b = %w(a b).map{|n| emit :name => n }
139
+ a.root_id.should.equal a.id
140
+ b.root_id.should.equal b.id
141
+ end
142
+
143
+ specify "properly replant a branch" do
144
+ root_node_1 = emit :name => "First root"
145
+ root_node_2 = emit :name => "Second root"
146
+ root_node_3 = emit :name => "Third root"
147
+
148
+ # Now make a subtree on the third root node
149
+ child = emit :name => "Child", :parent_id => root_node_3.id
150
+ grand_child = emit :name => "Grand child", :parent_id => child.id
151
+ grand_grand_child = emit :name => "Grand grand child", :parent_id => grand_child.id
152
+
153
+ reload(root_node_1, root_node_2, root_node_3, child, grand_child, grand_grand_child)
154
+
155
+ child._lr.should.blaming("the complete branch indices").equal [6,11]
156
+ root_node_3._lr.should.blaming("inclusive for the child branch").equal [5, 12]
157
+
158
+ root_node_1.add_child(child)
159
+
160
+ reload(root_node_1, root_node_2)
161
+
162
+ root_node_1._lr.should.blaming("branch containment expanded the range").equal [1, 8]
163
+ root_node_2._lr.should.blaming("shifted right to make room").equal [9, 10]
164
+ end
165
+
166
+ specify "properly report size after moving a branch from underneath" do
167
+ root_node_1 = emit :name => "First root"
168
+ root_node_2 = emit :name => "First root"
169
+
170
+ child = emit :name => "Some child", :parent_id => root_node_2.id
171
+
172
+ root_node_2.reload
173
+
174
+ root_node_2.might_have_children?.should.blaming("might_have_children? is true - our indices are #{root_node_2._lr.inspect}").equal true
175
+ root_node_2.child_count.should.blaming("only one child available").equal 1
176
+
177
+ # Now replant the child
178
+ root_node_1.add_child(child)
179
+
180
+ root_node_2.child_count.should.blaming("all children removed").be.zero
181
+ root_node_1.child_count.should.blaming("now has one child").equal 1
182
+ end
183
+
184
+ specify "properly return siblings" do
185
+ root_1 = emit :name => "Foo"
186
+ root_2 = emit :name => "Bar"
187
+
188
+ reload(root_1, root_2)
189
+
190
+ root_1.siblings.should.equal [root_2]
191
+ root_2.siblings.should.equal [root_1]
192
+ end
193
+
194
+ specify "properly return siblings and self" do
195
+ root_1 = emit :name => "Foo"
196
+ root_2 = emit :name => "Bar"
197
+
198
+ reload(root_1, root_2)
199
+
200
+ root_1.siblings_and_self.should.equal [root_1, root_2]
201
+ root_2.siblings_and_self.should.equal [root_1, root_2]
202
+ end
203
+
204
+ specify "provide index_in_parent" do
205
+ root_nodes = (0...3).map do | i |
206
+ emit :name => "Root_#{i}"
207
+ end
208
+
209
+ root_nodes.each_with_index do | rn, i |
210
+ rn.should.respond_to :index_in_parent
211
+ rn.index_in_parent.should.blaming("is at index #{i}").equal i
212
+ end
213
+ end
214
+
215
+ specify 'do nothing on move when only item in the list' do
216
+ a = emit :name => "Boo"
217
+ a.move_to(0).should.equal true
218
+ a.move_to(200).should.equal true
219
+ end
220
+
221
+ specify "do nothing if we move from the same position to the same position" do
222
+ a = emit :name => "Foo"
223
+ b = emit :name => "Boo"
224
+
225
+ a.move_to(0).should.equal true
226
+ b.move_to(1).should.equal true
227
+ end
228
+
229
+ specify "move a root node up" do
230
+ root_1 = emit :name => "First root"
231
+ root_2 = emit :name => "Second root"
232
+ root_2.move_to(0)
233
+
234
+ reload(root_1, root_2)
235
+
236
+ root_1._lr.should.equal [3, 4]
237
+ root_2._lr.should.equal [1, 2]
238
+ end
239
+
240
+ specify "reorder including subtrees" do
241
+ root_1 = emit :name => "First root"
242
+ root_2 = emit :name => "Second root with children"
243
+ 4.times{ emit :name => "Child of root2", :parent_id => root_2.id }
244
+
245
+ reload(root_2)
246
+ root_2._lr.should.equal [3, 12]
247
+
248
+ root_2.move_to(0)
249
+ reload(root_2)
250
+
251
+ root_2._lr.should.blaming("Shifted range").equal [1, 10]
252
+ root_2.children_count.should.blaming("the same children count").equal 4
253
+
254
+ reload(root_1)
255
+ root_1._lr.should.blaming("Shifted down").equal [11, 12]
256
+ root_1.children_count.should.blaming("the same children count").be.zero
257
+ end
258
+
259
+ specify "support move_up" do
260
+ root_1, root_2 = emit(:name => "First"), emit(:name => "Second")
261
+ root_2.should.respond_to :move_up
262
+
263
+ root_2.move_up
264
+
265
+ reload(root_1, root_2)
266
+ root_2._lr.should.equal [1,2]
267
+ end
268
+
269
+ specify "support move_down" do
270
+ root_1, root_2 = emit(:name => "First"), emit(:name => "Second")
271
+
272
+ root_1.should.respond_to :move_down
273
+ root_1.move_up
274
+
275
+ reload(root_1, root_2)
276
+ root_2._lr.should.equal [1,2]
277
+ root_1._lr.should.equal [3,4]
278
+ end
279
+
280
+ specify "support move_to_top" do
281
+ root_1, root_2, root_3 = emit(:name => "First"), emit(:name => "Second"), emit(:name => "Third")
282
+
283
+ root_3.should.respond_to :move_to_top
284
+ root_3.move_to_top
285
+ reload(root_1, root_2, root_3)
286
+
287
+ root_3._lr.should.blaming("is now on top").equal [1,2]
288
+ root_1._lr.should.blaming("is now second").equal [3,4]
289
+ root_2._lr.should.blaming("is now third").equal [5,6]
290
+ end
291
+
292
+ specify "support move_to_bottom" do
293
+ root_1, root_2, root_3, root_4 = (1..4).map{|e| emit :name => "Root_#{e}"}
294
+ root_1.should.respond_to :move_to_bottom
295
+
296
+ root_1.move_to_bottom
297
+ reload(root_1, root_2, root_3, root_4)
298
+
299
+ root_2._lr.should.blaming("is now on top").equal [1,2]
300
+ root_1._lr.should.blaming("is now on the bottom").equal [7,8]
301
+ end
302
+
303
+ specify "support move_to_top for the second item of three" do
304
+ a, b, c = emit_many(3)
305
+ b.move_to_top
306
+ reload(a, b, c)
307
+
308
+ a._lr.should.equal [3, 4]
309
+ b._lr.should.equal [1, 2]
310
+ c._lr.should.equal [5, 6]
311
+ end
312
+
313
+ #specify "support promote_to_root" do
314
+ def test_promote_to_root
315
+ a, b = emit_many(2)
316
+ c = emit(:name => "Subtree", :parent_id => a.id)
317
+
318
+ reload(a, b, c)
319
+ $l = true
320
+ c.promote_to_root
321
+
322
+ reload(a, b, c)
323
+
324
+ c.depth.should.blaming("is at top level").equal 0
325
+ c.root_id.should.blaming("is now self-root").equal c.id
326
+ c._lr.should.blaming("now promoted to root").equal [7, 8]
327
+ end
328
+
329
+ specify "support replanting by changing parent_id" do
330
+ a, b = emit_many(2)
331
+ sub = emit :name => "Child", :parent_id => a.id
332
+ sub.update_attributes(:parent_id => b.id)
333
+
334
+ reload(a, b, sub)
335
+ a.all_children.should.blaming("replanted branch from there").not.include( sub)
336
+ b.all_children.should.blaming("replanted branch here").include( sub)
337
+ end
338
+
339
+ def emit(attributes = {})
340
+ Node.create!({:project_id => 1}.merge(attributes))
341
+ end
342
+
343
+ def emit_many(how_many, extras = {})
344
+ (1..how_many).map{|i| emit({:name => "Item_#{i}"}.merge(extras)) }
345
+ end
346
+
347
+ def reload(*all)
348
+ all.flatten.map(&:reload)
349
+ end
350
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: julik-make_like_a_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Julik
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-25 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.8.2
23
+ version:
24
+ description: "Implement orderable trees in ActiveRecord using the nested set model, with multiple roots and scoping, and most importantly user-defined ordering of subtrees. Fetches preordered trees in one go, updates are write-heavy. This is a substantially butchered-up version/offspring of acts_as_threaded. The main additional perk is the ability to reorder nodes, which are always fetched ordered. Example: root = Folder.create! :name => \"Main folder\" subfolder_1 = Folder.create! :name => \"Subfolder\", :parent_id => root.id subfolder_2 = Folder.create! :name => \"Another subfolder\", :parent_id => root.id subfolder_2.move_to_top # just like acts_as_list but nestedly awesome root.all_children # => [subfolder_2, subfolder_1] See the rdocs for examples the method names. It also inherits the awesome properties of acts_as_threaded, namely materialized depth, root_id and parent_id values on each object which are updated when nodes get moved. Thanks to the authors of acts_as_threaded, awesome_nested_set, better_nested_set and all the others for inspiration."
25
+ email:
26
+ - me@julik.nl
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - History.txt
33
+ - Manifest.txt
34
+ - README.txt
35
+ files:
36
+ - History.txt
37
+ - Manifest.txt
38
+ - README.txt
39
+ - Rakefile
40
+ - init.rb
41
+ - lib/make_like_a_tree.rb
42
+ - test/test_ordered_tree.rb
43
+ has_rdoc: true
44
+ homepage: http://github.com/julik/make_like_a_tree
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --main
48
+ - README.txt
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project: make_like_a_tree
66
+ rubygems_version: 1.2.0
67
+ signing_key:
68
+ specification_version: 2
69
+ summary: Implement orderable trees in ActiveRecord using the nested set model, with multiple roots and scoping, and most importantly user-defined ordering of subtrees
70
+ test_files:
71
+ - test/test_ordered_tree.rb