ancestry 3.0.7 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5962b504410b75525481574c43d8122fc4f4607e1c56f8827bbc565a7704adb6
4
- data.tar.gz: 51c13aec4d355421c4e095804890c58eba3a8a774ce9286e3eaf28351ac22a11
3
+ metadata.gz: 9455bbdda28f2f0084ff4b1e96af32ca5d6b3efe40ae062c51f75b3fee84fb30
4
+ data.tar.gz: 5b06653e17bbe2a238c4d49d9147dd79f2156c33432298290b5b17da190ce203
5
5
  SHA512:
6
- metadata.gz: b6dc1d80c7a77fc1f97232634d11e188863d25e7e9da3f74654ada7f4d3fc2951c6962b4f2b516476772eecce98f66db7c4625298465d2f9de4a7d2f17ff2553
7
- data.tar.gz: 7199de05c78a159e1e1924de1331617a9cc06d4f1403bd82f88a8da5e7f0c127343c1b905bfb714ba2e0e88c285fb9694f467eda9c56af4089db2077fe5c1da2
6
+ metadata.gz: 4cb481c338a78cd5c294a40ccc43ccbad27232cc0a646b4827fd2446ad3d3fd626410f4a35d3b9361b367c2474ba229113aa191ba7799ddcc7930b1d29eab106
7
+ data.tar.gz: 6876e974e00c0ee4c5bb4ea4facbfb9ce12fe04eebfab2bc4623d6a74c9591781d3945b4999437054a81fb1ea82a656822c679e38c434541173ee7d192d664dc
data/README.md CHANGED
@@ -4,12 +4,12 @@
4
4
 
5
5
  Ancestry is a gem that allows the records of a Ruby on Rails
6
6
  ActiveRecord model to be organised as a tree structure (or hierarchy). It uses
7
- a single database column, using the materialised path pattern. It exposes all the standard tree structure
8
- relations (ancestors, parent, root, children, siblings, descendants) and all
9
- of them can be fetched in a single SQL query. Additional features are STI
7
+ a single database column, employing the materialised path pattern. It exposes all the standard tree structure
8
+ relations (ancestors, parent, root, children, siblings, descendants) and allows all
9
+ of them to be fetched in a single SQL query. Additional features are STI
10
10
  support, scopes, depth caching, depth constraints, easy migration from older
11
11
  gems, integrity checking, integrity restoration, arrangement of
12
- (sub)tree into hashes and different strategies for dealing with orphaned
12
+ (sub)trees into hashes, and various strategies for dealing with orphaned
13
13
  records.
14
14
 
15
15
  NOTE:
@@ -55,7 +55,7 @@ $ rake db:migrate
55
55
 
56
56
 
57
57
  ## Add ancestry to your model
58
- * Add to [app/models/](model).rb:
58
+ * Add to app/models/[model.rb]:
59
59
 
