mongoid-ancestry-fixes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,248 @@
1
+ module Mongoid
2
+ module Ancestry
3
+
4
+ # Validate that the ancestors don't include itself
5
+ def ancestry_exclude_self
6
+ if ancestor_ids.include? id
7
+ errors.add(:base, "#{self.class.name.humanize} cannot be a descendant of itself.")
8
+ end
9
+ end
10
+
11
+ # Update descendants with new ancestry
12
+ def update_descendants_with_new_ancestry
13
+ # Skip this if callbacks are disabled
14
+ unless ancestry_callbacks_disabled?
15
+ # If node is valid, not a new record and ancestry was updated ...
16
+ if changed.include?(self.base_class.ancestry_field.to_s) && !new_record? && valid?
17
+ # ... for each descendant ...
18
+ descendants.each do |descendant|
19
+ # ... replace old ancestry with new ancestry
20
+ descendant.without_ancestry_callbacks do
21
+ for_replace = \
22
+ if read_attribute(self.class.ancestry_field).blank?
23
+ id.to_s
24
+ else
25
+ "#{read_attribute self.class.ancestry_field}/#{id}"
26
+ end
27
+ new_ancestry = descendant.read_attribute(descendant.class.ancestry_field).gsub(/^#{self.child_ancestry}/, for_replace)
28
+ descendant.update_attribute(self.base_class.ancestry_field, new_ancestry)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ # Apply orphan strategy
36
+ def apply_orphan_strategy
37
+ # Skip this if callbacks are disabled
38
+ unless ancestry_callbacks_disabled?
39
+ # If this isn't a new record ...
40
+ unless new_record?
41
+ # ... make al children root if orphan strategy is rootify
42
+ if self.base_class.orphan_strategy == :rootify
43
+ descendants.each do |descendant|
44
+ descendant.without_ancestry_callbacks do
45
+ val = \
46
+ unless descendant.ancestry == child_ancestry
47
+ descendant.read_attribute(descendant.class.ancestry_field).gsub(/^#{child_ancestry}\//, '')
48
+ end
49
+ descendant.update_attribute descendant.class.ancestry_field, val
50
+ end
51
+ end
52
+ # ... destroy all descendants if orphan strategy is destroy
53
+ elsif self.base_class.orphan_strategy == :destroy
54
+ descendants.all.each do |descendant|
55
+ descendant.without_ancestry_callbacks { descendant.destroy }
56
+ end
57
+ # ... throw an exception if it has children and orphan strategy is restrict
58
+ elsif self.base_class.orphan_strategy == :restrict
59
+ raise Error.new('Cannot delete record because it has descendants.') unless is_childless?
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # The ancestry value for this record's children
66
+ def child_ancestry
67
+ # New records cannot have children
68
+ raise Error.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
69
+
70
+ if self.send("#{self.base_class.ancestry_field}_was").blank?
71
+ id.to_s
72
+ else
73
+ "#{self.send "#{self.base_class.ancestry_field}_was"}/#{id}"
74
+ end
75
+ end
76
+
77
+ # Scope
78
+ def current_search_scope
79
+ self.embedded? ? self._parent.send(self.base_class.to_s.tableize) : self.base_class
80
+ end
81
+
82
+ # Ancestors
83
+ def ancestor_ids
84
+ read_attribute(self.base_class.ancestry_field).to_s.split('/').map { |id| cast_primary_key(id) }
85
+ end
86
+
87
+ def ancestor_conditions
88
+ { :_id.in => ancestor_ids }
89
+ end
90
+
91
+ def ancestors depth_options = {}
92
+ self.base_class.scope_depth(depth_options, depth).where(ancestor_conditions)
93
+ end
94
+
95
+ def path_ids
96
+ ancestor_ids + [id]
97
+ end
98
+
99
+ def path_conditions
100
+ { :_id.in => path_ids }
101
+ end
102
+
103
+ def path depth_options = {}
104
+ self.base_class.scope_depth(depth_options, depth).where(path_conditions)
105
+ end
106
+
107
+ def depth
108
+ ancestor_ids.size
109
+ end
110
+
111
+ def cache_depth
112
+ write_attribute self.base_class.depth_cache_field, depth
113
+ end
114
+
115
+ # Parent
116
+ def parent= parent
117
+ write_attribute(self.base_class.ancestry_field, parent.blank? ? nil : parent.child_ancestry)
118
+ end
119
+
120
+ def parent_id= parent_id
121
+ self.parent = parent_id.blank? ? nil : current_search_scope.find(parent_id)
122
+ end
123
+
124
+ def parent_id
125
+ ancestor_ids.empty? ? nil : ancestor_ids.last
126
+ end
127
+
128
+ def parent
129
+ parent_id.blank? ? nil : current_search_scope.find(parent_id)
130
+ end
131
+
132
+ # Root
133
+ def root_id
134
+ (root_id == id) ? self : current_search.find(root_id)
135
+ end
136
+
137
+ def root
138
+ (root_id == id) ? self : current_search_scope.find(root_id)
139
+ end
140
+
141
+ def is_root?
142
+ read_attribute(self.base_class.ancestry_field).blank?
143
+ end
144
+
145
+ # Children
146
+ def child_conditions
147
+ {self.base_class.ancestry_field => child_ancestry}
148
+ end
149
+
150
+ def children
151
+ current_search_scope.where(child_conditions)
152
+ end
153
+
154
+ def child_ids
155
+ children.only(:_id).map(&:id)
156
+ end
157
+
158
+ def has_children?
159
+ self.children.present?
160
+ end
161
+
162
+ def is_childless?
163
+ !has_children?
164
+ end
165
+
166
+ # Siblings
167
+ def sibling_conditions
168
+ {self.base_class.ancestry_field => read_attribute(self.base_class.ancestry_field)}
169
+ end
170
+
171
+ def siblings
172
+ self.base_class.where sibling_conditions
173
+ end
174
+
175
+ def sibling_ids
176
+ siblings.only(:_id).map(&:id)
177
+ end
178
+
179
+ def has_siblings?
180
+ self.siblings.count > 1
181
+ end
182
+
183
+ def is_only_child?
184
+ !has_siblings?
185
+ end
186
+
187
+ # Descendants
188
+ def descendant_conditions
189
+ [
190
+ { self.base_class.ancestry_field => /^#{child_ancestry}\// },
191
+ { self.base_class.ancestry_field => child_ancestry }
192
+ ]
193
+ end
194
+
195
+ def descendants depth_options = {}
196
+ self.base_class.scope_depth(depth_options, depth).any_of(descendant_conditions)
197
+ end
198
+
199
+ def descendant_ids depth_options = {}
200
+ descendants(depth_options).only(:_id).map(&:id)
201
+ end
202
+
203
+ # Subtree
204
+ def subtree_conditions
205
+ [
206
+ { :_id => id },
207
+ { self.base_class.ancestry_field => /^#{child_ancestry}\// },
208
+ { self.base_class.ancestry_field => child_ancestry }
209
+ ]
210
+ end
211
+
212
+ def subtree depth_options = {}
213
+ self.base_class.scope_depth(depth_options, depth).any_of(subtree_conditions)
214
+ end
215
+
216
+ def subtree_ids depth_options = {}
217
+ subtree(depth_options).only(:_id).map(&:id)
218
+ end
219
+
220
+ # Callback disabling
221
+ def without_ancestry_callbacks
222
+ @disable_ancestry_callbacks = true
223
+ yield
224
+ @disable_ancestry_callbacks = false
225
+ end
226
+
227
+ def ancestry_callbacks_disabled?
228
+ !!@disable_ancestry_callbacks
229
+ end
230
+
231
+ private
232
+
233
+ def cast_primary_key(key)
234
+ if primary_key_type == Integer
235
+ key.to_i
236
+ elsif primary_key_type == BSON::ObjectId && key =~ /[a-z0-9]{24}/
237
+ BSON::ObjectId.convert(self, key)
238
+ else
239
+ key
240
+ end
241
+ end
242
+
243
+ def primary_key_type
244
+ @primary_key_type ||= self.base_class.fields['_id'].options[:type]
245
+ end
246
+ end
247
+
248
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module Ancestry
3
+ VERSION = '0.2.3'
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ module Mongoid
2
+ module Ancestry
3
+ extend ActiveSupport::Concern
4
+
5
+ autoload :ClassMethods, 'mongoid-ancestry/class_methods'
6
+ autoload :Error, 'mongoid-ancestry/exceptions'
7
+
8
+ included do
9
+ cattr_accessor :base_class
10
+ self.base_class = self
11
+ end
12
+
13
+ require 'mongoid-ancestry/instance_methods'
14
+ end
15
+ end
data/log/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ # Ignore everything in this directory
2
+ *
3
+ # Except this file
4
+ !.gitignore
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "mongoid-ancestry/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'mongoid-ancestry-fixes'
7
+ s.version = '0.0.1'
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Stefan Kroes", "Anton Orel"]
10
+ s.email = ["eagle.anton@gmail.com"]
11
+ s.description = %q{Organise Mongoid model into a tree structure}
12
+ s.homepage = "http://github.com/skyeagle/mongoid-ancestry"
13
+ s.summary = %q{Ancestry allows the records of a Mongoid model to be organised in a tree structure, using a single, intuitively formatted database field. It exposes all the standard tree structure relations (ancestors, parent, root, children, siblings, descendants) and all of them can be fetched in a single query. Additional features are named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.}
14
+ s.licenses = ["MIT"]
15
+
16
+ s.rubyforge_project = "mongoid-ancestry-fixes"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+ s.extra_rdoc_files = [
23
+ "README.md"
24
+ ]
25
+
26
+ s.add_dependency('mongoid', ">= 2.0")
27
+ s.add_dependency('bson_ext', ">= 1.3")
28
+ end
29
+
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+
3
+ require 'mongoid-ancestry/exceptions'
4
+
5
+
6
+ describe MongoidAncestry do
7
+
8
+ subject { MongoidAncestry }
9
+
10
+ it "should have ancestry fields" do
11
+ subject.with_model do |model|
12
+ model.fields['ancestry'].options[:type].should eql(String)
13
+ end
14
+ end
15
+
16
+ it "should have non default ancestry field" do
17
+ subject.with_model :ancestry_field => :alternative_ancestry do |model|
18
+ model.ancestry_field.should eql(:alternative_ancestry)
19
+ end
20
+ end
21
+
22
+ it "should set ancestry field" do
23
+ subject.with_model do |model|
24
+ model.ancestry_field = :ancestors
25
+ model.ancestry_field.should eql(:ancestors)
26
+ model.ancestry_field = :ancestry
27
+ model.ancestry_field.should eql(:ancestry)
28
+ end
29
+ end
30
+
31
+ it "should have default orphan strategy" do
32
+ subject.with_model do |model|
33
+ model.orphan_strategy.should eql(:destroy)
34
+ end
35
+ end
36
+
37
+ it "should have non default orphan strategy" do
38
+ subject.with_model :orphan_strategy => :rootify do |model|
39
+ model.orphan_strategy.should eql(:rootify)
40
+ end
41
+ end
42
+
43
+ it "should set orphan strategy" do
44
+ subject.with_model do |model|
45
+ model.orphan_strategy = :rootify
46
+ model.orphan_strategy.should eql(:rootify)
47
+ model.orphan_strategy = :destroy
48
+ model.orphan_strategy.should eql(:destroy)
49
+ end
50
+ end
51
+
52
+ it "should not set invalid orphan strategy" do
53
+ subject.with_model do |model|
54
+ expect {
55
+ model.orphan_strategy = :non_existent_orphan_strategy
56
+ }.to raise_error Mongoid::Ancestry::Error
57
+ end
58
+ end
59
+
60
+ it "should setup test nodes" do
61
+ subject.with_model :depth => 3, :width => 3 do |model, roots|
62
+ roots.class.should eql(Array)
63
+ roots.length.should eql(3)
64
+ roots.each do |node, children|
65
+ node.class.should eql(model)
66
+ children.class.should eql(Array)
67
+ children.length.should eql(3)
68
+ children.each do |node, children|
69
+ node.class.should eql(model)
70
+ children.class.should eql(Array)
71
+ children.length.should eql(3)
72
+ children.each do |node, children|
73
+ node.class.should eql(model)
74
+ children.class.should eql(Array)
75
+ children.length.should eql(0)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ it "should have STI support" do
83
+ subject.with_model :extra_columns => {:type => :string} do |model|
84
+ subclass1 = Object.const_set 'Subclass1', Class.new(model)
85
+ (class << subclass1; self; end).send(:define_method, :model_name) do
86
+ Struct.new(:human, :underscore).new 'Subclass1', 'subclass1'
87
+ end
88
+ subclass2 = Object.const_set 'Subclass2', Class.new(model)
89
+ (class << subclass2; self; end).send(:define_method, :model_name) do
90
+ Struct.new(:human, :underscore).new 'Subclass1', 'subclass1'
91
+ end
92
+
93
+ node1 = subclass1.create
94
+ node2 = subclass2.create :parent => node1
95
+ node3 = subclass1.create :parent => node2
96
+ node4 = subclass2.create :parent => node3
97
+ node5 = subclass1.create :parent => node4
98
+
99
+ model.all.each do |node|
100
+ [subclass1, subclass2].include?(node.class).should be_true
101
+ end
102
+
103
+ node1.descendants.map(&:id).should eql([node2.id, node3.id, node4.id, node5.id])
104
+ node1.subtree.map(&:id).should eql([node1.id, node2.id, node3.id, node4.id, node5.id])
105
+ node5.ancestors.map(&:id).should eql([node1.id, node2.id, node3.id, node4.id])
106
+ node5.path.map(&:id).should eql([node1.id, node2.id, node3.id, node4.id, node5.id])
107
+ end
108
+ end
109
+
110
+ end