julik-make_like_a_tree 1.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.
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