60
60
  ```ruby
61
61
  # app/models/[model.rb]
@@ -69,9 +69,9 @@ Your model is now a tree!
69
69
 
70
70
  # Using acts_as_tree instead of has_ancestry
71
71
 
72
- In version 1.2.0 the **acts_as_tree** method was **renamed to has_ancestry**
72
+ In version 1.2.0, the **acts_as_tree** method was **renamed to has_ancestry**
73
73
  in order to allow usage of both the acts_as_tree gem and the ancestry gem in a
74
- single application. method `acts_as_tree` will continue to be supported in the future.
74
+ single application. The `acts_as_tree` method will continue to be supported in the future.
75
75
 
76
76
  # Organising records into a tree
77
77
 
@@ -107,13 +107,13 @@ To navigate an Ancestry model, use the following instance methods:
107
107
  |`ancestors?` |true if the record has ancestors (aka not a root node)|
108
108
  |`ancestor_ids` |ancestor ids of the record|
109
109
  |`path` |path of the record, starting with the root and ending with self|
110
- |`path_ids` |a list the path ids, starting with the root id and ending with the node's own id|
110
+ |`path_ids` |a list of the path ids, starting with the root id and ending with the node's own id|
111
111
  |`children` |direct children of the record|
112
112
  |`child_ids` |direct children's ids|
113
113
  |`has_parent?` <br/> `ancestors?` |true if the record has a parent, false otherwise|
114
114
  |`has_children?` <br/> `children?` |true if the record has any children, false otherwise|
115
115
  |`is_childless?` <br/> `childless?` |true is the record has no children, false otherwise|
116
- |`siblings` |siblings of the record, the record itself is included*|
116
+ |`siblings` |siblings of the record, including the record itself*|
117
117
  |`sibling_ids` |sibling ids|
118
118
  |`has_siblings?` <br/> `siblings?` |true if the record's parent has more than one child|
119
119
  |`is_only_child?` <br/> `only_child?` |true if the record is the only child of its parent|
@@ -123,7 +123,7 @@ To navigate an Ancestry model, use the following instance methods:
123
123
  |`indirect_ids` |indirect children's ids of the record|
124
124
  |`subtree` |the model on descendants and itself|
125
125
  |`subtree_ids` |a list of all ids in the record's subtree|
126
- |`depth` |the depth of the node, root nodes are at depth 0|
126
+ |`depth` |the depth of the node (root nodes are at depth 0)|
127
127
 
128
128
  \* If the record is a root, other root records are considered siblings
129
129
  \* Siblings returns the record itself
@@ -139,6 +139,57 @@ There are also instance methods to determine the relationship between 2 nodes:
139
139
  |`descendant_of?(node)` | node is one of this record's ancestors|
140
140
  |`indirect_of?(node)` | node is one of this record's ancestors but not a parent|
141
141
 
142
+ ## Visual guide for navigation
143
+
144
+ In all examples the node with the large border is the reference node, the node
145
+ from which the navigation method is invoked. The yellow nodes are the nodes
146
+ returned by the method.
147
+
148
+ <table>
149
+ <tr>
150
+ <td>
151
+ <p align="center">parent</p>
152
+ <img src="img/parent.png" alt="parent"/>
153
+ </td>
154
+ <td>
155
+ <p align="center">root</p>
156
+ <img src="img/root.png" alt="root"/>
157
+ </td>
158
+ <td>
159
+ <p align="center">ancestors</p>
160
+ <img src="img/ancestors.png" alt="ancestors"/>
161
+ </td>
162
+ </tr>
163
+ <tr>
164
+ <td>
165
+ <p align="center">path</p>
166
+ <img src="img/path.png" alt="path"/>
167
+ </td>
168
+ <td>
169
+ <p align="center">children</p>
170
+ <img src="img/children.png" alt="children"/>
171
+ </td>
172
+ <td>
173
+ <p align="center">siblings</p>
174
+ <img src="img/siblings.png" alt="siblings"/>
175
+ </td>
176
+ </tr>
177
+ <tr>
178
+ <td>
179
+ <p align="center">descendants</p>
180
+ <img src="img/descendants.png" alt="descendants"/>
181
+ </td>
182
+ <td>
183
+ <p align="center">indirects</p>
184
+ <img src="img/indirects.png" alt="indirects"/>
185
+ </td>
186
+ <td>
187
+ <p align="center">subtree</p>
188
+ <img src="img/subtree.png" alt="subtree"/>
189
+ </td>
190
+ </tr>
191
+ </table>
192
+
142
193
  # Options for `has_ancestry`
143
194
 
144
195
  The has_ancestry method supports the following options:
@@ -148,23 +199,23 @@ The has_ancestry method supports the following options:
148
199
  :destroy All children are destroyed as well (default)
149
200
  :rootify The children of the destroyed node become root nodes
150
201
  :restrict An AncestryException is raised if any children exist
151
- :adopt The orphan subtree is added to the parent of the deleted node.
152
- If the deleted node is Root, then rootify the orphan subtree.
202
+ :adopt The orphan subtree is added to the parent of the deleted node
203
+ If the deleted node is Root, then rootify the orphan subtree
153
204
  :cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
154
205
  If you turn depth_caching on for an existing model:
155
206
  - Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
156
207
  - Build cache: TreeNode.rebuild_depth_cache!
157
208
  :depth_cache_column Pass in a symbol to store depth cache in a different column
158
- :primary_key_format Supply a regular expression that matches the format of your primary key.
159
- By default, primary keys only match integers ([0-9]+).
209
+ :primary_key_format Supply a regular expression that matches the format of your primary key
210
+ By default, primary keys only match integers ([0-9]+)
160
211
  :touch Instruct Ancestry to touch the ancestors of a node when it changes, to
161
212
  invalidate nested key-based caches. (default: false)
162
213
 
163
214
  # (Named) Scopes
164
215
 
165
- Where possible, the navigation methods return scopes instead of records, this
216
+ Where possible, the navigation methods return scopes instead of records. This
166
217
  means additional ordering, conditions, limits, etc. can be applied and that
167
- the result can be either retrieved, counted or checked for existence. For
218
+ the result can be either retrieved, counted, or checked for existence. For
168
219
  example:
169
220
 
170
221
  ```ruby
@@ -237,7 +288,7 @@ on type for that.
237
288
  # Arrangement
238
289
 
239
290
  Ancestry can arrange an entire subtree into nested hashes for easy navigation
240
- after retrieval from the database. TreeNode.arrange could for example return:
291
+ after retrieval from the database. `TreeNode.arrange` could for example return:
241
292
 
242
293
  ```ruby
243
294
  {
@@ -250,14 +301,14 @@ after retrieval from the database. TreeNode.arrange could for example return:
250
301
  }
251
302
  ```
252
303
 
253
- The arrange method also works on a scoped class, for example:
304
+ The `arrange` method also works on a scoped class, for example:
254
305
 
