better_nested_set 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "bundler", "~> 1.0.0"
10
+ gem "jeweler", "~> 1.5.2"
11
+ gem "rcov", ">= 0"
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,18 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ git (1.2.5)
5
+ jeweler (1.5.2)
6
+ bundler (~> 1.0.0)
7
+ git (>= 1.2.5)
8
+ rake
9
+ rake (0.8.7)
10
+ rcov (0.9.9)
11
+
12
+ PLATFORMS
13
+ ruby
14
+
15
+ DEPENDENCIES
16
+ bundler (~> 1.0.0)
17
+ jeweler (~> 1.5.2)
18
+ rcov
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2006 Jean-Christophe Michel, Symétrie
2
+
3
+ The MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE
data/README.rdoc ADDED
@@ -0,0 +1,224 @@
1
+ = Better nested set
2
+
3
+ This plugin provides an enhanced acts_as_nested_set mixin for ActiveRecord, the
4
+ object-relational mapping layer of the framework Ruby on Rails. The original
5
+ nested set in Rails lacks many important features, such as moving branches within a tree.
6
+
7
+ = Installation
8
+
9
+ script/plugin install svn://rubyforge.org/var/svn/betternestedset/trunk
10
+
11
+ == Details
12
+
13
+ A nested set is a smart way to implement an _ordered_ tree that allows for
14
+ fast, non-recursive queries. For example, you can fetch all descendants of
15
+ a node in a single query, no matter how deep the tree. The drawback is that
16
+ insertions/moves/deletes require complex SQL, but that is handled behind
17
+ the curtains by this plugin!
18
+
19
+ Nested sets are appropriate for ordered trees
20
+ (e.g. menus, commercial categories) and big trees that must be queried
21
+ efficiently (e.g. threaded posts).
22
+
23
+ See http://www.dbmsmag.com/9603d06.html for nested sets theory, and a tutorial here:
24
+ http://threebit.net/tutorials/nestedset/tutorial1.html
25
+
26
+ == Small nested set theory reminder
27
+
28
+ An easy way to visualize how a nested set works is to think of a parent entity surrounding all
29
+ of its children, and its parent surrounding it, etc. So this tree:
30
+ root
31
+ |_ Child 1
32
+ |_ Child 1.1
33
+ |_ Child 1.2
34
+ |_ Child 2
35
+ |_ Child 2.1
36
+ |_ Child 2.2
37
+
38
+ Could be visualized like this:
39
+ ___________________________________________________________________
40
+ | Root |
41
+ | ____________________________ ____________________________ |
42
+ | | Child 1 | | Child 2 | |
43
+ | | __________ _________ | | __________ _________ | |
44
+ | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | |
45
+ 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14
46
+ | |___________________________| |___________________________| |
47
+ |___________________________________________________________________|
48
+
49
+ The numbers represent the left and right boundaries. The table then might
50
+ look like this:
51
+ id | parent_id | lft | rgt | data
52
+ 1 | | 1 | 14 | root
53
+ 2 | 1 | 2 | 7 | Child 1
54
+ 3 | 2 | 3 | 4 | Child 1.1
55
+ 4 | 2 | 5 | 6 | Child 1.2
56
+ 5 | 1 | 8 | 13 | Child 2
57
+ 6 | 5 | 9 | 10 | Child 2.1
58
+ 7 | 5 | 11 | 12 | Child 2.2
59
+
60
+ To get all children of an entry +parent+, you
61
+ SELECT * WHERE lft IS BETWEEN parent.lft AND parent.rgt
62
+
63
+ To get the number of children, it's
64
+ (right - left - 1)/2
65
+
66
+ To get a node and all its ancestors going back to the root, you
67
+ SELECT * WHERE node.lft IS BETWEEN lft AND rgt
68
+
69
+ As you can see, queries that would be recursive and prohibitively slow on ordinary trees are suddenly quite fast. Nifty, isn't it? There are instance methods for each of the above, plus many others.
70
+
71
+
72
+ = API
73
+ Method names are mostly the same as in acts_as_tree, to make replacment from one
74
+ by another easier, except for object creation:
75
+
76
+ in acts_as_tree:
77
+
78
+ my_item.children.create(:name => "child1")
79
+
80
+ in acts_as_nested_set:
81
+
82
+ # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right) + 1
83
+ child = MyClass.create(:name => "child1")
84
+ # now move the item to its desired location
85
+ child.move_to_child_of my_item
86
+
87
+ You can use:
88
+ * <tt>move_to_child_of</tt>
89
+ * <tt>move_to_right_of</tt>
90
+ * <tt>move_to_left_of</tt>
91
+ and pass them an id or an object.
92
+
93
+ Other instance methods added by this plugin include:
94
+ * <tt>root</tt> - root item of the tree (the one that has a nil parent)
95
+ * <tt>roots</tt> - root items, in case of multiple roots (the ones that have a nil parent)
96
+ * <tt>level</tt> - number indicating the level, a root being level 0
97
+ * <tt>ancestors</tt> - array of all parents, with root as first item
98
+ * <tt>self_and_ancestors</tt> - array of all parents and self
99
+ * <tt>siblings</tt> - array of all siblings (items sharing the same parent)
100
+ * <tt>self_and_siblings</tt> - array of itself and all siblings
101
+ * <tt>children_count</tt> - count of all direct children
102
+ * <tt>children</tt> - array of all immediate children
103
+ * <tt>all_children</tt> - array of all children and nested children
104
+ * <tt>all_children_count</tt> - count of all nested children
105
+ * <tt>full_set</tt> - array of itself and all children and nested children
106
+ * <tt>leaves</tt> - array of the children of this node who do not have children
107
+ * <tt>leaves_count</tt> - the number of leaves
108
+ * <tt>check_subtree</tt> - check the left/right indexes of this node and all descendants
109
+ * <tt>check_full_tree</tt> - check the whole tree this node belongs to
110
+ * <tt>renumber_full_tree</tt> - recreate the left/right indexes for the whole tree
111
+
112
+ These should not be of interest, unless you want to write schema-independent SQL:
113
+ * <tt>left_col_name</tt> - name of the left column passed on the declaration line
114
+ * <tt>right_col_name</tt> - name of the right column passed on the declaration line
115
+ * <tt>parent_col_name</tt> - name of the parent column passed on the declaration line
116
+
117
+ Please see the generated RDoc files in doc/ for the full API (run 'rake rdoc' if they need to be created).
118
+
119
+ == Concurrency and callbacks
120
+
121
+ ActiveRecord does not yet provide a way to treat columns as read-only, which causes problems for
122
+ nested sets and other things (http://dev.rubyonrails.org/ticket/6896). As a workaround, we have overridden
123
+ ActiveRecord::Base#update to prevent it from writing to the left/right columns. This protects the left/right
124
+ values from corruption under concurrent usage, but it breaks the update-related callbacks (before_update and friends).
125
+ If you need the callbacks and aren't worried about concurrency, you can comment out the update method and the two
126
+ methods below it (all at the very bottom of better_nested_set.rb).
127
+
128
+ If this situation bugs you as much as it does us, leave a comment on the above ticket asking the core team to
129
+ please apply the patch soon.
130
+
131
+
132
+ == Scopes and roots
133
+
134
+ Scope separates trees from each other, and roots are nodes without a parent. The complication is that a tree can
135
+ have multiple ("virtual") roots.
136
+
137
+ Virtual roots?! In some situations, such as a menu, the root of the tree is ignored, and becomes a nuisance to the programmer.
138
+ In that case it makes sense to remove the root, turning each of its children into a 'virtual root'. These virtual roots
139
+ are still members of the same tree, sharing a single continuous left/right index.
140
+
141
+ Here's an example that demonstrates scopes, roots and virtual roots:
142
+ class Set < ActiveRecord::Base
143
+ acts_as_nested_set :scope => :tree_id
144
+ end
145
+
146
+ # This will create two trees, each with a single (real) root.
147
+ a = Set.create(:tree_id => 1)
148
+ b = Set.create(:tree_id => 2)
149
+
150
+ # This will add a second root to tree #2, so it will have two (virtual) roots.
151
+ # New objects are by default created as virtual roots at the right side of the tree.
152
+ c = Set.create(:tree_id => 2) # c.lft is 3, c.rgt is 4 -- the lft/rgt values are contiguous between the two roots
153
+
154
+ # When we move c to be a child of b, tree #2 will have a single (real) root again.
155
+ c.move_to_child_of(b)
156
+
157
+ # The table would now look like this:
158
+ id | parent_id | tree_id | lft | rgt | data
159
+ 1 | NULL | 1 | 1 | 2 | a
160
+ 2 | NULL | 2 | 1 | 4 | b
161
+ 3 | 2 | 2 | 2 | 3 | c
162
+
163
+ == Recommendations
164
+
165
+ Don't name your left and right columns 'left' and 'right', since most databases reserve these words.
166
+ Use something like 'lft' and 'rgt' instead.
167
+
168
+ If you have a choice between multiple separate trees or one large tree with multiple roots, separate trees will
169
+ offer better performance when altering tree structure (inserts/moves/deletes).
170
+
171
+ = Where to find better_nested_set
172
+
173
+ This plugin is provided by Jean-Christophe Michel from Symétrie, and the home page is:
174
+
175
+ http://opensource.symetrie.com/trac/better_nested_set/
176
+
177
+ = What databases?
178
+
179
+ The code has so far been tested on MySQL 5, SQLite3 and PostgreSQL 8, but is thought to work on others.
180
+ Databases featuring transactions will help protect the left/right indexes from corruption during concurrent usage.
181
+
182
+ = Compatibility
183
+
184
+ Future versions of this code will break compatibility with the original acts_as_nested_set, but this version
185
+ is intended to be (almost completely) compatible. Differences include:
186
+ * New records automatically have their left/right values set to place them at the far right of the tree.
187
+ * Very minor changes to the deprecated method #root?.
188
+
189
+
190
+ = Running the unit tests
191
+
192
+ 1) Set up a test database as specified in database.yml. Example for MySQL:
193
+
194
+ create database acts_as_nested_set_plugin_test;
195
+ grant all on acts_as_nested_set_plugin_test.* to 'rails'@'localhost' identified by '';
196
+
197
+ 2) The tests must be run with the plugin installed in a Rails project, so do that if you haven't already.
198
+
199
+ 3) Run 'rake test_mysql' (or test_sqlite3 or test_postgresql) in plugins/betternestedset. The default rake task attempts to use
200
+ all three adapters.
201
+
202
+ = License
203
+
204
+ Copyright (c) 2006 Jean-Christophe Michel, Symétrie
205
+
206
+ The MIT License
207
+
208
+ Permission is hereby granted, free of charge, to any person obtaining a copy
209
+ of this software and associated documentation files (the "Software"), to deal
210
+ in the Software without restriction, including without limitation the rights
211
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
212
+ copies of the Software, and to permit persons to whom the Software is
213
+ furnished to do so, subject to the following conditions:
214
+
215
+ The above copyright notice and this permission notice shall be included in
216
+ all copies or substantial portions of the Software.
217
+
218
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
219
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
220
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
221
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
222
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
223
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
224
+ THE SOFTWARE
data/Rakefile ADDED
@@ -0,0 +1,59 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "better_nested_set"
16
+ gem.homepage = "http://github.com/railsbros-dirk/better_nested_set"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{This plugin provides an ehanced acts_as_nested_set mixin for ActiveRecord}
19
+ gem.description = %Q{This plugin provides an enhanced acts_as_nested_set mixin for ActiveRecord, the object-relational mapping layer of the framework Ruby on Rails. The original nested set in Rails lacks many important features, such as moving branches within a tree.}
20
+ gem.email = "dirk.breuer@gmail.com"
21
+ gem.authors = ["Chris Bailey", "Jean-Christophe Michel", "Dirk Breuer"]
22
+ gem.files += FileList['lib/**/*.rb', 'app/**/*.rb'].to_a
23
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
24
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
25
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
26
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
27
+ end
28
+ Jeweler::RubygemsDotOrgTasks.new
29
+
30
+ require 'rake/testtask'
31
+ desc 'Run tests on all database adapters. See README.'
32
+ task :default => [:test_mysql, :test_sqlite3, :test_postgresql]
33
+
34
+ %w(mysql postgresql sqlite3).each do |adapter|
35
+ Rake::TestTask.new("test_#{adapter}") { |t|
36
+ t.libs << 'lib'
37
+ t.pattern = "test/#{adapter}.rb"
38
+ t.verbose = true
39
+ }
40
+ end
41
+
42
+ require 'rcov/rcovtask'
43
+ Rcov::RcovTask.new do |test|
44
+ test.libs << 'test'
45
+ test.pattern = 'test/**/test_*.rb'
46
+ test.verbose = true
47
+ end
48
+
49
+ task :default => :test
50
+
51
+ require 'rake/rdoctask'
52
+ Rake::RDocTask.new do |rdoc|
53
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
54
+
55
+ rdoc.rdoc_dir = 'rdoc'
56
+ rdoc.title = "better_nested_set #{version}"
57
+ rdoc.rdoc_files.include('README*')
58
+ rdoc.rdoc_files.include('lib/**/*.rb')
59
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,121 @@
1
+ # This module provides some helpers for the model classes using acts_as_nested_set.
2
+ # It is included by default in all views. If you need to remove it, edit the last line
3
+ # of init.rb.
4
+ #
5
+ module BetterNestedSetHelper
6
+
7
+ # Prints a line of ancestors with links, on the form
8
+ # root > parent > item
9
+ #
10
+ # == Usage
11
+ # Default is to use links to {your_cotroller}/show with the first string column of your model.
12
+ # You can tweak this by passing your parameters, or better, pass a block that will receive
13
+ # an item from your nested set tree and a boolean flag (true for current item) and that
14
+ # should return the line with the link.
15
+ #
16
+ # == Examples
17
+ #
18
+ # nested_set_full_outline(category)
19
+ #
20
+ # # non standard actions and separators
21
+ # nested_set_full_outline(category, :action => :search, :separator => ' | ')
22
+ #
23
+ # # with a block that will return the link to the item
24
+ # # note that the current item will lead to another action
25
+ # nested_set_full_outline(category) { |item, current?|
26
+ # if current?
27
+ # link_to "#{item.name} (#{item.})", product_url(:action => :show_category, :category => item.whole_url)
28
+ # else
29
+ # link_to "#{item.name} (#{item.})", category_url(:action => :browse, :criteria => item.whole_url)
30
+ # end
31
+ # }
32
+ #
33
+ # == Params are:
34
+ # +item+ - the object to display
35
+ # +hash+ - containing :
36
+ # * +text_column+ - the title column, defaults to the first string column of the model
37
+ # * +:action+ - the action to be called (defaults to :show)
38
+ # * +:controller+ - the controller name (defaults to the model name)
39
+ # * +:separator+ - the separator (defaults to >)
40
+ # * +&block+ - a block { |item, current?| ... item.name }
41
+ #
42
+ def nested_set_full_outline(item, options={})
43
+ return if item.nil?
44
+ raise 'Not a nested set model !' unless item.respond_to?(:acts_as_nested_set_options)
45
+
46
+ options = {
47
+ :text_column => options[:text_column] || item.acts_as_nested_set_options[:text_column],
48
+ :action => options[:action] || :show,
49
+ :controller => options[:controller] || item.class.to_s.underscore,
50
+ :separator => options[:separator] || ' &gt; ' }
51
+
52
+ s = ''
53
+ for it in item.ancestors
54
+ if block_given?
55
+ s += yield(it) + options[:separator]
56
+ else
57
+ s += link_to( it[options[:text_column]], { :controller => options[:controller], :action => options[:action], :id => it }) + options[:separator]
58
+ end
59
+ end
60
+ if block_given?
61
+ s + yield(item)
62
+ else
63
+ s + h(item[options[:text_column]])
64
+ end
65
+ end
66
+
67
+ # Returns options for select.
68
+ # You can exclude some items from the tree.
69
+ # You can pass a block receiving an item and returning the string displayed in the select.
70
+ #
71
+ # == Usage
72
+ # Default is to use the whole tree and to print the first string column of your model.
73
+ # You can tweak this by passing your parameters, or better, pass a block that will receive
74
+ # an item from your nested set tree and that should return the line with the link.
75
+ #
76
+ # == Examples
77
+ #
78
+ # nested_set_options_for_select(Category)
79
+ #
80
+ # # show only a part of the tree, and exclude a category and its subtree
81
+ # nested_set_options_for_select(selected_category, :exclude => category)
82
+ #
83
+ # # add a custom string
84
+ # nested_set_options_for_select(Category, :exclude => category) { |item| "#{'&nbsp;' * item.level}#{item.name} (#{item.url})" }
85
+ #
86
+ # == Params
87
+ # * +class_or_item+ - Class name or item or array of items to start the display with
88
+ # * +text_column+ - the title column, defaults to the first string column of the model
89
+ # * +&block+ - a block { |item| ... item.name }
90
+ # If no block passed, uses {|item| "#{'··' * item.level}#{item[text_column]}"}
91
+ def nested_set_options_for_select(class_or_item, options=nil)
92
+ # find class
93
+ if class_or_item.is_a? Class
94
+ first_item = class_or_item.roots
95
+ else
96
+ first_item = class_or_item
97
+ end
98
+ return [] if first_item.nil?
99
+ raise 'Not a nested set model !' unless class_or_item.respond_to?(:acts_as_nested_set_options)
100
+
101
+ # exclude some items and all their children
102
+ if options.is_a? Hash
103
+ text_column = options[:text_column]
104
+ options.delete_if { |key, value| key != :exclude }
105
+ else
106
+ options = nil
107
+ end
108
+ text_column ||= class_or_item.acts_as_nested_set_options[:text_column]
109
+
110
+ if first_item.is_a?(Array)
111
+ tree = first_item.collect{|i| i.full_set(options)}.flatten
112
+ else
113
+ tree = first_item.full_set(options)
114
+ end
115
+ if block_given?
116
+ tree.map{|item| [yield(item), item.id] }
117
+ else
118
+ tree.map{|item| [ "#{'··' * item.level}#{item[text_column]}", item.id]}
119
+ end
120
+ end
121
+ end