mongoid-ancestry-fixes 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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