255
306
  ```ruby
256
307
  TreeNode.find_by_name('Crunchy').subtree.arrange
257
308
  ```
258
309
 
259
- The arrange method takes `ActiveRecord` find options. If you want your hashes to
260
- be ordered, you should pass the order to the arrange method instead of to the
310
+ The `arrange` method takes `ActiveRecord` find options. If you want your hashes to
311
+ be ordered, you should pass the order to the `arrange` method instead of to the
261
312
  scope. example:
262
313
 
263
314
  ```ruby
@@ -266,7 +317,7 @@ TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
266
317
 
267
318
  To get the arranged nodes as a nested array of hashes for serialization:
268
319
 
269
- TreeNode.arrange_serializable
320
+ `TreeNode.arrange_serializable`
270
321
 
271
322
  ```ruby
272
323
  [
@@ -299,15 +350,15 @@ TreeNode.arrange_serializable do |parent, children|
299
350
  end
300
351
  ```
301
352
 
302
- The result of arrange_serializable can easily be serialized to json with
353
+ The result of `arrange_serializable` can easily be serialized to json with
303
354
  `to_json`, or some other format:
304
355
 
305
356
  ```
306
357
  TreeNode.arrange_serializable.to_json
307
358
  ```
308
359
 
309
- You can also pass the order to the arrange_serializable method just as you can
310
- pass it to the arrange method:
360
+ You can also pass the order to the `arrange_serializable` method just as you can
361
+ pass it to the `arrange` method:
311
362
 
312
363
  ```
313
364
  TreeNode.arrange_serializable(:order => :name)
@@ -329,8 +380,8 @@ the order of siblings depends on their order in the original array.
329
380
 
330
381
  Most current tree plugins use a parent_id column (has_ancestry,
331
382
  awesome_nested_set, better_nested_set, acts_as_nested_set). With ancestry it is
332
- easy to migrate from any of these plugins, to do so, use the
333
- build_ancestry_from_parent_ids! method on your ancestry model. These steps
383
+ easy to migrate from any of these plugins. To do so, use the
384
+ `build_ancestry_from_parent_ids!` method on your ancestry model. These steps
334
385
  provide a more detailed explanation:
335
386
 
336
387
  1. Add ancestry column to your table
@@ -352,7 +403,7 @@ provide a more detailed explanation:
352
403
 
353
404
 
354
405
  4. Generate ancestry columns
355
- * In './script.console': **[model].build_ancestry_from_parent_ids!**
406
+ * In 'rails console': **[model].build_ancestry_from_parent_ids!**
356
407
  * Make sure it worked ok: **[model].check_ancestry_integrity!**
357
408
 
358
409
 
@@ -371,14 +422,14 @@ provide a more detailed explanation:
371
422
 
372
423
  I don't see any way Ancestry tree integrity could get compromised without
373
424
  explicitly setting cyclic parents or invalid ancestry and circumventing
374
- validation with update_attribute, if you do, please let me know.
425
+ validation with update_attribute. If you do, please let me know.
375
426
 
376
427
  Ancestry includes some methods for detecting integrity problems and restoring
377
- integrity just to be sure. To check integrity use:
378
- [Model].check_ancestry_integrity!. An AncestryIntegrityException will be
428
+ integrity just to be sure. To check integrity, use:
429
+ `[Model].check_ancestry_integrity!`. An AncestryIntegrityException will be
379
430
  raised if there are any problems. You can also specify :report => :list to
380
431
  return an array of exceptions or :report => :echo to echo any error messages.
381
- To restore integrity use: [Model].restore_ancestry_integrity!.
432
+ To restore integrity use: `[Model].restore_ancestry_integrity!`.
382
433
 
383
434
  For example, from IRB:
384
435
 
@@ -42,12 +42,10 @@ EOF
42
42
  'README.md'
43
43
  ]
44
44
 
45
- s.required_ruby_version = '>= 1.8.7'
46
- s.add_runtime_dependency 'activerecord', '>= 3.2.0'
47
- s.add_development_dependency 'rdoc'
48
- s.add_development_dependency 'yard'
49
- s.add_development_dependency 'rake', '~> 10.0'
50
- s.add_development_dependency 'test-unit'
45
+ s.required_ruby_version = '>= 2.0.0'
46
+ s.add_runtime_dependency 'activerecord', '>= 4.2.0'
47
+ s.add_development_dependency 'appraisal'
51
48
  s.add_development_dependency 'minitest'
52
- s.add_development_dependency 'sqlite3'
49
+ s.add_development_dependency 'rake', '~> 13.0'
50
+ s.add_development_dependency 'yard'
53
51
  end
@@ -6,5 +6,4 @@ require_relative 'ancestry/has_ancestry'
6
6
  require_relative 'ancestry/materialized_path'
7
7
 
8
8
  module Ancestry
9
- ANCESTRY_PATTERN = /\A[0-9]+(\/[0-9]+)*\Z/
10
9
  end
