├── .gitignore
├── .rspec
├── .travis.yml
├── Appraisals
├── Gemfile
├── History.md
├── LICENSE.txt
├── README.md
├── Rakefile
├── acts_as_ordered_tree.gemspec
├── gemfiles
├── rails3.1.gemfile
├── rails3.2.gemfile
├── rails4.0.gemfile
├── rails4.1.gemfile
└── rails4.2.gemfile
├── lib
├── acts_as_ordered_tree.rb
└── acts_as_ordered_tree
│ ├── adapters.rb
│ ├── adapters
│ ├── abstract.rb
│ ├── postgresql.rb
│ └── recursive.rb
│ ├── compatibility.rb
│ ├── compatibility
│ ├── active_record
│ │ ├── association_scope.rb
│ │ ├── default_scoped.rb
│ │ └── null_relation.rb
│ └── features.rb
│ ├── deprecate.rb
│ ├── hooks.rb
│ ├── hooks
│ └── update.rb
│ ├── instance_methods.rb
│ ├── iterators
│ ├── arranger.rb
│ ├── level_calculator.rb
│ └── orphans_pruner.rb
│ ├── node.rb
│ ├── node
│ ├── attributes.rb
│ ├── movement.rb
│ ├── movements.rb
│ ├── predicates.rb
│ ├── reloading.rb
│ ├── siblings.rb
│ └── traversals.rb
│ ├── persevering_transaction.rb
│ ├── position.rb
│ ├── relation
│ ├── arrangeable.rb
│ ├── iterable.rb
│ └── preloaded.rb
│ ├── transaction
│ ├── base.rb
│ ├── callbacks.rb
│ ├── create.rb
│ ├── destroy.rb
│ ├── dsl.rb
│ ├── factory.rb
│ ├── move.rb
│ ├── passthrough.rb
│ ├── reorder.rb
│ ├── save.rb
│ └── update.rb
│ ├── tree.rb
│ ├── tree
│ ├── association.rb
│ ├── callbacks.rb
│ ├── children_association.rb
│ ├── columns.rb
│ ├── deprecated_columns_accessors.rb
│ ├── parent_association.rb
│ ├── perseverance.rb
│ └── scopes.rb
│ ├── validators.rb
│ └── version.rb
└── spec
├── acts_as_ordered_tree_spec.rb
├── adapters
├── postgresql_spec.rb
├── recursive_spec.rb
└── shared.rb
├── callbacks_spec.rb
├── counter_cache_spec.rb
├── create_spec.rb
├── destroy_spec.rb
├── inheritance_spec.rb
├── move_spec.rb
├── node
├── movements
│ ├── concurrent_movements_spec.rb
│ ├── move_higher_spec.rb
│ ├── move_lower_spec.rb
│ ├── move_to_child_of_spec.rb
│ ├── move_to_child_with_index_spec.rb
│ ├── move_to_child_with_position_spec.rb
│ ├── move_to_left_of_spec.rb
│ ├── move_to_right_of_spec.rb
│ └── move_to_root_spec.rb
├── predicates_spec.rb
├── reloading_spec.rb
├── siblings_spec.rb
└── traversals_spec.rb
├── persevering_transaction_spec.rb
├── relation
├── arrangeable_spec.rb
├── iterable_spec.rb
└── preloaded_spec.rb
├── reorder_spec.rb
├── spec_helper.rb
├── support
├── db
│ ├── boot.rb
│ ├── config.travis.yml
│ ├── config.yml
│ └── schema.rb
├── factories.rb
├── matchers.rb
├── models.rb
└── tree_factory.rb
└── tree
├── children_association_spec.rb
├── columns_spec.rb
└── scopes_spec.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 | .idea/*
6 | coverage/*
7 | .rbx/*
8 | gemfiles/*.lock
9 | *.sqlite3.*
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format progress
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | cache: bundler
3 | rvm:
4 | - 1.9.3
5 | - 2.0.0
6 | - 2.1.2
7 | - jruby-19mode
8 | before_install: gem install bundler
9 | before_script:
10 | - psql -c 'create database acts_as_ordered_tree_test;' -U postgres
11 | - mysql -e 'create database acts_as_ordered_tree_test;'
12 | env:
13 | - DBCONF=config.travis.yml
14 | gemfile:
15 | - gemfiles/rails3.1.gemfile
16 | - gemfiles/rails3.2.gemfile
17 | - gemfiles/rails4.0.gemfile
18 | - gemfiles/rails4.1.gemfile
19 | - gemfiles/rails4.2.gemfile
20 | script: "bundle exec rake db spec"
21 | after_success:
22 | - bundle exec rake coverage:push
23 | notifications:
24 | recipients:
25 | - amikhailov83@gmail.com
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise 'rails3.1' do
2 | gem 'activerecord', '~> 3.1.12'
3 |
4 | gem 'activerecord-jdbcpostgresql-adapter', '~> 1.2.2', :platform => :jruby
5 | gem 'activerecord-jdbcsqlite3-adapter', '~> 1.2.2', :platform => :jruby
6 | gem 'activerecord-jdbcmysql-adapter', '~> 1.2.2', :platform => :jruby
7 | end
8 |
9 | appraise 'rails3.2' do
10 | gem 'activerecord', '~> 3.2.17'
11 |
12 | gem 'activerecord-jdbcpostgresql-adapter', '~> 1.2.2', :platform => :jruby
13 | gem 'activerecord-jdbcsqlite3-adapter', '~> 1.2.2', :platform => :jruby
14 | gem 'activerecord-jdbcmysql-adapter', '~> 1.2.2', :platform => :jruby
15 | end
16 |
17 | appraise 'rails4.0' do
18 | gem 'activerecord', '~> 4.0.3'
19 |
20 | gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3.6', :platform => :jruby
21 | gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.6', :platform => :jruby
22 | gem 'activerecord-jdbcmysql-adapter', '~> 1.3.6', :platform => :jruby
23 | end
24 |
25 | appraise 'rails4.1' do
26 | gem 'activerecord', '~> 4.1.6'
27 |
28 | gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3.6', :platform => :jruby
29 | gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.6', :platform => :jruby
30 | gem 'activerecord-jdbcmysql-adapter', '~> 1.3.6', :platform => :jruby
31 | end
32 |
33 | appraise 'rails4.2' do
34 | gem 'activerecord', '~> 4.2.0'
35 |
36 | gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3.6', :platform => :jruby
37 | gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.6', :platform => :jruby
38 | gem 'activerecord-jdbcmysql-adapter', '~> 1.3.6', :platform => :jruby
39 | end
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org'
2 |
3 | # Specify your gem's dependencies in acts_as_ordered_tree.gemspec
4 | gemspec
5 |
6 | gem 'pg', :platform => :ruby
7 | gem 'sqlite3', :platform => :ruby
8 | gem 'mysql2', :platform => :ruby
9 | gem 'simplecov', :platform => :ruby
10 | gem 'coveralls', :platform => :ruby
11 |
12 | gem 'activerecord-jdbcpostgresql-adapter', '~> 1.2.0', :platform => :jruby
13 | gem 'activerecord-jdbcsqlite3-adapter', '~> 1.2.0', :platform => :jruby
14 |
--------------------------------------------------------------------------------
/History.md:
--------------------------------------------------------------------------------
1 | # 2.0.0 (not released yet)
2 |
3 | The library completely redesigned, tons of refactorings applied.
4 |
5 | ### Breaking changes
6 |
7 | * Movement methods now use `ActiveRecord::Base#save` so all
8 | user validations and callbacks are invoked now.
9 | * `parent_id` and `position` attributes are no longer protected
10 | by default. It's up to developer to make them `attr_protected` now.
11 |
12 | ### New features
13 |
14 | * Reduced amount of UPDATE SQL-queries during movements and reorders.
15 | * Added collection iterators:
16 | 1. `each_with_level` which yields each node of collection with its level.
17 | 2. `each_without_orphans` which yields only that nodes which are not orphaned
18 | (i.e. that nodes which don't have corresponding parent within iterated collection).
19 | 3. `arrange` which constructs hash of hashes where each key is node and value
20 | is its hash of its children.
21 | * Added ability to explicitly set left/right sibling for node via
22 | `#left_sibling=` and `#right_sibling=` methods (#30).
23 | * `.leaves` scope now works even with models that don't have `counter_cache` column (#29).
24 | * Added method instance method `#move_to_child_with_position`
25 | which is similar to `#move_to_child_with_index` but is more human readable.
26 | * Full support for `before_add`, `after_add`, `before_remove` and `after_remove`
27 | callbacks (#25).
28 | * Descendants now can be arranged into hash with `#arrange` method (#22).
29 | * Flexible control over tree traversals via blocks passed to `#descendants`
30 | and `#ancestors` (#21).
31 |
32 | ### Deprecations
33 |
34 | * Deprecated methods:
35 | 1. `#insert_at`. It is recommended to use `#move_to_child_with_position` instead.
36 | 2. `#branch? is deprecated in favour of `#has_children?`.
37 | 3. `#child?` is deprecated in favour of `#has_parent?`.
38 | 4. `#move_possible?` is deprecated in favour of built-in and user defined validations.
39 |
40 | Bug fixes:
41 |
42 | * Fixed several issues that broke tree integrity.
43 | * Fixed bug when two root nodes could be created with same position (#24).
44 | * Fixed support of STI and basic inheritance.
45 | * Fixed bug when user validations and callbacks weren't invoked on movements
46 | via `move_to_*` methods (#23).
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013-2014 Alexei Mikhailov
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Acts As Ordered Tree [](http://travis-ci.org/take-five/acts_as_ordered_tree) [](https://codeclimate.com/github/take-five/acts_as_ordered_tree) [](https://coveralls.io/r/take-five/acts_as_ordered_tree)
2 | WARNING! THIS GEM IS NOT COMPATIBLE WITH ordered_tree gem.
3 |
4 | Specify this `acts_as` extension if you want to model an ordered tree structure ([adjacency list hierarchical structure](http://www.sqlsummit.com/AdjacencyList.htm)) by providing a parent association, a children association and a sort column. For proper use you should have a foreign key column, which by default is called `parent_id`, and a sort column, which is by default called `position`.
5 | Comparison of Adjacency List model to others: http://vadimtropashko.wordpress.com/2008/08/09/one-more-nested-intervals-vs-adjacency-list-comparison/
6 |
7 | This extension is mostly compatible with [`awesome_nested_set`](https://github.com/collectiveidea/awesome_nested_set/) gem
8 |
9 | ## Requirements
10 |
11 | Gem is supposed to work with Rails 3.1 and higher including newest Rails 4.2. We test it with `ruby-1.9.3`, `ruby-2.0.0`, `ruby-2.1.0` and `jruby-1.7.8`. Sorry, support for ruby 1.9.2 and 1.8.7 is dropped. Also, `rubunius` isn't supported since it's quite unstable (I could not even launch rails 3.2 with rbx-2.1.1).
12 |
13 | ## Features
14 | 1. Supports PostgreSQL recursive queries (requires at least `postgresql-8.3`)
15 | 2. Holds integrity control via pessimistic database locks. Common situation for `acts_as_list` users is non-unique positions within list. It happens when two concurrent users modify list sumultaneously. `acts_as_ordered_tree` uses pessimistic locks to keep your tree consistent.
16 |
17 | ## Installation
18 | Install it via rubygems:
19 |
20 | ```bash
21 | gem install acts_as_ordered_tree
22 | ```
23 |
24 | ## Usage
25 |
26 | To make use of `acts_as_ordered_tree`, your model needs to have 2 fields: parent_id and position. You can also have an optional fields: `depth` and `children_count`:
27 | ```ruby
28 | class CreateCategories < ActiveRecord::Migration
29 | def self.up
30 | create_table :categories do |t|
31 | t.integer :company_id
32 | t.string :name
33 | t.integer :parent_id # this is mandatory
34 | t.integer :position # this is mandatory
35 | t.integer :depth # this is optional
36 | t.integer :children_count # this is optional
37 | end
38 | end
39 |
40 | def self.down
41 | drop_table :categories
42 | end
43 | end
44 | ```
45 |
46 | Setup your model:
47 |
48 | ```ruby
49 | class Category < ActiveRecord::Base
50 | acts_as_ordered_tree
51 |
52 | # gem introduces new ActiveRecord callbacks:
53 | # *_reorder - fires when position (but not parent node) is changed
54 | # *_move - fires when parent node is changed
55 | before_reorder :do_smth
56 | before_move :do_smth_else
57 | end
58 | ```
59 |
60 | Now you can use `acts_as_ordered_tree` features:
61 |
62 | ```ruby
63 | # root
64 | # \_ child1
65 | # \_ subchild1
66 | # \_ subchild2
67 |
68 |
69 | root = Category.create(:name => "root")
70 | child1 = root.children.create(:name => "child1")
71 | subchild1 = child1.children.create("name" => "subchild1")
72 | subchild2 = child1.children.create("name" => "subchild2")
73 |
74 | Category.roots # => [root]
75 |
76 | root.root? # => true
77 | root.parent # => nil
78 | root.ancestors # => []
79 | root.descendants # => [child1, subchild1, subchild2]
80 | root.descendants.arrange # => {child1 => {subchild1 => {}, subchild2 => {}}}
81 | # you may pass an option to discard possible orphans from selection
82 | root.descendants.arrange(:orphans => :discard)
83 |
84 | child1.parent # => root
85 | child1.ancestors # => [root]
86 | child1.children # => [subchild1, subchild2]
87 | child1.descendants # => [subchild1, subchild2]
88 | child1.root? # => false
89 | child1.leaf? # => false
90 |
91 | subchild1.ancestors # => [child1, root]
92 | subchild1.root # => [root]
93 | subchild1.leaf? # => true
94 | subchild1.first? # => true
95 | subchild1.last? # => false
96 | subchild2.last? # => true
97 |
98 | subchild1.move_to_above_of(child1)
99 | subchild1.move_to_bottom_of(child1)
100 | subchild1.move_to_child_of(root)
101 | subchild1.move_lower
102 | subchild1.move_higher
103 | ```
104 |
105 | ## Contributing
106 |
107 | 1. Fork it
108 | 2. Create your feature branch (`git checkout -b my-new-feature`)
109 | 3. Commit your changes (`git commit -am 'Added some feature'`)
110 | 4. Push to the branch (`git push origin my-new-feature`)
111 | 5. Create new Pull Request
112 |
113 | ## TODO
114 | 1. Fix README typos and grammatical errors (english speaking contributors are welcomed)
115 | 2. Add moar examples and docs.
116 | 3. Implement converter from other structures (nested_set, closure_tree)
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 | require 'rspec/core/rake_task'
3 | require 'yaml'
4 | require 'erb'
5 |
6 | namespace :db do
7 | config_name = ENV['DBCONF'] || 'config.yml'
8 |
9 | databases = YAML.load(ERB.new(IO.read(File.join(File.dirname(__FILE__), 'spec', 'support', 'db', config_name))).result)
10 |
11 | databases.each do |name, spec|
12 | desc "Run given task for #{spec['adapter']} database"
13 | task name do
14 | with_database(name) do
15 | announce
16 | exec
17 | end
18 | end
19 | end
20 |
21 | task :all do
22 | require 'benchmark'
23 |
24 | time = Benchmark.realtime do
25 | databases.keys.each do |name|
26 | with_database(name) do
27 | announce
28 | run
29 | end
30 | end
31 | end
32 |
33 | puts
34 | puts 'Time taken: %.2f sec' % time
35 |
36 | exit
37 | end
38 |
39 | private
40 | def exec
41 | Kernel.exec(ENV, *command)
42 | end
43 |
44 | def run
45 | unless Kernel.system(ENV, *command)
46 | exit(1)
47 | end
48 | end
49 |
50 | def with_database(name)
51 | ENV['DB'] = name
52 |
53 | yield
54 | ensure
55 | ENV['DB'] = nil
56 | end
57 |
58 | def command
59 | ['bundle', 'exec', 'rake', *ARGV.slice(1, ARGV.size)]
60 | end
61 |
62 | def announce
63 | puts ">> DB=#{ENV['DB']} #{command.join(' ')}"
64 | end
65 | end
66 |
67 | desc 'Run given task for all databases'
68 | task :db => 'db:all'
69 |
70 | RSpec::Core::RakeTask.new(:spec) do |t|
71 | t.rspec_opts = '--color --format progress'
72 | end
73 |
74 |
75 | begin
76 | require 'coveralls/rake/task'
77 | Coveralls::RakeTask.new
78 | rescue LoadError, NameError
79 | task 'coveralls:push'
80 | end
81 |
82 | namespace :coverage do
83 | desc 'Turn on code coverage'
84 | task :enable do
85 | ENV['COVERAGE'] = '1' unless ENV.key?('COVERAGE')
86 | end
87 |
88 | desc 'Turn off code coverage'
89 | task :disable do
90 | ENV['COVERAGE'] = ''
91 | end
92 |
93 | desc 'Push code coverage to coveralls'
94 | task :push => 'coveralls:push'
95 | end
96 |
97 | task :spec => 'coverage:enable'
--------------------------------------------------------------------------------
/acts_as_ordered_tree.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path('../lib', __FILE__)
3 | require 'acts_as_ordered_tree/version'
4 |
5 | Gem::Specification.new do |s|
6 | s.name = 'acts_as_ordered_tree'
7 | s.version = ActsAsOrderedTree::VERSION
8 | s.authors = ['Alexei Mikhailov', 'Vladimir Kuznetsov']
9 | s.email = %w(amikhailov83@gmail.com kv86@mail.ru)
10 | s.homepage = 'https://github.com/take-five/acts_as_ordered_tree'
11 | s.summary = %q{ActiveRecord extension for sorted adjacency lists support}
12 |
13 | s.rubyforge_project = 'acts_as_ordered_tree'
14 |
15 | s.files = `git ls-files -- lib/*`.split("\n")
16 | s.test_files = `git ls-files -- spec/* features/*`.split("\n")
17 | s.require_paths = %w(lib)
18 |
19 | s.add_dependency 'activerecord', '>= 3.1.0'
20 | s.add_dependency 'activerecord-hierarchical_query', '~> 0.0.7'
21 |
22 | s.add_development_dependency 'rake', '~> 10.3.2'
23 | s.add_development_dependency 'bundler', '~> 1.5'
24 | s.add_development_dependency 'rspec', '~> 2.99.0'
25 | s.add_development_dependency 'database_cleaner', '~> 1.3.0'
26 | s.add_development_dependency 'factory_girl', '~> 4.4.0'
27 | s.add_development_dependency 'appraisal', '>= 1.0.2'
28 | end
--------------------------------------------------------------------------------
/gemfiles/rails3.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "pg", :platform => :ruby
6 | gem "sqlite3", :platform => :ruby
7 | gem "mysql2", "~> 0.3.20", :platform => :ruby
8 | gem "simplecov", :platform => :ruby
9 | gem "coveralls", :platform => :ruby
10 | gem "activerecord-jdbcpostgresql-adapter", "~> 1.2.2", :platform => :jruby
11 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.2.2", :platform => :jruby
12 | gem "activerecord", "~> 3.1.12"
13 | gem "activerecord-jdbcmysql-adapter", "~> 1.2.2", :platform => :jruby
14 |
15 | gemspec :path => "../"
16 |
--------------------------------------------------------------------------------
/gemfiles/rails3.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "pg", :platform => :ruby
6 | gem "sqlite3", :platform => :ruby
7 | gem "mysql2", "~> 0.3.20", :platform => :ruby
8 | gem "simplecov", :platform => :ruby
9 | gem "coveralls", :platform => :ruby
10 | gem "activerecord-jdbcpostgresql-adapter", "~> 1.2.2", :platform => :jruby
11 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.2.2", :platform => :jruby
12 | gem "activerecord", "~> 3.2.17"
13 | gem "activerecord-jdbcmysql-adapter", "~> 1.2.2", :platform => :jruby
14 |
15 | gemspec :path => "../"
16 |
--------------------------------------------------------------------------------
/gemfiles/rails4.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "pg", :platform => :ruby
6 | gem "sqlite3", :platform => :ruby
7 | gem "mysql2", "~> 0.3.20", :platform => :ruby
8 | gem "simplecov", :platform => :ruby
9 | gem "coveralls", :platform => :ruby
10 | gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.6", :platform => :jruby
11 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.6", :platform => :jruby
12 | gem "activerecord", "~> 4.0.3"
13 | gem "activerecord-jdbcmysql-adapter", "~> 1.3.6", :platform => :jruby
14 |
15 | gemspec :path => "../"
16 |
--------------------------------------------------------------------------------
/gemfiles/rails4.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "pg", :platform => :ruby
6 | gem "sqlite3", :platform => :ruby
7 | gem "mysql2", "~> 0.3.20", :platform => :ruby
8 | gem "simplecov", :platform => :ruby
9 | gem "coveralls", :platform => :ruby
10 | gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.6", :platform => :jruby
11 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.6", :platform => :jruby
12 | gem "activerecord", "~> 4.1.6"
13 | gem "activerecord-jdbcmysql-adapter", "~> 1.3.6", :platform => :jruby
14 |
15 | gemspec :path => "../"
16 |
--------------------------------------------------------------------------------
/gemfiles/rails4.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "pg", :platform => :ruby
6 | gem "sqlite3", :platform => :ruby
7 | gem "mysql2", "~> 0.4.2", :platform => :ruby
8 | gem "simplecov", :platform => :ruby
9 | gem "coveralls", :platform => :ruby
10 | gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.19", :platform => :jruby
11 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.19", :platform => :jruby
12 | gem "activerecord", "~> 4.2.0"
13 | gem "activerecord-jdbcmysql-adapter", "~> 1.3.19", :platform => :jruby
14 |
15 | gemspec :path => "../"
16 |
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree.rb:
--------------------------------------------------------------------------------
1 | require 'acts_as_ordered_tree/version'
2 | require 'active_support/lazy_load_hooks'
3 |
4 | module ActsAsOrderedTree
5 | autoload :Tree, 'acts_as_ordered_tree/tree'
6 |
7 | # @!attribute [r] ordered_tree
8 | # @return [ActsAsOrderedTree::Tree] ordered tree object
9 |
10 | # == Usage
11 | # class Category < ActiveRecord::Base
12 | # acts_as_ordered_tree :parent_column => :parent_id,
13 | # :position_column => :position,
14 | # :depth_column => :depth,
15 | # :counter_cache => :children_count
16 | # end
17 | def acts_as_ordered_tree(options = {})
18 | Tree.setup!(self, options)
19 | end
20 |
21 | # @api private
22 | def self.extended(base)
23 | base.class_attribute :ordered_tree, :instance_writer => false
24 | end
25 |
26 | # Rebuild ordered tree structure for subclasses. It needs to be rebuilt
27 | # mainly because of :children and :parent associations, which are created
28 | # with option :class_name. It matters for class hierarchies without STI,
29 | # they can't work properly with associations inherited from superclass.
30 | #
31 | # @api private
32 | def inherited(subclass)
33 | super
34 |
35 | subclass.acts_as_ordered_tree(ordered_tree.options) if ordered_tree?
36 | end
37 | end # module ActsAsOrderedTree
38 |
39 | ActiveSupport.on_load(:active_record) do
40 | extend ActsAsOrderedTree
41 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/adapters.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'active_support/hash_with_indifferent_access'
4 | require 'acts_as_ordered_tree/adapters/recursive'
5 | require 'acts_as_ordered_tree/adapters/postgresql'
6 |
7 | module ActsAsOrderedTree
8 | module Adapters
9 | # adapters map
10 | ADAPTERS = HashWithIndifferentAccess['PostgreSQL' => PostgreSQL]
11 | ADAPTERS.default = Recursive
12 |
13 | def self.lookup(name)
14 | ADAPTERS[name]
15 | end
16 | end
17 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/adapters/abstract.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | module Adapters
5 | class Abstract
6 | attr_reader :tree
7 |
8 | # @param [ActsAsOrderedTree::Tree] tree
9 | def initialize(tree)
10 | @tree = tree
11 | end
12 |
13 | protected
14 | def preloaded(records)
15 | tree.klass.where(nil).extending(Relation::Preloaded).records(records)
16 | end
17 |
18 | def none
19 | tree.klass.where(nil).none
20 | end
21 | end # class Abstract
22 | end # module Adapters
23 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/adapters/postgresql.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'active_record/hierarchical_query'
4 |
5 | require 'acts_as_ordered_tree/adapters/abstract'
6 | require 'acts_as_ordered_tree/relation/preloaded'
7 |
8 | module ActsAsOrderedTree
9 | module Adapters
10 | # PostgreSQL adapter implements traverse operations with CTEs
11 | class PostgreSQL < Abstract
12 | attr_reader :tree
13 |
14 | delegate :columns, :to => :tree
15 | delegate :quote_column_name, :to => 'tree.klass.connection'
16 |
17 | def self_and_descendants(node, &block)
18 | traverse_down(node) do
19 | descendants_scope(node.ordered_tree_node, &block)
20 | end
21 | end
22 |
23 | def descendants(node, &block)
24 | traverse_down(node) do
25 | without(node) { self_and_descendants(node, &block) }
26 | end
27 | end
28 |
29 | def self_and_ancestors(node, &block)
30 | traverse_up(node, [node]) do
31 | ancestors_scope(node.ordered_tree_node, &block)
32 | end
33 | end
34 |
35 | def ancestors(node, &block)
36 | traverse_up(node) do
37 | without(node) { self_and_ancestors(node, &block) }
38 | end
39 | end
40 |
41 | private
42 | def without(node)
43 | scope = yield
44 | scope.where(scope.table[columns.id].not_eq(node.id))
45 | end
46 |
47 | def traverse_down(node)
48 | if node && node.persisted?
49 | yield
50 | else
51 | none
52 | end
53 | end
54 |
55 | # Yields to block if record is persisted and its parent was not changed.
56 | # Returns empty scope (or scope with +including+ records) if record is root.
57 | # Otherwise recursively fetches ancestors and returns preloaded relation.
58 | def traverse_up(node, including = [])
59 | return none unless node
60 |
61 | if can_traverse_up?(node)
62 | if node.ordered_tree_node.has_parent?
63 | yield
64 | else
65 | including.empty? ? none : preloaded(including)
66 | end
67 | else
68 | preloaded(persisted_ancestors(node) + including)
69 | end
70 | end
71 |
72 | # Generates scope that traverses tree down to deep, starting from given +scope+
73 | def descendants_scope(node)
74 | node.scope.join_recursive do |query|
75 | query.connect_by(join_columns(columns.id => columns.parent))
76 | .start_with(node.to_relation)
77 |
78 | yield query if block_given?
79 |
80 | query.order_siblings(position)
81 | end
82 | end
83 |
84 | # Generates scope that traverses tree up to root, starting from given +scope+
85 | def ancestors_scope(node, &block)
86 | if columns.depth?
87 | build_ancestors_query(node, &block).reorder(depth)
88 | else
89 | build_ancestors_query(node) do |query|
90 | query.start_with { |start| start.select Arel.sql('0').as('__depth') }
91 | .select(query.prior['__depth'] - 1, :start_with => false)
92 |
93 | yield query if block_given?
94 | end.reorder('__depth')
95 | end
96 | end
97 |
98 | def build_ancestors_query(node)
99 | node.scope.join_recursive do |query|
100 | query.connect_by(join_columns(columns.parent => columns.id))
101 | .start_with(node.to_relation)
102 |
103 | yield query if block_given?
104 | end
105 | end
106 |
107 | def attribute(name)
108 | @tree.klass.arel_table[name]
109 | end
110 |
111 | def depth
112 | attribute(columns.depth)
113 | end
114 |
115 | def position
116 | attribute(columns.position)
117 | end
118 |
119 | def can_traverse_up?(node)
120 | node.persisted? && !node.ordered_tree_node.parent_id_changed?
121 | end
122 |
123 | # Recursively fetches node's parents until one of them will be persisted.
124 | # Returns persisted ancestor and array of non-persistent ancestors
125 | def persisted_ancestors(node)
126 | queue = []
127 |
128 | parent = node
129 |
130 | while (parent = parent.parent)
131 | break if parent && parent.persisted?
132 |
133 | queue.unshift(parent)
134 | end
135 |
136 | ancestors(parent) + [parent].compact + queue
137 | end
138 |
139 | def scope_columns_hash
140 | Hash[tree.columns.scope.map { |x| [x, x] }]
141 | end
142 |
143 | def join_columns(hash)
144 | scope_columns_hash.merge(hash).each_with_object({}) do |(k, v), h|
145 | h[k.to_sym] = v.to_sym
146 | end
147 | end
148 | end # class PostgreSQL
149 | end # module Adapters
150 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/adapters/recursive.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/adapters/abstract'
4 |
5 | module ActsAsOrderedTree
6 | module Adapters
7 | # Recursive adapter implements tree traversal in pure Ruby.
8 | class Recursive < Abstract
9 | def self_and_ancestors(node, &block)
10 | return none unless node
11 |
12 | ancestors_scope(node, :include_first => true, &block)
13 | end
14 |
15 | def ancestors(node, &block)
16 | ancestors_scope(node, :include_first => false, &block)
17 | end
18 |
19 | def descendants(node, &block)
20 | descendants_scope(node, :include_first => false, &block)
21 | end
22 |
23 | def self_and_descendants(node, &block)
24 | descendants_scope(node, :include_first => true, &block)
25 | end
26 |
27 | private
28 | def ancestors_scope(node, options, &block)
29 | traversal = Traversal.new(node, options, &block)
30 | traversal.follow :parent
31 | traversal.to_scope.reverse_order!
32 | end
33 |
34 | def descendants_scope(node, options, &block)
35 | return none unless node.persisted?
36 |
37 | traversal = Traversal.new(node, options, &block)
38 | traversal.follow :children
39 | traversal.to_scope
40 | end
41 |
42 | class Traversal
43 | delegate :klass, :to => :@start_record
44 | attr_accessor :include_first
45 |
46 | def initialize(start_record, options = {})
47 | @start_record = start_record
48 | @start_with = nil
49 | @order_values = []
50 | @where_values = []
51 | @include_first = options[:include_first]
52 | follow(options[:follow]) if options.key?(:follow)
53 |
54 | yield self if block_given?
55 | end
56 |
57 | def follow(association_name)
58 | @association = association_name
59 |
60 | self
61 | end
62 |
63 | def start_with(scope = nil, &block)
64 | @start_with = scope || block
65 |
66 | self
67 | end
68 |
69 | def order_siblings(*values)
70 | @order_values << values
71 |
72 | self
73 | end
74 | alias_method :order, :order_siblings
75 |
76 | def where(*values)
77 | @where_values << values
78 |
79 | self
80 | end
81 |
82 | def table
83 | klass.arel_table
84 | end
85 |
86 | def klass
87 | @start_record.class
88 | end
89 |
90 | def to_scope
91 | null_scope.records(to_enum.to_a)
92 | end
93 |
94 | private
95 | def each(&block)
96 | return unless validate_start_conditions
97 |
98 | yield @start_record if include_first
99 |
100 | expand(@start_record, &block)
101 | end
102 |
103 | def validate_start_conditions
104 | start_scope ? start_scope.exists? : true
105 | end
106 |
107 | def start_scope
108 | return nil unless @start_with
109 |
110 | if @start_with.is_a?(Proc)
111 | @start_with.call klass.where(klass.primary_key => @start_record.id)
112 | else
113 | @start_with
114 | end
115 | end
116 |
117 | def expand(record, &block)
118 | expand_association(record).each do |child|
119 | yield child
120 |
121 | expand(child, &block)
122 | end
123 | end
124 |
125 | def expand_association(record)
126 | if constraints?
127 | build_scope(record)
128 | else
129 | follow_association(record)
130 | end
131 | end
132 |
133 | def build_scope(record)
134 | scope = record.association(@association).scope
135 |
136 | @where_values.each { |v| scope = scope.where(*v) }
137 | scope = scope.except(:order).order(*@order_values.flatten) if @order_values.any?
138 |
139 | scope
140 | end
141 |
142 | def follow_association(record)
143 | Array.wrap(record.send(@association))
144 | end
145 |
146 | def null_scope
147 | klass.where(nil).extending(Relation::Preloaded)
148 | end
149 |
150 | def constraints?
151 | @where_values.any? || @order_values.any?
152 | end
153 | end
154 | private_constant :Traversal
155 | end # class Recursive
156 | end # module Adapters
157 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/compatibility.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/compatibility/features'
4 |
5 | module ActsAsOrderedTree
6 | # Since we support multiple Rails versions, we need to turn on some features
7 | # for old Rails versions.
8 | #
9 | # @api private
10 | module Compatibility
11 | features do
12 | scope :active_record do
13 | versions '< 4.0.0' do
14 | feature :association_scope
15 | feature :null_relation
16 | end
17 |
18 | feature :default_scoped, '< 4.1.0'
19 | end
20 | end
21 | end
22 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/compatibility/active_record/association_scope.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 | module Associations
3 | class Association
4 | def scope
5 | scoped
6 | end
7 | end
8 | end
9 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/compatibility/active_record/default_scoped.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 | module Scoping
3 | module Default
4 | module ClassMethods
5 | # default_scoped is a new method from Rails 4.1.
6 | # Used in RecursiveRelation
7 | def default_scoped
8 | scope = relation.merge(send(:build_default_scope))
9 | scope.default_scoped = true
10 | scope
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
17 | ActsAsOrderedTree::Compatibility.version '< 3.2.0' do
18 | ActiveRecord::Base.extend(ActiveRecord::Scoping::Default::ClassMethods)
19 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/compatibility/active_record/null_relation.rb:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | module ActiveRecord
4 | module NullRelation # :nodoc:
5 | def exec_queries
6 | @records = []
7 | end
8 |
9 | def to_a
10 | []
11 | end
12 |
13 | def pluck(*column_names)
14 | []
15 | end
16 |
17 | def delete_all(_conditions = nil)
18 | 0
19 | end
20 |
21 | def update_all(_updates, _conditions = nil, _options = {})
22 | 0
23 | end
24 |
25 | def delete(_id_or_array)
26 | 0
27 | end
28 |
29 | def size
30 | 0
31 | end
32 |
33 | def empty?
34 | true
35 | end
36 |
37 | def any?
38 | false
39 | end
40 |
41 | def many?
42 | false
43 | end
44 |
45 | def to_sql
46 | @to_sql ||= ""
47 | end
48 |
49 | def count(*)
50 | 0
51 | end
52 |
53 | def sum(*)
54 | 0
55 | end
56 |
57 | def calculate(_operation, _column_name, _options = {})
58 | nil
59 | end
60 |
61 | def exists?(_id = false)
62 | false
63 | end
64 | end
65 |
66 | class Relation
67 | def none
68 | extending(NullRelation)
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/compatibility/features.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'tsort'
4 |
5 | require 'active_support/core_ext/string/inflections'
6 | require 'active_support/core_ext/array/wrap'
7 |
8 | module ActsAsOrderedTree
9 | module Compatibility
10 | UnknownFeature = Class.new(StandardError)
11 |
12 | class Feature
13 | class Version
14 | def initialize(operator, version = nil)
15 | operator, version = operator.split unless version
16 | operator, version = '=', operator unless version
17 | operator = '==' if operator == '='
18 |
19 | @operator, @version = operator, version.to_s
20 | end
21 |
22 | def matches?
23 | ActiveRecord::VERSION::STRING.send(@operator, @version)
24 | end
25 |
26 | def to_s
27 | [@operator, @version].join(' ')
28 | end
29 | end
30 |
31 | attr_reader :name, :versions, :prerequisites
32 |
33 | def initialize(name, versions, prerequisites)
34 | @name = name.to_s
35 | @versions = Array.wrap(versions).map { |v| Version.new(v) }
36 | @prerequisites = Array.wrap(prerequisites)
37 | end
38 |
39 | # Requires dependency
40 | def require
41 | Kernel.require(path) if @versions.all?(&:matches?)
42 | end
43 |
44 | private
45 | def path
46 | "acts_as_ordered_tree/compatibility/#{name}"
47 | end
48 | end
49 |
50 | class DependencyTree
51 | include TSort
52 |
53 | def initialize
54 | @features = Hash.new
55 | end
56 |
57 | def require
58 | @features.each_value(&:require)
59 | end
60 |
61 | def <<(feature)
62 | feature.prerequisites.each do |pre|
63 | unless @features.key?(pre.to_s)
64 | @features[pre.to_s] = Feature.new(pre, feature.versions.map(&:to_s), [])
65 | end
66 | end
67 |
68 | @features[feature.name] = feature
69 | end
70 |
71 | def [](name)
72 | @features[name.to_s]
73 | end
74 |
75 | def each_dependency(name, &block)
76 | raise UnknownFeature, "Unknown compatibility feature #{name}" unless @features.key?(name.to_s)
77 |
78 | each_strongly_connected_component_from(name.to_s, &block)
79 | end
80 |
81 | private
82 | def tsort_each_node(&block)
83 | @features.each_key(&block)
84 | end
85 |
86 | def tsort_each_child(node, &block)
87 | @features[node].prerequisites.each(&block) if @features[node]
88 | end
89 | end
90 |
91 | class DependencyTreeBuilder
92 | attr_reader :tree
93 |
94 | def initialize
95 | @tree = DependencyTree.new
96 | @default_versions = nil
97 | @prerequisites = []
98 | @scope = ''
99 | end
100 |
101 | def versions(*versions, &block)
102 | @default_versions = versions
103 |
104 | instance_eval(&block)
105 | ensure
106 | @default_versions = nil
107 | end
108 | alias_method :version, :versions
109 |
110 | def scope(name, &block)
111 | if name.is_a?(Hash)
112 | @scope = name.keys.first.to_s
113 | @prerequisites = Array.wrap(name.values.first)
114 | else
115 | @scope = name.to_s
116 | end
117 |
118 | instance_eval(&block)
119 | ensure
120 | @scope = ''
121 | @prerequisites = []
122 | end
123 |
124 | def feature(name, options = {})
125 | @tree << if name.is_a?(Hash)
126 | version = name.delete(:versions) || @default_versions
127 | name, prereq = *name.first
128 |
129 | prereq = @prerequisites + Array.wrap(prereq).map { |x| [@scope, x].join('/') }
130 |
131 | Feature.new([@scope, name].join('/'), version, prereq)
132 | else
133 | version = options.is_a?(Hash) ? options.delete(:versions) : options
134 | Feature.new([@scope, name].join('/'), version || @default_versions, @prerequisites)
135 | end
136 | end
137 | end
138 |
139 | module DSL
140 | def features(&block)
141 | builder = DependencyTreeBuilder.new
142 | builder.instance_eval(&block)
143 | builder.tree.require
144 | end
145 |
146 | def version(*versions)
147 | versions = versions.map { |v| Feature::Version.new(*v) }
148 | yield if versions.all?(&:matches?)
149 | end
150 | end
151 | extend DSL
152 | end
153 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/deprecate.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | # @api private
5 | module Deprecate
6 | NEXT_VERSION = '2.1'
7 |
8 | def deprecated_method(method, replacement = nil, &block)
9 | define_method(method) do |*args, &method_block|
10 | message = "#{self.class.name}##{__method__} is "\
11 | "deprecated and will be removed in acts_as_ordered_tree-#{NEXT_VERSION}"
12 | message << ", use ##{replacement} instead" if replacement
13 |
14 | ActiveSupport::Deprecation.warn message, caller(2)
15 |
16 | if block
17 | instance_exec(*args, &block)
18 | elsif replacement
19 | __send__(replacement, *args, &method_block)
20 | end
21 | end
22 | end
23 | end
24 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/hooks.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'active_support/concern'
4 |
5 | require 'acts_as_ordered_tree/hooks/update'
6 |
7 | module ActsAsOrderedTree
8 | # Included into AR::Base this module allows to intercept
9 | # internal AR calls, such as +create_record+ and execute
10 | # patched code.
11 | #
12 | # Hooks intention is to execute well optimized INSERTs and
13 | # UPDATEs at certain cases.
14 | #
15 | # @example
16 | # class Category < ActiveRecord::Base
17 | # include ActsAsOrderedTree::Hooks
18 | # end
19 | #
20 | # category.hook_update do |update|
21 | # update.scope = category.parent.children
22 | # update.values = {:counter => Category.arel_table[:counter] + 1}
23 | #
24 | # # all callbacks, including :before_save and :after_save will
25 | # # be invoked, but patched UPDATE will be called instead of
26 | # # original AR `ActiveRecord::Persistence#update_record`
27 | # category.save
28 | # end
29 | #
30 | # @api private
31 | module Hooks
32 | extend ActiveSupport::Concern
33 |
34 | included do
35 | include Update
36 | end
37 | end
38 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/hooks/update.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'active_support/concern'
4 |
5 | module ActsAsOrderedTree
6 | module Hooks
7 | # This AR-hook is used in Move transactions to update parent_id, position
8 | # and other changed attributes using single SQL-query.
9 | #
10 | # @example
11 | # class Category < ActiveRecord::Base
12 | # include ActsAsOrderedTree::Hooks
13 | # end
14 | #
15 | # category = Category.first
16 | # category.hook_update do |update|
17 | # update.scope = Category.where(:parent_id => category.parent_id)
18 | # update.values = { :name => Arel.sql('CASE WHEN parent_id IS NULL THEN name ELSE name || name END') }
19 | #
20 | # # `update.update!` will be called instead of usual `AR::Persistence#update`
21 | # record.save
22 | # end
23 | #
24 | # @api private
25 | module Update
26 | extend ActiveSupport::Concern
27 |
28 | included do
29 | attr_accessor :__update_hook
30 |
31 | # Since rails 4.0 :update_record is used for actual updates
32 | # Since rails 4.0.x and 4.1.x (i really don't know which is x) :_update_record is used
33 | method_name = [:update_record, :_update_record].detect { |m| private_method_defined?(m) } || :update
34 |
35 | alias_method :update_without_hook, method_name
36 | alias_method method_name, :update_with_hook
37 | end
38 |
39 | def hook_update
40 | self.__update_hook = UpdateManager.new(self)
41 | yield __update_hook
42 | ensure
43 | self.__update_hook = nil
44 | end
45 |
46 | private
47 | def update_with_hook(*args)
48 | if __update_hook
49 | __update_hook.update!
50 | else
51 | update_without_hook(*args)
52 | end
53 | end
54 |
55 | class UpdateManager
56 | attr_reader :record
57 | attr_accessor :scope, :values
58 |
59 | def initialize(record)
60 | @record = record
61 | @values = {}
62 | end
63 |
64 | def update!
65 | scope.update_all(to_sql)
66 | record.reload
67 | end
68 |
69 | private
70 | def to_sql
71 | values.keys.map do |attr|
72 | name = attr.is_a?(Arel::Attributes::Attribute) ? attr.name : attr.to_s
73 |
74 | quoted = record.class.connection.quote_column_name(name)
75 | "#{quoted} = (#{value_of(attr)})"
76 | end.join(', ')
77 | end
78 |
79 | def value_of(attr)
80 | value = values[attr]
81 | value.respond_to?(:to_sql) ? value.to_sql : record.class.connection.quote(value)
82 | end
83 | end # class CustomUpdate
84 | end # module Update
85 | end # module Hooks
86 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/instance_methods.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/node'
4 | require 'acts_as_ordered_tree/transaction/factory'
5 | require 'acts_as_ordered_tree/deprecate'
6 |
7 | module ActsAsOrderedTree
8 | module InstanceMethods
9 | extend Deprecate
10 |
11 | delegate :root?,
12 | :leaf?,
13 | :has_children?,
14 | :has_parent?,
15 | :first?,
16 | :last?,
17 | :is_descendant_of?,
18 | :is_or_is_descendant_of?,
19 | :is_ancestor_of?,
20 | :is_or_is_ancestor_of?,
21 | :to => :ordered_tree_node
22 |
23 | delegate :move_to_root,
24 | :move_higher,
25 | :move_left,
26 | :move_lower,
27 | :move_right,
28 | :move_to_above_of,
29 | :move_to_left_of,
30 | :move_to_bottom_of,
31 | :move_to_right_of,
32 | :move_to_child_of,
33 | :move_to_child_with_index,
34 | :move_to_child_with_position,
35 | :to => :ordered_tree_node
36 |
37 | delegate :ancestors,
38 | :self_and_ancestors,
39 | :descendants,
40 | :self_and_descendants,
41 | :root,
42 | :siblings,
43 | :self_and_siblings,
44 | :left_siblings,
45 | :higher_items,
46 | :left_sibling,
47 | :higher_item,
48 | :left_sibling=,
49 | :left_sibling_id=,
50 | :higher_item=,
51 | :higher_item_id=,
52 | :right_siblings,
53 | :lower_items,
54 | :right_sibling,
55 | :lower_item,
56 | :right_sibling=,
57 | :right_sibling_id=,
58 | :lower_item=,
59 | :lower_item_id=,
60 | :to => :ordered_tree_node
61 |
62 | delegate :level, :to => :ordered_tree_node
63 |
64 | # Returns ordered tree node - an object which maintains tree integrity.
65 | # WARNING: THIS METHOD IS NOT THREAD SAFE!
66 | # Though I'm not sure if it can cause any problems.
67 | #
68 | # @return [ActsAsOrderedTree::Node]
69 | def ordered_tree_node
70 | @ordered_tree_node ||= ActsAsOrderedTree::Node.new(self)
71 | end
72 |
73 | # Insert the item at the given position (defaults to the top position of 1).
74 | # +acts_as_list+ compatibility
75 | #
76 | # @deprecated
77 | deprecated_method :insert_at, :move_to_child_with_position do |position = 1|
78 | move_to_child_with_position(parent, position)
79 | end
80 |
81 | # Returns +true+ if it is possible to move node to left/right/child of +target+.
82 | #
83 | # @param [ActiveRecord::Base] target
84 | # @deprecated
85 | deprecated_method :move_possible? do |target|
86 | ordered_tree_node.same_scope?(target) &&
87 | !ordered_tree_node.is_or_is_ancestor_of?(target)
88 | end
89 |
90 | # Returns true if node contains any children.
91 | #
92 | # @deprecated
93 | deprecated_method :branch?, :has_children?
94 |
95 | # Returns true is node is not a root node.
96 | #
97 | # @deprecated
98 | deprecated_method :child?, :has_parent?
99 |
100 | private
101 | # Around callback that starts ActsAsOrderedTree::Transaction
102 | def save_ordered_tree_node(&block)
103 | Transaction::Factory.create(ordered_tree_node).start(&block)
104 | end
105 |
106 | # Around callback that starts ActsAsOrderedTree::Transaction
107 | def destroy_ordered_tree_node(&block)
108 | Transaction::Factory.create(ordered_tree_node, true).start(&block)
109 | end
110 | end # module InstanceMethods
111 | end # module ActsAsOrderedTree
112 |
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/iterators/arranger.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | module Iterators
5 | # @api private
6 | class Arranger
7 | include Enumerable
8 |
9 | delegate :each, :to => :arrange
10 |
11 | def initialize(collection)
12 | @collection = collection
13 | @cache = Hash.new
14 | end
15 |
16 | def arrange
17 | @collection.each_with_object(Hash.new) do |node, result|
18 | @cache[node.id] ||= node
19 |
20 | insertion_point = result
21 |
22 | ancestors(node).each { |a| insertion_point = (insertion_point[a] ||= {}) }
23 |
24 | insertion_point[node] = {}
25 | end
26 | end
27 |
28 | private
29 | def ancestors(node)
30 | parent = @cache[node.ordered_tree_node.parent_id]
31 | parent ? ancestors(parent) + [parent] : []
32 | end
33 | end # class Arranger
34 | end # module Iterators
35 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/iterators/level_calculator.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | module Iterators
5 | # @api private
6 | class LevelCalculator
7 | include Enumerable
8 |
9 | def initialize(collection)
10 | @collection = collection
11 | @level = nil # minimal nodes level (first item level)
12 | end
13 |
14 | def each(&block)
15 | return to_enum unless block_given?
16 |
17 | if @collection.klass.ordered_tree.columns.depth?
18 | each_with_cached_level(&block)
19 | else
20 | each_without_cached_level(&block)
21 | end
22 | end
23 |
24 | private
25 | def each_with_cached_level
26 | @collection.each { |node| yield node, node.level }
27 | end
28 |
29 | def each_without_cached_level
30 | path = []
31 |
32 | @collection.each do |node|
33 | parent_id = node.ordered_tree_node.parent_id
34 |
35 | @level ||= node.level
36 | path << parent_id if path.empty?
37 |
38 | if parent_id != path.last
39 | # parent changed
40 | if path.include?(parent_id) # ascend
41 | path.pop while path.last != parent_id
42 | else # descend
43 | path << parent_id
44 | end
45 | end
46 |
47 | yield node, @level + path.length - 1
48 | end
49 | end
50 | end # class LevelCalculator
51 | end
52 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/iterators/orphans_pruner.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | module Iterators
5 | # @api private
6 | class OrphansPruner
7 | include Enumerable
8 |
9 | def initialize(collection)
10 | @collection = collection
11 | @cache = Hash.new
12 | @level = nil # minimal node level
13 | end
14 |
15 | def each
16 | return to_enum unless block_given?
17 |
18 | prepare if @cache.empty?
19 |
20 | @collection.each do |node|
21 | if orphan?(node)
22 | discard(node.id)
23 | else
24 | yield node
25 | end
26 | end
27 | end
28 |
29 | private
30 | def orphan?(node)
31 | !has_parent?(node) && node.level > @level
32 | end
33 |
34 | def has_parent?(node)
35 | @cache.key?(node.ordered_tree_node.parent_id)
36 | end
37 |
38 | def prepare
39 | @collection.each do |node|
40 | @cache[node.id] = []
41 |
42 | if has_parent?(node)
43 | @cache[node.ordered_tree_node.parent_id] << node.id
44 | else
45 | @level = [@level, node.level].compact.min
46 | end
47 | end
48 | end
49 |
50 | def discard(id)
51 | if @cache.key?(id)
52 | @cache[id].each { |k| discard(k) }
53 | @cache.delete(id)
54 | end
55 | end
56 | end # class OrphansPruner
57 | end # module Iterators
58 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/node.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/node/attributes'
4 | require 'acts_as_ordered_tree/node/movements'
5 | require 'acts_as_ordered_tree/node/predicates'
6 | require 'acts_as_ordered_tree/node/reloading'
7 | require 'acts_as_ordered_tree/node/siblings'
8 | require 'acts_as_ordered_tree/node/traversals'
9 |
10 | module ActsAsOrderedTree
11 | # ActsAsOrderedTree::Node takes care of tree integrity when record is saved
12 | # via usual ActiveRecord mechanism
13 | class Node
14 | include Attributes
15 | include Movements
16 | include Predicates
17 | include Reloading
18 | include Siblings
19 | include Traversals
20 |
21 | # @attr_reader [ActiveRecord::Base] original AR record, created, updated or destroyed
22 | attr_reader :record
23 |
24 | delegate :id, :parent, :children, :==, :to => :record
25 |
26 | def initialize(record)
27 | @record = record
28 | end
29 |
30 | # Returns scope to which record should be applied
31 | def scope
32 | if tree.columns.scope?
33 | tree.base_class.where Hash[tree.columns.scope.map { |column| [column, record[column]] }]
34 | else
35 | tree.base_class.where(nil)
36 | end
37 | end
38 |
39 | # Convert node to AR::Relation
40 | #
41 | # @return [ActiveRecord::Relation]
42 | def to_relation
43 | scope.where(tree.columns.id => id)
44 | end
45 |
46 | # @return [ActsAsOrderedTree::Tree]
47 | def tree
48 | record.class.ordered_tree
49 | end
50 |
51 | # Returns node level value (0 for root)
52 | #
53 | # @return [Fixnum]
54 | def level
55 | case
56 | when root? then 0
57 | when depth_column_could_be_used? then depth
58 | when parent_association_loaded? then parent.level + 1
59 | # @todo move it adapters
60 | else ancestors.size
61 | end
62 | end
63 |
64 | private
65 | # @return [Arel::Table]
66 | def table
67 | record.class.arel_table
68 | end
69 |
70 | def depth_column_could_be_used?
71 | tree.columns.depth? && record.persisted? && !parent_id_changed? && depth?
72 | end
73 |
74 | def parent_association_loaded?
75 | record.association(:parent).loaded?
76 | end
77 | end # class Node
78 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/node/attributes.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'active_support/concern'
4 |
5 | module ActsAsOrderedTree
6 | class Node
7 | # This module when included creates accessor to record's attributes that related to tree structure
8 | #
9 | # @example
10 | # class Node
11 | # include Attributes
12 | # end
13 | #
14 | # node = Node.new(record)
15 | # node.parent_id # => record.parent_id
16 | # node.position # => record.position
17 | # node.position_was # => record.position_was
18 | # # etc.
19 | module Attributes
20 | extend ActiveSupport::Concern
21 |
22 | METHODS = ['', '?', ?=, '_was', '_changed?', %w(reset_ !)].freeze
23 |
24 | included do
25 | dynamic_attribute_accessor :position
26 | dynamic_attribute_accessor :parent_id, :parent
27 | dynamic_attribute_accessor :depth
28 | dynamic_attribute_accessor :counter_cache
29 | end
30 |
31 | module ClassMethods
32 | # Generates methods based on configurable record attributes
33 | #
34 | # @api private
35 | def dynamic_attribute_accessor(name, column_name_accessor = name)
36 | METHODS.each do |prefix, suffix|
37 | prefix, suffix = suffix, prefix unless suffix
38 | method_name = "#{prefix}#{name}#{suffix}"
39 |
40 | define_method method_name do |*args|
41 | record.send "#{prefix}#{tree.columns[column_name_accessor]}#{suffix}", *args
42 | end
43 | end
44 | end
45 | end # module ClassMethods
46 | end # module Attributes
47 | end # class Node
48 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/node/movement.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/persevering_transaction'
4 |
5 | module ActsAsOrderedTree
6 | class Node
7 | # @api private
8 | class Movement
9 | attr_reader :node, :options
10 |
11 | delegate :record, :position, :position=, :to => :node
12 |
13 | def initialize(node, target = nil, options = {}, &block)
14 | @node, @options, @block = node, options, block
15 | @_target = target
16 | end
17 |
18 | def parent=(id_or_record)
19 | if id_or_record.is_a?(ActiveRecord::Base)
20 | record.parent = id_or_record
21 | else
22 | node.parent_id = id_or_record
23 | end
24 | end
25 |
26 | def start
27 | transaction do
28 | record.reload
29 |
30 | @block[self] if @block
31 |
32 | record.save
33 | end
34 | end
35 |
36 | def target
37 | return @target if defined?(@target)
38 |
39 | # load target
40 | @target = case @_target
41 | when ActiveRecord::Base, nil
42 | @_target.lock!
43 | when nil
44 | nil
45 | else
46 | scope = node.scope.lock
47 |
48 | if options.fetch(:strict, false)
49 | scope.find(@_target)
50 | else
51 | scope.where(:id => @_target).first
52 | end
53 | end.try(:ordered_tree_node)
54 | end
55 |
56 | private
57 | def transaction(&block)
58 | PerseveringTransaction.new(record.class.connection).start(&block)
59 | end
60 | end # class Movement
61 | end # class Node
62 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/node/movements.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'active_support/core_ext/module/aliasing'
4 |
5 | require 'acts_as_ordered_tree/node/movement'
6 |
7 | module ActsAsOrderedTree
8 | class Node
9 | # This module provides node with movement functionality
10 | #
11 | # Methods:
12 | # * move_to_root
13 | # * move_left (move_higher)
14 | # * move_right (move_lower)
15 | # * move_to_left_of (move_to_above_of)
16 | # * move_to_right_of (move_to_bottom_of)
17 | # * move_to_child_of
18 | # * move_to_child_with_index
19 | # * move_to_child_with_position
20 | module Movements
21 | # Transform node into root node
22 | def move_to_root
23 | movement do |to|
24 | to.position = record.root? ? position : nil
25 | to.parent = nil
26 | end
27 | end
28 |
29 | # Swap node with higher sibling
30 | def move_higher
31 | movement { self.position -= 1 }
32 | end
33 | alias_method :move_left, :move_higher
34 |
35 | # Swap node with lower sibling
36 | def move_lower
37 | movement { self.position += 1 }
38 | end
39 | alias_method :move_right, :move_lower
40 |
41 | # Move node to above(left) of another node
42 | #
43 | # @param [ActiveRecord::Base, #to_i] node may be another record of ID
44 | def move_to_above_of(node)
45 | movement(node, :strict => true) do |to|
46 | self.right_sibling = to.target.record
47 | end
48 | end
49 | alias_method :move_to_left_of, :move_to_above_of
50 |
51 | # Move node to bottom (right) of another node
52 | #
53 | # @param [ActiveRecord::Base, #to_i] node may be another record of ID
54 | def move_to_bottom_of(node)
55 | movement(node, :strict => true) do |to|
56 | self.left_sibling = to.target.record
57 | end
58 | end
59 | alias_method :move_to_right_of, :move_to_bottom_of
60 |
61 | # Move node to child of another node
62 | #
63 | # @param [ActiveRecord::Base, #to_i] node may be another record of ID
64 | def move_to_child_of(node)
65 | if node
66 | movement(node) do |to|
67 | to.parent = node
68 | to.position = nil if parent_id_changed?
69 | end
70 | else
71 | move_to_root
72 | end
73 | end
74 |
75 | # Move node to child of another node with specified index (which may be negative)
76 | #
77 | # @param [ActiveRecord::Base, Fixnum] node
78 | # @param [#to_i] index
79 | def move_to_child_with_index(node, index)
80 | index = index.to_i
81 |
82 | if index >= 0
83 | move_to_child_with_position node, index + 1
84 | elsif node
85 | movement(node, :strict => true) do |to|
86 | to.parent = node
87 | to.position = to.target.children.size + index + 1
88 | end
89 | else
90 | move_to_child_with_position nil, scope.roots.size + index + 1
91 | end
92 | end
93 |
94 | # Move node to child of another node with specified position
95 | #
96 | # @param [ActiveRecord::Base, Fixnum] node
97 | # @param [Integer, nil] position
98 | def move_to_child_with_position(node, position)
99 | movement(node) do |to|
100 | to.parent = node
101 | to.position = position
102 | end
103 | end
104 |
105 | private
106 | def movement(target = nil, options = {}, &block)
107 | Movement.new(self, target, options, &block).start
108 | end
109 | end # module Movements
110 | end # class Node
111 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/node/predicates.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | class Node
5 | module Predicates
6 | # Returns true if this is a root node.
7 | def root?
8 | !parent_id?
9 | end
10 |
11 | # Returns true if this is the end of a branch.
12 | def leaf?
13 | record.persisted? && if children.loaded? || tree.columns.counter_cache?
14 | # no SQL-queries here
15 | children.empty?
16 | else
17 | !children.exists?
18 | end
19 | end
20 |
21 | # Returns true if node contains any children.
22 | def has_children?
23 | !leaf?
24 | end
25 |
26 | # Returns true is node is not a root node.
27 | def has_parent?
28 | !root?
29 | end
30 |
31 | # Returns true if current node is descendant of +other+ node.
32 | #
33 | # @param [ActiveRecord::Base] other
34 | def is_descendant_of?(other)
35 | same_scope?(other) &&
36 | ancestors.include?(other) &&
37 | ancestors.all? { |x| x.ordered_tree_node.same_scope?(record) }
38 | end
39 |
40 | # Returns true if current node is equal to +other+ node or is descendant of +other+ node.
41 | #
42 | # @param [ActiveRecord::Base] other
43 | def is_or_is_descendant_of?(other)
44 | record == other || is_descendant_of?(other)
45 | end
46 |
47 | # Returns true if current node is ancestor of +other+ node.
48 | #
49 | # @param [ActiveRecord::Base] other
50 | def is_ancestor_of?(other)
51 | same_scope?(other) && other.is_descendant_of?(record)
52 | end
53 |
54 | # Returns true if current node is equal to +other+ node or is ancestor of +other+ node.
55 | #
56 | # @param [ActiveRecord::Base] other
57 | def is_or_is_ancestor_of?(other)
58 | same_scope?(other) && other.is_or_is_descendant_of?(record)
59 | end
60 |
61 | # Return +true+ if this object is the first in the list.
62 | def first?
63 | position <= 1
64 | end
65 |
66 | # Return +true+ if this object is the last in the list.
67 | def last?
68 | if tree.columns.counter_cache? && parent
69 | parent.children.size == position
70 | else
71 | !right_sibling
72 | end
73 | end
74 |
75 | # Check if other node is in the same scope.
76 | #
77 | # @api private
78 | def same_scope?(other)
79 | same_kind?(other) && tree.columns.scope.all? do |attr|
80 | record[attr] == other[attr]
81 | end
82 | end
83 |
84 | # Check if other node has the same parent
85 | #
86 | # @api private
87 | def same_parent?(other)
88 | same_scope?(other) && parent_id == other.ordered_tree_node.parent_id
89 | end
90 |
91 | private
92 | # Check if other node belongs to same class hierarchy.
93 | def same_kind?(other)
94 | other.ordered_tree && other.ordered_tree.base_class == tree.base_class
95 | end
96 | end # module Predicates
97 | end # class Node
98 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/node/reloading.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/compatibility'
4 |
5 | module ActsAsOrderedTree
6 | class Node
7 | module Reloading
8 | Compatibility.version '< 4.0.0' do
9 | # Reloads node's attributes related to tree structure
10 | def reload(options = {})
11 | record.reload(options.merge(:select => tree_columns))
12 | end
13 | end
14 |
15 | Compatibility.version '>= 4.0.0' do
16 | # Reloads node's attributes related to tree structure
17 | def reload(options = {})
18 | record.association_cache.delete(:parent)
19 | record.association_cache.delete(:children)
20 |
21 | fresh_object = reload_scope(options).find(record.id)
22 |
23 | fresh_object.attributes.each_pair do |key, value|
24 | record[key] = value
25 | end
26 |
27 | record.instance_eval do
28 | # @attributes.update(fresh_object.instance_variable_get(:@attributes))
29 | @attributes_cache = {}
30 | end
31 |
32 | record
33 | end
34 |
35 | private
36 | def reload_scope(options)
37 | options ||= {}
38 | lock_value = options.fetch(:lock, false)
39 | record.class.unscoped.select(tree_columns).lock(lock_value)
40 | end
41 | end
42 |
43 | private
44 | def tree_columns
45 | tree.columns.to_a
46 | end
47 | end
48 | end
49 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/node/siblings.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/node/predicates'
4 |
5 | module ActsAsOrderedTree
6 | class Node
7 | module Siblings
8 | include Predicates
9 |
10 | # Returns collection of all children of the parent, including self
11 | #
12 | # @return [ActiveRecord::Relation]
13 | def self_and_siblings
14 | scope.where( tree.columns.parent => parent_id ).preorder
15 | end
16 |
17 | # Returns collection of all children of the parent, except self
18 | #
19 | # @return [ActiveRecord::Relation]
20 | def siblings
21 | self_and_siblings.where( table[tree.columns.id].not_eq(id) )
22 | end
23 |
24 | # Returns siblings lying to the left of (upper than) current node.
25 | #
26 | # @return [ActiveRecord::Relation]
27 | def left_siblings
28 | siblings.where( table[tree.columns.position].lteq(position) )
29 | end
30 | alias :higher_items :left_siblings
31 |
32 | # Returns a left (upper) sibling of node.
33 | #
34 | # @return [ActiveRecord::Base, nil]
35 | def left_sibling
36 | higher_items.last
37 | end
38 | alias :higher_item :left_sibling
39 |
40 | # Set node new left (upper) sibling.
41 | # Just changes node's parent_id and position attributes.
42 | #
43 | # @param [ActiveRecord::Base] node new left sibling
44 | # @raise [ActiveRecord::AssociationTypeMismatch] if +node+ class does not
45 | # match current node class.
46 | def left_sibling=(node)
47 | return node if record == node
48 |
49 | to = validate_sibling!(node)
50 |
51 | self.position = higher_than?(node) ? to.position : to.position + 1
52 | self.parent_id = to.parent_id
53 |
54 | node
55 | end
56 | alias :higher_item= :left_sibling=
57 |
58 | # Set node new left sibling by its ID.
59 | # Changes node's parent_id and position.
60 | #
61 | # @param [Fixnum] id new left sibling ID
62 | # @raise [ActiveRecord::RecordNotFound] if given +id+ was not found
63 | def left_sibling_id=(id)
64 | assign_sibling_by_id(id, :left)
65 | end
66 | alias :higher_item_id= :left_sibling_id=
67 |
68 | # Returns siblings lying to the right of (lower than) current node.
69 | #
70 | # @return [ActiveRecord::Relation]
71 | def right_siblings
72 | siblings.where( table[tree.columns.position].gteq(position) )
73 | end
74 | alias :lower_items :right_siblings
75 |
76 | # Returns a right (lower) sibling of the node
77 | #
78 | # @return [ActiveRecord::Base, nil]
79 | def right_sibling
80 | right_siblings.first
81 | end
82 | alias :lower_item :right_sibling
83 |
84 | # Set node new right (lower) sibling.
85 | # Just changes node's parent_id and position attributes.
86 | #
87 | # @param [ActiveRecord::Base] node new right sibling
88 | # @raise [ActiveRecord::AssociationTypeMismatch] if +node+ class does not
89 | # match current node class.
90 | def right_sibling=(node)
91 | to = validate_sibling!(node)
92 |
93 | self.position = higher_than?(node) ? to.position - 1 : to.position
94 | self.parent_id = to.parent_id
95 |
96 | node
97 | end
98 | alias :lower_item= :right_sibling=
99 |
100 | # Set node new right sibling by its ID.
101 | # Changes node's parent_id and position.
102 | #
103 | # @param [Fixnum] id new right sibling ID
104 | # @raise [ActiveRecord::RecordNotFound] if given +id+ was not found
105 | def right_sibling_id=(id)
106 | assign_sibling_by_id(id, :right)
107 | end
108 | alias :lower_item_id= :right_sibling_id=
109 |
110 | private
111 | def higher_than?(other)
112 | same_parent?(other) && position < other.ordered_tree_node.position
113 | end
114 |
115 | # Raises exception if +other+ is kind of wrong class
116 | #
117 | # @return [ActsAsOrderedTree::Node]
118 | def validate_sibling!(other)
119 | unless other.is_a?(tree.base_class)
120 | message = "#{tree.base_class.name} expected, got #{other.class.name}"
121 | raise ActiveRecord::AssociationTypeMismatch, message
122 | end
123 |
124 | other.ordered_tree_node
125 | end
126 |
127 | # @api private
128 | def assign_sibling_by_id(id, position)
129 | node = tree.base_class.find(id)
130 |
131 | case position
132 | when :left, :higher then self.left_sibling = node
133 | when :right, :lower then self.right_sibling = node
134 | else raise RuntimeError, 'Unknown sibling position'
135 | end
136 | end
137 | end # module Siblings
138 | end # class Node
139 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/node/traversals.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/relation/arrangeable'
4 | require 'acts_as_ordered_tree/relation/iterable'
5 |
6 | module ActsAsOrderedTree
7 | class Node
8 | module Traversals
9 | # Returns relation that contains all node's parents, starting from root.
10 | #
11 | # @return [ActiveRecord::Relation]
12 | def ancestors
13 | iterable tree.adapter.ancestors(record)
14 | end
15 |
16 | # Returns relation that containt all node's parents
17 | # and node itself, starting from root.
18 | #
19 | # @return [ActiveRecord::Relation]
20 | def self_and_ancestors
21 | iterable tree.adapter.self_and_ancestors(record)
22 | end
23 |
24 | # Returns collection of all node's children including their nested children.
25 | #
26 | # @return [ActiveRecord::Relation]
27 | def descendants
28 | iterable tree.adapter.descendants(record)
29 | end
30 |
31 | # Returns collection of all node's children including their
32 | # nested children, and node itself.
33 | #
34 | # @return [ActiveRecord::Relation]
35 | def self_and_descendants
36 | iterable tree.adapter.self_and_descendants(record)
37 | end
38 |
39 | # Returns very first ancestor of current node. If current node is root,
40 | # then method returns node itself.
41 | #
42 | # @return [ActiveRecord::Base]
43 | def root
44 | root? ? record : ancestors.first
45 | end
46 |
47 | private
48 | def iterable(scope)
49 | scope.extending(Relation::Arrangeable, Relation::Iterable)
50 | end
51 | end # module Traversals
52 | end # module Node
53 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/persevering_transaction.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | class PerseveringTransaction
5 | module State
6 | # Generate helper methods for given +state+.
7 | # AR adapter calls :committed! and :rolledback! methods
8 | #
9 | # @api private
10 | def state_method(state)
11 | define_method "#{state}!" do |*|
12 | @state = state
13 | end
14 |
15 | define_method "#{state}?" do
16 | @state == state
17 | end
18 | end
19 | end
20 | extend State
21 |
22 | # Which errors should be treated as deadlocks
23 | DEADLOCK_MESSAGES = Regexp.new [
24 | 'Deadlock found when trying to get lock',
25 | 'Lock wait timeout exceeded',
26 | 'deadlock detected',
27 | 'database is locked'
28 | ].join(?|).freeze
29 | # How many times we should retry transaction
30 | RETRY_COUNT = 10
31 |
32 | attr_reader :connection, :attempts
33 | delegate :logger, :to => :connection
34 |
35 | state_method :committed
36 | state_method :rolledback
37 |
38 | def initialize(connection)
39 | @connection = connection
40 | @attempts = 0
41 | @callbacks = []
42 | @state = nil
43 | end
44 |
45 | # Starts persevering transaction
46 | def start(&block)
47 | @attempts += 1
48 |
49 | with_transaction_state(&block)
50 | rescue ActiveRecord::StatementInvalid => error
51 | raise unless connection.open_transactions.zero?
52 | raise unless error.message =~ DEADLOCK_MESSAGES
53 | raise if attempts >= RETRY_COUNT
54 |
55 | logger.info "Deadlock detected on attempt #{attempts}, restarting transaction"
56 |
57 | pause and retry
58 | end
59 |
60 | # Execute given +block+ when after transaction _real_ commit
61 | def after_commit(&block)
62 | @callbacks << block if block_given?
63 | end
64 |
65 | # This method is called by AR adapter
66 | # @api private
67 | def has_transactional_callbacks?
68 | true
69 | end
70 |
71 | # Marks this transaction as committed and executes its commit callbacks
72 | # @api private
73 | def committed_with_callbacks!
74 | committed_without_callbacks!
75 | @callbacks.each { |callback| callback.call }
76 | end
77 | alias_method_chain :committed!, :callbacks
78 |
79 | private
80 | def pause
81 | sleep(rand(attempts) * 0.1)
82 | end
83 |
84 | # Runs real transaction and remembers its state
85 | def with_transaction_state
86 | connection.transaction do
87 | connection.add_transaction_record(self)
88 |
89 | yield
90 | end
91 | end
92 | end # class PerseveringTransaction
93 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/position.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | # Position structure aggregates knowledge about node's position in the tree
5 | #
6 | # @api private
7 | class Position
8 | # This class represents node position change
9 | #
10 | # @api private
11 | class Transition
12 | # @return [ActsAsOrderedTree::Position]
13 | attr_reader :from
14 |
15 | # @return [ActsAsOrderedTree::Position]
16 | attr_reader :to
17 |
18 | # @param [ActsAsOrderedTree::Position] from
19 | # @param [ActsAsOrderedTree::Position] to
20 | def initialize(from, to)
21 | @from, @to = from, to
22 | end
23 |
24 | def changed?
25 | from != to
26 | end
27 |
28 | def reorder?
29 | changed? && from.parent_id == to.parent_id
30 | end
31 |
32 | def movement?
33 | changed? && from.parent_id != to.parent_id
34 | end
35 |
36 | def level_changed?
37 | from.depth != to.depth
38 | end
39 |
40 | def update_counters
41 | if movement?
42 | from.decrement_counter
43 | to.increment_counter
44 | end
45 | end
46 | end
47 |
48 | attr_reader :node, :position
49 | attr_accessor :parent_id
50 |
51 | delegate :record, :to => :node
52 |
53 | # @param [ActsAsOrderedTree::Node] node
54 | # @param [Integer] parent_id
55 | # @param [Integer] position
56 | def initialize(node, parent_id, position)
57 | @node, @parent_id, self.position = node, parent_id, position
58 | end
59 |
60 | # attr_writer with coercion to [nil or Integer]
61 | def position=(value)
62 | @position = value.presence && value.to_i
63 | end
64 |
65 | def klass
66 | record.class
67 | end
68 |
69 | def parent
70 | return @parent if defined?(@parent)
71 |
72 | @parent = parent_id ? fetch_parent : nil
73 | end
74 |
75 | def parent?
76 | parent.present?
77 | end
78 |
79 | def root?
80 | parent.blank?
81 | end
82 |
83 | def depth
84 | @depth ||= parent ? parent.level + 1 : 0
85 | end
86 |
87 | # Locks current position. Technically, it means that pessimistic
88 | # lock will be obtained on parent node (or all root nodes if position is root)
89 | def lock!
90 | if parent
91 | parent.lock!
92 | else
93 | siblings.lock.reload
94 | end
95 | end
96 |
97 | # predicate
98 | def position?
99 | position.present?
100 | end
101 |
102 | # Returns all nodes within given position
103 | def siblings
104 | node.scope.where(klass.ordered_tree.columns.parent => parent_id)
105 | end
106 |
107 | # Returns all nodes that are lower than current position
108 | def lower
109 | position? ?
110 | siblings.where(klass.arel_table[klass.ordered_tree.columns.position].gteq(position)) :
111 | siblings
112 | end
113 |
114 | def increment_counter
115 | update_counter(:increment_counter)
116 | end
117 |
118 | def decrement_counter
119 | update_counter(:decrement_counter)
120 | end
121 |
122 | # @param [ActsAsOrderedTree::Node::Position] other
123 | def ==(other)
124 | other.is_a?(self.class) &&
125 | other.record == record &&
126 | other.parent_id == parent_id &&
127 | other.position == position
128 | end
129 |
130 | private
131 | def fetch_parent
132 | parent_id == record[klass.ordered_tree.columns.parent] ?
133 | record.parent :
134 | node.scope.find(parent_id)
135 | end
136 |
137 | def update_counter(method)
138 | if (column = klass.ordered_tree.columns.counter_cache) && parent_id
139 | klass.send(method, column, parent_id)
140 | end
141 | end
142 | end # class Position
143 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/relation/arrangeable.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/iterators/arranger'
4 | require 'acts_as_ordered_tree/iterators/orphans_pruner'
5 |
6 | module ActsAsOrderedTree
7 | module Relation
8 | # This AR::Relation extension allows to arrange collection into
9 | # Hash of nested Hashes
10 | module Arrangeable
11 | # Arrange associated collection into a nested hash of the form
12 | # {node => children}, where children = {} if the node has no children.
13 | #
14 | # It is possible to discard orphaned nodes (nodes which don't have
15 | # corresponding parent node in this collection) by passing `:orphans => :discard`
16 | # as option.
17 | #
18 | # @param [Hash] options
19 | # @option options [:discard, nil] :orphans
20 | # @return [Hash Hash>]
21 | def arrange(options = {})
22 | collection = self
23 |
24 | if options && options[:orphans] == :discard
25 | collection = Iterators::OrphansPruner.new(self)
26 | end
27 |
28 | @arranger ||= Iterators::Arranger.new(collection)
29 | @arranger.arrange
30 | end
31 | end
32 | end
33 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/relation/iterable.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/iterators/level_calculator'
4 | require 'acts_as_ordered_tree/iterators/orphans_pruner'
5 |
6 | module ActsAsOrderedTree
7 | module Relation
8 | module Iterable
9 | # Iterates over tree elements and determines the current level in the tree.
10 | # Only accepts default ordering, no orphans allowed (they considered as root elements).
11 | # This method is efficient on trees that don't cache level.
12 | #
13 | # @example
14 | # node.descendants.each_with_level do |descendant, level|
15 | # end
16 | #
17 | # @return [Enumerator] if block is not given
18 | def each_with_level(&block)
19 | Iterators::LevelCalculator.new(self).each(&block)
20 | end
21 |
22 | # Iterates over tree elements but discards any orphaned nodes (e.g. nodes
23 | # which have a parent, but parent isn't in current collection).
24 | #
25 | # @example Collection with orphaned nodes
26 | # # Assume we have following tree:
27 | # # root 1
28 | # # child 1
29 | # # root 2
30 | # # child 2
31 | #
32 | # MyModel.where('id != ?', root_1.id).extending(Iterable).each_without_orphans.to_a
33 | # # => [root_2, child_2]
34 | #
35 | # @return [Enumerator] if block is not given
36 | def each_without_orphans(&block)
37 | Iterators::OrphansPruner.new(self).each(&block)
38 | end
39 | end # module Iterable
40 | end # module Relation
41 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/relation/preloaded.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | module Relation
5 | # AR::Relation extension which adds ability to explicitly set records
6 | #
7 | # @example
8 | # records = MyModel.where(:parent_id => nil).to_a
9 | # relation = MyModel.where(:parent_id => nil).
10 | # extending(ActsAsOrderedTree::Relation::Preloaded).
11 | # records(records)
12 | # relation.to_a.should be records
13 | module Preloaded
14 | def records(records)
15 | @loaded = false
16 | @records = records
17 |
18 | build_where!
19 |
20 | @loaded = true
21 |
22 | self
23 | end
24 |
25 | # Reverse the existing order of records on the relation.
26 | def reverse_order
27 | (respond_to?(:spawn) ? spawn : clone).records(@records.reverse)
28 | end
29 |
30 | def reverse_order!
31 | @records = @records.reverse
32 |
33 | self
34 | end
35 |
36 | # Extending relation is not really intrusive operation, so we can save preloaded records
37 | def extending(*)
38 | super.tap { |relation| relation.records(@records) if loaded? }
39 | end
40 |
41 | private
42 | def record_ids
43 | @records.map { |r| r.id if r }.compact
44 | end
45 |
46 | def build_where!
47 | self.where_values = build_where(:id => record_ids)
48 | end
49 | end # module Preloaded
50 | end # module Relation
51 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/base.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/persevering_transaction'
4 | require 'acts_as_ordered_tree/transaction/callbacks'
5 |
6 | module ActsAsOrderedTree
7 | module Transaction
8 | # Persevering transaction, which restarts on deadlock
9 | #
10 | # Here we have a tree of possible transaction types:
11 | #
12 | # Base (abstract)
13 | # Save (abstract)
14 | # Create
15 | # Update (abstract)
16 | # Move
17 | # Reorder
18 | # Destroy
19 | #
20 | # @api private
21 | class Base
22 | extend Callbacks
23 |
24 | attr_reader :node
25 |
26 | delegate :record, :tree, :to => :node
27 | delegate :connection, :to => :klass
28 |
29 | # @param [ActsAsOrderedTree::Node] node
30 | def initialize(node)
31 | @node = node
32 | end
33 |
34 | # Start persevering transaction, which will restart on deadlock
35 | def start(&block)
36 | transaction.start do
37 | run_callbacks(:transaction, &block)
38 | end
39 | end
40 |
41 | protected
42 | def klass
43 | record.class
44 | end
45 |
46 | # Returns underlying transaction object
47 | def transaction
48 | @transaction ||= PerseveringTransaction.new(connection)
49 | end
50 |
51 | # Trigger tree callback (before_add, after_add, before_remove, after_remove)
52 | def trigger_callback(kind, owner)
53 | tree.callbacks.send(kind, owner, record) if owner.present?
54 | end
55 | end
56 | end
57 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/callbacks.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/callbacks'
2 |
3 | module ActsAsOrderedTree
4 | module Transaction
5 | module Callbacks
6 | def self.extended(base)
7 | base.send(:include, ActiveSupport::Callbacks)
8 | base.define_callbacks :transaction
9 | end
10 |
11 | def before(filter, *options, &block)
12 | set_callback :transaction, :before, filter, *options, &block
13 | end
14 |
15 | def after(filter, *options, &block)
16 | set_callback :transaction, :after, filter, *options, &block
17 | end
18 |
19 | def around(filter, *options, &block)
20 | set_callback :transaction, :around, filter, *options, &block
21 | end
22 |
23 | # This method should be called in concrete transaction classes to prevent
24 | # race conditions in multi-threaded environments.
25 | #
26 | # @api private
27 | def finalize
28 | finalize_callbacks :transaction
29 | end
30 |
31 | private
32 | Compatibility.version '< 3.2.0' do
33 | def finalize_callbacks(kind)
34 | __define_runner(kind)
35 | end
36 | end
37 |
38 | Compatibility.version '>= 3.2.0', '< 4.0.0' do
39 | def finalize_callbacks(kind)
40 | __reset_runner(kind)
41 |
42 | object = allocate
43 |
44 | name = __callback_runner_name(nil, kind)
45 | unless object.respond_to?(name, true)
46 | str = object.send("_#{kind}_callbacks").compile(nil, object)
47 | class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
48 | def #{name}() #{str} end
49 | protected :#{name}
50 | RUBY_EVAL
51 | end
52 | end
53 | end
54 |
55 | Compatibility.version '>= 4.0.0', '< 4.1.0' do
56 | def finalize_callbacks(kind)
57 | __define_callbacks(kind, allocate)
58 | end
59 | end
60 |
61 | # Rails 4.1 is thread safe
62 | Compatibility.version '>= 4.1.0' do
63 | def finalize_callbacks(kind) end
64 | end
65 | end # module Callbacks
66 | end # module Transaction
67 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/create.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/transaction/save'
4 | require 'acts_as_ordered_tree/transaction/dsl'
5 |
6 | module ActsAsOrderedTree
7 | module Transaction
8 | # Create transaction (for new records only)
9 | # @api private
10 | class Create < Save
11 | include DSL
12 |
13 | before :push_to_bottom_after_commit, :if => 'push_to_bottom? && to.root?'
14 | before :set_counter_cache, :if => 'tree.columns.counter_cache?'
15 | before :increment_lower_positions, :unless => :push_to_bottom?
16 | before 'trigger_callback(:before_add, to.parent)'
17 |
18 | after 'to.increment_counter'
19 | after 'node.reload'
20 | after 'trigger_callback(:after_add, to.parent)'
21 |
22 | finalize
23 |
24 | private
25 | def set_counter_cache
26 | record[tree.columns.counter_cache] = 0
27 | end
28 |
29 | def increment_lower_positions
30 | to.lower.update_all set position => position + 1
31 | end
32 |
33 | # If record was created as root there is a chance that position will collide,
34 | # but this callback will force record to placed at the bottom of tree.
35 | #
36 | # Yep, concurrency is a tough thing.
37 | #
38 | # @see https://github.com/take-five/acts_as_ordered_tree/issues/24
39 | def push_to_bottom_after_commit
40 | transaction.after_commit do
41 | connection.logger.debug { "Forcing new record (id=#{record.id}, position=#{node.position}) to be placed to bottom" }
42 |
43 | connection.transaction do
44 | # lock new siblings
45 | to.siblings.lock.reload
46 |
47 | if positions_collided?
48 | update_created set position => siblings.select(coalesce(max(position), 0) + 1)
49 | end
50 | end
51 | end
52 | end
53 |
54 | # Checks if there is +position_column+ collision within new parent
55 | def positions_collided?
56 | to.siblings.where(id.not_eq(record.id).and(position.eq(to.position))).exists?
57 | end
58 |
59 | def update_created(*args)
60 | to.siblings.where(id.eq(record.id)).update_all(*args)
61 | end
62 |
63 | def siblings
64 | to.siblings.where(id.not_eq(record.id))
65 | end
66 | end # class Create
67 | end # module Transaction
68 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/destroy.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/transaction/base'
4 | require 'acts_as_ordered_tree/transaction/dsl'
5 |
6 | module ActsAsOrderedTree
7 | module Transaction
8 | class Destroy < Base
9 | include DSL
10 |
11 | attr_reader :from
12 |
13 | before 'trigger_callback(:before_remove, from.parent)'
14 |
15 | after :decrement_lower_positions
16 | after 'from.decrement_counter'
17 | after 'trigger_callback(:after_remove, from.parent)'
18 |
19 | finalize
20 |
21 | # @param [ActsAsOrderedTree::Node] node
22 | # @param [ActsAsOrderedTree::Position] from from which position given +node+ is destroyed
23 | def initialize(node, from)
24 | super(node)
25 | @from = from
26 | end
27 |
28 | private
29 | def decrement_lower_positions
30 | from.lower.update_all set position => position - 1
31 | end
32 | end
33 | end
34 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/dsl.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'active_support/core_ext/string/inflections'
4 |
5 | unless defined? Arel::Nodes::Case
6 | module Arel
7 | module Nodes
8 | # Case node
9 | #
10 | # @example
11 | # switch.when(table[:x].gt(1), table[:y]).else(table[:z])
12 | # # CASE WHEN "table"."x" > 1 THEN "table"."y" ELSE "table"."z" END
13 | # switch.when(table[:x].gt(1)).then(table[:y]).else(table[:z])
14 | class Case < Arel::Nodes::Node
15 | include Arel::OrderPredications
16 | include Arel::Predications
17 |
18 | attr_reader :conditions, :default
19 |
20 | def initialize
21 | @conditions = []
22 | @default = nil
23 | end
24 |
25 | def when(condition, expression = nil)
26 | @conditions << When.new(condition, expression)
27 | self
28 | end
29 |
30 | def then(expression)
31 | @conditions.last.right = expression
32 | self
33 | end
34 |
35 | def else(expression)
36 | @default = Else.new(expression)
37 | self
38 | end
39 | end
40 |
41 | class When < Arel::Nodes::Binary
42 | end
43 |
44 | class Else < Arel::Nodes::Unary
45 | end
46 | end
47 |
48 | module Visitors
49 | class ToSql < Arel::Visitors::ToSql.superclass
50 | private
51 | def visit_Arel_Nodes_Case o, *a
52 | conditions = o.conditions.map { |x| visit x, *a }.join(' ')
53 | default = o.default && visit(o.default, *a)
54 |
55 | "CASE #{[conditions, default].compact.join(' ')} END"
56 | end
57 |
58 | def visit_Arel_Nodes_When o, *a
59 | "WHEN #{visit o.left, *a} THEN #{visit o.right, *a}"
60 | end
61 |
62 | def visit_Arel_Nodes_Else o, *a
63 | "ELSE #{visit o.expr, *a}"
64 | end
65 |
66 | if Arel::VERSION >= '6.0.0'
67 | def visit_Arel_Nodes_Case o, collector
68 | collector << 'CASE '
69 | o.conditions.each do |x|
70 | visit x, collector
71 | collector << ' '
72 | end
73 | if o.default
74 | visit o.default, collector
75 | collector << ' '
76 | end
77 | collector << 'END'
78 | end
79 |
80 | def visit_Arel_Nodes_When o, collector
81 | collector << 'WHEN '
82 | visit o.left, collector
83 | collector << ' THEN '
84 | visit o.right, collector
85 | end
86 |
87 | def visit_Arel_Nodes_Else o, collector
88 | collector << 'ELSE'
89 | visit o.expr, collector
90 | end
91 |
92 | def visit_NilClass o, collector
93 | collector << 'NULL'
94 | end
95 | end
96 | end
97 |
98 | class DepthFirst < Arel::Visitors::Visitor
99 | def visit_Arel_Nodes_Case o, *a
100 | visit o.conditions, *a
101 | visit o.default, *a
102 | end
103 | alias :visit_Arel_Nodes_When :binary
104 | alias :visit_Arel_Nodes_Else :unary
105 | end
106 | end
107 | end
108 | end
109 |
110 | module ActsAsOrderedTree
111 | module Transaction
112 | # Simple DSL to generate complex UPDATE queries.
113 | # Requires +record+ method.
114 | #
115 | # @api private
116 | module DSL
117 | module Shortcuts
118 | INFIX_OPERATIONS = Hash[
119 | :== => Arel::Nodes::Equality,
120 | :'!=' => Arel::Nodes::NotEqual,
121 | :> => Arel::Nodes::GreaterThan,
122 | :>= => Arel::Nodes::GreaterThanOrEqual,
123 | :< => Arel::Nodes::LessThan,
124 | :<= => Arel::Nodes::LessThanOrEqual,
125 | :=~ => Arel::Nodes::Matches,
126 | :'!~' => Arel::Nodes::DoesNotMatch,
127 | :| => Arel::Nodes::Or
128 | ]
129 |
130 | # generate subclasses and methods
131 | INFIX_OPERATIONS.each do |operator, klass|
132 | subclass = Class.new(klass) { include Shortcuts }
133 | const_set(klass.name.demodulize, subclass)
134 | INFIX_OPERATIONS[operator] = subclass
135 |
136 | define_method(operator) do |arg|
137 | subclass.new(self, arg)
138 | end
139 | end
140 |
141 | And = Class.new(Arel::Nodes::And) { include Shortcuts }
142 |
143 | def &(arg)
144 | And.new [self, arg]
145 | end
146 | end
147 |
148 | Attribute = Class.new(Arel::Attributes::Attribute) { include Shortcuts }
149 | SqlLiteral = Class.new(Arel::Nodes::SqlLiteral) { include Shortcuts }
150 |
151 | NamedFunction = Class.new(Arel::Nodes::NamedFunction) {
152 | include Shortcuts
153 | include Arel::Math
154 | }
155 |
156 | # Create Arel::Nodes::Case node
157 | def switch
158 | Arel::Nodes::Case.new
159 | end
160 |
161 | # Create assignments expression for UPDATE statement
162 | #
163 | # @example
164 | # Model.where(:parent_id => nil).update_all(set :name => switch.when(x < 10).then('OK').else('TOO LARGE'))
165 | #
166 | # @param [Hash] assignments
167 | def set(assignments)
168 | assignments.map do |attr, value|
169 | next unless attr.present?
170 |
171 | name = attr.is_a?(Arel::Attributes::Attribute) ? attr.name : attr.to_s
172 |
173 | quoted = record.class.connection.quote_column_name(name)
174 | "#{quoted} = (#{value.to_sql})"
175 | end.join(', ')
176 | end
177 |
178 | def attribute(name)
179 | name && Attribute.new(table, name.to_sym)
180 | end
181 |
182 | def expression(expr)
183 | SqlLiteral.new(expr.to_s)
184 | end
185 |
186 | def id
187 | attribute(record.ordered_tree.columns.id)
188 | end
189 |
190 | def parent_id
191 | attribute(record.ordered_tree.columns.parent)
192 | end
193 |
194 | def position
195 | attribute(record.ordered_tree.columns.position)
196 | end
197 |
198 | def depth
199 | attribute(record.ordered_tree.columns.depth)
200 | end
201 |
202 | def table
203 | record.class.arel_table
204 | end
205 |
206 | def method_missing(id, *args)
207 | if args.length > 0
208 | # function
209 | NamedFunction.new(id.to_s.upcase, args)
210 | else
211 | super
212 | end
213 | end
214 | end # module DSL
215 | end # module Transaction
216 | end # module ActsAsOrderedTree
217 |
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/factory.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/position'
4 | require 'acts_as_ordered_tree/transaction/create'
5 | require 'acts_as_ordered_tree/transaction/destroy'
6 | require 'acts_as_ordered_tree/transaction/move'
7 | require 'acts_as_ordered_tree/transaction/passthrough'
8 | require 'acts_as_ordered_tree/transaction/reorder'
9 |
10 | module ActsAsOrderedTree
11 | module Transaction
12 | # @api private
13 | module Factory
14 | # Creates previous and current position objects for node
15 | # @api private
16 | class PositionFactory
17 | def initialize(node)
18 | @node = node
19 | end
20 |
21 | def previous
22 | Position.new @node, @node.parent_id_was, @node.position_was
23 | end
24 |
25 | def current
26 | Position.new @node, @node.parent_id, @node.position
27 | end
28 |
29 | def transition
30 | Position::Transition.new(previous, current)
31 | end
32 | end
33 | private_constant :PositionFactory
34 |
35 | # Creates proper transaction according to +node+
36 | #
37 | # @param [ActsAsOrderedTree::Node] node
38 | # @param [true, false] destroy set to true if node should be destroyed
39 | # @return [ActsAsOrderedTree::Transaction::Base]
40 | def create(node, destroy = false)
41 | pos = PositionFactory.new(node)
42 |
43 | case
44 | when destroy
45 | Destroy.new(node, pos.previous)
46 | when node.record.new_record?
47 | Create.new(node, pos.current)
48 | else
49 | create_from_transition(node, pos.transition)
50 | end
51 | end
52 | module_function :create
53 |
54 | def create_from_transition(node, transition)
55 | case
56 | when transition.movement?
57 | Move.new(node, transition)
58 | when transition.reorder?
59 | Reorder.new(node, transition)
60 | else
61 | Passthrough.new
62 | end
63 | end
64 | module_function :create_from_transition
65 | end
66 | end
67 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/move.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/transaction/update'
4 |
5 | module ActsAsOrderedTree
6 | module Transaction
7 | class Move < Update
8 | before 'trigger_callback(:before_remove, from.parent)'
9 | before 'trigger_callback(:before_add, to.parent)'
10 |
11 | after :update_descendants_depth, :if => [
12 | 'transition.movement?',
13 | 'tree.columns.depth?',
14 | 'transition.level_changed?',
15 | 'record.children.size > 0'
16 | ]
17 |
18 | after 'trigger_callback(:after_add, to.parent)'
19 | after 'trigger_callback(:after_remove, from.parent)'
20 | after 'transition.update_counters'
21 |
22 | finalize
23 |
24 | private
25 | def update_values
26 | updates = Hash[
27 | position => position_value,
28 | parent_id => parent_id_value
29 | ]
30 |
31 | updates[depth] = depth_value if tree.columns.depth? && transition.level_changed?
32 |
33 | updates
34 | end
35 |
36 | # Records to be updated
37 | def update_scope
38 | filter = (id == record.id) | (parent_id == from.parent_id) | (parent_id == to.parent_id)
39 | node.scope.where(filter.to_sql)
40 | end
41 |
42 | def parent_id_value
43 | switch.when(id == record.id, to.parent_id).else(parent_id)
44 | end
45 |
46 | def position_value
47 | switch.
48 | when(id == record.id).
49 | then(@to.position).
50 | # decrement lower positions in old parent
51 | when((parent_id == from.parent_id) & (position > from.position)).
52 | then(position - 1).
53 | # increment positions in new parent
54 | when((parent_id == to.parent_id) & (position >= to.position)).
55 | then(position + 1).
56 | else(position)
57 | end
58 |
59 | def depth_value
60 | switch.
61 | when(id == record.id, to.depth).
62 | else(depth)
63 | end
64 |
65 | def update_descendants_depth
66 | record.descendants.update_all set depth => depth + (to.depth - from.depth)
67 | end
68 | end
69 | end
70 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/passthrough.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | module Transaction
5 | # Null transaction, does nothing but delegates to caller
6 | class Passthrough
7 | def start(&block)
8 | block.call if block_given?
9 | end
10 | end
11 | end
12 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/reorder.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/transaction/update'
4 |
5 | module ActsAsOrderedTree
6 | module Transaction
7 | class Reorder < Update
8 | finalize
9 |
10 | protected
11 | # if we reorder node then we cannot put it to position higher than highest
12 | def push_to_bottom
13 | to.position = highest_position.zero? ? 1 : highest_position
14 | end
15 |
16 | private
17 | def update_scope
18 | to.siblings.where(positions_range)
19 | end
20 |
21 | def update_values
22 | { position => position_value }
23 | end
24 |
25 | def positions_range
26 | position.in([from.position, to.position].min..[from.position, to.position].max)
27 | end
28 |
29 | def position_value
30 | expr = switch.
31 | when(position == from.position).then(to.position).
32 | else(position)
33 |
34 | if to.position > from.position
35 | expr.when(positions_range).then(position - 1)
36 | else
37 | expr.when(positions_range).then(position + 1)
38 | end
39 | end
40 | end # class Reorder
41 | end # module Transaction
42 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/save.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/transaction/base'
4 |
5 | module ActsAsOrderedTree
6 | module Transaction
7 | class Save < Base
8 | attr_reader :to
9 |
10 | before 'to.lock!'
11 | before :set_scope!, :if => 'to.parent?'
12 | before :push_to_bottom, :if => :push_to_bottom?
13 | before 'to.position = 1', :if => 'to.position <= 0'
14 |
15 | around :copy_attributes
16 |
17 | # @param [ActsAsOrderedTree::Node] node
18 | # @param [ActsAsOrderedTree::Position] to to which position given +node+ is saved
19 | def initialize(node, to)
20 | super(node)
21 | @to = to
22 | end
23 |
24 | protected
25 | # Copies parent_id, position and depth from destination to record
26 | def copy_attributes
27 | record.parent = to.parent
28 | node.position = to.position
29 | node.depth = to.depth if tree.columns.depth?
30 |
31 | yield
32 | end
33 |
34 | # Returns highest position within node's siblings
35 | def highest_position
36 | @highest_position ||= to.siblings.maximum(tree.columns.position) || 0
37 | end
38 |
39 | # Should be fired when given position is empty
40 | def push_to_bottom
41 | to.position = highest_position + 1
42 | end
43 |
44 | # Returns true if record should be pushed to bottom of list
45 | def push_to_bottom?
46 | to.position.blank? ||
47 | position_out_of_bounds?
48 | end
49 |
50 | private
51 | def set_scope!
52 | tree.columns.scope.each do |column|
53 | record[column] = to.parent[column]
54 | end
55 |
56 | nil
57 | end
58 |
59 | def position_out_of_bounds?
60 | to.position > highest_position
61 | end
62 | end # class Save
63 | end # module Transaction
64 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/transaction/update.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'active_support/core_ext/object/with_options'
4 |
5 | require 'acts_as_ordered_tree/position'
6 | require 'acts_as_ordered_tree/transaction/save'
7 | require 'acts_as_ordered_tree/transaction/dsl'
8 |
9 | module ActsAsOrderedTree
10 | module Transaction
11 | # Update transaction includes Move and Reorder
12 | #
13 | # @abstract
14 | # @api private
15 | class Update < Save
16 | include DSL
17 |
18 | attr_reader :from, :transition
19 |
20 | around :update_tree
21 |
22 | # @param [ActsAsOrderedTree::Node] node
23 | # @param [ActsAsOrderedTree::Position::Transition] transition
24 | def initialize(node, transition)
25 | @transition = transition
26 | @from = transition.from
27 |
28 | super(node, transition.to)
29 | end
30 |
31 | protected
32 | def update_tree
33 | callbacks = transition.reorder? ? :reorder : :move
34 |
35 | record.run_callbacks(callbacks) do
36 | record.hook_update do |update|
37 | update.scope = update_scope
38 | update.values = update_values.merge(changed_attributes)
39 |
40 | yield
41 | end
42 | end
43 | end
44 |
45 | def update_scope
46 | # implement in successors
47 | end
48 |
49 | def update_values
50 | # implement in successors
51 | end
52 |
53 | private
54 | # Returns hash of UPDATE..SET expressions for each
55 | # changed record attribute (except tree attributes)
56 | #
57 | # @return [Hash Arel::Nodes::Node>]
58 | def changed_attributes
59 | changed_attributes_names.each_with_object({}) do |attr, hash|
60 | hash[attr] = attribute_value(attr)
61 | end
62 | end
63 |
64 | def attribute_value(attr)
65 | attr_value = record.read_attribute(attr)
66 | quoted = record.class.connection.quote(attr_value)
67 |
68 | switch.
69 | when(id == record.id).then(Arel.sql(quoted)).
70 | else(attribute(attr))
71 | end
72 |
73 | def changed_attributes_names
74 | record.changed - (tree.columns.to_a - tree.columns.scope)
75 | end
76 | end
77 | end
78 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/tree.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/compatibility'
4 |
5 | require 'acts_as_ordered_tree/tree/callbacks'
6 | require 'acts_as_ordered_tree/tree/columns'
7 | require 'acts_as_ordered_tree/tree/children_association'
8 | require 'acts_as_ordered_tree/tree/deprecated_columns_accessors'
9 | require 'acts_as_ordered_tree/tree/parent_association'
10 | require 'acts_as_ordered_tree/tree/perseverance'
11 | require 'acts_as_ordered_tree/tree/scopes'
12 |
13 | require 'acts_as_ordered_tree/hooks'
14 |
15 | require 'acts_as_ordered_tree/adapters'
16 | require 'acts_as_ordered_tree/validators'
17 |
18 | require 'acts_as_ordered_tree/instance_methods'
19 |
20 | module ActsAsOrderedTree
21 | # ActsAsOrderedTree::Tree
22 | class Tree
23 | # Default ordered tree options
24 | DEFAULT_OPTIONS = {
25 | :parent_column => :parent_id,
26 | :position_column => :position,
27 | :depth_column => :depth
28 | }.freeze
29 |
30 | PROTECTED_ATTRIBUTES = :left_sibling, :left_sibling_id,
31 | :higher_item, :higher_item_id,
32 | :right_sibling, :right_sibling_id,
33 | :lower_item, :lower_item_id
34 |
35 | attr_reader :klass
36 |
37 | # @!attribute [r] columns
38 | # Columns information aggregator
39 | #
40 | # @return [ActsAsOrderedTree::Tree::Columns] column object
41 | attr_reader :columns
42 |
43 | # @!attribute [r] callbacks
44 | # :before_add, :after_add, :before_remove and :after_remove callbacks storage
45 | #
46 | # @return [ActsAsOrderedTree::Tree::Callbacks] callbacks object
47 | attr_reader :callbacks
48 |
49 | # @!attribute [r] adapter
50 | # Ordered tree adapter which contains implementation of some traverse methods
51 | #
52 | # @return [ActsAsOrderedTree::Adapters::Abstract] adapter object
53 | attr_reader :adapter
54 |
55 | # @!attribute [r] options
56 | # Ordered tree options
57 | #
58 | # @return [Hash]
59 | attr_reader :options
60 |
61 | # Create and setup tree object
62 | #
63 | # @param [Class] klass
64 | # @param [Hash] options
65 | def self.setup!(klass, options)
66 | klass.ordered_tree = new(klass, options).setup
67 | end
68 |
69 | # @param [Class] klass
70 | # @param [Hash] options
71 | def initialize(klass, options)
72 | @klass = klass
73 | @options = DEFAULT_OPTIONS.merge(options).freeze
74 | @columns = Columns.new(klass, @options)
75 | @callbacks = Callbacks.new(klass, @options)
76 | @children = ChildrenAssociation.new(self)
77 | @parent = ParentAssociation.new(self)
78 | @adapter = Adapters.lookup(klass.connection.adapter_name).new(self)
79 | end
80 |
81 | # Builds associations, callbacks, validations etc.
82 | def setup
83 | setup_associations
84 | setup_once
85 |
86 | self
87 | end
88 |
89 | # Returns Class object which will be used for associations,
90 | # scopes and tree traversals.
91 | #
92 | # @return [Class]
93 | def base_class
94 | if klass.finder_needs_type_condition?
95 | klass.base_class
96 | else
97 | klass
98 | end
99 | end
100 |
101 | private
102 | def already_setup?
103 | klass.ordered_tree?
104 | end
105 |
106 | def setup_once
107 | return if already_setup?
108 |
109 | setup_validations
110 | setup_callbacks
111 | protect_attributes *PROTECTED_ATTRIBUTES
112 |
113 | klass.class_eval do
114 | extend Scopes
115 | extend DeprecatedColumnsAccessors
116 |
117 | include InstanceMethods
118 | include Perseverance
119 | include Hooks
120 | end
121 | end
122 |
123 | def setup_associations
124 | @parent.build
125 | @children.build
126 | end
127 |
128 | def setup_validations
129 | if columns.scope?
130 | klass.validates_with Validators::ScopeValidator, :on => :update, :if => :parent
131 | end
132 |
133 | klass.validates_with Validators::CyclicReferenceValidator, :on => :update, :if => :parent
134 | end
135 |
136 | def setup_callbacks
137 | klass.define_model_callbacks(:move, :reorder)
138 | klass.around_save(:save_ordered_tree_node)
139 | klass.around_destroy(:destroy_ordered_tree_node)
140 | end
141 |
142 | def protect_attributes(*attributes)
143 | Compatibility.version '< 4.0.0' do
144 | klass.attr_protected *attributes
145 | end
146 | end
147 | end
148 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/tree/association.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | class Tree
5 | class Association
6 | attr_reader :tree
7 |
8 | delegate :klass, :to => :tree
9 |
10 | def initialize(tree)
11 | @tree = tree
12 | end
13 |
14 | protected
15 | def class_name
16 | "::#{tree.base_class.name}"
17 | end
18 | end # class Association
19 | end # class Tree
20 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/tree/callbacks.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'active_support/core_ext/hash/slice'
4 |
5 | module ActsAsOrderedTree
6 | class Tree
7 | # Tree callbacks storage
8 | #
9 | # @example
10 | # MyModel.ordered_tree.callbacks.before_add(parent, child)
11 | #
12 | # @api private
13 | class Callbacks
14 | VALID_KEYS = :before_add,
15 | :after_add,
16 | :before_remove,
17 | :after_remove
18 |
19 | def initialize(klass, options)
20 | @klass = klass
21 | @callbacks = {}
22 |
23 | options.slice(*VALID_KEYS).each do |k, v|
24 | @callbacks[k] = v if v
25 | end
26 | end
27 |
28 | # generate accessors and predicates
29 | VALID_KEYS.each do |method|
30 | define_method(method) do |parent, record| # def before_add(parent, record)
31 | run_callbacks(method, parent, record)
32 | end
33 | end
34 |
35 | private
36 | def run_callbacks(method, parent, record)
37 | callback = callback_for(method)
38 |
39 | case callback
40 | when Symbol
41 | parent.send(callback, record)
42 | when Proc
43 | callback.call(parent, record)
44 | when nil, false
45 | # do nothing
46 | else
47 | # parent.before_add(record)
48 | callback.send(method, parent, record)
49 | end
50 | end
51 |
52 | def callback_for(method)
53 | @callbacks[method]
54 | end
55 | end # class Callbacks
56 | end # class Tree
57 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/tree/children_association.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/compatibility'
4 | require 'acts_as_ordered_tree/tree/association'
5 | require 'acts_as_ordered_tree/relation/iterable'
6 |
7 | module ActsAsOrderedTree
8 | class Tree
9 | # @api private
10 | class ChildrenAssociation < Association
11 | # CounterCache extensions will allow to use cached value
12 | #
13 | # @api private
14 | module CounterCache
15 | def size
16 | ordered_tree_node.parent_id_changed? ? super : ordered_tree_node.counter_cache
17 | end
18 |
19 | def empty?
20 | size == 0
21 | end
22 |
23 | private
24 | def ordered_tree_node
25 | @association.owner.ordered_tree_node
26 | end
27 | end
28 |
29 | # Builds association object
30 | def build
31 | Compatibility.version '< 4.0.0' do
32 | opts = options.merge(:conditions => conditions, :order => order)
33 |
34 | klass.has_many(:children, opts)
35 | end
36 |
37 | Compatibility.version '>= 4.0.0' do
38 | klass.has_many(:children, scope, options)
39 | end
40 | end
41 |
42 | private
43 | def options
44 | Hash[
45 | :class_name => class_name,
46 | :foreign_key => tree.columns.parent,
47 | :inverse_of => inverse_of,
48 | :dependent => :destroy,
49 | :extend => [extension, Relation::Iterable].compact
50 | ]
51 | end
52 |
53 | def inverse_of
54 | :parent unless tree.options[:polymorphic]
55 | end
56 |
57 | # rails 4.x scope for has_many association
58 | def scope
59 | assoc_scope = method(:association_scope)
60 | join_scope = method(:join_association_scope)
61 |
62 | ->(join_or_parent) {
63 | if join_or_parent.is_a?(ActiveRecord::Associations::JoinDependency::JoinAssociation)
64 | join_scope[join_or_parent]
65 | elsif join_or_parent.is_a?(ActiveRecord::Base)
66 | assoc_scope[join_or_parent]
67 | else
68 | where(nil)
69 | end.extending(Relation::Iterable)
70 | }
71 | end
72 |
73 | # Rails 3.x :conditions options for has_many association
74 | def conditions
75 | return nil unless tree.columns.scope?
76 |
77 | assoc_scope = method(:association_scope)
78 | join_scope = method(:join_association_scope)
79 |
80 | Proc.new do |join_association|
81 | conditions = if join_association.is_a?(ActiveRecord::Associations::JoinDependency::JoinAssociation)
82 | join_scope[join_association]
83 | elsif self.is_a?(ActiveRecord::Base)
84 | assoc_scope[self]
85 | else
86 | where(nil)
87 | end.where_values.reduce(:and)
88 |
89 | conditions.try(:to_sql)
90 | end
91 | end
92 |
93 | def order
94 | tree.columns.position
95 | end
96 |
97 | def extension
98 | if tree.columns.counter_cache?
99 | CounterCache
100 | end
101 | end
102 |
103 | def join_association_scope(join_association)
104 | parent = join_association.respond_to?(:parent) ?
105 | join_association.parent.table :
106 | join_association.base_klass.arel_table
107 |
108 | child = join_association.table
109 |
110 | conditions = tree.columns.scope.map { |column| parent[column].eq child[column] }.reduce(:and)
111 |
112 | klass.unscoped.where(conditions)
113 | end
114 |
115 | def association_scope(owner)
116 | owner.ordered_tree_node.scope.order(tree.columns.position)
117 | end
118 | end # class ChildrenAssociation
119 | end # class Tree
120 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/tree/columns.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | class Tree
5 | # Ordered tree columns store
6 | #
7 | # @example
8 | # MyModel.tree.columns.parent # => "parent_id"
9 | # MyModel.tree.columns.counter_cache # => nil
10 | # MyModel.tree.columns.counter_cache? # => false
11 | class Columns
12 | # This error is raised when unknown column given in :scope option
13 | UnknownColumn = Class.new(StandardError)
14 |
15 | # @api private
16 | def self.column_accessor(*names)
17 | names.each do |name|
18 | define_method "#{name}=" do |value|
19 | @columns[name] = value.to_s if column_exists?(value)
20 | end
21 | private "#{name}=".to_sym
22 |
23 | define_method "#{name}?" do
24 | @columns[name].present?
25 | end
26 |
27 | define_method name do
28 | @columns[name]
29 | end
30 | end
31 | end
32 |
33 | # @!method parent
34 | # @!method parent?
35 | # @!method parent=(value)
36 | # @!method position
37 | # @!method position?
38 | # @!method position=(value)
39 | # @!method depth
40 | # @!method depth?
41 | # @!method depth=(value)
42 | # @!method counter_cache
43 | # @!method counter_cache?
44 | # @!method counter_cache=(value)
45 | # @!method scope
46 | # @!method scope?
47 | column_accessor :parent,
48 | :position,
49 | :depth,
50 | :counter_cache,
51 | :scope
52 |
53 | def initialize(klass, options = {})
54 | @klass = klass
55 | @columns = { :id => id }
56 |
57 | self.parent = options[:parent_column]
58 | self.position = options[:position_column]
59 | self.depth = options[:depth_column]
60 | self.counter_cache = counter_cache_name(options[:counter_cache])
61 | self.scope = options[:scope]
62 | end
63 |
64 | def [](name)
65 | @columns[name]
66 | end
67 |
68 | def id
69 | @klass.primary_key
70 | end
71 |
72 | # Returns array of columns names associated with ordered tree structure
73 | def to_a
74 | @columns.values.flatten.compact
75 | end
76 |
77 | private
78 | undef_method :scope=
79 | def scope=(value)
80 | columns = Array.wrap(value)
81 |
82 | unknown = columns.reject { |name| column_exists?(name) }
83 |
84 | raise UnknownColumn, "Unknown column#{'s' if unknown.size > 1} passed to :scope option: #{unknown.join(', ')}" if unknown.any?
85 |
86 | @columns[:scope] = columns.map(&:to_s)
87 | end
88 |
89 | def counter_cache_name(value)
90 | if value == true
91 | "#{@klass.name.demodulize.underscore.pluralize}_count"
92 | else
93 | value
94 | end
95 | end
96 |
97 | def column_exists?(name)
98 | name.present? && @klass.columns_hash.include?(name.to_s)
99 | end
100 | end # class Columns
101 | end # class Tree
102 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/tree/deprecated_columns_accessors.rb:
--------------------------------------------------------------------------------
1 | module ActsAsOrderedTree
2 | class Tree
3 | # @deprecated Use `ordered_tree.columns` object
4 | module DeprecatedColumnsAccessors
5 | class << self
6 | # @api private
7 | def deprecated_method(method, delegate)
8 | define_method(method) do
9 | ActiveSupport::Deprecation.warn("#{name}.#{method} is deprecated in favor of #{name}.ordered_tree.columns.#{delegate}", caller(1))
10 |
11 | ordered_tree.columns.send(delegate)
12 | end
13 | end
14 | private :deprecated_method
15 | end
16 |
17 | deprecated_method :parent_column, :parent
18 | deprecated_method :position_column, :position
19 | deprecated_method :depth_column, :depth
20 | deprecated_method :children_counter_cache_column, :counter_cache
21 | deprecated_method :scope_column_name, :scope
22 | end
23 | end
24 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/tree/parent_association.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/tree/association'
4 |
5 | module ActsAsOrderedTree
6 | class Tree
7 | class ParentAssociation < Association
8 | # create parent association
9 | #
10 | # we cannot use native :counter_cache callbacks because they suck! :(
11 | # they act like this:
12 | # node.parent = new_parent # and here counters are updated, outside of transaction!
13 | def build
14 | klass.belongs_to(:parent, options)
15 | end
16 |
17 | private
18 | def options
19 | Hash[
20 | :class_name => class_name,
21 | :foreign_key => tree.columns.parent,
22 | :inverse_of => inverse_of
23 | ]
24 | end
25 |
26 | def inverse_of
27 | :children unless tree.options[:polymorphic]
28 | end
29 | end # class ParentAssociation
30 | end # class Tree
31 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/tree/perseverance.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'acts_as_ordered_tree/persevering_transaction'
4 |
5 | module ActsAsOrderedTree
6 | class Tree
7 | # This module contains overridden :with_transaction_returning_status method
8 | # which wraps itself into PerseveringTransaction.
9 | #
10 | # This module is mixed in into Class after Class.acts_as_ordered_tree invocation.
11 | #
12 | # @api private
13 | module Perseverance
14 | def with_transaction_returning_status
15 | PerseveringTransaction.new(self.class.connection).start { super }
16 | end
17 | end
18 | end
19 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/tree/scopes.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | module ActsAsOrderedTree
4 | class Tree
5 | module Scopes
6 | # Returns nodes ordered by their position.
7 | #
8 | # @return [ActiveRecord::Relation]
9 | def preorder
10 | order arel_table[ordered_tree.columns.position].asc
11 | end
12 |
13 | # Returns all nodes that don't have parent.
14 | #
15 | # @return [ActiveRecord::Relation]
16 | def roots
17 | preorder.where arel_table[ordered_tree.columns.parent].eq nil
18 | end
19 |
20 | # Returns all nodes that do not have any children. May be quite inefficient.
21 | #
22 | # @return [ActiveRecord::Relation]
23 | def root
24 | roots.first
25 | end
26 |
27 | # Returns all nodes that do not have any children. May be quite inefficient.
28 | #
29 | # @return [ActiveRecord::Relation]
30 | def leaves
31 | if ordered_tree.columns.counter_cache?
32 | leaves_with_counter_cache
33 | else
34 | leaves_without_counter_cache
35 | end
36 | end
37 |
38 | private
39 | def leaves_without_counter_cache
40 | aliaz = Arel::Nodes::TableAlias.new(arel_table, 't')
41 |
42 | subquery = unscoped.select('1').
43 | from(aliaz).
44 | where(aliaz[ordered_tree.columns.parent].eq(arel_table[primary_key])).
45 | limit(1).
46 | reorder(nil)
47 |
48 | where "NOT EXISTS (#{subquery.to_sql})"
49 | end
50 |
51 | def leaves_with_counter_cache
52 | where arel_table[ordered_tree.columns.counter_cache].eq 0
53 | end
54 | end # module Scopes
55 | end # class Tree
56 | end # module ActsAsOrderedTree
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/validators.rb:
--------------------------------------------------------------------------------
1 | module ActsAsOrderedTree
2 | module Validators #:nodoc:all:
3 | class CyclicReferenceValidator < ActiveModel::Validator
4 | def validate(record)
5 | record.errors.add(:parent, :invalid) if record.is_or_is_ancestor_of?(record.parent)
6 | end
7 | end
8 |
9 | class ScopeValidator < ActiveModel::Validator
10 | def validate(record)
11 | record.errors.add(:parent, :scope) unless record.ordered_tree_node.same_scope?(record.parent)
12 | end
13 | end
14 | end
15 | end
--------------------------------------------------------------------------------
/lib/acts_as_ordered_tree/version.rb:
--------------------------------------------------------------------------------
1 | module ActsAsOrderedTree
2 | VERSION = '2.0.0.beta3'
3 | end
4 |
--------------------------------------------------------------------------------
/spec/acts_as_ordered_tree_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../spec_helper', __FILE__)
2 |
3 | describe ActsAsOrderedTree, :transactional do
4 | example 'creation_with_altered_column_names' do
5 | expect{RenamedColumns.create!}.not_to raise_error
6 | end
7 |
8 | describe '#level' do
9 | context 'given a persistent root node' do
10 | subject { create :default }
11 |
12 | it { expect(subject.level).to eq 0 }
13 | end
14 |
15 | context 'given a new root record' do
16 | subject { build :default }
17 |
18 | it { expect(subject.level).to eq 0 }
19 | end
20 |
21 | context 'given a persistent node with parent' do
22 | let(:root) { create :default }
23 | subject { create :default, :parent => root }
24 |
25 | it { expect(subject.level).to eq 1 }
26 | end
27 |
28 | context 'given a new node with parent' do
29 | let(:root) { create :default }
30 | subject { build :default, :parent => root }
31 |
32 | it { expect(subject.level).to eq 1 }
33 | end
34 |
35 | context 'a model without depth column' do
36 | tree :factory => :scoped do
37 | root {
38 | child {
39 | grandchild
40 | }
41 | }
42 | end
43 |
44 | before { root.reload }
45 | before { child.reload }
46 | before { grandchild.reload }
47 |
48 | it { expect(root.level).to eq 0 }
49 | it { expect{root.level}.not_to query_database }
50 |
51 | it { expect(child.level).to eq 1 }
52 | it { expect{child.level}.to query_database.once }
53 |
54 | it { expect(grandchild.level).to eq 2 }
55 | it { expect{grandchild.level}.to query_database.at_least(:once) }
56 |
57 | context 'given a record with already loaded parent' do
58 | before { child.association(:parent).load_target }
59 | before { grandchild.parent.association(:parent).load_target }
60 |
61 | it { expect(child.level).to eq 1 }
62 | it { expect{child.level}.not_to query_database }
63 |
64 | it { expect(grandchild.level).to eq 2 }
65 | it { expect{grandchild.level}.not_to query_database }
66 | end
67 | end
68 | end
69 |
70 | describe 'move actions' do
71 | tree :factory => :default_with_counter_cache do
72 | root {
73 | child_1
74 | child_2 :name => 'child_2'
75 | child_3 {
76 | child_4
77 | }
78 | }
79 | end
80 |
81 | describe '#insert_at' do
82 | before { ActiveSupport::Deprecation.silence { child_3.insert_at(1) } }
83 | before { child_3.reload }
84 |
85 | specify { expect([child_3, child_1, child_2]).to be_sorted }
86 | end
87 | end
88 |
89 | describe 'scoped trees' do
90 | tree :factory => :scoped do
91 | root1 :scope_type => 't1' do
92 | child1
93 | orphan
94 | end
95 | root2 :scope_type => 't2' do
96 | child2
97 | end
98 | end
99 |
100 | before { Scoped.where(:id => orphan.id).update_all(:scope_type => 't0', :position => 1) }
101 |
102 | it 'should not stick positions together for different scopes' do
103 | expect(root1.position).to eq root2.position
104 | end
105 | it 'should automatically set scope for new records with parent' do
106 | expect(child1.ordered_tree_node).to be_same_scope root1
107 | end
108 | it 'should not include orphans' do
109 | expect(root1.children.reload).not_to include orphan
110 | expect(root1.descendants.reload).not_to include orphan
111 | end
112 | it 'should not allow to move records between scopes' do
113 | expect(child2.move_to_child_of(root1)).to be false
114 | expect(child2.errors_on(:parent).size).to be >= 1
115 | end
116 | it 'should not allow to change scope' do
117 | child2.parent = root1
118 | expect(child2.errors_on(:parent).size).to be >= 1
119 | end
120 | it 'should not allow to add scoped record to children collection' do
121 | root1.children << child2
122 | expect(root1.children.reload).not_to include child2
123 | end
124 | end
125 |
126 | describe "potential vulnerabilities" do
127 | describe "attempt to link parent to one of descendants" do
128 | let(:root) { create :default }
129 | let(:child) { create :default, :parent => root }
130 | let(:grandchild) { create :default, :parent => child }
131 |
132 | subject { root }
133 |
134 | context "given self as parent" do
135 | before { root.parent = root }
136 |
137 | it 'has at least 1 error_on' do
138 | expect(subject.error_on(:parent).size).to be >= 1
139 | end
140 | end
141 |
142 | context "given child as parent" do
143 | before { root.parent = child }
144 |
145 | it 'has at least 1 error_on' do
146 | expect(subject.error_on(:parent).size).to be >= 1
147 | end
148 | end
149 |
150 | context "given grandchild as parent" do
151 | before { root.parent = grandchild }
152 |
153 | it 'has at least 1 error_on' do
154 | expect(subject.error_on(:parent).size).to be >= 1
155 | end
156 | end
157 | end
158 | end
159 | end
160 |
--------------------------------------------------------------------------------
/spec/adapters/postgresql_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | require 'acts_as_ordered_tree/adapters/postgresql'
6 | require 'adapters/shared'
7 |
8 | describe ActsAsOrderedTree::Adapters::PostgreSQL, :transactional, :if => ENV['DB'] == 'pg' do
9 | it { expect(Default.ordered_tree.adapter).to be_a described_class }
10 |
11 | it_behaves_like 'ActsAsOrderedTree adapter', ActsAsOrderedTree::Adapters::PostgreSQL, :default
12 | it_behaves_like 'ActsAsOrderedTree adapter', ActsAsOrderedTree::Adapters::PostgreSQL, :default_with_counter_cache
13 | it_behaves_like 'ActsAsOrderedTree adapter', ActsAsOrderedTree::Adapters::PostgreSQL, :scoped
14 | end
--------------------------------------------------------------------------------
/spec/adapters/recursive_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | require 'acts_as_ordered_tree/adapters/recursive'
6 | require 'adapters/shared'
7 |
8 | describe ActsAsOrderedTree::Adapters::Recursive, :transactional do
9 | it_behaves_like 'ActsAsOrderedTree adapter', ActsAsOrderedTree::Adapters::Recursive, :default
10 | it_behaves_like 'ActsAsOrderedTree adapter', ActsAsOrderedTree::Adapters::Recursive, :default_with_counter_cache
11 | it_behaves_like 'ActsAsOrderedTree adapter', ActsAsOrderedTree::Adapters::Recursive, :scoped
12 | end
--------------------------------------------------------------------------------
/spec/callbacks_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree, 'before/after add/remove callbacks', :transactional do
6 | class Class1 < Default
7 | cattr_accessor :triggered_callbacks
8 |
9 | def self.triggered?(kind, *args)
10 | if args.any?
11 | triggered_callbacks.include?([kind, *args])
12 | else
13 | triggered_callbacks.any? { |c| c.first == kind }
14 | end
15 | end
16 |
17 | acts_as_ordered_tree :before_add => :before_add,
18 | :after_add => :after_add,
19 | :before_remove => :before_remove,
20 | :after_remove => :after_remove
21 |
22 | def run_callback(kind, *args)
23 | self.class.triggered_callbacks ||= []
24 | self.class.triggered_callbacks << [kind, self, *args]
25 | yield if block_given?
26 | end
27 |
28 | CALLBACKS = [:before, :after, :around].product([:add, :remove, :move, :reorder]).map { |a, b| "#{a}_#{b}".to_sym }
29 |
30 | CALLBACKS.each do |callback|
31 | define_method(callback) { |*args, &block| run_callback(callback, *args, &block) }
32 | send(callback, callback) if respond_to?(callback)
33 | end
34 | end
35 |
36 | matcher :trigger_callback do |*callbacks, &block|
37 | supports_block_expectations
38 |
39 | match_for_should do |proc|
40 | @with ||= nil
41 | Class1.triggered_callbacks = []
42 | proc.call
43 | callbacks.all? { |callback| Class1.triggered?(callback, *@with) }
44 | end
45 |
46 | match_for_should_not do |proc|
47 | @with ||= nil
48 | Class1.triggered_callbacks = []
49 | proc.call
50 | callbacks.none? { |callback| Class1.triggered?(callback, *@with) }
51 | end
52 |
53 | chain :with do |*args, &blk|
54 | @with = args
55 | @block = blk
56 | end
57 |
58 | description do
59 | description = "trigger callbacks #{callbacks.map(&:inspect).join(', ')}"
60 | description << " with arguments #{@with.inspect}" if @with
61 | description
62 | end
63 |
64 | failure_message_for_should do
65 | "expected that block would #{description}"
66 | end
67 |
68 | failure_message_for_should_not do
69 | desc = "expected that block would not #{description}, but following callbacks were triggered:"
70 | Class1.triggered_callbacks.each do |kind, *args|
71 | desc << "\n * #{kind.inspect} #{args.inspect}"
72 | end
73 | desc
74 | end
75 | end
76 | alias_method :trigger_callbacks, :trigger_callback
77 |
78 | # root
79 | # child 1
80 | # child 2
81 | # child 3
82 | # child 4
83 | # child 5
84 | let(:root) { Class1.create :name => 'root' }
85 | let!(:child1) { Class1.create :name => 'child1', :parent => root }
86 | let!(:child2) { Class1.create :name => 'child2', :parent => child1 }
87 | let!(:child3) { Class1.create :name => 'child3', :parent => child1 }
88 | let!(:child4) { Class1.create :name => 'child4', :parent => root }
89 | let!(:child5) { Class1.create :name => 'child5', :parent => child4 }
90 |
91 | it 'does not trigger any callbacks when tree attributes were not changed' do
92 | expect {
93 | child2.update_attributes :name => 'x'
94 | }.not_to trigger_callbacks(*Class1::CALLBACKS)
95 | end
96 |
97 | it 'does not trigger any callbacks except :before_remove and :after_remove when node is destroyed' do
98 | expect {
99 | child2.destroy
100 | }.not_to trigger_callbacks(*Class1::CALLBACKS - [:before_remove, :after_remove])
101 | end
102 |
103 | describe '*_add callbacks' do
104 | let(:new_record) { Class1.new :name => 'child6' }
105 |
106 | it 'fires *_add callbacks when new children added to node' do
107 | expect {
108 | child1.children << new_record
109 | }.to trigger_callbacks(:before_add, :after_add).with(child1, new_record)
110 | end
111 |
112 | it 'fires *_add callbacks when node is moved from another parent' do
113 | expect {
114 | child2.update_attributes!(:parent => child4)
115 | }.to trigger_callbacks(:before_add, :after_add).with(child4, child2)
116 | end
117 | end
118 |
119 | describe '*_remove callbacks' do
120 | it 'fires *_remove callbacks when node is moved from another parent' do
121 | expect {
122 | child2.update_attributes!(:parent => child4)
123 | }.to trigger_callbacks(:before_remove, :after_remove).with(child1, child2)
124 | end
125 |
126 | it 'fires *_remove callbacks when node is destroyed' do
127 | expect {
128 | child2.destroy
129 | }.to trigger_callbacks(:before_remove, :after_remove).with(child1, child2)
130 | end
131 | end
132 |
133 | describe '*_move callbacks' do
134 | it 'fires *_move callbacks when node is moved to another parent' do
135 | expect {
136 | child2.update_attributes!(:parent => child4)
137 | }.to trigger_callbacks(:before_move, :around_move, :after_move).with(child2)
138 | end
139 |
140 | it 'does not trigger *_move callbacks when node is not moved to another parent' do
141 | expect {
142 | child2.move_lower
143 | }.not_to trigger_callbacks(:before_move, :around_move, :after_move)
144 |
145 | expect {
146 | root.move_to_root
147 | }.not_to trigger_callbacks(:before_move, :around_move, :after_move)
148 | end
149 | end
150 |
151 | describe '*_reorder callbacks' do
152 | it 'fires *_reorder callbacks when node position is changed but parent not' do
153 | expect {
154 | child2.position += 1
155 | child2.save
156 | }.to trigger_callbacks(:before_reorder, :around_reorder, :after_reorder).with(child2)
157 | end
158 |
159 | it 'does not fire *_reorder callbacks when node is moved to another parent' do
160 | expect {
161 | child2.move_to_root
162 | }.not_to trigger_callbacks(:before_reorder, :around_reorder, :after_reorder)
163 | end
164 | end
165 |
166 | describe 'Callbacks context' do
167 | specify 'new parent_id should be available in before_move' do
168 | expect(child2).to receive(:before_move) { expect(child2.parent_id).to eq child4.id }
169 | child2.update_attributes! :parent => child4
170 | end
171 |
172 | specify 'new position should be available in before_reorder' do
173 | expect(child2).to receive(:before_reorder) { expect(child2.position).to eq 2 }
174 | child2.move_lower
175 | end
176 | end
177 | end
--------------------------------------------------------------------------------
/spec/counter_cache_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree, ':counter_cache option', :transactional do
6 | describe 'Class without counter cache, #children.size' do
7 | tree :factory => :default do
8 | root {
9 | child_1
10 | child_2
11 | }
12 | end
13 |
14 | it { expect(root.children.size).to eq 2 }
15 | it { expect{root.children.size}.to query_database.once }
16 | end
17 |
18 | describe 'Class with counter cache, #children.size' do
19 | tree :factory => :default_with_counter_cache do
20 | root {
21 | child_1
22 | child_2
23 | }
24 | end
25 |
26 | before { root.reload }
27 |
28 | it { expect(root.children.size).to eq 2 }
29 | it { expect{root.children.size}.not_to query_database }
30 | end
31 | end
--------------------------------------------------------------------------------
/spec/create_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree, 'Create node', :transactional do
6 | shared_examples 'create ordered tree node' do |model = :default|
7 | let(:record) { build model }
8 |
9 | before { record.parent = parent }
10 |
11 | context 'when position is nil' do
12 | before { record.position = nil }
13 |
14 | it 'does not change node parent' do
15 | expect{record.save}.not_to change(record, :parent)
16 | end
17 |
18 | it 'puts record to position = 1 when there are no siblings' do
19 | expect{record.save}.to change(record, :position).from(nil).to(1)
20 | end
21 |
22 | it 'puts record to bottom position when there are some siblings' do
23 | create model, :parent => parent
24 |
25 | expect{record.save}.to change(record, :position).from(nil).to(2)
26 | end
27 |
28 | it 'calculates depth column' do
29 | if record.ordered_tree.columns.depth?
30 | expect{record.save}.to change(record, :depth).from(nil).to(parent ? parent.depth + 1 : 0)
31 | end
32 | end
33 | end
34 |
35 | context 'when position != nil' do
36 | before { record.position = 3 }
37 |
38 | it 'changes position to 1 if siblings is empty' do
39 | expect{record.save}.to change(record, :position).from(3).to(1)
40 | end
41 |
42 | it 'changes position to highest if there are too few siblings' do
43 | create model, :parent => parent
44 |
45 | expect{record.save}.to change(record, :position).from(3).to(2)
46 | end
47 |
48 | it 'increments position of lower siblings on insert' do
49 | first = create model, :parent => parent
50 | second = create model, :parent => parent
51 | third = create model, :parent => parent
52 |
53 | expect(first.reload.position).to eq 1
54 | expect(second.reload.position).to eq 2
55 | expect(third.reload.position).to eq 3
56 |
57 | expect{record.save and third.reload}.to change(third, :position).from(3).to(4)
58 |
59 | expect(first.reload.position).to eq 1
60 | expect(second.reload.position).to eq 2
61 | expect(record.reload.position).to eq 3
62 | end
63 |
64 | it 'calculates depth column' do
65 | if record.ordered_tree.columns.depth?
66 | expect{record.save}.to change(record, :depth).from(nil).to(parent ? parent.depth + 1 : 0)
67 | end
68 | end
69 | end
70 | end
71 |
72 | context 'when parent is nil' do
73 | include_examples 'create ordered tree node' do
74 | let(:parent) { nil }
75 | end
76 | end
77 |
78 | context 'when parent exists' do
79 | include_examples 'create ordered tree node' do
80 | let(:parent) { create :default }
81 | end
82 | end
83 |
84 | context 'when parent exists (scoped)' do
85 | include_examples 'create ordered tree node', :scoped do
86 | let(:type) { 'scope' }
87 | let(:parent) { create :scoped, :scope_type => type }
88 |
89 | it 'copies scope columns values from parent node' do
90 | expect{record.save}.to change(record, :scope_type).to(parent.scope_type)
91 | end
92 | end
93 | end
94 |
95 | describe 'when counter_cache exists' do
96 | include_examples 'create ordered tree node', :default_with_counter_cache do
97 | let(:parent) { create :default_with_counter_cache }
98 |
99 | it 'sets counter_cache to 0 for new record' do
100 | record.save
101 |
102 | expect(record.categories_count).to eq 0
103 | end
104 |
105 | it 'increments counter_cache of parent' do
106 | expect{record.save and parent.reload}.to change(parent, :categories_count).by(1)
107 | end
108 | end
109 | end
110 | end
--------------------------------------------------------------------------------
/spec/destroy_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree, 'Destroy node', :transactional do
6 | shared_examples 'destroy ordered tree node' do |model = :default, attrs = {}|
7 | tree :factory => model, :attributes => attrs do
8 | root {
9 | child_1 {
10 | grandchild_1 {
11 | grandchild_2
12 | }
13 | }
14 | child_2
15 | child_3
16 | }
17 | end
18 |
19 | def assert_destroyed(record)
20 | expect(record.class).not_to exist(record.id)
21 | end
22 |
23 | it 'destroys descendants' do
24 | child_1.destroy
25 |
26 | assert_destroyed(grandchild_1)
27 | assert_destroyed(grandchild_2)
28 | end
29 |
30 | it 'decrements lower siblings positions' do
31 | child_1.destroy
32 |
33 | [child_2, child_3].each(&:reload)
34 |
35 | expect(child_2.position).to eq 1
36 | expect(child_3.position).to eq 2
37 | end
38 | end
39 |
40 | context 'Default model' do
41 | include_examples 'destroy ordered tree node', :default
42 | end
43 |
44 | context 'Scoped model' do
45 | include_examples 'destroy ordered tree node', :scoped, :scope_type => 't'
46 | end
47 |
48 | context 'Model with counter cache' do
49 | include_examples 'destroy ordered tree node', :default_with_counter_cache
50 |
51 | before { root.reload }
52 |
53 | it 'decrements parent children counter' do
54 | expect{child_1.destroy and root.reload}.to change(root, :categories_count).from(3).to(2)
55 | end
56 | end
57 | end
--------------------------------------------------------------------------------
/spec/inheritance_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree, 'inheritance without STI', :transactional do
6 | class BaseCategory < ActiveRecord::Base
7 | self.table_name = 'categories'
8 |
9 | acts_as_ordered_tree
10 | end
11 |
12 | class ConcreteCategory < BaseCategory
13 | end
14 |
15 | class ConcreteCategoryWithScope < BaseCategory
16 | default_scope { where(arel_table[:name].matches('* %')) }
17 | end
18 |
19 | let!(:root) { BaseCategory.create(:name => '* root') }
20 | let!(:child_1) { BaseCategory.create(:name => 'child 1', :parent => root) }
21 | let!(:child_2) { BaseCategory.create(:name => 'child 2', :parent => child_1) }
22 | let!(:child_3) { BaseCategory.create(:name => 'child 3', :parent => child_1) }
23 | let!(:child_4) { BaseCategory.create(:name => '* child 4', :parent => root) }
24 | let!(:child_5) { BaseCategory.create(:name => '* child 5', :parent => child_4) }
25 | let!(:child_6) { BaseCategory.create(:name => 'child 6', :parent => child_4) }
26 |
27 | matcher :be_of do |klass|
28 | match do |relation|
29 | expect(relation.map(&:class).uniq).to eq [klass]
30 | end
31 | end
32 |
33 | shared_examples 'Inheritance test' do |klass|
34 | describe "#{klass.name}#children" do
35 | let(:root_node) { root.becomes(klass) }
36 |
37 | it { expect(root_node).to be_a klass }
38 | it { expect(root_node.children).to be_of klass }
39 | end
40 |
41 | describe "#{klass.name}#parent" do
42 | let(:node) { child_5.becomes(klass) }
43 |
44 | it { expect(node.parent).to be_an_instance_of klass }
45 | end
46 |
47 | describe "#{klass.name}#root" do
48 | let(:root_node) { root.becomes(klass) }
49 | let(:node) { child_5.becomes(klass) }
50 |
51 | it { expect(node.root).to eq root_node }
52 | it { expect(node.root).to be_an_instance_of klass }
53 | end
54 |
55 | describe "#{klass.name}#descendants" do
56 | let(:node) { child_4.becomes(klass) }
57 |
58 | it { expect(node.descendants).to be_of klass }
59 | end
60 |
61 | describe "#{klass.name}#ancestors" do
62 | let(:node) { child_5.becomes(klass) }
63 |
64 | it { expect(node.ancestors).to be_of klass }
65 | end
66 |
67 | describe "#{klass.name} tree validators" do
68 | it 'calls validators only once' do
69 | expect_any_instance_of(ActsAsOrderedTree::Validators::CyclicReferenceValidator).to receive(:validate).once
70 |
71 | child_1.becomes(klass).save
72 | end
73 | end
74 | end
75 |
76 | include_examples 'Inheritance test', BaseCategory
77 | include_examples 'Inheritance test', ConcreteCategory
78 | include_examples 'Inheritance test', ConcreteCategoryWithScope do
79 | let(:klass) { ConcreteCategoryWithScope }
80 |
81 | describe 'ConcreteCategoryWithScope#children' do
82 | let(:node) { klass.find(child_4.id) }
83 |
84 | it { expect(node).to be_a klass }
85 | it { expect(node.children).to be_of klass }
86 |
87 | it 'applies class default scope to #children' do
88 | expect(node.children.size).to eq 1
89 | end
90 | end
91 |
92 | describe 'ConcreteCategoryWithScope#parent' do
93 | let(:orphaned) { child_2.becomes(klass) }
94 | let(:out_of_scope_with_proper_parent) { child_1.becomes(klass) }
95 |
96 | it { expect(orphaned.parent).to be_nil }
97 | it { expect(out_of_scope_with_proper_parent.parent).to eq root.becomes(klass) }
98 | end
99 |
100 | describe 'ConcreteCategoryWithScope#descendants' do
101 | let(:root_node) { klass.find(root.id) }
102 |
103 | it { expect(root_node.descendants).to be_of klass }
104 | it { expect(root_node.descendants.map(&:id)).to eq [child_4.id, child_5.id] }
105 | end
106 | end
107 | end
108 |
109 | describe ActsAsOrderedTree, 'inheritance with STI', :transactional do
110 | class StiRoot < StiExample
111 | end
112 |
113 | class StiExample1 < StiExample
114 | end
115 |
116 | class StiExample2 < StiExample
117 | end
118 |
119 | # build tree
120 | let!(:root) { StiRoot.create(:name => 'root') }
121 | let!(:child_1) { StiExample1.create(:name => 'child 1', :parent => root) }
122 | let!(:child_2) { StiExample1.create(:name => 'child 2', :parent => child_1) }
123 | let!(:child_3) { StiExample1.create(:name => 'child 3', :parent => child_1) }
124 | let!(:child_4) { StiExample2.create(:name => 'child 4', :parent => root) }
125 | let!(:child_5) { StiExample2.create(:name => 'child 5', :parent => child_4) }
126 | let!(:child_6) { StiExample2.create(:name => 'child 6', :parent => child_4) }
127 |
128 | before { [root, child_1, child_2, child_3, child_4, child_5, child_6].each(&:reload) }
129 |
130 | describe '#children' do
131 | it { expect(root.children).to eq [child_1, child_4] }
132 | end
133 |
134 | describe '#parent' do
135 | it { expect(child_1.parent).to eq root }
136 | end
137 |
138 | describe '#descendants' do
139 | it { expect(root.descendants).to eq [child_1, child_2, child_3, child_4, child_5, child_6] }
140 | end
141 |
142 | describe '#ancestors' do
143 | it { expect(child_5.ancestors).to eq [root, child_4] }
144 | end
145 |
146 | describe '#root' do
147 | it { expect(child_5.root).to eq root }
148 | end
149 |
150 | describe '#left_sibling' do
151 | it { expect(child_4.left_sibling).to eq child_1 }
152 | end
153 |
154 | describe '#right_sibling' do
155 | it { expect(child_1.right_sibling).to eq child_4 }
156 | end
157 |
158 | describe 'predicates' do
159 | it { expect(root).to be_is_ancestor_of(child_1) }
160 | it { expect(root).to be_is_or_is_ancestor_of(child_1) }
161 | it { expect(child_1).to be_is_descendant_of(root) }
162 | it { expect(child_1).to be_is_or_is_descendant_of(root) }
163 | end
164 |
165 | describe 'node reload' do
166 | it { expect(child_1.ordered_tree_node.reload).to eq child_1 }
167 | end
168 |
169 | describe 'node moving' do
170 | before { child_4.move_to_child_of(child_1) }
171 |
172 | it { expect(child_4.parent).to eq child_1 }
173 | it { expect(child_1.children).to include child_4 }
174 | it { expect(child_1.descendants).to include child_4 }
175 | end
176 | end
--------------------------------------------------------------------------------
/spec/move_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree, 'Movement via save', :transactional do
6 | tree :factory => :default_with_counter_cache do
7 | root {
8 | child_1 {
9 | child_2
10 | child_3
11 | }
12 | child_4 {
13 | child_5
14 | }
15 | }
16 | end
17 |
18 | def move(node, new_parent, new_position)
19 | name = "category #{rand(100..1000)}"
20 | node.parent = new_parent
21 | node.position = new_position
22 | node.name = name
23 |
24 | node.save
25 |
26 | expect(node.reload.parent).to eq new_parent
27 | expect(node.position).to eq new_position
28 | expect(node.name).to eq name
29 | end
30 |
31 | context 'when child 2 moved under child 4 to position 1' do
32 | before { move child_2, child_4, 1 }
33 |
34 | expect_tree_to_match {
35 | root {
36 | child_1 :categories_count => 1 do
37 | child_3 :position => 1
38 | end
39 | child_4 :categories_count => 2 do
40 | child_2 :position => 1
41 | child_5 :position => 2
42 | end
43 | }
44 | }
45 | end
46 |
47 | context 'when child 2 moved under child 4 to position 2' do
48 | before { move child_2, child_4, 2 }
49 |
50 | expect_tree_to_match {
51 | root {
52 | child_1 :categories_count => 1 do
53 | child_3 :position => 1
54 | end
55 | child_4 :categories_count => 2 do
56 | child_5 :position => 1
57 | child_2 :position => 2
58 | end
59 | }
60 | }
61 | end
62 |
63 | context 'when level changed' do
64 | before { move child_1, child_4, 1 }
65 |
66 | expect_tree_to_match {
67 | root {
68 | child_4 :categories_count => 2 do
69 | child_1 :position => 1, :categories_count => 2, :depth => 2 do
70 | child_2 :position => 1, :depth => 3
71 | child_3 :position => 2, :depth => 3
72 | end
73 | child_5 :position => 2, :depth => 2
74 | end
75 | }
76 | }
77 | end
78 |
79 | context 'when node moved to root' do
80 | before { move child_1, nil, 1 }
81 |
82 | expect_tree_to_match {
83 | child_1 :position => 1, :depth => 0 do
84 | child_2 :position => 1, :depth => 1
85 | child_3 :position => 2, :depth => 1
86 | end
87 | root :position => 2, :depth => 0 do
88 | child_4 :depth => 1 do
89 | child_5 :depth => 2
90 | end
91 | end
92 | }
93 | end
94 | end
--------------------------------------------------------------------------------
/spec/node/movements/move_higher_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Movements, '#move_higher', :transactional do
6 | shared_examples '#move_higher' do |factory, attrs = {}|
7 | describe "#move_higher #{factory}" do
8 | tree :factory => factory, :attributes => attrs do
9 | node_1
10 | node_2
11 | node_3
12 | end
13 |
14 | context 'trying to move highest node up' do
15 | before { node_1.move_higher }
16 |
17 | expect_tree_to_match {
18 | node_1
19 | node_2
20 | node_3
21 | }
22 | end
23 |
24 | context 'trying to move node with position > 1' do
25 | before { node_2.move_higher }
26 |
27 | expect_tree_to_match {
28 | node_2
29 | node_1
30 | node_3
31 | }
32 | end
33 |
34 | context 'when attribute, not related to tree changed' do
35 | before { @old_name = node_3.name }
36 | before { node_3.name = 'new name' }
37 |
38 | it { expect{node_3.move_higher}.to change(node_3, :name).to(@old_name) }
39 | end
40 | end
41 | end
42 |
43 | include_examples '#move_higher', :default
44 | include_examples '#move_higher', :default_with_counter_cache
45 | include_examples '#move_higher', :scoped, :scope_type => 's'
46 | end
--------------------------------------------------------------------------------
/spec/node/movements/move_lower_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Movements, '#move_lower', :transactional do
6 | shared_examples '#move_lower' do |factory, attrs = {}|
7 | describe "#move_lower #{factory}" do
8 | tree :factory => factory, :attributes => attrs do
9 | node_1
10 | node_2
11 | node_3
12 | end
13 |
14 | context 'trying to lowest node down' do
15 | before { node_3.move_lower }
16 |
17 | expect_tree_to_match {
18 | node_1
19 | node_2
20 | node_3
21 | }
22 | end
23 |
24 | context 'trying to move node with not lowest position' do
25 | before { node_2.move_lower }
26 |
27 | expect_tree_to_match {
28 | node_1
29 | node_3
30 | node_2
31 | }
32 | end
33 |
34 | context 'when attribute, not related to tree changed' do
35 | before { @old_name = node_2.name }
36 | before { node_2.name = 'new name' }
37 |
38 | it { expect{node_2.move_lower}.to change(node_2, :name).to(@old_name) }
39 | end
40 | end
41 | end
42 |
43 | include_examples '#move_lower', :default
44 | include_examples '#move_lower', :default_with_counter_cache
45 | include_examples '#move_lower', :scoped, :scope_type => 's'
46 | end
--------------------------------------------------------------------------------
/spec/node/movements/move_to_child_of_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Movements, '#move_to_child_of', :transactional do
6 | shared_examples '#move_to_child_of' do |factory|
7 | describe "#move_to_child_of #{factory}" do
8 | tree :factory => factory do
9 | root {
10 | child_1
11 | child_2
12 | child_3 {
13 | child_4
14 | }
15 | }
16 | end
17 |
18 | context 'when AR object given' do
19 | it 'moves node' do
20 | expect {
21 | child_3.move_to_child_of(child_1)
22 | }.to change(child_3, :parent).from(root).to(child_1)
23 | end
24 |
25 | it 'does not change attributes unrelated to tree' do
26 | old, child_4.name = child_4.name, 'new name'
27 |
28 | expect {
29 | child_4.move_to_child_of(root)
30 | }.to change(child_4, :name).to(old)
31 | end
32 |
33 | context 'moving to child of self' do
34 | it { expect(child_3.move_to_child_of(child_3)).to be false }
35 |
36 | it 'does not move node' do
37 | expect {
38 | child_3.move_to_child_of(child_3)
39 | }.not_to change(child_3, :reload)
40 | end
41 |
42 | it 'invalidates node' do
43 | expect {
44 | child_3.move_to_child_of(child_3)
45 | }.to change(child_3, :valid?).from(true).to(false)
46 | end
47 | end
48 |
49 | context 'moving to child of current parent' do
50 | it 'does not move node' do
51 | expect {
52 | child_2.move_to_child_of(root)
53 | }.not_to change(child_2, :reload)
54 | end
55 | end
56 |
57 | context 'moving to child of descendant' do
58 | it { expect(root.move_to_child_of(child_1)).to be false }
59 |
60 | it 'does node move node' do
61 | expect {
62 | root.move_to_child_of(child_1)
63 | }.not_to change(root, :reload)
64 | end
65 |
66 | it 'invalidates node' do
67 | expect {
68 | root.move_to_child_of(child_1)
69 | }.to change(root, :valid?).from(true).to(false)
70 | end
71 | end
72 |
73 | context 'moving node deeper' do
74 | before { child_3.move_to_child_of(child_2) }
75 |
76 | expect_tree_to_match {
77 | root {
78 | child_1
79 | child_2 {
80 | child_3 {
81 | child_4
82 | }
83 | }
84 | }
85 | }
86 | end
87 |
88 | context 'moving node upper' do
89 | before { child_4.move_to_child_of(root) }
90 |
91 | expect_tree_to_match {
92 | root {
93 | child_1
94 | child_2
95 | child_3
96 | child_4
97 | }
98 | }
99 | end
100 | end
101 |
102 | context 'when ID given' do
103 | it 'moves node' do
104 | expect {
105 | child_3.move_to_child_of(child_1.id)
106 | }.to change(child_3, :parent).from(root).to(child_1)
107 | end
108 |
109 | it 'does not move node if parent was not changed' do
110 | expect {
111 | child_2.move_to_child_of(root.id)
112 | }.not_to change(child_2, :reload)
113 | end
114 |
115 | context 'moving to non-existent ID' do
116 | before { allow(child_3).to receive(:valid?).and_return(false) }
117 |
118 | it { expect(child_3.move_to_child_of(-1)).to be false }
119 |
120 | it 'does not move node' do
121 | expect {
122 | child_3.move_to_child_of(-1)
123 | }.not_to change(child_3, :reload)
124 | end
125 | end
126 | end
127 |
128 | context 'when nil given' do
129 | before { child_2.move_to_child_of(nil) }
130 |
131 | expect_tree_to_match {
132 | root {
133 | child_1
134 | child_3 {
135 | child_4
136 | }
137 | }
138 | child_2
139 | }
140 | end
141 | end
142 | end
143 |
144 | include_examples '#move_to_child_of', :default
145 | include_examples '#move_to_child_of', :default_with_counter_cache
146 | include_examples '#move_to_child_of', :scoped
147 | end
--------------------------------------------------------------------------------
/spec/node/movements/move_to_child_with_index_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Movements, '#move_to_child_with_index', :transactional do
6 | shared_examples '#move_to_child_with_index' do |factory|
7 | describe "#move_to_child_with_index #{factory}" do
8 | tree :factory => factory do
9 | root {
10 | node_1
11 | node_2
12 | node_3 {
13 | node_4
14 | }
15 | }
16 | end
17 |
18 | context 'moving node to same parent and same position' do
19 | before { node_2.move_to_child_with_index(root, 1) }
20 |
21 | expect_tree_to_match {
22 | root {
23 | node_1
24 | node_2
25 | node_3 {
26 | node_4
27 | }
28 | }
29 | }
30 | end
31 |
32 | context 'moving node to same parent with another position' do
33 | before { node_1.move_to_child_with_index(root, 1) }
34 |
35 | expect_tree_to_match {
36 | root {
37 | node_2
38 | node_1
39 | node_3 {
40 | node_4
41 | }
42 | }
43 | }
44 | end
45 |
46 | context 'moving node to same parent to lowest position' do
47 | before { node_1.move_to_child_with_index(root, -1) }
48 |
49 | expect_tree_to_match {
50 | root {
51 | node_2
52 | node_3 {
53 | node_4
54 | }
55 | node_1
56 | }
57 | }
58 | end
59 |
60 | context 'moving node to position with negative index' do
61 | before { node_4.move_to_child_with_index(root, -2) }
62 |
63 | expect_tree_to_match {
64 | root {
65 | node_1
66 | node_4
67 | node_2
68 | node_3
69 | }
70 | }
71 | end
72 |
73 | context 'moving node to root with index starting from end' do
74 | before { node_4.move_to_child_with_index(nil, -1) }
75 |
76 | expect_tree_to_match {
77 | node_4
78 | root {
79 | node_1
80 | node_2
81 | node_3
82 | }
83 | }
84 | end
85 |
86 | context 'moving to node to very position with large negative index' do
87 | before { node_4.move_to_child_with_index(root, -100) }
88 |
89 | expect_tree_to_match {
90 | root {
91 | node_4
92 | node_1
93 | node_2
94 | node_3
95 | }
96 | }
97 | end
98 |
99 | context 'moving to node to very large position' do
100 | before { node_4.move_to_child_with_index(root, 100) }
101 |
102 | expect_tree_to_match {
103 | root {
104 | node_1
105 | node_2
106 | node_3
107 | node_4
108 | }
109 | }
110 | end
111 |
112 | context 'when attribute, not related to tree, changed' do
113 | before { @old_name = node_2.name }
114 | before { node_2.name = 'new name' }
115 |
116 | it { expect{node_2.move_to_child_with_index(root, 1)}.to change(node_2, :name).to(@old_name) }
117 | end
118 | end
119 | end
120 |
121 | include_examples '#move_to_child_with_index', :default
122 | include_examples '#move_to_child_with_index', :default_with_counter_cache
123 | include_examples '#move_to_child_with_index', :scoped
124 | end
--------------------------------------------------------------------------------
/spec/node/movements/move_to_child_with_position_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Movements, '#move_to_child_with_position', :transactional do
6 | shared_examples '#move_to_child_with_position' do |factory|
7 | describe "#move_to_child_with_position #{factory}" do
8 | tree :factory => factory do
9 | root {
10 | node_1
11 | node_2
12 | node_3 {
13 | node_4
14 | }
15 | }
16 | end
17 |
18 | context 'moving node to same parent and same position' do
19 | before { node_2.move_to_child_with_position(root, 2) }
20 |
21 | expect_tree_to_match {
22 | root {
23 | node_1
24 | node_2
25 | node_3 {
26 | node_4
27 | }
28 | }
29 | }
30 | end
31 |
32 | context 'moving node to same parent with another position' do
33 | before { node_1.move_to_child_with_position(root, 2) }
34 |
35 | expect_tree_to_match {
36 | root {
37 | node_2
38 | node_1
39 | node_3 {
40 | node_4
41 | }
42 | }
43 | }
44 | end
45 |
46 | context 'moving node to same parent to lowest position' do
47 | before { node_1.move_to_child_with_position(root, 3) }
48 |
49 | expect_tree_to_match {
50 | root {
51 | node_2
52 | node_3 {
53 | node_4
54 | }
55 | node_1
56 | }
57 | }
58 | end
59 |
60 | context 'moving to node to very large position' do
61 | before { node_4.move_to_child_with_position(root, 100) }
62 |
63 | expect_tree_to_match {
64 | root {
65 | node_1
66 | node_2
67 | node_3
68 | node_4
69 | }
70 | }
71 | end
72 |
73 | context 'when attribute, not related to tree, changed' do
74 | before { @old_name = node_2.name }
75 | before { node_2.name = 'new name' }
76 |
77 | it { expect{node_2.move_to_child_with_position(root, 2)}.to change(node_2, :name).to(@old_name) }
78 | end
79 | end
80 | end
81 |
82 | include_examples '#move_to_child_with_position', :default
83 | include_examples '#move_to_child_with_position', :default_with_counter_cache
84 | include_examples '#move_to_child_with_position', :scoped
85 | end
--------------------------------------------------------------------------------
/spec/node/movements/move_to_left_of_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Movements, '#move_to_left_of', :transactional do
6 | shared_examples '#move_to_left_of' do |factory|
7 | describe "#move_to_left_of #{factory}" do
8 | tree :factory => factory do
9 | root {
10 | node_1
11 | node_2
12 | node_3 {
13 | node_4
14 | }
15 | }
16 | end
17 |
18 | context 'moving node to next position' do
19 | before { node_1.move_to_left_of(node_2) }
20 |
21 | expect_tree_to_match {
22 | root {
23 | node_1
24 | node_2
25 | node_3 {
26 | node_4
27 | }
28 | }
29 | }
30 | end
31 |
32 | context 'moving node to same parent higher' do
33 | before { node_3.move_to_left_of(node_1) }
34 |
35 | expect_tree_to_match {
36 | root {
37 | node_3 {
38 | node_4
39 | }
40 | node_1
41 | node_2
42 | }
43 | }
44 | end
45 |
46 | context 'moving node to same parent lower' do
47 | before { node_1.move_to_left_of(node_3) }
48 |
49 | expect_tree_to_match {
50 | root {
51 | node_2
52 | node_1
53 | node_3 {
54 | node_4
55 | }
56 | }
57 | }
58 | end
59 |
60 | context 'moving inner node to left of root' do
61 | before { node_3.move_to_left_of(root) }
62 |
63 | expect_tree_to_match {
64 | node_3 {
65 | node_4
66 | }
67 | root {
68 | node_1
69 | node_2
70 | }
71 | }
72 | end
73 |
74 | context 'moving inner node to left of another inner node (shallower)' do
75 | before { node_4.move_to_left_of(node_1) }
76 |
77 | expect_tree_to_match {
78 | root {
79 | node_4
80 | node_1
81 | node_2
82 | node_3
83 | }
84 | }
85 | end
86 |
87 | context 'moving inner node to left of another inner node (deeper)' do
88 | before { node_1.move_to_left_of(node_4) }
89 |
90 | expect_tree_to_match {
91 | root {
92 | node_2
93 | node_3 {
94 | node_1
95 | node_4
96 | }
97 | }
98 | }
99 | end
100 |
101 | context 'attempt to perform impossible movement' do
102 | it { expect{ root.move_to_left_of(node_1) }.not_to change(current_tree, :all) }
103 | it { expect{ node_3.move_to_left_of(node_4) }.not_to change(current_tree, :all) }
104 | it { expect{ node_1.move_to_left_of(node_1) }.not_to change(current_tree, :all) }
105 | it { expect{ node_3.move_to_left_of(node_3) }.not_to change(current_tree, :all) }
106 | end
107 |
108 | context 'when attribute, not related to tree, changed' do
109 | before { @old_name = node_2.name }
110 | before { node_2.name = 'new name' }
111 |
112 | it { expect{node_2.move_to_left_of(node_4)}.to change(node_2, :name).to(@old_name) }
113 | end
114 | end
115 | end
116 |
117 | include_examples '#move_to_left_of', :default
118 | include_examples '#move_to_left_of', :default_with_counter_cache
119 | include_examples '#move_to_left_of', :scoped, :scope_type => 's'
120 | end
--------------------------------------------------------------------------------
/spec/node/movements/move_to_right_of_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Movements, '#move_to_right_of', :transactional do
6 | shared_examples '#move_to_right_of' do |factory|
7 | describe "#move_to_right_of #{factory}" do
8 | tree :factory => factory do
9 | root {
10 | node_1
11 | node_2
12 | node_3 {
13 | node_4
14 | }
15 | }
16 | end
17 |
18 | context 'moving node to same parent higher' do
19 | before { node_3.move_to_right_of(node_1) }
20 |
21 | expect_tree_to_match {
22 | root {
23 | node_1
24 | node_3 {
25 | node_4
26 | }
27 | node_2
28 | }
29 | }
30 | end
31 |
32 | context 'moving node to next position' do
33 | before { node_1.move_to_right_of(node_2) }
34 |
35 | expect_tree_to_match {
36 | root {
37 | node_2
38 | node_1
39 | node_3 {
40 | node_4
41 | }
42 | }
43 | }
44 | end
45 |
46 | context 'moving node to same parent lower' do
47 | before { node_1.move_to_right_of(node_3) }
48 |
49 | expect_tree_to_match {
50 | root {
51 | node_2
52 | node_3 {
53 | node_4
54 | }
55 | node_1
56 | }
57 | }
58 | end
59 |
60 | context 'moving inner node to right of root node' do
61 | before { node_3.move_to_right_of(root) }
62 |
63 | expect_tree_to_match {
64 | root {
65 | node_1
66 | node_2
67 | }
68 | node_3 {
69 | node_4
70 | }
71 | }
72 | end
73 |
74 | context 'moving inner node to right of another inner node (shallower)' do
75 | before { node_4.move_to_right_of(node_1) }
76 |
77 | expect_tree_to_match {
78 | root {
79 | node_1
80 | node_4
81 | node_2
82 | node_3
83 | }
84 | }
85 | end
86 |
87 | context 'moving inner node to right of another inner node (deeper)' do
88 | before { node_1.move_to_right_of(node_4) }
89 |
90 | expect_tree_to_match {
91 | root {
92 | node_2
93 | node_3 {
94 | node_4
95 | node_1
96 | }
97 | }
98 | }
99 | end
100 |
101 | context 'Attempt to perform impossible movement' do
102 | it { expect{ root.move_to_right_of(node_1) }.not_to change(current_tree, :all) }
103 | it { expect{ node_3.move_to_right_of(node_4) }.not_to change(current_tree, :all) }
104 | it { expect{ node_1.move_to_right_of(node_1) }.not_to change(current_tree, :all) }
105 | it { expect{ node_3.move_to_right_of(node_3) }.not_to change(current_tree, :all) }
106 | end
107 |
108 | context 'when attribute, not related to tree changed' do
109 | before { @old_name = node_2.name }
110 | before { node_2.name = 'new name' }
111 |
112 | it { expect{node_2.move_to_right_of(node_4)}.to change(node_2, :name).to(@old_name) }
113 | end
114 | end
115 | end
116 |
117 | include_examples '#move_to_right_of', :default
118 | include_examples '#move_to_right_of', :default_with_counter_cache
119 | include_examples '#move_to_right_of', :scoped, :scope_type => 's'
120 | end
--------------------------------------------------------------------------------
/spec/node/movements/move_to_root_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Movements, '#move_to_root', :transactional do
6 | shared_examples '#move_to_root' do |factory, attrs = {}|
7 | describe "#move_to_root #{factory}" do
8 | tree :factory => factory, :attributes => attrs do
9 | root_1 {
10 | node_1
11 | node_2
12 | node_3 {
13 | node_4
14 | }
15 | }
16 | root_2 {
17 | node_5
18 | }
19 | end
20 |
21 | context 'moving root node to root' do
22 | before { root_2.move_to_root }
23 |
24 | expect_tree_to_match {
25 | root_1 {
26 | node_1
27 | node_2
28 | node_3 {
29 | node_4
30 | }
31 | }
32 | root_2 {
33 | node_5
34 | }
35 | }
36 | end
37 |
38 | context 'moving inner node to root' do
39 | before { node_3.move_to_root }
40 |
41 | expect_tree_to_match {
42 | root_1 {
43 | node_1
44 | node_2
45 | }
46 | root_2 {
47 | node_5
48 | }
49 | node_3 {
50 | node_4
51 | }
52 | }
53 | end
54 |
55 | context 'when attribute, not related to tree changed' do
56 | before { @old_name = node_2.name }
57 | before { node_2.name = 'new name' }
58 |
59 | it { expect{node_2.move_to_root}.to change(node_2, :name).to(@old_name) }
60 | end
61 | end
62 | end
63 |
64 | include_examples '#move_to_root', :default
65 | include_examples '#move_to_root', :default_with_counter_cache
66 | include_examples '#move_to_root', :scoped, :scope_type => 's'
67 | end
--------------------------------------------------------------------------------
/spec/node/reloading_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Reloading, :transactional do
6 | shared_examples 'ActsAsOrderedTree::Node#reload' do |model, attrs = {}|
7 | describe model.to_s.camelize, '#reload' do
8 | let(:record) { create model, attrs }
9 | let(:node) { record.ordered_tree_node }
10 |
11 | # change all attributes
12 | before { node.parent_id = create(model, attrs).id }
13 | before { node.position = 3 }
14 | before { node.depth = 2 if record.ordered_tree.columns.depth? }
15 | before { record[record.ordered_tree.columns.counter_cache] = 5 if record.ordered_tree.columns.counter_cache? }
16 | before { record.name = 'another name' }
17 |
18 | it 'reloads attributes related to tree' do
19 | node.reload
20 |
21 | expect(node.parent_id).to eq nil
22 | expect(node.position).to eq 1
23 |
24 | if record.ordered_tree.columns.depth?
25 | expect(node.depth).to eq 0
26 | end
27 |
28 | if record.class.ordered_tree.columns.counter_cache?
29 | expect(record.children.size).to eq 0
30 | end
31 | end
32 |
33 | it 'does not reload another attributes' do
34 | expect{node.reload}.not_to change(record, :name)
35 | end
36 | end
37 | end
38 |
39 | include_examples 'ActsAsOrderedTree::Node#reload', :default
40 | include_examples 'ActsAsOrderedTree::Node#reload', :default_with_counter_cache
41 | include_examples 'ActsAsOrderedTree::Node#reload', :scoped
42 | end
--------------------------------------------------------------------------------
/spec/node/siblings_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Siblings, :transactional do
6 | shared_examples 'siblings' do |model|
7 | # silence pending examples
8 | #
9 | # @todo fix all xits
10 | def self.xit(*) end
11 |
12 | let(:root) { create model }
13 | let(:items) { create_list model, 3, :parent => root }
14 |
15 | # first
16 | it { expect(items.first.self_and_siblings).to eq items }
17 | it { expect(items.first.siblings).to eq [items.second, items.last] }
18 |
19 | it { expect(items.first.left_sibling).to be nil }
20 | it { expect(items.first.right_sibling).to eq items.second }
21 |
22 | it { expect(items.first.left_siblings).to be_empty }
23 | it { expect(items.first.right_siblings).to eq [items.second, items.last] }
24 |
25 | # second
26 | it { expect(items.second.self_and_siblings).to eq items }
27 | it { expect(items.second.siblings).to eq [items.first, items.last] }
28 |
29 | it { expect(items.second.left_sibling).to eq items.first }
30 | it { expect(items.second.right_sibling).to eq items.last }
31 |
32 | it { expect(items.second.left_siblings).to eq [items.first] }
33 | it { expect(items.second.right_siblings).to eq [items.last] }
34 |
35 | # last
36 | it { expect(items.last.self_and_siblings).to eq items }
37 | it { expect(items.last.siblings).to eq [items.first, items.second] }
38 |
39 | it { expect(items.last.left_sibling).to eq items.second }
40 | it { expect(items.last.right_sibling).to be nil }
41 |
42 | it { expect(items.last.left_siblings).to eq [items.first, items.second] }
43 | it { expect(items.last.right_siblings).to be_empty }
44 |
45 | context 'trying to set left or right sibling with random object' do
46 | def self.expect_type_mismatch_on(value)
47 | it "throws error when #{value.class} given" do
48 | expect {
49 | items.first.left_sibling = value
50 | }.to raise_error ActiveRecord::AssociationTypeMismatch
51 |
52 | expect {
53 | items.first.right_sibling = value
54 | }.to raise_error ActiveRecord::AssociationTypeMismatch
55 | end
56 | end
57 |
58 | def self.generate_class
59 | Class.new(ActiveRecord::Base){ self.table_name = 'categories' }
60 | end
61 |
62 | expect_type_mismatch_on(generate_class.new)
63 | expect_type_mismatch_on(nil)
64 | end
65 |
66 | context 'when left sibling is set' do
67 | context 'and new left sibling has same parent' do
68 | context 'and new left sibling is higher' do
69 | let(:item) { items.last }
70 | before { item.left_sibling = items.first }
71 |
72 | it { expect(item.parent).to eq items.first.parent }
73 | it { expect(item.position).to eq 2 }
74 |
75 | xit { expect(item.left_sibling).to eq items.first }
76 | xit { expect(item.right_siblings).to eq [items.second] }
77 | end
78 |
79 | context 'and new left sibling is lower' do
80 | let(:item) { items.first }
81 | before { item.left_sibling = items.last }
82 |
83 | it { expect(item.parent).to eq items.first.parent }
84 | it { expect(item.position).to eq 3 }
85 |
86 | xit { expect(item.left_sibling).to eq items.last }
87 | xit { expect(item.left_siblings).to eq [items.second, items.last] }
88 | end
89 | end
90 |
91 | context 'and new left sibling has other parent' do
92 | let(:item) { items.first }
93 | before { item.left_sibling = root }
94 |
95 | it { expect(item.parent).to be nil }
96 | it { expect(item.position).to eq 2 }
97 |
98 | xit { expect(item.left_sibling).to eq root }
99 | end
100 |
101 | context 'via #left_sibling_id=' do
102 | let(:item) { items.first }
103 |
104 | it 'throws error when non-existent ID given' do
105 | expect {
106 | item.left_sibling_id = -1
107 | }.to raise_error ActiveRecord::RecordNotFound
108 | end
109 |
110 | it 'delegates to #left_sibling=' do
111 | new_sibling = items.last
112 |
113 | expect(item.ordered_tree_node).to receive(:left_sibling=).with(new_sibling)
114 | item.left_sibling_id = new_sibling.id
115 | end
116 | end
117 | end
118 |
119 | context 'when right sibling is set' do
120 | context 'and new right sibling has same parent' do
121 | context 'and new right sibling is higher' do
122 | let(:item) { items.last }
123 | before { item.right_sibling = items.first }
124 |
125 | it { expect(item.parent).to eq items.first.parent }
126 | it { expect(item.position).to eq 1 }
127 |
128 | xit { expect(item.right_sibling).to eq item.first }
129 | xit { expect(item.left_siblings).to be_empty }
130 | xit { expect(item.right_siblings).to eq [items.first, items.second] }
131 | end
132 |
133 | context 'and new right sibling is lower' do
134 | let(:item) { items.first }
135 | before { item.right_sibling = items.last }
136 |
137 | it { expect(item.parent).to eq items.first.parent }
138 | it { expect(item.position).to eq 2 }
139 |
140 | xit { expect(item.right_sibling).to eq items.last }
141 | xit { expect(item.right_siblings).to eq [items.last] }
142 |
143 | xit { expect(item.left_sibling).to eq items.first }
144 | xit { expect(item.left_siblings).to eq [items.first] }
145 | end
146 | end
147 |
148 | context 'and new right sibling has other parent' do
149 | let(:item) { items.first }
150 | before { item.right_sibling = root }
151 |
152 | it { expect(item.parent).to be nil }
153 | it { expect(item.position).to eq 1 }
154 |
155 | xit { expect(item.right_sibling).to eq root }
156 | end
157 |
158 | context 'via #right_sibling_id=' do
159 | let(:item) { items.first }
160 |
161 | it 'throws error when non-existent ID given' do
162 | expect {
163 | item.right_sibling_id = -1
164 | }.to raise_error ActiveRecord::RecordNotFound
165 | end
166 |
167 | it 'delegates to #right_sibling=' do
168 | new_sibling = items.last
169 |
170 | expect(item.ordered_tree_node).to receive(:right_sibling=).with(new_sibling)
171 | item.right_sibling_id = new_sibling.id
172 | end
173 | end
174 | end
175 | end
176 |
177 | context 'Tree without scopes' do
178 | include_examples 'siblings', :default
179 | include_examples 'siblings', :default_with_counter_cache
180 | end
181 |
182 | context 'Tree with scope' do
183 | let!(:items_1) { create_list :scoped, 3, :scope_type => 's1' }
184 | let!(:items_2) { create_list :scoped, 3, :scope_type => 's2' }
185 |
186 | include_examples 'siblings', :scoped do
187 | let(:items) { items_1 }
188 | end
189 | include_examples 'siblings', :scoped do
190 | let(:items) { items_2 }
191 | end
192 | end
193 | end
--------------------------------------------------------------------------------
/spec/node/traversals_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Node::Traversals, :transactional do
6 | shared_examples 'ActsAsOrderedTree::Node traversals' do |model, attrs = {}|
7 | let(:root_1) { create model, attrs }
8 | let(:child_1) { create model, attrs.merge(:parent => root_1) }
9 | let(:grandchild_1) { create model, attrs.merge(:parent => child_1) }
10 | let(:root_2) { create model, attrs }
11 | let(:child_2) { create model, attrs.merge(:parent => root_2) }
12 | let(:grandchild_2) { create model, attrs.merge(:parent => child_2) }
13 |
14 | before { [root_1, child_1, grandchild_1].each(&:reload) }
15 | before { [root_2, child_2, grandchild_2].each(&:reload) }
16 |
17 | describe '#root' do
18 | it { expect(root_1.root).to eq root_1 }
19 | it { expect{root_1.root}.not_to query_database }
20 | it { expect(child_1.root).to eq root_1 }
21 | it { expect(grandchild_1.root).to eq root_1 }
22 |
23 | it { expect(root_2.root).to eq root_2 }
24 | it { expect{root_2.root}.not_to query_database }
25 | it { expect(child_2.root).to eq root_2 }
26 | it { expect(grandchild_2.root).to eq root_2 }
27 | end
28 |
29 | describe '#self_and_ancestors' do
30 | it { expect(root_1.self_and_ancestors).to eq [root_1] }
31 | it { expect{root_1.self_and_ancestors}.not_to query_database }
32 | it { expect(child_1.self_and_ancestors).to eq [root_1, child_1] }
33 | it { expect(grandchild_1.self_and_ancestors).to eq [root_1, child_1, grandchild_1] }
34 |
35 | it { expect(child_1.self_and_ancestors).to respond_to :each_with_level }
36 | it { expect(child_1.self_and_ancestors).to respond_to :each_without_orphans }
37 | end
38 |
39 | describe '#ancestors' do
40 | it { expect(root_1.ancestors).to eq [] }
41 | it { expect{root_1.ancestors}.not_to query_database }
42 | it { expect(child_1.ancestors).to eq [root_1] }
43 | it { expect(grandchild_1.ancestors).to eq [root_1, child_1] }
44 |
45 | it { expect(child_1.ancestors).to respond_to :each_with_level }
46 | it { expect(child_1.ancestors).to respond_to :each_without_orphans }
47 | end
48 |
49 | describe '#self_and_descendants' do
50 | it { expect(root_1.self_and_descendants).to eq [root_1, child_1, grandchild_1] }
51 | it { expect(child_1.self_and_descendants).to eq [child_1, grandchild_1] }
52 | it { expect(grandchild_1.self_and_descendants).to eq [grandchild_1] }
53 |
54 | it { expect(root_1.self_and_descendants).to respond_to :each_with_level }
55 | it { expect(root_1.self_and_descendants).to respond_to :each_without_orphans }
56 | end
57 |
58 | describe '#descendants' do
59 | it { expect(root_1.descendants).to eq [child_1, grandchild_1] }
60 | it { expect(child_1.descendants).to eq [grandchild_1] }
61 | it { expect(grandchild_1.descendants).to eq [] }
62 |
63 | it { expect(root_1.descendants).to respond_to :each_with_level }
64 | it { expect(root_1.descendants).to respond_to :each_without_orphans }
65 | end
66 | end
67 |
68 | include_examples 'ActsAsOrderedTree::Node traversals', :default
69 | include_examples 'ActsAsOrderedTree::Node traversals', :default_with_counter_cache
70 | include_examples 'ActsAsOrderedTree::Node traversals', :scoped, :scope_type => 'a'
71 | end
--------------------------------------------------------------------------------
/spec/persevering_transaction_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | require 'acts_as_ordered_tree/persevering_transaction'
6 |
7 | describe ActsAsOrderedTree::PerseveringTransaction, :non_transactional do
8 | def create_transaction(connection = ActiveRecord::Base.connection)
9 | described_class.new(connection)
10 | end
11 |
12 | describe 'Transaction state' do
13 | def transaction
14 | @transaction ||= create_transaction
15 | end
16 |
17 | it 'becomes committed only when real transaction ends' do
18 | transaction.start do
19 | nested_transaction = create_transaction
20 |
21 | nested_transaction.start { }
22 |
23 | expect(nested_transaction).not_to be_committed
24 | expect(transaction).not_to be_committed
25 | end
26 |
27 | expect(transaction).to be_committed
28 | end
29 |
30 | it 'becomes rolledback when real transaction is rolledback' do
31 | transaction.start do
32 | raise ActiveRecord::Rollback
33 | end
34 |
35 | expect(transaction).to be_rolledback
36 | end
37 | end
38 |
39 | describe 'After commit callbacks' do
40 | it 'executes callbacks only when real transaction commits' do
41 | executed = []
42 |
43 | outer = create_transaction
44 | outer.after_commit { executed << 1 }
45 |
46 | outer.start do
47 | inner = create_transaction
48 | inner.after_commit { executed << 2 }
49 |
50 | inner.start { }
51 |
52 | expect(executed).to be_empty
53 | end
54 |
55 | expect(executed).to eq [1, 2]
56 | end
57 | end
58 |
59 | describe 'Deadlock handling' do
60 | def start_in_thread(&block)
61 | Thread.start do
62 | ActiveRecord::Base.connection_pool.with_connection do |connection|
63 | trans = create_transaction(connection)
64 | trans.start(&block)
65 | trans
66 | end
67 | end
68 | end
69 |
70 | let!(:resource1) { Default.create!(:name => 'resource 1') }
71 | let!(:resource2) { Default.create!(:name => 'resource 2') }
72 |
73 | # this test randomly fails on Rails 3.1
74 | it 'Restarts transaction when deadlock occurred' do
75 | threads = []
76 |
77 | threads << start_in_thread do
78 | resource1.lock!
79 | sleep 0.1
80 | resource2.lock!
81 | sleep 0.1
82 | end
83 |
84 | threads << start_in_thread do
85 | resource2.lock!
86 | sleep 0.1
87 | resource1.lock!
88 | sleep 0.1
89 | end
90 |
91 | transactions = threads.map(&:value)
92 |
93 | expect(transactions[0]).to be_committed
94 | expect(transactions[1]).to be_committed
95 | end
96 | end
97 |
98 | end
--------------------------------------------------------------------------------
/spec/relation/arrangeable_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Relation::Arrangeable, :transactional do
6 | tree :factory => :default do
7 | root {
8 | child_1 {
9 | grandchild_11
10 | grandchild_12
11 | }
12 | child_2 {
13 | grandchild_21
14 | grandchild_22
15 | }
16 | }
17 | end
18 |
19 | specify '#descendants scope should be arrangeable' do
20 | expect(root.descendants.arrange).to eq Hash[
21 | child_1 => {
22 | grandchild_11 => {},
23 | grandchild_12 => {}
24 | },
25 | child_2 => {
26 | grandchild_21 => {},
27 | grandchild_22 => {}
28 | }
29 | ]
30 | end
31 |
32 | specify '#self_and_descendants should be arrangeable' do
33 | expect(root.self_and_descendants.arrange).to eq Hash[
34 | root => {
35 | child_1 => {
36 | grandchild_11 => {},
37 | grandchild_12 => {}
38 | },
39 | child_2 => {
40 | grandchild_21 => {},
41 | grandchild_22 => {}
42 | }
43 | }
44 | ]
45 | end
46 |
47 | specify '#ancestors should be arrangeable' do
48 | expect(grandchild_11.ancestors.arrange).to eq Hash[
49 | root => {
50 | child_1 => {}
51 | }
52 | ]
53 | end
54 |
55 | specify '#self_and_ancestors should be arrangeable' do
56 | expect(grandchild_11.self_and_ancestors.arrange).to eq Hash[
57 | root => {
58 | child_1 => {
59 | grandchild_11 => {}
60 | }
61 | }
62 | ]
63 | end
64 |
65 | it 'should not discard orphaned nodes by default' do
66 | relation = root.descendants.where(root.class.arel_table[:id].not_eq(child_1.id))
67 |
68 | expect(relation.arrange).to eq Hash[
69 | grandchild_11 => {},
70 | grandchild_12 => {},
71 | child_2 => {
72 | grandchild_21 => {},
73 | grandchild_22 => {}
74 | }
75 | ]
76 | end
77 |
78 | it 'should discard orphans if option :discard passed' do
79 | relation = root.descendants.where(root.class.arel_table[:id].not_eq(child_1.id))
80 |
81 | expect(relation.arrange(:orphans => :discard)).to eq Hash[
82 | child_2 => {
83 | grandchild_21 => {},
84 | grandchild_22 => {}
85 | }
86 | ]
87 | end
88 | end
--------------------------------------------------------------------------------
/spec/relation/iterable_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | require 'acts_as_ordered_tree/relation/iterable'
6 |
7 | describe ActsAsOrderedTree::Relation::Iterable, :transactional do
8 | shared_examples 'iterable' do |model|
9 | tree :factory => model do
10 | root_1 {
11 | child_1 {
12 | child_2
13 | }
14 | child_3 {
15 | child_4
16 | child_5
17 | }
18 | }
19 | root_2 {
20 | child_6
21 | }
22 | end
23 |
24 | describe '#each_with_level' do
25 | it 'iterates over collection and yields level' do
26 | relation = current_tree.order(:id).extending(described_class)
27 |
28 | expect { |b|
29 | relation.each_with_level(&b)
30 | }.to yield_successive_args [root_1, 0],
31 | [child_1, 1],
32 | [child_2, 2],
33 | [child_3, 1],
34 | [child_4, 2],
35 | [child_5, 2],
36 | [root_2, 0],
37 | [child_6, 1]
38 | end
39 |
40 | it 'computes level relative to first selected node' do
41 | expect { |b|
42 | root_1.descendants.extending(described_class).each_with_level(&b)
43 | }.to yield_successive_args [child_1, 1],
44 | [child_2, 2],
45 | [child_3, 1],
46 | [child_4, 2],
47 | [child_5, 2]
48 | end
49 | end
50 |
51 | describe '#each_without_orphans' do
52 | let(:relation) { current_tree.order(:id).extending(described_class) }
53 |
54 | it 'iterates over collection' do
55 | expect { |b|
56 | relation.each_without_orphans(&b)
57 | }.to yield_successive_args root_1,
58 | child_1,
59 | child_2,
60 | child_3,
61 | child_4,
62 | child_5,
63 | root_2,
64 | child_6
65 | end
66 |
67 | it 'iterates over collection and discards orphans' do
68 | expect { |b|
69 | relation.where('id != ?', child_3.id).each_without_orphans(&b)
70 | }.to yield_successive_args root_1,
71 | child_1,
72 | child_2,
73 | root_2,
74 | child_6
75 | end
76 |
77 | it 'iterates over collection and discards orphans' do
78 | expect { |b|
79 | relation.where('id != ?', root_2.id).each_without_orphans(&b)
80 | }.to yield_successive_args root_1,
81 | child_1,
82 | child_2,
83 | child_3,
84 | child_4,
85 | child_5
86 | end
87 |
88 | it 'iterates over collection and discards orphans' do
89 | expect { |b|
90 | relation.where('id != ?', root_1.id).each_without_orphans(&b)
91 | }.to yield_successive_args root_2,
92 | child_6
93 | end
94 | end
95 | end
96 |
97 | describe 'Model with cached level' do
98 | it_behaves_like 'iterable', :default
99 | end
100 |
101 | describe 'Model without cached level' do
102 | it_behaves_like 'iterable', :default_without_depth
103 | end
104 | end
--------------------------------------------------------------------------------
/spec/relation/preloaded_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | require 'acts_as_ordered_tree/relation/preloaded'
6 |
7 | describe ActsAsOrderedTree::Relation::Preloaded, :transactional do
8 | let!(:records) { create_list :default, 2 }
9 |
10 | def relation
11 | Default.where(nil).extending(described_class)
12 | end
13 |
14 | context 'when preloaded records were not set' do
15 | it { expect(relation).to match_array records }
16 | it { expect(relation.to_a).not_to be records }
17 | it { expect{relation.to_a}.to query_database.once }
18 | end
19 |
20 | context 'when preloaded records were set directly' do
21 | let(:preloaded) { relation.records(records) }
22 |
23 | it { expect(preloaded).to eq records }
24 |
25 | it { expect(preloaded.to_a).to be records }
26 | it { expect{preloaded.to_a}.not_to query_database }
27 |
28 | it { expect(preloaded.size).to eq 2 }
29 | it { expect{preloaded.size}.not_to query_database }
30 |
31 | context 'when preloaded relation was extended' do
32 | let(:extended) { preloaded.extending(Module.new) }
33 |
34 | it { expect(extended).to eq records }
35 |
36 | it { expect(extended.to_a).to be records }
37 | it { expect{extended.to_a}.not_to query_database }
38 |
39 | it { expect(extended.size).to eq 2 }
40 | it { expect{extended.size}.not_to query_database }
41 | end
42 |
43 | describe '#reverse_order' do
44 | it { expect(preloaded.reverse_order).not_to be preloaded }
45 | it { expect(preloaded.reverse_order.size).to eq 2 }
46 | it { expect(preloaded.reverse_order).to eq records.reverse }
47 | it { expect{preloaded.reverse_order.to_a}.not_to query_database }
48 | end
49 |
50 | describe '#reverse_order!' do
51 | it { expect(preloaded.reverse_order!).to be preloaded }
52 | it { expect(preloaded.reverse_order!.size).to eq 2 }
53 | it { expect(preloaded.reverse_order!.to_a).to eq records.reverse }
54 | it { expect{preloaded.reverse_order!.to_a}.not_to query_database }
55 | end
56 | end
57 | end
--------------------------------------------------------------------------------
/spec/reorder_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree, 'Reorder via save', :transactional do
6 | tree :factory => :default do
7 | root {
8 | child_1
9 | child_2
10 | child_3
11 | }
12 | end
13 |
14 | def reorder(node, position)
15 | name = "category #{rand(100..1000)}"
16 | node.position = position
17 | node.name = name
18 | expect { node.save! }.not_to raise_error
19 | expect(node.name).to eq name
20 | end
21 |
22 | def assert_order(*nodes)
23 | nodes.each_with_index do |node, index|
24 | expect(node.reload.position).to eq index + 1
25 | end
26 | end
27 |
28 | context 'when I change position to lower' do
29 | before { reorder child_2, 1 }
30 |
31 | it 'moves node up' do
32 | assert_order child_2, child_1, child_3
33 | end
34 | end
35 |
36 | context 'when I change position to lower' do
37 | before { reorder child_2, 3 }
38 |
39 | it 'moves node down' do
40 | assert_order child_1, child_3, child_2
41 | end
42 | end
43 |
44 | context 'when I move highest node lower' do
45 | before { reorder child_1, 3 }
46 |
47 | it 'moves node down' do
48 | assert_order child_2, child_3, child_1
49 | end
50 | end
51 |
52 | context 'when I move lowest node upper' do
53 | before { reorder child_3, 1 }
54 |
55 | it 'moves node down' do
56 | assert_order child_3, child_1, child_2
57 | end
58 | end
59 |
60 | context 'when I move to very high position' do
61 | before { reorder child_1, 5 }
62 |
63 | it 'moves node to bottom' do
64 | assert_order child_2, child_3, child_1
65 | end
66 | end
67 |
68 | context 'when I move to zero position' do
69 | before { reorder child_2, 0 }
70 |
71 | it 'moves it to top' do
72 | assert_order child_2, child_1, child_3
73 | end
74 | end
75 |
76 | context 'when I move to same position' do
77 | before { reorder child_2, 2 }
78 |
79 | specify 'order remains the same' do
80 | assert_order child_1, child_2, child_3
81 | end
82 | end
83 | end
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['DB'] ||= 'pg'
2 |
3 | require 'rubygems'
4 | require 'bundler/setup'
5 |
6 | require 'rspec'
7 | require 'rspec/expectations'
8 |
9 | if ENV['COVERAGE'].to_i.nonzero?
10 | begin
11 | require 'simplecov'
12 | SimpleCov.command_name "rspec/#{File.basename(ENV['BUNDLE_GEMFILE'])}/#{ENV['DB']}"
13 | SimpleCov.start 'test_frameworks' do
14 | add_filter 'vendor/'
15 | end
16 | rescue LoadError
17 | #ignore
18 | end
19 | end
20 |
21 | require 'support/db/boot'
22 |
23 | require 'factory_girl'
24 | require 'support/factories'
25 | require 'support/matchers'
26 | require 'support/tree_factory'
27 | require 'database_cleaner'
28 |
29 | RSpec.configure do |config|
30 | config.include FactoryGirl::Syntax::Methods
31 | config.extend TreeFactory
32 |
33 | config.treat_symbols_as_metadata_keys_with_true_values = true
34 |
35 | config.around :each, :transactional do |example|
36 | DatabaseCleaner.strategy = :transaction
37 |
38 | DatabaseCleaner.start
39 |
40 | example.run
41 |
42 | DatabaseCleaner.clean
43 | end
44 |
45 | config.around :each, :non_transactional do |example|
46 | DatabaseCleaner.strategy = :truncation
47 |
48 | DatabaseCleaner.start
49 |
50 | example.run
51 |
52 | DatabaseCleaner.clean
53 | end
54 | end
--------------------------------------------------------------------------------
/spec/support/db/boot.rb:
--------------------------------------------------------------------------------
1 | # This script establishes connection, creates DB schema and loads models definitions.
2 | # Used by both rspec and cucumber
3 |
4 | require 'acts_as_ordered_tree'
5 | require 'active_record'
6 |
7 | require 'logger'
8 | require 'yaml'
9 | require 'erb'
10 |
11 | base_dir = File.dirname(__FILE__)
12 | config_file = File.join(base_dir, ENV['DBCONF'] || 'config.yml')
13 |
14 | ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(config_file)).result)
15 | ActiveRecord::Base.establish_connection(ENV['DB'].to_sym)
16 | ActiveRecord::Base.logger = Logger.new(ENV['DEBUG'] ? $stderr : '/dev/null')
17 | ActiveRecord::Migration.verbose = false
18 | I18n.enforce_available_locales = false if I18n.respond_to?(:enforce_available_locales=)
19 |
20 | load(File.join(base_dir, 'schema.rb'))
21 |
22 | require File.join(base_dir, '..', 'models')
--------------------------------------------------------------------------------
/spec/support/db/config.travis.yml:
--------------------------------------------------------------------------------
1 | pg:
2 | adapter: postgresql
3 | username: postgres
4 | database: acts_as_ordered_tree_test
5 | allow_concurrency: true
6 | min_messages: ERROR
7 | mysql:
8 | adapter: mysql2
9 | database: acts_as_ordered_tree_test
10 | username: travis
11 | password:
12 | encoding: utf8
13 | sqlite3:
14 | adapter: sqlite3
15 | database: acts_as_ordered_tree.sqlite3.db
--------------------------------------------------------------------------------
/spec/support/db/config.yml:
--------------------------------------------------------------------------------
1 | pg:
2 | adapter: postgresql
3 | database: acts_as_ordered_tree_test
4 | allow_concurrency: true
5 | min_messages: ERROR
6 | mysql:
7 | adapter: mysql2
8 | database: acts_as_ordered_tree_test
9 | username: root
10 | password:
11 | encoding: utf8
12 | sqlite3:
13 | adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
14 | database: acts_as_ordered_tree.sqlite3.db
--------------------------------------------------------------------------------
/spec/support/db/schema.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define(:version => 0) do
2 | create_table :categories, :force => true do |t|
3 | t.column :name, :string
4 | t.column :parent_id, :integer
5 | t.column :position, :integer
6 | t.column :depth, :integer
7 | t.column :categories_count, :integer
8 | end
9 |
10 | create_table :renamed_columns, :force => true do |t|
11 | t.column :name, :string
12 | t.column :mother_id, :integer
13 | t.column :red, :integer
14 | t.column :pitch, :integer
15 | end
16 |
17 | create_table :scoped, :force => true do |t|
18 | t.column :scope_type, :string
19 | t.column :name, :string
20 | t.column :parent_id, :integer
21 | t.column :position, :integer
22 | end
23 |
24 | create_table :sti_examples, :force => true do |t|
25 | t.column :type, :string, :null => false
26 | t.column :name, :string
27 | t.column :parent_id, :integer
28 | t.column :position, :integer
29 | t.column :depth, :integer
30 | t.column :children_count, :integer
31 | end
32 | end
--------------------------------------------------------------------------------
/spec/support/factories.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :default, :aliases => [:node] do
3 | sequence(:name) { |n| "category #{n}" }
4 | end
5 |
6 | factory :default_with_counter_cache do
7 | sequence(:name) { |n| "category #{n}" }
8 | end
9 |
10 | factory :default_without_depth do
11 | sequence(:name) { |n| "category #{n}" }
12 | end
13 |
14 | factory :scoped do
15 | sequence(:scope_type) { |n| "type_#{n}" }
16 | sequence(:name) { |n| "category #{n}" }
17 | end
18 | end
--------------------------------------------------------------------------------
/spec/support/matchers.rb:
--------------------------------------------------------------------------------
1 | module RSpec::Matchers
2 | # it { expect{...}.to query_database.once }
3 | # it { expect{...}.to query_database.at_most(2).times }
4 | # it { expect{...}.not_to query_database }
5 | def query_database(regexp = nil)
6 | QueryDatabaseMatcher.new(regexp)
7 | end
8 |
9 | # example { expect(record1, record2, record3).to be_sorted }
10 | def be_sorted
11 | OrderMatcher.new
12 | end
13 |
14 | class QueryDatabaseMatcher
15 | def initialize(regexp)
16 | @min = nil
17 | @max = nil
18 | @regexp = regexp
19 | end
20 |
21 | def times
22 | self
23 | end
24 | alias time times
25 |
26 | def at_least(times)
27 | @min = times == :once ? 1 : times
28 | self
29 | end
30 |
31 | def at_most(times)
32 | @max = times == :once ? 1 : times
33 | self
34 | end
35 |
36 | def exactly(times)
37 | at_least(times).at_most(times)
38 | self
39 | end
40 |
41 | def once
42 | exactly(1)
43 | end
44 |
45 | def twice
46 | exactly(2)
47 | end
48 |
49 | def matches?(subject)
50 | record_queries { subject.call }
51 |
52 | result = expected_queries_count.include?(@queries.size)
53 | result &&= @queries.any? { |q| @regexp === q } if @regexp
54 | result
55 | end
56 |
57 | def description
58 | desc = 'query database'
59 |
60 | if @min && !@max
61 | desc << ' at least ' << human_readable_count(@min)
62 | end
63 |
64 | if @max && !@min
65 | desc << ' at most ' << human_readable_count(@max)
66 | end
67 |
68 | if @min && @max && @min != @max
69 | desc << " #{@min}..#{@max} times"
70 | end
71 |
72 | if @min && @max && @min == @max
73 | desc << ' ' << human_readable_count(@min)
74 | end
75 |
76 | if @regexp
77 | desc << ' and match ' << @regexp.to_s
78 | end
79 |
80 | desc
81 | end
82 |
83 | def failure_message_for_should(negative = false)
84 | verb = negative ? 'not to' : 'to'
85 | message = "expected given block #{verb} #{description}, but #{@queries.size} queries sent"
86 |
87 | if @queries.any?
88 | message << ":\n#{@queries.each_with_index.map { |q, i| "#{i+1}. #{q}"}.join("\n")}"
89 | end
90 |
91 | message
92 | end
93 |
94 | def failure_message_for_should_not
95 | failure_message_for_should(true)
96 | end
97 |
98 | def supports_block_expectations?
99 | true
100 | end
101 |
102 | private
103 | def record_queries
104 | @queries = []
105 |
106 | subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |*, sql|
107 | next if sql[:name] == 'SCHEMA'
108 |
109 | @queries << sql[:sql]
110 | end
111 |
112 | yield
113 | ensure
114 | ActiveSupport::Notifications.unsubscribe(subscriber)
115 | end
116 |
117 | def expected_queries_count
118 | ((@min||1)..@max || 10000)
119 | end
120 |
121 | def human_readable_count(n)
122 | n == 1 ? 'once' : "#{n} times"
123 | end
124 | end
125 |
126 | class OrderMatcher
127 | def matches?(*records)
128 | @records = Array.wrap(records).flatten
129 |
130 | @records.sort_by { |record| record.reload.ordered_tree_node.position } == @records
131 | end
132 |
133 | def failure_message_for_should
134 | "expected #{@records.inspect} to be ordered by position, but they are not"
135 | end
136 |
137 | def failure_message_for_should_not
138 | "expected #{@records.inspect} not to be ordered by position, but they are"
139 | end
140 | end
141 | end
142 |
143 | # Taken from rspec-rails
144 | module ::ActiveModel::Validations
145 | # Extension to enhance `should have` on AR Model instances. Calls
146 | # model.valid? in order to prepare the object's errors object.
147 | #
148 | # You can also use this to specify the content of the error messages.
149 | #
150 | # @example
151 | #
152 | # model.should have(:no).errors_on(:attribute)
153 | # model.should have(1).error_on(:attribute)
154 | # model.should have(n).errors_on(:attribute)
155 | #
156 | # model.errors_on(:attribute).should include("can't be blank")
157 | def errors_on(attribute)
158 | self.valid?
159 | [self.errors[attribute]].flatten.compact
160 | end
161 | alias :error_on :errors_on
162 | end
--------------------------------------------------------------------------------
/spec/support/models.rb:
--------------------------------------------------------------------------------
1 | class Default < ActiveRecord::Base
2 | self.table_name = 'categories'
3 |
4 | default_scope { where('1=1') }
5 |
6 | acts_as_ordered_tree
7 | end
8 |
9 | class RenamedColumns < ActiveRecord::Base
10 | acts_as_ordered_tree :parent_column => :mother_id,
11 | :position_column => :red,
12 | :depth_column => :pitch
13 |
14 | default_scope { where('1=1') }
15 | end
16 |
17 | class DefaultWithCounterCache < ActiveRecord::Base
18 | self.table_name = 'categories'
19 |
20 | acts_as_ordered_tree :counter_cache => :categories_count
21 |
22 | default_scope { where('1=1') }
23 | end
24 |
25 | class DefaultWithoutDepth < ActiveRecord::Base
26 | self.table_name = 'categories'
27 |
28 | acts_as_ordered_tree :depth_column => false
29 | end
30 |
31 | class Scoped < ActiveRecord::Base
32 | self.table_name = 'scoped'
33 |
34 | default_scope { where('1=1') }
35 |
36 | acts_as_ordered_tree :scope => :scope_type
37 | end
38 |
39 | class StiExample < ActiveRecord::Base
40 | acts_as_ordered_tree :counter_cache => :children_count
41 | end
--------------------------------------------------------------------------------
/spec/tree/children_association_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | # All these examples throw deprecation errors with Rails 4.2
6 | describe ActsAsOrderedTree::Tree::ChildrenAssociation, :transactional do
7 | shared_examples 'ChildrenAssociation' do |model|
8 | describe model.to_s do
9 | around(:each) do |example|
10 | ActiveSupport::Deprecation.silence(&example)
11 | end
12 |
13 | tree :factory => model do
14 | root {
15 | child_1
16 | child_2
17 | child_3
18 | }
19 | end
20 |
21 | let(:klass) { current_tree }
22 |
23 | describe 'joining to association' do
24 | let(:relation) { klass.joins(:children) }
25 |
26 | it { expect(relation).to eq [root, root, root] }
27 | end
28 |
29 | describe 'loading association' do
30 | it { expect(root.children.size).to eq 3 }
31 | it { expect(root.children).to eq [child_1, child_2, child_3] }
32 |
33 | it { expect{root.children.to_a}.to query_database(/ORDER BY .*position/) }
34 | end
35 |
36 | describe 'eager_loading association' do
37 | let(:relation) { klass.eager_load(:children).order(klass.arel_table[:id]) }
38 | let(:first) { relation.to_a.first }
39 |
40 | it { expect(relation).to eq [root, child_1, child_2, child_3] }
41 | it { expect(first.children).to be_loaded }
42 | it { expect(first.children.size).to eq 3 }
43 | end
44 |
45 | describe 'preloading association' do
46 | let(:relation) { klass.preload(:children).order(klass.arel_table[:id]) }
47 | let(:first) { relation.to_a.first }
48 |
49 | it { expect(relation).to eq [root, child_1, child_2, child_3] }
50 | it { expect(first.children).to be_loaded }
51 | it { expect(first.children.size).to eq 3 }
52 | end
53 |
54 | describe 'preloading association (via includes method)' do
55 | let(:relation) { klass.includes(:children).order(klass.arel_table[:id]) }
56 | let(:first) { relation.to_a.first }
57 |
58 | it { expect(relation).to eq [root, child_1, child_2, child_3] }
59 | it { expect(first.children).to be_loaded }
60 | it { expect(first.children.size).to eq 3 }
61 | end
62 |
63 | describe 'extensions' do
64 | it { expect(root.children).to respond_to :each_with_level }
65 | it { expect(root.children).to respond_to :each_without_orphans }
66 | end
67 | end
68 | end
69 |
70 | include_examples 'ChildrenAssociation', :default
71 | include_examples 'ChildrenAssociation', :scoped
72 | end
--------------------------------------------------------------------------------
/spec/tree/columns_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | require 'acts_as_ordered_tree/tree'
6 |
7 | describe ActsAsOrderedTree::Tree::Columns do
8 | let(:klass) { Default }
9 |
10 | shared_examples 'ordered tree column' do |method, option_name, value, klass=Default|
11 | context 'when existing column name given' do
12 | subject(:columns) { described_class.new(klass, option_name => value) }
13 |
14 | it { expect(columns.send(method)).to eq value.to_s }
15 | it { expect(columns.send("#{method}?")).to be true }
16 | end
17 |
18 | context 'when column name given but klass.columns_hash does not contain given name' do
19 | subject(:columns) { described_class.new(klass, method => :x) }
20 |
21 | it { expect(columns.send(method)).to be nil }
22 | it { expect(columns.send("#{method}?")).to be false }
23 | end
24 |
25 | context 'when column name not given' do
26 | subject(:columns) { described_class.new(klass, {}) }
27 |
28 | it { expect(columns.send(method)).to be nil }
29 | it { expect(columns.send("#{method}?")).to be false }
30 | end
31 |
32 | context 'when false given' do
33 | subject(:columns) { described_class.new(klass, option_name => false) }
34 |
35 | it { expect(columns.send(method)).to be nil }
36 | it { expect(columns.send("#{method}?")).to be false }
37 | end
38 | end
39 |
40 | include_examples 'ordered tree column', :parent, :parent_column, :parent_id
41 | include_examples 'ordered tree column', :position, :position_column, :position
42 | include_examples 'ordered tree column', :depth, :depth_column, :depth
43 | include_examples 'ordered tree column', :counter_cache, :counter_cache, :categories_count do
44 | context 'when true value as column name given' do
45 | class Category < ActiveRecord::Base
46 | end
47 |
48 | subject(:columns) { described_class.new(Category, :counter_cache => true) }
49 |
50 | it { expect(columns.counter_cache).to eq 'categories_count' }
51 | end
52 | end
53 |
54 | describe 'scope columns' do
55 | it 'raises error when any unknown column given' do
56 | expect{described_class.new(Scoped, :scope => :x)}.to raise_error(described_class::UnknownColumn)
57 | end
58 |
59 | it 'returns array' do
60 | subject = described_class.new(Scoped, :scope => :scope_type)
61 |
62 | expect(subject.scope).to eq %w(scope_type)
63 | end
64 | end
65 | end
--------------------------------------------------------------------------------
/spec/tree/scopes_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'spec_helper'
4 |
5 | describe ActsAsOrderedTree::Tree::Scopes, :transactional do
6 | shared_examples 'ActsAsOrderedTree scopes' do |model, attrs = {}|
7 | describe model.to_s do
8 | tree :factory => model, :attributes => attrs do
9 | root_1 {
10 | child_1 {
11 | grandchild_1
12 | }
13 | }
14 | root_2 {
15 | child_2 {
16 | grandchild_2
17 | }
18 | }
19 | end
20 |
21 | describe '.leaves' do
22 | it { expect(current_tree.leaves.order(:id)).to eq [grandchild_1, grandchild_2] }
23 | it { expect(root_1.descendants.leaves).to eq [grandchild_1] }
24 | end
25 |
26 | describe '.roots' do
27 | it { expect(current_tree.roots).to eq [root_1, root_2] }
28 | end
29 |
30 | describe '.root' do
31 | it { expect(current_tree.root).to eq root_1 }
32 | end
33 | end
34 | end
35 |
36 | include_examples 'ActsAsOrderedTree scopes', :default
37 | include_examples 'ActsAsOrderedTree scopes', :default_with_counter_cache
38 | include_examples 'ActsAsOrderedTree scopes', :scoped
39 | end
--------------------------------------------------------------------------------