closure_tree 1.0.0.beta1

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/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
@@ -0,0 +1,3 @@
1
+ require 'closure_tree/acts_as_tree'
2
+
3
+ ActiveRecord::Base.send :extend, ClosureTree::ActsAsTree
@@ -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
@@ -0,0 +1,3 @@
1
+ module ClosureTree
2
+ VERSION = "1.0.0.beta1" unless defined?(::ClosureTree::VERSION)
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :closure_tree do
3
+ # # Task goes here
4
+ # end
@@ -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,3 @@
1
+ class Tag < ActiveRecord::Base
2
+ acts_as_tree
3
+ end
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Dummy::Application
@@ -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,10 @@
1
+ require 'rubygems'
2
+ gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3
+
4
+ if File.exist?(gemfile)
5
+ ENV['BUNDLE_GEMFILE'] = gemfile
6
+ require 'bundler'
7
+ Bundler.setup
8
+ end
9
+
10
+ $:.unshift File.expand_path('../../../../lib', __FILE__)
@@ -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,5 @@
1
+ # Load the rails application
2
+ require File.expand_path('../application', __FILE__)
3
+
4
+ # Initialize the rails application
5
+ Dummy::Application.initialize!
@@ -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,3 @@
1
+ Dummy::Application.routes.draw do
2
+
3
+ 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
+
@@ -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