@@ -2,7 +2,11 @@ module Ancestry
2
2
  module ClassMethods
3
3
  # Fetch tree node if necessary
4
4
  def to_node object
5
- if object.is_a?(self.ancestry_base_class) then object else unscoped_where{|scope| scope.find object} end
5
+ if object.is_a?(self.ancestry_base_class)
6
+ object
7
+ else
8
+ unscoped_where { |scope| scope.find(object.try(primary_key) || object) }
9
+ end
6
10
  end
7
11
 
8
12
  # Scope on relative depth options
@@ -129,7 +133,7 @@ module Ancestry
129
133
 
130
134
  # Integrity restoration
131
135
  def restore_ancestry_integrity!
132
- parents = {}
136
+ parent_ids = {}
133
137
  # Wrap the whole thing in a transaction ...
134
138
  self.ancestry_base_class.transaction do
135
139
  unscoped_where do |scope|
@@ -138,29 +142,29 @@ module Ancestry
138
142
  # ... set its ancestry to nil if invalid
139
143
  if !node.valid? and !node.errors[node.class.ancestry_column].blank?
140
144
  node.without_ancestry_callbacks do
141
- node.update_attribute node.ancestry_column, nil
145
+ node.update_attribute :ancestor_ids, []
142
146
  end
143
147
  end
144
- # ... save parent of this node in parents array if it exists
145
- parents[node.id] = node.parent_id if exists? node.parent_id
148
+ # ... save parent id of this node in parent_ids array if it exists
149
+ parent_ids[node.id] = node.parent_id if exists? node.parent_id
146
150
 
147
151
  # Reset parent id in array to nil if it introduces a cycle
148
- parent = parents[node.id]
149
- until parent.nil? || parent == node.id
150
- parent = parents[parent]
152
+ parent_id = parent_ids[node.id]
153
+ until parent_id.nil? || parent_id == node.id
154
+ parent_id = parent_ids[parent_id]
151
155
  end
152
- parents[node.id] = nil if parent == node.id
156
+ parent_ids[node.id] = nil if parent_id == node.id
153
157
  end
154
158
 
155
159
  # For each node ...
156
160
  scope.find_each do |node|
157
- # ... rebuild ancestry from parents array
158
- ancestry, parent = nil, parents[node.id]
159
- until parent.nil?
160
- ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
161
+ # ... rebuild ancestry from parent_ids array
162
+ ancestor_ids, parent_id = [], parent_ids[node.id]
163
+ until parent_id.nil?
164
+ ancestor_ids, parent_id = [parent_id] + ancestor_ids, parent_ids[parent_id]
161
165
  end
162
166
  node.without_ancestry_callbacks do
163
- node.update_attribute node.ancestry_column, ancestry
167
+ node.update_attribute :ancestor_ids, ancestor_ids
164
168
  end
165
169
  end
166
170
  end
@@ -168,13 +172,13 @@ module Ancestry
168
172
  end
169
173
 
170
174
  # Build ancestry from parent id's for migration purposes
171
- def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
175
+ def build_ancestry_from_parent_ids! column=:parent_id, parent_id = nil, ancestor_ids = []
172
176
  unscoped_where do |scope|
173
- scope.where(:parent_id => parent_id).find_each do |node|
177
+ scope.where(column => parent_id).find_each do |node|
174
178
  node.without_ancestry_callbacks do
175
- node.update_attribute ancestry_column, ancestry
179
+ node.update_attribute :ancestor_ids, ancestor_ids
176
180
  end
177
- build_ancestry_from_parent_ids! node.id, if ancestry.nil? then "#{node.id}" else "#{ancestry}/#{node.id}" end
181
+ build_ancestry_from_parent_ids! column, node.id, ancestor_ids + [node.id]
178
182
  end
179
183
  end
180
184
  end
@@ -4,7 +4,7 @@ module Ancestry
4
4
  # Check options
5
5
  raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash
6
6
  options.each do |key, value|
7
- unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache].include? key
7
+ unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format].include? key
8
8
  raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.")
9
9
  end
10
10
  end
@@ -27,6 +27,7 @@ module Ancestry
27
27
  # Include dynamic class methods
28
28
  extend Ancestry::ClassMethods
29
29
 
30
+ validates_format_of self.ancestry_column, :with => derive_ancestry_pattern(options[:primary_key_format]), :allow_nil => true
30
31
  extend Ancestry::MaterializedPath
31
32
 
32
33
  # Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
@@ -36,27 +37,6 @@ module Ancestry
36
37
  # Validate that the ancestor ids don't include own id
37
38
  validate :ancestry_exclude_self
38
39
 
