ancestry 3.0.7 → 3.1.0

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.
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
  - - ">="