glebtv-mongoid_nested_set 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +44 -0
- data/.rspec +1 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +147 -0
- data/Rakefile +1 -0
- data/glebtv-mongoid_nested_set.gemspec +29 -0
- data/lib/glebtv-mongoid_nested_set.rb +1 -0
- data/lib/mongoid_nested_set/base.rb +90 -0
- data/lib/mongoid_nested_set/document.rb +238 -0
- data/lib/mongoid_nested_set/fields.rb +60 -0
- data/lib/mongoid_nested_set/outline_number.rb +122 -0
- data/lib/mongoid_nested_set/rebuild.rb +40 -0
- data/lib/mongoid_nested_set/relations.rb +100 -0
- data/lib/mongoid_nested_set/remove_order_by.rb +15 -0
- data/lib/mongoid_nested_set/update.rb +230 -0
- data/lib/mongoid_nested_set/validation.rb +59 -0
- data/lib/mongoid_nested_set/version.rb +3 -0
- data/lib/mongoid_nested_set.rb +40 -0
- data/spec/matchers/nestedset_pos.rb +46 -0
- data/spec/models/circle_node.rb +4 -0
- data/spec/models/node.rb +10 -0
- data/spec/models/node_without_nested_set.rb +6 -0
- data/spec/models/numbering_node.rb +10 -0
- data/spec/models/renamed_fields.rb +7 -0
- data/spec/models/shape_node.rb +18 -0
- data/spec/models/square_node.rb +4 -0
- data/spec/models/test_document.rb +36 -0
- data/spec/models/unscoped_node.rb +9 -0
- data/spec/mongoid_nested_set_spec.rb +786 -0
- data/spec/spec_helper.rb +43 -0
- metadata +147 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 953791c3a0f6c4ac721064a644618e1483cf0e84
|
4
|
+
data.tar.gz: 6b4bd39932a7b028bdf028d28224c6eb3f9b1cc5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 574d7024162ace5b17a05231ab3b9eb1cc2277cfd1b46882438e41582230cf19a07d9feaf9d43a4ac932a1015d96edbc79c81e9115fcbaf5bd2bd633fbed5e8c
|
7
|
+
data.tar.gz: 057192f6c849596541d59e1f124d8b292b4e07bb57b4b7efa9564351d9fafb9564ea4d7e1fb49ef926e264335e8802f5a8331bdf84363f3dca5e51cc1dc0713e
|
data/.document
ADDED
data/.gitignore
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
Gemfile.lock
|
2
|
+
# rcov generated
|
3
|
+
coverage
|
4
|
+
|
5
|
+
# rdoc generated
|
6
|
+
rdoc
|
7
|
+
|
8
|
+
# yard generated
|
9
|
+
doc
|
10
|
+
.yardoc
|
11
|
+
|
12
|
+
# bundler
|
13
|
+
.bundle
|
14
|
+
|
15
|
+
# jeweler generated
|
16
|
+
pkg
|
17
|
+
|
18
|
+
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
19
|
+
#
|
20
|
+
# * Create a file at ~/.gitignore
|
21
|
+
# * Include files you want ignored
|
22
|
+
# * Run: git config --global core.excludesfile ~/.gitignore
|
23
|
+
#
|
24
|
+
# After doing this, these files will be ignored in all your git projects,
|
25
|
+
# saving you from having to 'pollute' every project you touch with them
|
26
|
+
#
|
27
|
+
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
|
28
|
+
#
|
29
|
+
# For MacOS:
|
30
|
+
#
|
31
|
+
#.DS_Store
|
32
|
+
#
|
33
|
+
# For TextMate
|
34
|
+
#*.tmproj
|
35
|
+
#tmtags
|
36
|
+
#
|
37
|
+
# For emacs:
|
38
|
+
#*~
|
39
|
+
#\#*
|
40
|
+
#.\#*
|
41
|
+
#
|
42
|
+
# For vim:
|
43
|
+
#*.swp
|
44
|
+
.rvmrc
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Brandon Turner
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
Mongoid Nested Set
|
2
|
+
==================
|
3
|
+
|
4
|
+
Mongoid Nested Set is an implementation of the nested set pattern for Mongoid.
|
5
|
+
It is a port of [AwesomeNestedSet for ActiveRecord](https://github.com/galetahub/awesome_nested_set).
|
6
|
+
It supports Mongoid 2 and Rails 3.
|
7
|
+
|
8
|
+
Nested Set represents hierarchies of trees in MongoDB using references rather
|
9
|
+
than embedded documents. A tree is stored as a flat list of documents in a
|
10
|
+
collection. Nested Set allows quick, ordered queries of nodes and their
|
11
|
+
descendants. Tree modification is more costly. The nested set pattern is
|
12
|
+
ideal for models that are read more frequently than modified.
|
13
|
+
|
14
|
+
For more on the nested set pattern: <http://en.wikipedia.org/wiki/Nested_set_model>
|
15
|
+
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Install as Gem
|
20
|
+
|
21
|
+
gem install mongoid_nested_set
|
22
|
+
|
23
|
+
via Gemfile
|
24
|
+
|
25
|
+
gem 'mongoid_nested_set', '0.1.3'
|
26
|
+
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
To start using Mongoid Nested Set, just declare `acts_as_nested_set` on your
|
31
|
+
model:
|
32
|
+
|
33
|
+
class Category
|
34
|
+
include Mongoid::Document
|
35
|
+
acts_as_nested_set
|
36
|
+
end
|
37
|
+
|
38
|
+
### Creating a root node
|
39
|
+
|
40
|
+
root = Category.create(:name => 'Root Category')
|
41
|
+
|
42
|
+
### Inserting a node
|
43
|
+
|
44
|
+
child1 = root.children.create(:name => 'Child Category #1')
|
45
|
+
|
46
|
+
child2 = Category.create(:name => 'Child Category #2')
|
47
|
+
root.children << child2
|
48
|
+
|
49
|
+
### Deleting a node
|
50
|
+
|
51
|
+
child1.destroy
|
52
|
+
|
53
|
+
Descendants of a destroyed nodes will also be deleted. By default, descendant's
|
54
|
+
`destroy` method will not be called. To enable calling descendant's `destroy`
|
55
|
+
method:
|
56
|
+
|
57
|
+
class Category
|
58
|
+
include Mongoid::Document
|
59
|
+
acts_as_nested_set :dependent => :destroy
|
60
|
+
end
|
61
|
+
|
62
|
+
### Moving a node
|
63
|
+
|
64
|
+
Several methods exist for moving nodes:
|
65
|
+
|
66
|
+
* move\_left
|
67
|
+
* move\_right
|
68
|
+
* move\_to\_left\_of(other_node)
|
69
|
+
* move\_to\_right\_of(other_node)
|
70
|
+
* move\_to\_child\_of(other_node)
|
71
|
+
* move\_to\_root
|
72
|
+
|
73
|
+
|
74
|
+
### Scopes
|
75
|
+
|
76
|
+
Scopes restrict what is considered a list. This is commonly used to represent multiple trees
|
77
|
+
(or multiple roots) in a single collection.
|
78
|
+
|
79
|
+
class Category
|
80
|
+
include Mongoid::Document
|
81
|
+
acts_as_nested_set :scope => :root_id
|
82
|
+
end
|
83
|
+
|
84
|
+
### Conversion from other trees
|
85
|
+
|
86
|
+
Coming from acts_as_tree or adjacency list system where you only have parent_id?
|
87
|
+
No problem. Simply add `acts_as_nested_set` and run:
|
88
|
+
|
89
|
+
Category.rebuild!
|
90
|
+
|
91
|
+
Your tree will be converted to a valid nested set.
|
92
|
+
|
93
|
+
|
94
|
+
### Outline Numbering
|
95
|
+
|
96
|
+
Mongoid Nested Set can manage outline numbers (e.g. 1.3.2) for your documents if
|
97
|
+
you wish. Simply add `:outline_number_field`:
|
98
|
+
|
99
|
+
acts_as_nested_set, :outline_number_field => 'number'
|
100
|
+
|
101
|
+
Your documents will now include a `number` field (you can call it anything you
|
102
|
+
wish) that will contain outline numbers.
|
103
|
+
|
104
|
+
Don't like the outline numbers format? Simply override `outline_number_seperator`,
|
105
|
+
`build_outline_number`, or `outline_number_sequence` in your model classes. For
|
106
|
+
example:
|
107
|
+
|
108
|
+
class Category
|
109
|
+
include Mongoid::Document
|
110
|
+
acts_as_nested_set :scope => :root_id, :outline_number_field => 'number'
|
111
|
+
|
112
|
+
# Use a dash instead of a dot for outline numbers
|
113
|
+
# e.g. 1-3-2
|
114
|
+
def outline_number_seperator
|
115
|
+
'-'
|
116
|
+
end
|
117
|
+
|
118
|
+
# Use 0-based indexing instead of 1-based indexing
|
119
|
+
# e.g. 1.0
|
120
|
+
def outline_number_sequence(prev_siblings)
|
121
|
+
prev_siblings.count
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
## References
|
127
|
+
|
128
|
+
You can learn more about nested sets at:
|
129
|
+
|
130
|
+
<http://en.wikipedia.org/wiki/Nested_set_model>
|
131
|
+
<http://dev.mysql.com/tech-resources/articles/hierarchical-data.html>
|
132
|
+
|
133
|
+
|
134
|
+
## Contributing to mongoid\_nested\_set
|
135
|
+
|
136
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
137
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
138
|
+
* Fork the project
|
139
|
+
* Start a feature/bugfix branch
|
140
|
+
* Commit and push until you are happy with your contribution
|
141
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
142
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
143
|
+
|
144
|
+
## Copyright
|
145
|
+
|
146
|
+
Copyright (c) 2010 Brandon Turner. See LICENSE.txt for
|
147
|
+
further details.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "mongoid_nested_set/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "glebtv-mongoid_nested_set"
|
7
|
+
s.version = MongoidNestedSet::VERSION
|
8
|
+
s.authors = ["GlebTV", "Brandon Turner"]
|
9
|
+
s.email = ["bt@brandonturner.net"]
|
10
|
+
s.homepage = "http://github.com/55ideas/mongoid_nested_set"
|
11
|
+
s.summary = %q{Nested set based tree implementation for Mongoid}
|
12
|
+
s.description = %q{Fully featured tree implementation for Mongoid using the nested set model}
|
13
|
+
s.licenses = ["MIT"]
|
14
|
+
|
15
|
+
s.rubyforge_project = "mongoid_nested_set"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_runtime_dependency(%q<mongoid>, [">= 3.0.0"])
|
23
|
+
|
24
|
+
s.add_development_dependency(%q<rspec>, [">= 2.7.0"])
|
25
|
+
s.add_development_dependency(%q<bundler>, [">= 1.0.21"])
|
26
|
+
s.add_development_dependency(%q<simplecov>)
|
27
|
+
s.add_development_dependency(%q<simplecov-rcov>)
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'mongoid_nested_set'
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module Base
|
4
|
+
|
5
|
+
# Configuration options are:
|
6
|
+
#
|
7
|
+
# * +:parent_field+ - field name to use for keeping the parent id (default: parent_id)
|
8
|
+
# * +:left_field+ - field name for left boundary data, default 'lft'
|
9
|
+
# * +:right_field+ - field name for right boundary data, default 'rgt'
|
10
|
+
# * +:outline_number_field+ - field name for the number field, default nil. If set,
|
11
|
+
# the value will be used as a field name to keep track of each node's
|
12
|
+
# "outline number" (e.g. 1.2.5).
|
13
|
+
# * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach
|
14
|
+
# "_id" (if it hasn't been already) and use that as the foreign key restriction. You
|
15
|
+
# can also pass an array to scope by multiple attributes
|
16
|
+
# * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the child
|
17
|
+
# objects are destroyed alongside this object by calling their destroy method. If set
|
18
|
+
# to :delete_all (default), all the child objects are deleted without calling their
|
19
|
+
# destroy method.
|
20
|
+
# * +:klass+ - class to use for queries (defaults to self)
|
21
|
+
#
|
22
|
+
# See Mongoid::Acts::NestedSet::ClassMethods for a list of class methods and
|
23
|
+
# Mongoid::Acts::NestedSet::InstanceMethods for a list of instance methods added to
|
24
|
+
# acts_as_nested_set models
|
25
|
+
def acts_as_nested_set(options = {})
|
26
|
+
options = {
|
27
|
+
:parent_field => 'parent_id',
|
28
|
+
:left_field => 'lft',
|
29
|
+
:right_field => 'rgt',
|
30
|
+
:outline_number_field => nil,
|
31
|
+
:dependent => :delete_all, # or :destroy
|
32
|
+
:klass => self,
|
33
|
+
}.merge(options)
|
34
|
+
|
35
|
+
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
|
36
|
+
options[:scope] = "#{options[:scope]}_id".intern
|
37
|
+
end
|
38
|
+
|
39
|
+
class_attribute :acts_as_nested_set_options, :instance_writer => false
|
40
|
+
self.acts_as_nested_set_options = options
|
41
|
+
|
42
|
+
unless self.is_a?(Document::ClassMethods)
|
43
|
+
include Document
|
44
|
+
include OutlineNumber if outline_number_field_name
|
45
|
+
|
46
|
+
field left_field_name, :type => Integer
|
47
|
+
field right_field_name, :type => Integer
|
48
|
+
field outline_number_field_name, :type => String if outline_number_field_name
|
49
|
+
field :depth, :type => Integer
|
50
|
+
|
51
|
+
has_many :children, :class_name => self.name, :foreign_key => parent_field_name, :inverse_of => :parent, :order => left_field_name.to_sym.asc
|
52
|
+
belongs_to :parent, :class_name => self.name, :foreign_key => parent_field_name
|
53
|
+
|
54
|
+
attr_accessor :skip_before_destroy
|
55
|
+
|
56
|
+
#if accessible_attributes.blank?
|
57
|
+
# attr_protected left_field_name.intern, right_field_name.intern
|
58
|
+
#end
|
59
|
+
|
60
|
+
define_callbacks :move, :terminator => "result == false"
|
61
|
+
|
62
|
+
before_create :set_default_left_and_right
|
63
|
+
before_save :store_new_parent
|
64
|
+
after_save :move_to_new_parent
|
65
|
+
before_destroy :destroy_descendants
|
66
|
+
|
67
|
+
# no assignment to structure fields
|
68
|
+
[left_field_name, right_field_name].each do |field|
|
69
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
70
|
+
def #{field}=(x)
|
71
|
+
raise NameError, "Unauthorized assignment to #{field}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead.", "#{field}"
|
72
|
+
end
|
73
|
+
end_eval
|
74
|
+
end
|
75
|
+
|
76
|
+
scope :roots, lambda {
|
77
|
+
where(parent_field_name => nil).asc(left_field_name)
|
78
|
+
}
|
79
|
+
scope :leaves, lambda {
|
80
|
+
where("this.#{quoted_right_field_name} - this.#{quoted_left_field_name} == 1").asc(left_field_name)
|
81
|
+
}
|
82
|
+
scope :with_depth, lambda { |level|
|
83
|
+
where(:depth => level).asc(left_field_name)
|
84
|
+
}
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module Document
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
base.send(:include, InstanceMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
include Rebuild
|
14
|
+
include Validation
|
15
|
+
include Fields
|
16
|
+
|
17
|
+
# Returns the first root
|
18
|
+
def root
|
19
|
+
roots.first
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def scope_condition_by_options(options)
|
24
|
+
h = {}
|
25
|
+
scope_string = Array(acts_as_nested_set_options[:scope]).reject{|s| !options.has_key?(s) }.each do |c|
|
26
|
+
h[c] = options[c]
|
27
|
+
end
|
28
|
+
h
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Iterates over tree elements and determines the current level in the tree.
|
33
|
+
# Only accepts default ordering, ordering by an other field than lft
|
34
|
+
# does not work. This method is much more efficient then calling level
|
35
|
+
# because it doesn't require any additional database queries.
|
36
|
+
# This method does not used the cached depth field.
|
37
|
+
#
|
38
|
+
# Example:
|
39
|
+
# Category.each_with_level(Category.root.self_and_descendants) do |o, level|
|
40
|
+
#
|
41
|
+
def each_with_level(objects)
|
42
|
+
offset = nil
|
43
|
+
path = [nil]
|
44
|
+
objects.each do |o|
|
45
|
+
if offset == nil
|
46
|
+
offset = o.parent_id.nil? ? 0 : o.parent.level
|
47
|
+
end
|
48
|
+
if o.parent_id != path.last
|
49
|
+
# we are on a new level, did we descend or ascend?
|
50
|
+
if path.include?(o.parent_id)
|
51
|
+
# remove wrong tailing path elements
|
52
|
+
path.pop while path.last != o.parent_id
|
53
|
+
else
|
54
|
+
path << o.parent_id
|
55
|
+
end
|
56
|
+
end
|
57
|
+
yield(o, path.length - 1 + offset)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# Iterates over tree elements with ancestors.
|
63
|
+
# Only accepts default ordering, ordering by an other field than lft
|
64
|
+
# does not work. This is much more efficient than calling ancestors for
|
65
|
+
# each object because it doesn't require any additional database queries.
|
66
|
+
#
|
67
|
+
# Example:
|
68
|
+
# Category.each_with_ancestors(Category.root.self_and_descendants) do |o, ancestors|
|
69
|
+
#
|
70
|
+
def each_with_ancestors(objects)
|
71
|
+
ancestors = nil
|
72
|
+
last_parent = nil
|
73
|
+
objects.each do |o|
|
74
|
+
if ancestors == nil
|
75
|
+
ancestors = o.root? ? [] : o.ancestors.entries
|
76
|
+
end
|
77
|
+
if ancestors.empty? || o.parent_id != ancestors.last.id
|
78
|
+
# we are on a new level, did we descend or ascend?
|
79
|
+
if ancestors.any? {|a| a.id == o.parent_id}
|
80
|
+
# ascend
|
81
|
+
ancestors.pop while (!ancestors.empty? && ancestors.last.id != o.parent_id)
|
82
|
+
elsif !o.root?
|
83
|
+
# descend
|
84
|
+
ancestors << last_parent
|
85
|
+
end
|
86
|
+
end
|
87
|
+
yield(o, ancestors)
|
88
|
+
last_parent = o
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# Provides a chainable relation to select all descendants of a set of records,
|
94
|
+
# excluding the record set itself.
|
95
|
+
# Similar to parent.descendants, except this allows you to find all descendants
|
96
|
+
# of a set of nodes, rather than being restricted to find the descendants of only
|
97
|
+
# a single node.
|
98
|
+
#
|
99
|
+
# Example:
|
100
|
+
# parents = Category.roots.all
|
101
|
+
# parents_descendants = Category.where(:deleted => false).descendants_of(parents)
|
102
|
+
#
|
103
|
+
def descendants_of(parents)
|
104
|
+
# TODO: Add root or scope?
|
105
|
+
conditions = parents.map do |parent|
|
106
|
+
{left_field_name => {"$gt" => parent.left}, right_field_name => {"$lt" => parent.right}}
|
107
|
+
end
|
108
|
+
where("$or" => conditions)
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def before_move(*args, &block)
|
113
|
+
set_callback :move, :before, *args, &block
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
def after_move(*args, &block)
|
118
|
+
set_callback :move, :after, *args, &block
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
module InstanceMethods
|
127
|
+
|
128
|
+
include Comparable
|
129
|
+
include Relations
|
130
|
+
include Update
|
131
|
+
include Fields
|
132
|
+
|
133
|
+
# Value fo the parent field
|
134
|
+
def parent_id
|
135
|
+
self[parent_field_name]
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
# Value of the left field
|
140
|
+
def left
|
141
|
+
self[left_field_name]
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
# Value of the right field
|
146
|
+
def right
|
147
|
+
self[right_field_name]
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
# Returns true if this is a root node
|
152
|
+
def root?
|
153
|
+
parent_id.nil?
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
# Returns true if this is a leaf node
|
158
|
+
def leaf?
|
159
|
+
#!new_record? && right - left == 1
|
160
|
+
right - left == 1
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
# Returns true if this is a child node
|
165
|
+
def child?
|
166
|
+
!parent_id.nil?
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
# Returns true if depth is supported
|
171
|
+
def depth?
|
172
|
+
true
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
# Returns true if outline numbering is supported
|
177
|
+
def outline_numbering?
|
178
|
+
!!outline_number_field_name
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
# order by left field
|
183
|
+
def <=>(x)
|
184
|
+
left <=> x.left
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
# Redefine to act like active record
|
189
|
+
def ==(comparison_object)
|
190
|
+
comparison_object.equal?(self) ||
|
191
|
+
(comparison_object.instance_of?(scope_class) &&
|
192
|
+
comparison_object.id == id &&
|
193
|
+
!comparison_object.new_record?)
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
# Check if other model is in the same scope
|
198
|
+
def same_scope?(other)
|
199
|
+
Array(acts_as_nested_set_options[:scope]).all? do |attr|
|
200
|
+
self.send(attr) == other.send(attr)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
def to_text
|
206
|
+
self_and_descendants.map do |node|
|
207
|
+
"#('*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
|
208
|
+
end.join("\n")
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
# All nested set queries should use this nested_set_scope, which performs finds
|
213
|
+
# using the :scope declared in the acts_as_nested_set declaration
|
214
|
+
def nested_set_scope
|
215
|
+
scopes = Array(acts_as_nested_set_options[:scope])
|
216
|
+
conditions = scopes.inject({}) do |conditions,attr|
|
217
|
+
conditions.merge attr => self[attr]
|
218
|
+
end unless scopes.empty?
|
219
|
+
scope_class.criteria.where(conditions).asc(left_field_name)
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
|
224
|
+
protected
|
225
|
+
|
226
|
+
def without_self(scope)
|
227
|
+
scope.where(:_id.ne => self.id)
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
# reload left, right, and parent
|
232
|
+
def reload_nested_set
|
233
|
+
reload
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
end # Document
|
238
|
+
end # Mongoid::Acts::NestedSet
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
# Mixed int both classes and instances to provide easy access to the field names
|
4
|
+
module Fields
|
5
|
+
|
6
|
+
def left_field_name
|
7
|
+
acts_as_nested_set_options[:left_field]
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def right_field_name
|
12
|
+
acts_as_nested_set_options[:right_field]
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def parent_field_name
|
17
|
+
acts_as_nested_set_options[:parent_field]
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def outline_number_field_name
|
22
|
+
acts_as_nested_set_options[:outline_number_field]
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def scope_field_names
|
27
|
+
Array(acts_as_nested_set_options[:scope])
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def scope_class
|
32
|
+
acts_as_nested_set_options[:klass]
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def quoted_left_field_name
|
37
|
+
# TODO
|
38
|
+
left_field_name
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def quoted_right_field_name
|
43
|
+
# TODO
|
44
|
+
right_field_name
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def quoted_parent_field_name
|
49
|
+
# TODO
|
50
|
+
parent_field_name
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def quoted_scope_field_names
|
55
|
+
# TODO
|
56
|
+
scope_field_names
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|