39
- # Named scopes
40
- scope :roots, lambda { where(root_conditions) }
41
- scope :ancestors_of, lambda { |object| where(ancestor_conditions(object)) }
42
- scope :children_of, lambda { |object| where(child_conditions(object)) }
43
- scope :indirects_of, lambda { |object| where(indirect_conditions(object)) }
44
- scope :descendants_of, lambda { |object| where(descendant_conditions(object)) }
45
- scope :subtree_of, lambda { |object| where(subtree_conditions(object)) }
46
- scope :siblings_of, lambda { |object| where(sibling_conditions(object)) }
47
- scope :ordered_by_ancestry, Proc.new { |order|
48
- if %w(mysql mysql2 sqlite sqlite3 postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::MAJOR >= 5
49
- reorder(
50
- Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
51
- order
52
- )
53
- else
54
- reorder(Arel.sql("(CASE WHEN #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)} IS NULL THEN 0 ELSE 1 END), #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}"), order)
55
- end
56
- }
57
- scope :ordered_by_ancestry_and, Proc.new { |order| ordered_by_ancestry(order) }
58
- scope :path_of, lambda { |object| to_node(object).path }
59
-
60
40
  # Update descendants with new ancestry before save
61
41
  before_save :update_descendants_with_new_ancestry
62
42
 
@@ -114,6 +94,18 @@ module Ancestry
114
94
  return super if defined?(super)
115
95
  has_ancestry(*args)
116
96
  end
97
+
98
+ private
99
+
100
+ def derive_ancestry_pattern(primary_key_format, delimiter = '/')
101
+ primary_key_format ||= '[0-9]+'
102
+
103
+ if primary_key_format.to_s.include?('\A')
104
+ primary_key_format
105
+ else
106
+ /\A#{primary_key_format}(#{delimiter}#{primary_key_format})*\Z/
107
+ end
108
+ end
117
109
  end
118
110
  end
119
111
 
@@ -1,11 +1,8 @@
1
1
  module Ancestry
2
2
  module InstanceMethods
3
- BEFORE_LAST_SAVE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_before_last_save' : '_was'
4
- IN_DATABASE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_in_database' : '_was'
5
-
6
3
  # Validate that the ancestors don't include itself
7
4
  def ancestry_exclude_self
8
- errors.add(:base, "#{self.class.name.humanize} cannot be a descendant of itself.") if ancestor_ids.include? self.id
5
+ errors.add(:base, "#{self.class.model_name.human} cannot be a descendant of itself.") if ancestor_ids.include? self.id
9
6
  end
10
7
 
11
8
  # Update descendants with new ancestry (before save)
@@ -16,15 +13,8 @@ module Ancestry
16
13
  unscoped_descendants.each do |descendant|
17
14
  # ... replace old ancestry with new ancestry
18
15
  descendant.without_ancestry_callbacks do
19
- descendant.update_attribute(
20
- self.ancestry_base_class.ancestry_column,
21
- descendant.read_attribute(descendant.class.ancestry_column).gsub(
22
- # child_ancestry_was
23
- /^#{self.child_ancestry}/,
24
- # future child_ancestry
25
- if ancestors? then "#{read_attribute self.class.ancestry_column }/#{id}" else id.to_s end
26
- )
27
- )
16
+ new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_in_database)
17
+ descendant.update_attribute(:ancestor_ids, new_ancestor_ids)
28
18
  end
29
19
  end
30
20
  end
@@ -37,13 +27,7 @@ module Ancestry
37
27
  when :rootify # make all children root if orphan strategy is rootify
38
28
  unscoped_descendants.each do |descendant|
39
29
  descendant.without_ancestry_callbacks do
40
- new_ancestry = if descendant.ancestry == child_ancestry
41
- nil
42
- else
43
- # child_ancestry did not change so child_ancestry_was will work here
44
- descendant.ancestry.gsub(/^#{child_ancestry}\//, '')
45
- end
46
- descendant.update_attribute descendant.class.ancestry_column, new_ancestry
30
+ descendant.update_attribute :ancestor_ids, descendant.ancestor_ids - path_ids
47
31
  end
48
32
  end
49
33
  when :destroy # destroy all descendants if orphan strategy is destroy
@@ -55,10 +39,7 @@ module Ancestry
55
39
  when :adopt # make child elements of this node, child of its parent
56
40
  descendants.each do |descendant|
57
41
  descendant.without_ancestry_callbacks do
58
- new_ancestry = descendant.ancestor_ids.delete_if { |x| x == self.id }.join("/")
59
- # check for empty string if it's then set to nil
60
- new_ancestry = nil if new_ancestry.empty?
61
- descendant.update_attribute descendant.class.ancestry_column, new_ancestry || nil
42
+ descendant.update_attribute :ancestor_ids, descendant.ancestor_ids.delete_if { |x| x == self.id }
62
43
  end
63
44
  end
64
45
  when :restrict # throw an exception if it has children
@@ -79,19 +60,6 @@ module Ancestry
79
60
  end
80
61
  end
81
62
 
82
- # The ancestry value for this record's children (before save)
83
- # This is technically child_ancestry_was
84
- def child_ancestry
85
- # New records cannot have children
86
- raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
87
-
88
- if self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}").blank?
89
- id.to_s
90
- else
91
- "#{self.send "#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"}/#{id}"
92
- end
93
- end
94
-
95
63
  # Counter Cache
