mongoid_nested_set 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "mongoid", ">= 2.0.0.beta.20"
4
+
5
+ # Add dependencies to develop your gem here.
6
+ # Include everything needed to run rake, tests, features, etc.
7
+ group :development do
8
+ gem "bundler", "~> 1.0.0"
9
+ gem "jeweler", "~> 1.5.1"
10
+ gem "rcov", ">= 0"
11
+ gem 'rspec-core'
12
+ end
13
+
14
+ group :test do
15
+ gem 'rspec-expectations'
16
+ gem 'rr'
17
+ gem 'remarkable_mongoid'
18
+ gem 'database_cleaner'
19
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,59 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.0.3)
5
+ activesupport (= 3.0.3)
6
+ builder (~> 2.1.2)
7
+ i18n (~> 0.4)
8
+ activesupport (3.0.3)
9
+ bson (1.1.4)
10
+ builder (2.1.2)
11
+ database_cleaner (0.6.0)
12
+ diff-lcs (1.1.2)
13
+ git (1.2.5)
14
+ i18n (0.5.0)
15
+ jeweler (1.5.1)
16
+ bundler (~> 1.0.0)
17
+ git (>= 1.2.5)
18
+ rake
19
+ mongo (1.1.4)
20
+ bson (>= 1.1.1)
21
+ mongoid (2.0.0.beta.20)
22
+ activemodel (~> 3.0)
23
+ mongo (~> 1.1)
24
+ tzinfo (~> 0.3.22)
25
+ will_paginate (~> 3.0.pre)
26
+ rake (0.8.7)
27
+ rcov (0.9.9)
28
+ remarkable (4.0.0.alpha4)
29
+ rspec (>= 2.0.0.alpha11)
30
+ remarkable_activemodel (4.0.0.alpha4)
31
+ remarkable (~> 4.0.0.alpha4)
32
+ rspec (>= 2.0.0.alpha11)
33
+ remarkable_mongoid (0.5.0)
34
+ remarkable_activemodel (>= 4.0.0.alpha2)
35
+ rr (1.0.2)
36
+ rspec (2.2.0)
37
+ rspec-core (~> 2.2)
38
+ rspec-expectations (~> 2.2)
39
+ rspec-mocks (~> 2.2)
40
+ rspec-core (2.2.1)
41
+ rspec-expectations (2.2.0)
42
+ diff-lcs (~> 1.1.2)
43
+ rspec-mocks (2.2.0)
44
+ tzinfo (0.3.23)
45
+ will_paginate (3.0.pre2)
46
+
47
+ PLATFORMS
48
+ ruby
49
+
50
+ DEPENDENCIES
51
+ bundler (~> 1.0.0)
52
+ database_cleaner
53
+ jeweler (~> 1.5.1)
54
+ mongoid (>= 2.0.0.beta.20)
55
+ rcov
56
+ remarkable_mongoid
57
+ rr
58
+ rspec-core
59
+ rspec-expectations
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.0'
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,47 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "mongoid_nested_set"
16
+ gem.homepage = "http://github.com/thinkwell/mongoid_nested_set"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{Nested set based tree implementation for Mongoid}
19
+ gem.description = %Q{Fully featured tree implementation for Mongoid using the nested set model}
20
+ gem.email = "bturner@bltweb.net"
21
+ gem.authors = ["Brandon Turner"]
22
+
23
+ gem.add_runtime_dependency('mongoid', '>= 2.0.0.beta.20')
24
+ end
25
+ Jeweler::RubygemsDotOrgTasks.new
26
+
27
+ require 'rspec/core/rake_task'
28
+ RSpec::Core::RakeTask.new(:spec) do |spec|
29
+ spec.pattern = FileList['spec/**/*_spec.rb']
30
+ end
31
+
32
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.rcov = true
35
+ end
36
+
37
+ task :default => :spec
38
+
39
+ require 'rake/rdoctask'
40
+ Rake::RDocTask.new do |rdoc|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "mongoid_nested_set #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,40 @@
1
+
2
+ require 'mongoid_nested_set/remove_order_by'
3
+
4
+ # This acts provides Nested Set functionality. Nested Set is a smart way to implement
5
+ # an _ordered_ tree, with the added feature that you can select the children and all of
6
+ # their descendants with a single query. The drawback is that insertion or move need
7
+ # multiple queries. But everything is done here by this module!
8
+ #
9
+ # Nested sets are appropriate each time you want either an ordered tree (menus,
10
+ # commercial categories) or an efficient way of querying big trees (threaded posts).
11
+ #
12
+ # == API
13
+ #
14
+ # Method names are aligned with acts_as_tree as much as possible to make replacement
15
+ # from one by another easier.
16
+ #
17
+ # item.children.create(:name => 'child1')
18
+ #
19
+ module Mongoid
20
+ module Acts
21
+ module NestedSet
22
+ require 'mongoid_nested_set/base'
23
+ autoload :Document, 'mongoid_nested_set/document'
24
+ autoload :Fields, 'mongoid_nested_set/fields'
25
+ autoload :Rebuild, 'mongoid_nested_set/rebuild'
26
+ autoload :Relations, 'mongoid_nested_set/relations'
27
+ autoload :Update, 'mongoid_nested_set/update'
28
+ autoload :Validation, 'mongoid_nested_set/validation'
29
+ autoload :OutlineNumber, 'mongoid_nested_set/outline_number'
30
+
31
+ def self.included(base)
32
+ base.extend(Base)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+
39
+ # Enable the acts_as_nested_set method
40
+ Mongoid::Document::ClassMethods.send(:include, Mongoid::Acts::NestedSet::Base)
@@ -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 if outline_number_field_name
49
+ field :depth, :type => Integer
50
+
51
+ references_many :children, :class_name => self.name, :foreign_key => parent_field_name, :inverse_of => :parent, :default_order => criteria.asc(left_field_name)
52
+ referenced_in :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,207 @@
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
+ # Provides a chainable relation to select all descendants of a set of records,
63
+ # excluding the record set itself.
64
+ # Similar to parent.descendants, except this allows you to find all descendants
65
+ # of a set of nodes, rather than being restricted to find the descendants of only
66
+ # a single node.
67
+ #
68
+ # Example:
69
+ # parents = Category.roots.all
70
+ # parents_descendants = Category.where(:deleted => false).descendants_of(parents)
71
+ #
72
+ def descendants_of(parents)
73
+ # TODO: Add root or scope?
74
+ conditions = parents.map do |parent|
75
+ {left_field_name => {"$gt" => parent.left}, right_field_name => {"$lt" => parent.right}}
76
+ end
77
+ where("$or" => conditions)
78
+ end
79
+
80
+
81
+ def before_move(*args, &block)
82
+ set_callback :move, :before, *args, &block
83
+ end
84
+
85
+
86
+ def after_move(*args, &block)
87
+ set_callback :move, :after, *args, &block
88
+ end
89
+
90
+ end
91
+
92
+
93
+
94
+
95
+ module InstanceMethods
96
+
97
+ include Comparable
98
+ include Relations
99
+ include Update
100
+ include Fields
101
+
102
+ # Value fo the parent field
103
+ def parent_id
104
+ self[parent_field_name]
105
+ end
106
+
107
+
108
+ # Value of the left field
109
+ def left
110
+ self[left_field_name]
111
+ end
112
+
113
+
114
+ # Value of the right field
115
+ def right
116
+ self[right_field_name]
117
+ end
118
+
119
+
120
+ # Returns true if this is a root node
121
+ def root?
122
+ parent_id.nil?
123
+ end
124
+
125
+
126
+ # Returns true if this is a leaf node
127
+ def leaf?
128
+ #!new_record? && right - left == 1
129
+ right - left == 1
130
+ end
131
+
132
+
133
+ # Returns true if this is a child node
134
+ def child?
135
+ !parent_id.nil?
136
+ end
137
+
138
+
139
+ # Returns true if depth is supported
140
+ def depth?
141
+ true
142
+ end
143
+
144
+
145
+ # Returns true if outline numbering is supported
146
+ def outline_numbering?
147
+ !!outline_number_field_name
148
+ end
149
+
150
+
151
+ # order by left field
152
+ def <=>(x)
153
+ left <=> x.left
154
+ end
155
+
156
+
157
+ # Redefine to act like active record
158
+ def ==(comparison_object)
159
+ comparison_object.equal?(self) ||
160
+ (comparison_object.instance_of?(scope_class) &&
161
+ comparison_object.id == id &&
162
+ !comparison_object.new_record?)
163
+ end
164
+
165
+
166
+ # Check if other model is in the same scope
167
+ def same_scope?(other)
168
+ Array(acts_as_nested_set_options[:scope]).all? do |attr|
169
+ self.send(attr) == other.send(attr)
170
+ end
171
+ end
172
+
173
+
174
+ def to_text
175
+ self_and_descendants.map do |node|
176
+ "#('*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
177
+ end.join("\n")
178
+ end
179
+
180
+
181
+ # All nested set queries should use this nested_set_scope, which performs finds
182
+ # using the :scope declared in the acts_as_nested_set declaration
183
+ def nested_set_scope
184
+ scopes = Array(acts_as_nested_set_options[:scope])
185
+ conditions = scopes.inject({}) do |conditions,attr|
186
+ conditions.merge attr => self[attr]
187
+ end unless scopes.empty?
188
+ scope_class.criteria.where(conditions).asc(left_field_name)
189
+ end
190
+
191
+
192
+
193
+ protected
194
+
195
+ def without_self(scope)
196
+ scope.where(:_id.ne => self.id)
197
+ end
198
+
199
+
200
+ # reload left, right, and parent
201
+ def reload_nested_set
202
+ reload
203
+ end
204
+
205
+ end
206
+ end # Document
207
+ end # Mongoid::Acts::NestedSet