closure_tree 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +19 -0
- data/README.rdoc +109 -0
- data/Rakefile +31 -0
- data/lib/closure_tree.rb +3 -0
- data/lib/closure_tree/acts_as_tree.rb +198 -0
- data/lib/closure_tree/version.rb +3 -0
- data/lib/tasks/closure_tree_tasks.rake +4 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/models/.gitkeep +0 -0
- data/test/dummy/app/models/tag.rb +3 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +53 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +39 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +25 -0
- data/test/dummy/config/environments/production.rb +52 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/routes.rb +3 -0
- data/test/dummy/db/migrate/20110522004834_create_tags.rb +23 -0
- data/test/dummy/db/schema.rb +31 -0
- data/test/dummy/log/.gitkeep +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/dummy/test/fixtures/tags.yml +61 -0
- data/test/dummy/test/unit/tag_test.rb +75 -0
- data/test/test_helper.rb +10 -0
- metadata +112 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Matthew McEachen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
= Closure Tree
|
2
|
+
|
3
|
+
Closure Tree is a mostly-API-compatible replacement for the
|
4
|
+
acts_as_tree and awesome_nested_set gems, but with much better
|
5
|
+
mutation performance thanks to the Closure Tree storage algorithm.
|
6
|
+
|
7
|
+
See {Bill Karwin}[http://karwin.blogspot.com/]'s excellent
|
8
|
+
{Models for hierarchical data presentation}[http://www.slideshare.net/billkarwin/models-for-hierarchical-data]
|
9
|
+
for a description of different tree storage algorithms.
|
10
|
+
|
11
|
+
== Setup
|
12
|
+
|
13
|
+
Note that closure_tree is being developed for Rails 3.1.0.rc1
|
14
|
+
|
15
|
+
1. Add this to your Gemfile: <code>gem 'closure_tree'</code>
|
16
|
+
|
17
|
+
2. Run <code>bundle install</code>
|
18
|
+
|
19
|
+
3. Add <code>acts_as_tree</code> to your hierarchical model(s).
|
20
|
+
|
21
|
+
4. Add a migration to add a <code>parent_id</code> column to the model you want to act_as_tree.
|
22
|
+
|
23
|
+
Note that if the column is null, the tag will be considered a root node.
|
24
|
+
|
25
|
+
class AddParentIdToTag < ActiveRecord::Migration
|
26
|
+
def change
|
27
|
+
add_column :tag, :parent_id, :integer
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
5. Add a database migration to store the hierarchy for your model. By
|
32
|
+
convention the table name will be the model's table name, followed by
|
33
|
+
"_hierarchy". Note that by calling <code>acts_as_tree</code>, a "virtual model" (in this case, <code>TagsHierarchy</code>) will be added automatically, so you don't need to create it.
|
34
|
+
|
35
|
+
class CreateTagHierarchy < ActiveRecord::Migration
|
36
|
+
def change
|
37
|
+
create_table :tags_hierarchy do |t|
|
38
|
+
t.integer :ancestor_id, :null => false # ID of the parent/grandparent/great-grandparent/... tag
|
39
|
+
t.integer :descendant_id, :null => false # ID of the target tag
|
40
|
+
t.integer :generations, :null => false # Number of generations between the ancestor and the descendant. Parent/child = 1, for example.
|
41
|
+
end
|
42
|
+
|
43
|
+
# For "all progeny of..." selects:
|
44
|
+
add_index :tags_hierarchy, [:ancestor_id, :descendant_id], :unique => true
|
45
|
+
|
46
|
+
# For "all ancestors of..." selects
|
47
|
+
add_index :tags_hierarchy, [:descendant_id]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
6. Run <code>rake db:migrate</code>
|
52
|
+
|
53
|
+
7. If you're migrating away from another system where your model already has a
|
54
|
+
<code>parent_id</code> column, run <code>Tag.rebuild!</code> and the
|
55
|
+
..._hierarchy table will be truncated and rebuilt.
|
56
|
+
|
57
|
+
If you're starting from scratch you don't need to call <code>rebuild!</code>.
|
58
|
+
|
59
|
+
== Usage
|
60
|
+
|
61
|
+
=== Creation
|
62
|
+
|
63
|
+
Create a root node:
|
64
|
+
|
65
|
+
grandparent = Tag.create!(:name => 'Grandparent')
|
66
|
+
|
67
|
+
There are two equivalent ways to add children. Either use the <code>add_child</code> method:
|
68
|
+
|
69
|
+
parent = Tag.create!(:name => 'Parent')
|
70
|
+
grandparent.add_child parent
|
71
|
+
|
72
|
+
Or append to the <code>children</code> collection:
|
73
|
+
|
74
|
+
child = Tag.create!(:name => 'Child')
|
75
|
+
parent.children << child
|
76
|
+
|
77
|
+
Then:
|
78
|
+
|
79
|
+
puts grandparent.self_and_descendants.collect{ |t| t.name }.join(" > ")
|
80
|
+
"grandparent > parent > child"
|
81
|
+
|
82
|
+
== Accessing Data
|
83
|
+
|
84
|
+
=== Class methods
|
85
|
+
|
86
|
+
[Tag.root] returns an arbitrary root node
|
87
|
+
[Tag.roots] returns all root nodes
|
88
|
+
[Tag.leaves] returns all leaf nodes
|
89
|
+
|
90
|
+
=== Instance methods
|
91
|
+
|
92
|
+
[tag.root] returns the root for this node
|
93
|
+
[tag.root?] returns true if this is a root node
|
94
|
+
[tag.child?] returns true if this is a child node. It has a parent.
|
95
|
+
[tag.leaf?] returns true if this is a leaf node. It has no children.
|
96
|
+
[tag.level] returns the level, or "generation", for this node in the tree. A root node = 0
|
97
|
+
[tag.parent] returns the node's immediate parent
|
98
|
+
[tag.children] returns an array of immediate children (just those in the next level).
|
99
|
+
[tag.ancestors] returns an array of all parents, parents' parents, etc, excluding self.
|
100
|
+
[tag.self_and_ancestors] returns an array of all parents, parents' parents, etc, including self.
|
101
|
+
[tag.siblings] returns an array of brothers and sisters (all at that level), excluding self.
|
102
|
+
[tag.self_and_siblings] returns an array of brothers and sisters (all at that level), including self.
|
103
|
+
[tag.descendants] returns an array of all children, childrens' children, etc., excluding self.
|
104
|
+
[tag.self_and_descendants] returns an array of all children, childrens' children, etc., including self.
|
105
|
+
|
106
|
+
== Thanks to
|
107
|
+
|
108
|
+
* https://github.com/collectiveidea/awesome_nested_set
|
109
|
+
* https://github.com/patshaughnessy/class_factory
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
Bundler::GemHelper.install_tasks
|
8
|
+
|
9
|
+
|
10
|
+
require 'rdoc/task'
|
11
|
+
|
12
|
+
RDoc::Task.new do |rdoc|
|
13
|
+
rdoc.rdoc_dir = 'rdoc'
|
14
|
+
rdoc.title = 'ClosureTree'
|
15
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
16
|
+
rdoc.rdoc_files.include('README.rdoc')
|
17
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
|
23
|
+
Rake::TestTask.new(:test) do |t|
|
24
|
+
t.libs << 'lib'
|
25
|
+
t.libs << 'test'
|
26
|
+
t.pattern = 'test/**/*_test.rb'
|
27
|
+
t.verbose = false
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
task :default => :test
|
data/lib/closure_tree.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
module ClosureTree #:nodoc:
|
2
|
+
module ActsAsTree #:nodoc:
|
3
|
+
def acts_as_tree options = {}
|
4
|
+
|
5
|
+
class_attribute :closure_tree_options
|
6
|
+
self.closure_tree_options = {
|
7
|
+
:parent_column_name => 'parent_id',
|
8
|
+
:dependent => :delete_all, # or :destroy
|
9
|
+
:hierarchy_table_suffix => '_hierarchies'
|
10
|
+
}.merge(options)
|
11
|
+
|
12
|
+
include ClosureTree::Columns
|
13
|
+
extend ClosureTree::Columns
|
14
|
+
|
15
|
+
# Auto-inject the hierarchy table
|
16
|
+
# See https://github.com/patshaughnessy/class_factory/blob/master/lib/class_factory/class_factory.rb
|
17
|
+
class_attribute :hierarchy_class
|
18
|
+
self.hierarchy_class = Object.const_set hierarchy_class_name, Class.new(ActiveRecord::Base)
|
19
|
+
|
20
|
+
self.hierarchy_class.class_eval <<-RUBY
|
21
|
+
belongs_to :ancestor, :class_name => "#{base_class.to_s}"
|
22
|
+
belongs_to :descendant, :class_name => "#{base_class.to_s}"
|
23
|
+
RUBY
|
24
|
+
|
25
|
+
include ClosureTree::Model
|
26
|
+
|
27
|
+
belongs_to :parent, :class_name => base_class.to_s,
|
28
|
+
:foreign_key => parent_column_name
|
29
|
+
|
30
|
+
has_many :children,
|
31
|
+
:class_name => base_class.to_s,
|
32
|
+
:foreign_key => parent_column_name,
|
33
|
+
:before_add => :add_child
|
34
|
+
|
35
|
+
has_many :ancestors_hierarchy,
|
36
|
+
:class_name => hierarchy_class_name,
|
37
|
+
:foreign_key => "descendant_id"
|
38
|
+
|
39
|
+
has_many :ancestors, :through => :ancestors_hierarchy,
|
40
|
+
:order => "generations asc"
|
41
|
+
|
42
|
+
has_many :descendants_hierarchy,
|
43
|
+
:class_name => hierarchy_class_name,
|
44
|
+
:foreign_key => "ancestor_id"
|
45
|
+
|
46
|
+
has_many :descendants, :through => :descendants_hierarchy,
|
47
|
+
:order => "generations asc"
|
48
|
+
|
49
|
+
scope :roots, where(parent_column_name => nil)
|
50
|
+
|
51
|
+
scope :leaves, includes(:descendants_hierarchy).where("#{hierarchy_table_name}.descendant_id is null")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module Model
|
56
|
+
extend ActiveSupport::Concern
|
57
|
+
module InstanceMethods
|
58
|
+
def parent_id
|
59
|
+
self[parent_column_name]
|
60
|
+
end
|
61
|
+
|
62
|
+
def parent_id= new_parent_id
|
63
|
+
self[parent_column_name] = new_parent_id
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns true if this node has no parents.
|
67
|
+
def root?
|
68
|
+
parent_id.nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns true if this node has no children.
|
72
|
+
def leaf?
|
73
|
+
children.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
def leaves
|
77
|
+
self.class.scoped.includes(:descendants_hierarchy).where("#{hierarchy_table_name}.descendant_id is null and #{hierarchy_table_name}.ancestor_id = #{id}")
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns true if this node has a parent, and is not a root.
|
81
|
+
def child?
|
82
|
+
!parent_id.nil?
|
83
|
+
end
|
84
|
+
|
85
|
+
def level
|
86
|
+
ancestors.size
|
87
|
+
end
|
88
|
+
|
89
|
+
def self_and_ancestors
|
90
|
+
[self].concat ancestors.to_a
|
91
|
+
end
|
92
|
+
|
93
|
+
def self_and_descendants
|
94
|
+
[self].concat descendants.to_a
|
95
|
+
end
|
96
|
+
|
97
|
+
def self_and_siblings
|
98
|
+
self.class.scoped.where(:parent_id => parent_id)
|
99
|
+
end
|
100
|
+
|
101
|
+
def siblings
|
102
|
+
without_self(self_and_siblings)
|
103
|
+
end
|
104
|
+
|
105
|
+
# You must use this method, or add child nodes to the +children+ association, to
|
106
|
+
# make the hierarchy table stay consistent.
|
107
|
+
def add_child child_node
|
108
|
+
child_node.update_attribute :parent_id, self.id
|
109
|
+
self_and_ancestors.inject(1) do |gen, ancestor|
|
110
|
+
hierarchy_class.create!(:ancestor => ancestor, :descendant => child_node, :generations => gen)
|
111
|
+
gen + 1
|
112
|
+
end
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
|
116
|
+
def move_to_child_of new_parent
|
117
|
+
connection.execute <<-SQL
|
118
|
+
DELETE FROM #{quoted_hierarchy_table_name}
|
119
|
+
WHERE descendant_id = #{child_node.id}
|
120
|
+
SQL
|
121
|
+
new_parent.add_child self
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
|
126
|
+
def without_self(scope)
|
127
|
+
scope.where(["#{quoted_table_name}.#{self.class.primary_key} != ?", self])
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
module ClassMethods
|
133
|
+
# Returns an arbitrary node that has no parents.
|
134
|
+
def root
|
135
|
+
roots.first
|
136
|
+
end
|
137
|
+
|
138
|
+
# Rebuilds the hierarchy table based on the parent_id column in the database.
|
139
|
+
# Note that the hierarchy table will be truncated.
|
140
|
+
def rebuild!
|
141
|
+
connection.execute <<-SQL
|
142
|
+
DELETE FROM #{quoted_hierarchy_table_name}
|
143
|
+
SQL
|
144
|
+
roots.each { |n| rebuild_node_and_children n }
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
def rebuild_node_and_children node
|
150
|
+
node.parent.add_child node if node.parent
|
151
|
+
node.children.each { |child| rebuild_node_and_children child }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Mixed into both classes and instances to provide easy access to the column names
|
157
|
+
module Columns
|
158
|
+
|
159
|
+
protected
|
160
|
+
|
161
|
+
def parent_column_name
|
162
|
+
closure_tree_options[:parent_column_name]
|
163
|
+
end
|
164
|
+
|
165
|
+
def hierarchy_table_name
|
166
|
+
ct_table_name + closure_tree_options[:hierarchy_table_suffix]
|
167
|
+
end
|
168
|
+
|
169
|
+
def hierarchy_class_name
|
170
|
+
hierarchy_table_name.singularize.camelize
|
171
|
+
end
|
172
|
+
|
173
|
+
def quoted_hierarchy_table_name
|
174
|
+
connection.quote_column_name hierarchy_table_name
|
175
|
+
end
|
176
|
+
|
177
|
+
def scope_column_names
|
178
|
+
Array closure_tree_options[:scope]
|
179
|
+
end
|
180
|
+
|
181
|
+
def quoted_parent_column_name
|
182
|
+
connection.quote_column_name parent_column_name
|
183
|
+
end
|
184
|
+
|
185
|
+
def ct_class
|
186
|
+
(self.is_a?(Class) ? self : self.class)
|
187
|
+
end
|
188
|
+
|
189
|
+
def ct_table_name
|
190
|
+
ct_class.table_name
|
191
|
+
end
|
192
|
+
|
193
|
+
def quoted_table_name
|
194
|
+
connection.quote_column_name ct_table_name
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
end
|
data/test/dummy/Rakefile
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
3
|
+
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
4
|
+
|
5
|
+
require File.expand_path('../config/application', __FILE__)
|
6
|
+
|
7
|
+
Dummy::Application.load_tasks
|
File without changes
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require File.expand_path('../boot', __FILE__)
|
2
|
+
|
3
|
+
require 'rails/all'
|
4
|
+
|
5
|
+
Bundler.require
|
6
|
+
require "closure_tree"
|
7
|
+
|
8
|
+
module Dummy
|
9
|
+
class Application < Rails::Application
|
10
|
+
# Settings in config/environments/* take precedence over those specified here.
|
11
|
+
# Application configuration should go into files in config/initializers
|
12
|
+
# -- all .rb files in that directory are automatically loaded.
|
13
|
+
|
14
|
+
# Custom directories with classes and modules you want to be autoloadable.
|
15
|
+
# config.autoload_paths += %W(#{config.root}/extras)
|
16
|
+
|
17
|
+
# Only load the plugins named here, in the order given (default is alphabetical).
|
18
|
+
# :all can be used as a placeholder for all plugins not explicitly named.
|
19
|
+
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
|
20
|
+
|
21
|
+
# Activate observers that should always be running.
|
22
|
+
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
|
23
|
+
|
24
|
+
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
|
25
|
+
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
|
26
|
+
# config.time_zone = 'Central Time (US & Canada)'
|
27
|
+
|
28
|
+
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
29
|
+
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
30
|
+
# config.i18n.default_locale = :de
|
31
|
+
|
32
|
+
# Please note that JavaScript expansions are *ignored altogether* if the asset
|
33
|
+
# pipeline is enabled (see config.assets.enabled below). Put your defaults in
|
34
|
+
# app/assets/javascripts/application.js in that case.
|
35
|
+
#
|
36
|
+
# JavaScript files you want as :defaults (application.js is always included).
|
37
|
+
# config.action_view.javascript_expansions[:defaults] = %w(prototype prototype_ujs)
|
38
|
+
|
39
|
+
|
40
|
+
# Configure the default encoding used in templates for Ruby 1.9.
|
41
|
+
config.encoding = "utf-8"
|
42
|
+
|
43
|
+
# Configure sensitive parameters which will be filtered from the log file.
|
44
|
+
config.filter_parameters += [:password]
|
45
|
+
|
46
|
+
# Enable IdentityMap for Active Record, to disable set to false or remove the line below.
|
47
|
+
config.active_record.identity_map = true
|
48
|
+
|
49
|
+
# Enable the asset pipeline
|
50
|
+
config.assets.enabled = true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# MySQL. Versions 4.1 and 5.0 are recommended.
|
2
|
+
#
|
3
|
+
# Install the MySQL driver:
|
4
|
+
# gem install mysql2
|
5
|
+
#
|
6
|
+
# And be sure to use new-style password hashing:
|
7
|
+
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
|
8
|
+
development:
|
9
|
+
adapter: mysql2
|
10
|
+
encoding: utf8
|
11
|
+
reconnect: false
|
12
|
+
database: dummy_development
|
13
|
+
pool: 5
|
14
|
+
username: root
|
15
|
+
password:
|
16
|
+
socket: /tmp/mysql.sock
|
17
|
+
|
18
|
+
# Warning: The database defined as "test" will be erased and
|
19
|
+
# re-generated from your development database when you run "rake".
|
20
|
+
# Do not set this db to the same as development or production.
|
21
|
+
test:
|
22
|
+
adapter: mysql2
|
23
|
+
encoding: utf8
|
24
|
+
reconnect: false
|
25
|
+
database: dummy_test
|
26
|
+
pool: 5
|
27
|
+
username: root
|
28
|
+
password:
|
29
|
+
socket: /tmp/mysql.sock
|
30
|
+
|
31
|
+
production:
|
32
|
+
adapter: mysql2
|
33
|
+
encoding: utf8
|
34
|
+
reconnect: false
|
35
|
+
database: dummy_production
|
36
|
+
pool: 5
|
37
|
+
username: root
|
38
|
+
password:
|
39
|
+
socket: /tmp/mysql.sock
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Dummy::Application.configure do
|
2
|
+
# Settings specified here will take precedence over those in config/application.rb
|
3
|
+
|
4
|
+
# In the development environment your application's code is reloaded on
|
5
|
+
# every request. This slows down response time but is perfect for development
|
6
|
+
# since you don't have to restart the web server when you make code changes.
|
7
|
+
config.cache_classes = false
|
8
|
+
|
9
|
+
# Log error messages when you accidentally call methods on nil.
|
10
|
+
config.whiny_nils = true
|
11
|
+
|
12
|
+
# Show full error reports and disable caching
|
13
|
+
config.consider_all_requests_local = true
|
14
|
+
config.action_controller.perform_caching = false
|
15
|
+
|
16
|
+
# Don't care if the mailer can't send
|
17
|
+
config.action_mailer.raise_delivery_errors = false
|
18
|
+
|
19
|
+
# Print deprecation notices to the Rails logger
|
20
|
+
config.active_support.deprecation = :log
|
21
|
+
|
22
|
+
# Only use best-standards-support built into browsers
|
23
|
+
config.action_dispatch.best_standards_support = :builtin
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
Dummy::Application.configure do
|
2
|
+
# Settings specified here will take precedence over those in config/application.rb
|
3
|
+
|
4
|
+
# Code is not reloaded between requests
|
5
|
+
config.cache_classes = true
|
6
|
+
|
7
|
+
# Full error reports are disabled and caching is turned on
|
8
|
+
config.consider_all_requests_local = false
|
9
|
+
config.action_controller.perform_caching = true
|
10
|
+
|
11
|
+
# Disable Rails's static asset server (Apache or nginx will already do this)
|
12
|
+
config.serve_static_assets = false
|
13
|
+
|
14
|
+
# Compress both stylesheets and JavaScripts
|
15
|
+
config.assets.js_compressor = :uglifier
|
16
|
+
config.assets.css_compressor = :scss
|
17
|
+
|
18
|
+
# Specifies the header that your server uses for sending files
|
19
|
+
# (comment out if your front-end server doesn't support this)
|
20
|
+
config.action_dispatch.x_sendfile_header = "X-Sendfile" # Use 'X-Accel-Redirect' for nginx
|
21
|
+
|
22
|
+
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
|
23
|
+
# config.force_ssl = true
|
24
|
+
|
25
|
+
# See everything in the log (default is :info)
|
26
|
+
# config.log_level = :debug
|
27
|
+
|
28
|
+
# Use a different logger for distributed setups
|
29
|
+
# config.logger = SyslogLogger.new
|
30
|
+
|
31
|
+
# Use a different cache store in production
|
32
|
+
# config.cache_store = :mem_cache_store
|
33
|
+
|
34
|
+
# Enable serving of images, stylesheets, and javascripts from an asset server
|
35
|
+
# config.action_controller.asset_host = "http://assets.example.com"
|
36
|
+
|
37
|
+
# Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
|
38
|
+
# config.assets.precompile += %w( search.js )
|
39
|
+
|
40
|
+
# Disable delivery errors, bad email addresses will be ignored
|
41
|
+
# config.action_mailer.raise_delivery_errors = false
|
42
|
+
|
43
|
+
# Enable threaded mode
|
44
|
+
# config.threadsafe!
|
45
|
+
|
46
|
+
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
47
|
+
# the I18n.default_locale when a translation can not be found)
|
48
|
+
config.i18n.fallbacks = true
|
49
|
+
|
50
|
+
# Send deprecation notices to registered listeners
|
51
|
+
config.active_support.deprecation = :notify
|
52
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
Dummy::Application.configure do
|
2
|
+
# Settings specified here will take precedence over those in config/application.rb
|
3
|
+
|
4
|
+
# The test environment is used exclusively to run your application's
|
5
|
+
# test suite. You never need to work with it otherwise. Remember that
|
6
|
+
# your test database is "scratch space" for the test suite and is wiped
|
7
|
+
# and recreated between test runs. Don't rely on the data there!
|
8
|
+
config.cache_classes = true
|
9
|
+
|
10
|
+
# Configure static asset server for tests with Cache-Control for performance
|
11
|
+
config.serve_static_assets = true
|
12
|
+
config.static_cache_control = "public, max-age=3600"
|
13
|
+
|
14
|
+
# Log error messages when you accidentally call methods on nil
|
15
|
+
config.whiny_nils = true
|
16
|
+
|
17
|
+
# Show full error reports and disable caching
|
18
|
+
config.consider_all_requests_local = true
|
19
|
+
config.action_controller.perform_caching = false
|
20
|
+
|
21
|
+
# Raise exceptions instead of rendering exception templates
|
22
|
+
config.action_dispatch.show_exceptions = false
|
23
|
+
|
24
|
+
# Disable request forgery protection in test environment
|
25
|
+
config.action_controller.allow_forgery_protection = false
|
26
|
+
|
27
|
+
# Tell Action Mailer not to deliver emails to the real world.
|
28
|
+
# The :test delivery method accumulates sent emails in the
|
29
|
+
# ActionMailer::Base.deliveries array.
|
30
|
+
config.action_mailer.delivery_method = :test
|
31
|
+
|
32
|
+
# Use SQL instead of Active Record's schema dumper when creating the test database.
|
33
|
+
# This is necessary if your schema can't be completely dumped by the schema dumper,
|
34
|
+
# like if you have constraints or database-specific column types
|
35
|
+
# config.active_record.schema_format = :sql
|
36
|
+
|
37
|
+
# Print deprecation notices to the stderr
|
38
|
+
config.active_support.deprecation = :stderr
|
39
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class CreateTags < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
|
4
|
+
create_table :tags do |t|
|
5
|
+
t.string :name
|
6
|
+
t.integer :parent_id
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
create_table :tags_hierarchies, :id => false do |t|
|
11
|
+
t.integer :ancestor_id, :null => false # ID of the parent/grandparent/great-grandparent/... tag
|
12
|
+
t.integer :descendant_id, :null => false # ID of the target tag
|
13
|
+
t.integer :generations, :null => false # Number of generations between the ancestor and the descendant. Parent/child = 1, for example.
|
14
|
+
end
|
15
|
+
|
16
|
+
# For "all progeny of..." selects:
|
17
|
+
add_index :tags_hierarchies, [:ancestor_id, :descendant_id], :unique => true
|
18
|
+
|
19
|
+
# For "all ancestors of..." selects
|
20
|
+
add_index :tags_hierarchies, :descendant_id
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
4
|
+
#
|
5
|
+
# Note that this schema.rb definition is the authoritative source for your
|
6
|
+
# database schema. If you need to create the application database on another
|
7
|
+
# system, you should be using db:schema:load, not running all the migrations
|
8
|
+
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
9
|
+
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
10
|
+
#
|
11
|
+
# It's strongly recommended to check this file into your version control system.
|
12
|
+
|
13
|
+
ActiveRecord::Schema.define(:version => 20110522004834) do
|
14
|
+
|
15
|
+
create_table "tags", :force => true do |t|
|
16
|
+
t.string "name"
|
17
|
+
t.integer "parent_id"
|
18
|
+
t.datetime "created_at"
|
19
|
+
t.datetime "updated_at"
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table "tags_hierarchies", :id => false, :force => true do |t|
|
23
|
+
t.integer "ancestor_id", :null => false
|
24
|
+
t.integer "descendant_id", :null => false
|
25
|
+
t.integer "generations", :null => false
|
26
|
+
end
|
27
|
+
|
28
|
+
add_index "tags_hierarchies", ["ancestor_id", "descendant_id"], :name => "index_tags_hierarchies_on_ancestor_id_and_descendant_id", :unique => true
|
29
|
+
add_index "tags_hierarchies", ["descendant_id"], :name => "index_tags_hierarchies_on_descendant_id"
|
30
|
+
|
31
|
+
end
|
File without changes
|
@@ -0,0 +1,6 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
|
3
|
+
|
4
|
+
APP_PATH = File.expand_path('../../config/application', __FILE__)
|
5
|
+
require File.expand_path('../../config/boot', __FILE__)
|
6
|
+
require 'rails/commands'
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html
|
2
|
+
|
3
|
+
grandparent:
|
4
|
+
name: grandparent
|
5
|
+
|
6
|
+
parent:
|
7
|
+
name: parent
|
8
|
+
parent: grandparent
|
9
|
+
|
10
|
+
child:
|
11
|
+
name: child
|
12
|
+
parent: parent
|
13
|
+
|
14
|
+
|
15
|
+
people:
|
16
|
+
name: people
|
17
|
+
|
18
|
+
# people has no children
|
19
|
+
|
20
|
+
events:
|
21
|
+
name: events
|
22
|
+
|
23
|
+
# events has only one child
|
24
|
+
|
25
|
+
birthday:
|
26
|
+
name: birthday
|
27
|
+
parent: events
|
28
|
+
|
29
|
+
places:
|
30
|
+
name: places
|
31
|
+
|
32
|
+
# places has many children, with many depths
|
33
|
+
|
34
|
+
home:
|
35
|
+
name: home
|
36
|
+
parent: places
|
37
|
+
|
38
|
+
indoor:
|
39
|
+
name: indoor
|
40
|
+
parent: places
|
41
|
+
|
42
|
+
outdoor:
|
43
|
+
name: outdoor
|
44
|
+
parent: places
|
45
|
+
|
46
|
+
museum:
|
47
|
+
name: museum
|
48
|
+
parent: places
|
49
|
+
|
50
|
+
united_states:
|
51
|
+
name: united_states
|
52
|
+
parent: places
|
53
|
+
|
54
|
+
california:
|
55
|
+
name: california
|
56
|
+
parent: united_states
|
57
|
+
|
58
|
+
san_francisco:
|
59
|
+
name: san_francisco
|
60
|
+
parent: california
|
61
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TagTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
fixtures :tags
|
6
|
+
|
7
|
+
def setup
|
8
|
+
Tag.rebuild!
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_roots
|
12
|
+
roots = Tag.roots.to_a
|
13
|
+
assert roots.include?(tags(:people))
|
14
|
+
assert roots.include?(tags(:events))
|
15
|
+
assert !roots.include?(tags(:child))
|
16
|
+
assert tags(:people).root?
|
17
|
+
assert !tags(:child).root?
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_add_child
|
21
|
+
sb = Tag.create!(:name => "Santa Barbara")
|
22
|
+
assert sb.leaf?
|
23
|
+
tags(:california).add_child sb
|
24
|
+
assert sb.leaf?
|
25
|
+
validate_city_tag sb
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_add_through_children
|
29
|
+
eg = Tag.create!(:name => "El Granada")
|
30
|
+
assert eg.leaf?
|
31
|
+
tags(:california).children << eg
|
32
|
+
assert eg.leaf?
|
33
|
+
validate_city_tag eg
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_level
|
37
|
+
assert_equal 0, tags(:grandparent).level
|
38
|
+
assert_equal 1, tags(:parent).level
|
39
|
+
assert_equal 2, tags(:child).level
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_parent
|
43
|
+
assert_equal nil, tags(:grandparent).parent
|
44
|
+
assert_equal tags(:grandparent), tags(:parent).parent
|
45
|
+
assert_equal tags(:parent), tags(:child).parent
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_children
|
49
|
+
assert tags(:grandparent).children.include? tags(:parent)
|
50
|
+
assert tags(:parent).children.include? tags(:child)
|
51
|
+
assert tags(:child).children.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_ancestors
|
55
|
+
assert_equal [tags(:parent), tags(:grandparent)], tags(:child).ancestors
|
56
|
+
assert_equal [tags(:child), tags(:parent), tags(:grandparent)], tags(:child).self_and_ancestors
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_descendants
|
60
|
+
assert_equal [tags(:child)], tags(:parent).descendants
|
61
|
+
assert_equal [tags(:parent), tags(:child)], tags(:parent).self_and_descendants
|
62
|
+
|
63
|
+
assert_equal [tags(:parent), tags(:child)], tags(:grandparent).descendants
|
64
|
+
assert_equal [tags(:grandparent), tags(:parent), tags(:child)], tags(:grandparent).self_and_descendants
|
65
|
+
|
66
|
+
assert_equal "grandparent > parent > child", tags(:grandparent).self_and_descendants.collect { |t| t.name }.join(" > ")
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_city_tag city
|
70
|
+
assert tags(:california).children.include?(city)
|
71
|
+
assert_equal [tags(:california), tags(:united_states), tags(:places)], city.ancestors
|
72
|
+
assert_equal [city, tags(:california), tags(:united_states), tags(:places)], city.self_and_ancestors
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Configure Rails Environment
|
2
|
+
ENV["RAILS_ENV"] = "test"
|
3
|
+
|
4
|
+
require File.expand_path("../dummy/config/environment.rb", __FILE__)
|
5
|
+
require "rails/test_help"
|
6
|
+
|
7
|
+
Rails.backtrace_cleaner.remove_silencers!
|
8
|
+
|
9
|
+
# Load support files
|
10
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: closure_tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: 6
|
5
|
+
version: 1.0.0.beta1
|
6
|
+
platform: ruby
|
7
|
+
authors: []
|
8
|
+
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-05-24 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: activerecord
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 3.0.0
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *id001
|
27
|
+
description: " A mostly-API-compatible replacement for the acts_as_tree and awesome_nested_set gems,\n but with much better mutation performance thanks to the Closure Tree storage algorithm\n"
|
28
|
+
email:
|
29
|
+
executables: []
|
30
|
+
|
31
|
+
extensions: []
|
32
|
+
|
33
|
+
extra_rdoc_files: []
|
34
|
+
|
35
|
+
files:
|
36
|
+
- lib/closure_tree/acts_as_tree.rb
|
37
|
+
- lib/closure_tree/version.rb
|
38
|
+
- lib/closure_tree.rb
|
39
|
+
- lib/tasks/closure_tree_tasks.rake
|
40
|
+
- MIT-LICENSE
|
41
|
+
- Rakefile
|
42
|
+
- README.rdoc
|
43
|
+
- test/dummy/Rakefile
|
44
|
+
- test/dummy/app/models/.gitkeep
|
45
|
+
- test/dummy/app/models/tag.rb
|
46
|
+
- test/dummy/config.ru
|
47
|
+
- test/dummy/config/application.rb
|
48
|
+
- test/dummy/config/boot.rb
|
49
|
+
- test/dummy/config/database.yml
|
50
|
+
- test/dummy/config/environment.rb
|
51
|
+
- test/dummy/config/environments/development.rb
|
52
|
+
- test/dummy/config/environments/production.rb
|
53
|
+
- test/dummy/config/environments/test.rb
|
54
|
+
- test/dummy/config/routes.rb
|
55
|
+
- test/dummy/db/migrate/20110522004834_create_tags.rb
|
56
|
+
- test/dummy/db/schema.rb
|
57
|
+
- test/dummy/log/.gitkeep
|
58
|
+
- test/dummy/script/rails
|
59
|
+
- test/dummy/test/fixtures/tags.yml
|
60
|
+
- test/dummy/test/unit/tag_test.rb
|
61
|
+
- test/test_helper.rb
|
62
|
+
has_rdoc: true
|
63
|
+
homepage:
|
64
|
+
licenses: []
|
65
|
+
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 170453244524217629
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">"
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 1.3.1
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.6.2
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Hierarchies for ActiveRecord models using a Closure Tree storage algorithm
|
93
|
+
test_files:
|
94
|
+
- test/dummy/Rakefile
|
95
|
+
- test/dummy/app/models/.gitkeep
|
96
|
+
- test/dummy/app/models/tag.rb
|
97
|
+
- test/dummy/config.ru
|
98
|
+
- test/dummy/config/application.rb
|
99
|
+
- test/dummy/config/boot.rb
|
100
|
+
- test/dummy/config/database.yml
|
101
|
+
- test/dummy/config/environment.rb
|
102
|
+
- test/dummy/config/environments/development.rb
|
103
|
+
- test/dummy/config/environments/production.rb
|
104
|
+
- test/dummy/config/environments/test.rb
|
105
|
+
- test/dummy/config/routes.rb
|
106
|
+
- test/dummy/db/migrate/20110522004834_create_tags.rb
|
107
|
+
- test/dummy/db/schema.rb
|
108
|
+
- test/dummy/log/.gitkeep
|
109
|
+
- test/dummy/script/rails
|
110
|
+
- test/dummy/test/fixtures/tags.yml
|
111
|
+
- test/dummy/test/unit/tag_test.rb
|
112
|
+
- test/test_helper.rb
|