96
64
  def increase_parent_counter_cache
97
65
  self.class.increment_counter _counter_cache_column, parent_id
@@ -133,8 +101,7 @@ module Ancestry
133
101
  # Ancestors
134
102
 
135
103
  def ancestors?
136
- # ancestor_ids.present?
137
- read_attribute(self.ancestry_base_class.ancestry_column).present?
104
+ ancestor_ids.present?
138
105
  end
139
106
  alias :has_parent? :ancestors?
140
107
 
@@ -149,50 +116,21 @@ module Ancestry
149
116
  end
150
117
  end
151
118
 
152
- def ancestor_ids
153
- parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
154
- end
155
-
156
- def ancestor_conditions
157
- self.ancestry_base_class.ancestor_conditions(self)
158
- end
159
-
160
119
  def ancestors depth_options = {}
161
120
  return self.ancestry_base_class.none unless ancestors?
162
- self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions
163
- end
164
-
165
- # deprecate
166
- def ancestor_was_conditions
167
- {primary_key_with_table => ancestor_ids_before_last_save}
168
- end
169
-
170
- # deprecated - probably don't want to use anymore
171
- def ancestor_ids_was
172
- parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}_was"))
173
- end
174
-
175
- def ancestor_ids_before_last_save
176
- parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
177
- end
178
-
179
- def parent_id_before_last_save
180
- ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
181
- return unless ancestry_was.present?
182
-
183
- ancestry_was.split(ANCESTRY_DELIMITER).last.to_i
121
+ self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
184
122
  end
185
123
 
186
124
  def path_ids
187
125
  ancestor_ids + [id]
188
126
  end
189
127
 
190
- def path_conditions
191
- self.ancestry_base_class.path_conditions(self)
128
+ def path_ids_in_database
129
+ ancestor_ids_in_database + [id]
192
130
  end
193
131
 
194
132
  def path depth_options = {}
195
- self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where path_conditions
133
+ self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
196
134
  end
197
135
 
198
136
  def depth
@@ -212,7 +150,7 @@ module Ancestry
212
150
  # currently parent= does not work in after save callbacks
213
151
  # assuming that parent hasn't changed
214
152
  def parent= parent
215
- write_attribute(self.ancestry_base_class.ancestry_column, if parent.nil? then nil else parent.child_ancestry end)
153
+ self.ancestor_ids = parent ? parent.path_ids : []
216
154
  end
217
155
 
218
156
  def parent_id= new_parent_id
@@ -222,15 +160,12 @@ module Ancestry
222
160
  def parent_id
223
161
  ancestor_ids.last if ancestors?
224
162
  end
163
+ alias :parent_id? :ancestors?
225
164
 
226
165
  def parent
227
166
  unscoped_find(parent_id) if ancestors?
228
167
  end
229
168
 
230
- def parent_id?
231
- ancestors?
232
- end
233
-
234
169
  def parent_of?(node)
235
170
  self.id == node.parent_id
236
171
  end
@@ -246,7 +181,7 @@ module Ancestry
246
181
  end
247
182
 
248
183
  def is_root?
249
- read_attribute(self.ancestry_base_class.ancestry_column).blank?
184
+ !ancestors?
250
185
  end
251
186
  alias :root? :is_root?
252
187
 
@@ -256,12 +191,8 @@ module Ancestry
256
191
 
257
192
  # Children
258
193
 
259
- def child_conditions
260
- self.ancestry_base_class.child_conditions(self)
261
- end
262
-
263
194
  def children
264
- self.ancestry_base_class.where child_conditions
195
+ self.ancestry_base_class.children_of(self)
265
196
  end
266
197
 
267
198
  def child_ids
@@ -284,14 +215,11 @@ module Ancestry
284
215
 
285
216
  # Siblings
286
217
 
287
- def sibling_conditions
288
- self.ancestry_base_class.sibling_conditions(self)
289
- end
290
-
291
218
  def siblings
292
- self.ancestry_base_class.where sibling_conditions
219
+ self.ancestry_base_class.siblings_of(self)
293
220
  end
294
221
 
222
+ # NOTE: includes self
295
223
  def sibling_ids
296
224
  siblings.pluck(self.ancestry_base_class.primary_key)
297
225
  end
@@ -307,17 +235,13 @@ module Ancestry
307
235
  alias_method :only_child?, :is_only_child?
308
236
 
309
237
  def sibling_of?(node)
310
- self.ancestry == node.ancestry
238
+ self.ancestor_ids == node.ancestor_ids
311
239
  end
312
240
 
313
241
  # Descendants
314
242
 
315
- def descendant_conditions
316
- self.ancestry_base_class.descendant_conditions(self)
317
- end
318
-
319
243
  def descendants depth_options = {}
320
- self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where descendant_conditions
244
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
321
245
  end
