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 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
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
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
@@ -0,0 +1,10 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in glebtv-mongoid_nested_set.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec-expectations', "~> 2.0"
8
+ gem 'rr'
9
+ gem 'remarkable_mongoid'
10
+ end
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