322
246
 
323
247
  def descendant_ids depth_options = {}
@@ -330,12 +254,8 @@ module Ancestry
330
254
 
331
255
  # Indirects
332
256
 
333
- def indirect_conditions
334
- self.ancestry_base_class.indirect_conditions(self)
335
- end
336
-
337
257
  def indirects depth_options = {}
338
- self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where indirect_conditions
258
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
339
259
  end
340
260
 
341
261
  def indirect_ids depth_options = {}
@@ -348,12 +268,8 @@ module Ancestry
348
268
 
349
269
  # Subtree
350
270
 
351
- def subtree_conditions
352
- self.ancestry_base_class.subtree_conditions(self)
353
- end
354
-
355
271
  def subtree depth_options = {}
356
- self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where subtree_conditions
272
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
357
273
  end
358
274
 
359
275
  def subtree_ids depth_options = {}
@@ -373,17 +289,9 @@ module Ancestry
373
289
  end
374
290
 
375
291
  private
376
- ANCESTRY_DELIMITER = '/'.freeze
377
-
378
- def parse_ancestry_column obj
379
- return [] unless obj
380
- obj_ids = obj.split(ANCESTRY_DELIMITER)
381
- self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
382
- end
383
-
384
292
  def unscoped_descendants
385
293
  unscoped_where do |scope|
386
- scope.where descendant_conditions
294
+ scope.where self.ancestry_base_class.descendant_conditions(self)
387
295
  end
388
296
  end
389
297
 
@@ -1,44 +1,56 @@
1
1
  module Ancestry
2
2
  module MaterializedPath
3
+ BEFORE_LAST_SAVE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_before_last_save'.freeze : '_was'.freeze
4
+ IN_DATABASE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_in_database'.freeze : '_was'.freeze
5
+ ANCESTRY_DELIMITER='/'.freeze
6
+
3
7
  def self.extended(base)
4
- base.validates_format_of base.ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true
5
8
  base.send(:include, InstanceMethods)
6
9
  end
7
10
 
8
- def root_conditions
9
- arel_table[ancestry_column].eq(nil)
11
+ def path_of(object)
12
+ to_node(object).path
13
+ end
14
+
15
+ def roots
16
+ where(arel_table[ancestry_column].eq(nil))
10
17
  end
11
18
 
12
- def ancestor_conditions(object)
19
+ def ancestors_of(object)
13
20
  t = arel_table
14
21
  node = to_node(object)
15
- t[primary_key].in(node.ancestor_ids)
22
+ where(t[primary_key].in(node.ancestor_ids))
16
23
  end
17
24
 
18
- def path_conditions(object)
25
+ def inpath_of(object)
19
26
  t = arel_table
20
27
  node = to_node(object)
21
- t[primary_key].in(node.path_ids)
28
+ where(t[primary_key].in(node.path_ids))
22
29
  end
23
30
 
24
- def child_conditions(object)
31
+ def children_of(object)
25
32
  t = arel_table
26
33
  node = to_node(object)
27
- t[ancestry_column].eq(node.child_ancestry)
34
+ where(t[ancestry_column].eq(node.child_ancestry))
28
35
  end
29
36
 
30
37
  # indirect = anyone who is a descendant, but not a child
31
- def indirect_conditions(object)
38
+ def indirects_of(object)
32
39
  t = arel_table
33
40
  node = to_node(object)
34
41
  # rails has case sensitive matching.
35
42
  if ActiveRecord::VERSION::MAJOR >= 5
36
- t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true)
43
+ where(t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true))
37
44
  else
38
- t[ancestry_column].matches("#{node.child_ancestry}/%")
45
+ where(t[ancestry_column].matches("#{node.child_ancestry}/%"))
39
46
  end
40
47
  end
41
48
 
49
+ def descendants_of(object)
50
+ where(descendant_conditions(object))
51
+ end
52
+
53
+ # deprecated
42
54
  def descendant_conditions(object)
43
55
  t = arel_table
44
56
  node = to_node(object)
@@ -50,23 +62,94 @@ module Ancestry
50
62
  end
51
63
  end
52
64
 
53
- def subtree_conditions(object)
65
+ def subtree_of(object)
54
66
  t = arel_table
55
67
  node = to_node(object)
56
- descendant_conditions(node).or(t[primary_key].eq(node.id))
68
+ where(descendant_conditions(node).or(t[primary_key].eq(node.id)))
57
69
  end
58
70
 
59
- def sibling_conditions(object)
71
+ def siblings_of(object)
60
72
  t = arel_table
61
73
  node = to_node(object)
62
- t[ancestry_column].eq(node[ancestry_column])
74
+ where(t[ancestry_column].eq(node[ancestry_column]))
75
+ end
76
+
77
+ def ordered_by_ancestry(order = nil)
78
+ if %w(mysql mysql2 sqlite sqlite3).include?(connection.adapter_name.downcase)
79
+ reorder(arel_table[ancestry_column], order)
80
+ elsif %w(postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::STRING >= "6.1"
81
+ reorder(Arel::Nodes.new(arel_table[ancestry_column]).nulls_first)
82
+ else
83
+ reorder(
84
+ Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
85
+ order
86
+ )
87
+ end
88
+ end
89
+
90
+ def ordered_by_ancestry_and(order)
91
+ ordered_by_ancestry(order)
63
92
  end
64
93
 
65
94
  module InstanceMethods
95
+
66
96
  # Validates the ancestry, but can also be applied if validation is bypassed to determine if children should be affected
67
97
  def sane_ancestry?
68
98
  ancestry_value = read_attribute(self.ancestry_base_class.ancestry_column)
69
- ancestry_value.nil? || (ancestry_value.to_s =~ Ancestry::ANCESTRY_PATTERN && !ancestor_ids.include?(self.id))
99
+ (ancestry_value.nil? || !ancestor_ids.include?(self.id)) && valid?
100
+ end
101
+
102
+ # optimization - better to go directly to column and avoid parsing
103
+ def ancestors?
104
+ read_attribute(self.ancestry_base_class.ancestry_column).present?
105
+ end
106
+ alias :has_parent? :ancestors?
107
+
108
+ def ancestor_ids=(value)
109
+ col = self.ancestry_base_class.ancestry_column
110
+ value.present? ? write_attribute(col, value.join(ANCESTRY_DELIMITER)) : write_attribute(col, nil)
111
+ end
112
+
113
+ def ancestor_ids
114
+ parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
115
+ end
116
+
117
+ def ancestor_ids_in_database
118
+ parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"))
119
+ end
120
+
121
+ def ancestor_ids_before_last_save
122
+ parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
123
+ end
124
+
125
+ def parent_id_before_last_save
126
+ ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
127
+ return unless ancestry_was.present?
128
+
129
+ parse_ancestry_column(ancestry_was).last
130
+ end
131
+
132
+ # optimization - better to go directly to column and avoid parsing
133
+ def sibling_of?(node)
134
+ self.read_attribute(self.ancestry_base_class.ancestry_column) == node.read_attribute(self.ancestry_base_class.ancestry_column)
135
+ end
136
+
137
+ # private (public so class methods can find it)
138
+ # The ancestry value for this record's children (before save)
139
+ # This is technically child_ancestry_was
140
+ def child_ancestry
141
+ # New records cannot have children
142
+ raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
143
+ path_was = self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}")
144
+ path_was.blank? ? id.to_s : "#{path_was}/#{id}"
145
+ end
146
+
147
+ private
148
+
149
+ def parse_ancestry_column obj
150
+ return [] unless obj
151
+ obj_ids = obj.split(ANCESTRY_DELIMITER)
152
+ self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
70
153
  end
71
154
  end
72
155
  end
@@ -1,3 +1,3 @@
1
1
  module Ancestry
2
- VERSION = "3.0.7"
2
+ VERSION = "3.1.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ancestry
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.7
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Kroes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-06-06 00:00:00.000000000 Z
12
+ date: 2020-08-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -17,16 +17,16 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: 3.2.0
20
+ version: 4.2.0
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: 3.2.0
27
+ version: 4.2.0
28
28
  - !ruby/object:Gem::Dependency
29
- name: rdoc
29
+ name: appraisal
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - ">="
@@ -40,7 +40,7 @@ dependencies:
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0'
42
42
  - !ruby/object:Gem::Dependency
43
- name: yard
43
+ name: minitest
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - ">="
@@ -59,44 +59,16 @@ dependencies:
59
59
  requirements:
60
60
  - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: '10.0'
62
+ version: '13.0'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: '10.0'
70
- - !ruby/object:Gem::Dependency
71
- name: test-unit
72
- requirement: !ruby/object:Gem::Requirement
73
- requirements:
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- version: '0'
77
- type: :development
78
- prerelease: false
79
- version_requirements: !ruby/object:Gem::Requirement
80
- requirements:
81
- - - ">="
82
- - !ruby/object:Gem::Version
83
- version: '0'
84
- - !ruby/object:Gem::Dependency
85
- name: minitest
86
- requirement: !ruby/object:Gem::Requirement
87
- requirements:
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- version: '0'
91
- type: :development
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - ">="
96
- - !ruby/object:Gem::Version
97
- version: '0'
69
+ version: '13.0'
98
70
  - !ruby/object:Gem::Dependency
99
- name: sqlite3
71
+ name: yard
100
72
  requirement: !ruby/object:Gem::Requirement
101
73
  requirements:
102
74
  - - ">="
@@ -150,7 +122,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
150
122
  requirements:
151
123
  - - ">="
152
124
  - !ruby/object:Gem::Version
153
- version: 1.8.7
125
+ version: 2.0.0
154
126
  required_rubygems_version: !ruby/object:Gem::Requirement
155
127
  requirements:
156
128
  - - ">="