ancestry 3.1.0 → 3.2.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: 9455bbdda28f2f0084ff4b1e96af32ca5d6b3efe40ae062c51f75b3fee84fb30
4
- data.tar.gz: 5b06653e17bbe2a238c4d49d9147dd79f2156c33432298290b5b17da190ce203
3
+ metadata.gz: d11437b15407107e0d4690084bc462a50d97d4fe9615445ff295a96d74943c67
4
+ data.tar.gz: e2bdffb78316b524828c0a6d38c0c0e4101ef71c57bda836272d41e0381b44c3
5
5
  SHA512:
6
- metadata.gz: 4cb481c338a78cd5c294a40ccc43ccbad27232cc0a646b4827fd2446ad3d3fd626410f4a35d3b9361b367c2474ba229113aa191ba7799ddcc7930b1d29eab106
7
- data.tar.gz: 6876e974e00c0ee4c5bb4ea4facbfb9ce12fe04eebfab2bc4623d6a74c9591781d3945b4999437054a81fb1ea82a656822c679e38c434541173ee7d192d664dc
6
+ metadata.gz: 533b6b0cc2db821091081f09c7680cbe163abbcc85500d278c52625861bdd901a7623232cde8ccbf21f3e0960a5231adb889055555e4849d75ede13651f626ad
7
+ data.tar.gz: 6f754739730731009eee294e9a6fed1d268d3f0cbf4024cec96221a0a68b5f59569ac47bb42f13e3e7798f76982d35ff35b03a01fe44f4d217b7e3e37c9c62d4
data/README.md CHANGED
@@ -3,10 +3,10 @@
3
3
  # Ancestry
4
4
 
5
5
  Ancestry is a gem that allows the records of a Ruby on Rails
6
- ActiveRecord model to be organised as a tree structure (or hierarchy). It uses
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
6
+ ActiveRecord model to be organised as a tree structure (or hierarchy). It employs
7
+ the materialised path pattern and exposes all the standard tree structure
8
+ relations (ancestors, parent, root, children, siblings, descendants), allowing all
9
+ of them to be fetched in a single SQL query. Additional features include STI
10
10
  support, scopes, depth caching, depth constraints, easy migration from older
11
11
  gems, integrity checking, integrity restoration, arrangement of
12
12
  (sub)trees into hashes, and various strategies for dealing with orphaned
@@ -14,14 +14,13 @@ records.
14
14
 
15
15
  NOTE:
16
16
 
17
- - Ancestry 2.x supports Rails 4.1, and earlier
18
- - Ancestry 3.x supports Rails 5.0, and 4.2
17
+ - Ancestry 2.x supports Rails 4.1 and earlier
18
+ - Ancestry 3.x supports Rails 5.0 and 4.2
19
19
  - Ancestry 4.0 only supports rails 5.0 and higher
20
20
 
21
21
  # Installation
22
22
 
23
- To apply Ancestry to any `ActiveRecord` model, follow these simple steps:
24
-
23
+ Follow these simple steps to apply Ancestry to any ActiveRecord model:
25
24
 
26
25
  ## Install
27
26
 
@@ -82,115 +81,38 @@ parent_id can be set using parent= and parent_id= on a record or by including
82
81
  them in the hash passed to new, create, create!, update_attributes and
83
82
  update_attributes!. For example:
84
83
 
85
- ```ruby
86
- TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
87
- ```
84
+ `TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')`.
88
85
 
89
- You can also create children through the children relation on a node:
86
+ Children can be created through the children relation on a node: `node.children.create :name => 'Stinky'`.
90
87
 
91
- ```ruby
92
- node.children.create :name => 'Stinky'
93
- ```
88
+ # Tree Navigation
89
+
90
+ The node with the large border is the reference node (the node from which the navigation method is invoked.)
91
+ The yellow nodes are those returned by the method.
92
+
93
+ | | | |
94
+ |:-: |:-: |:-: |
95
+ |**parent** |**root**<sup><a href="#fn1" id="ref1">1</a></sup> |**ancestors** |
96
+ |![parent](/img/parent.png) |![root](/img/root.png) |![ancestors](/img/ancestors.png) |
97
+ | nil for a root node |self for a root node |root..parent |
98
+ | `parent_id` |`root_id` |`ancestor_ids` |
99
+ | `has_parent?` |`is_root?` |`ancestors?` |
100
+ |`parent_of?` |`root_of?` |`ancestor_of?` |
101
+ |**children** |**descendants** |**indirects** |
102
+ |![children](/img/children.png) |![descendants](/img/descendants.png) |![indirects](/img/indirects.png) |
103
+ | `child_ids` |`descendant_ids` |`indirect_ids` |
104
+ | `has_children?` | | |
105
+ | `child_of?` |`descendant_of?` |`indirect_of?` |
106
+ |**siblings** |**subtree** |**path** |
107
+ |![siblings](/img/siblings.png) |![subtree](/img/subtree.png) |![path](/img/path.png) |
108
+ | includes self |self..indirects |root..self |
109
+ |`sibling_ids` |`subtree_ids` |`path_ids` |
110
+ |`has_siblings?` | | |
111
+ |`sibling_of?(node)` | | |
94
112
 
95
- # Navigating your tree
96
-
97
- To navigate an Ancestry model, use the following instance methods:
98
-
99
- | method |return value|
100
- |-------------------|------------|
101
- |`parent` |parent of the record, nil for a root node|
102
- |`parent_id` |parent id of the record, nil for a root node|
103
- |`root` |root of the record's tree, self for a root node|
104
- |`root_id` |root id of the record's tree, self for a root node|
105
- |`root?` <br/> `is_root?` | true if the record is a root node, false otherwise|
106
- |`ancestors` |ancestors of the record, starting with the root and ending with the parent|
107
- |`ancestors?` |true if the record has ancestors (aka not a root node)|
108
- |`ancestor_ids` |ancestor ids of the record|
109
- |`path` |path of the record, starting with the root and ending with self|
110
- |`path_ids` |a list of the path ids, starting with the root id and ending with the node's own id|
111
- |`children` |direct children of the record|
112
- |`child_ids` |direct children's ids|
113
- |`has_parent?` <br/> `ancestors?` |true if the record has a parent, false otherwise|
114
- |`has_children?` <br/> `children?` |true if the record has any children, false otherwise|
115
- |`is_childless?` <br/> `childless?` |true is the record has no children, false otherwise|
116
- |`siblings` |siblings of the record, including the record itself*|
117
- |`sibling_ids` |sibling ids|
118
- |`has_siblings?` <br/> `siblings?` |true if the record's parent has more than one child|
119
- |`is_only_child?` <br/> `only_child?` |true if the record is the only child of its parent|
120
- |`descendants` |direct and indirect children of the record|
121
- |`descendant_ids` |direct and indirect children's ids of the record|
122
- |`indirects` |indirect children of the record|
123
- |`indirect_ids` |indirect children's ids of the record|
124
- |`subtree` |the model on descendants and itself|
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)|
127
-
128
- \* If the record is a root, other root records are considered siblings
129
- \* Siblings returns the record itself
130
-
131
- There are also instance methods to determine the relationship between 2 nodes:
132
-
133
- |method |return value|
134
- |-------------------|---------------|
135
- |`parent_of?(node)` | node's parent is this record|
136
- |`root_of?(node)` | node's root is this record|
137
- |`ancestor_of?(node)`| node's ancestors include this record|
138
- |`child_of?(node)` | node is record's parent|
139
- |`descendant_of?(node)` | node is one of this record's ancestors|
140
- |`indirect_of?(node)` | node is one of this record's ancestors but not a parent|
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
-
193
- # Options for `has_ancestry`
113
+ <sup id="fn1">1. [other root records are considered siblings]<a href="#ref1" title="Jump back to footnote 1.">↩</a></sup>
114
+
115
+ # `has_ancestry` options
194
116
 
195
117
  The has_ancestry method supports the following options:
196
118
 
@@ -213,10 +135,8 @@ The has_ancestry method supports the following options:
213
135
 
214
136
  # (Named) Scopes
215
137
 
216
- Where possible, the navigation methods return scopes instead of records. This
217
- means additional ordering, conditions, limits, etc. can be applied and that
218
- the result can be either retrieved, counted, or checked for existence. For
219
- example:
138
+ The navigation methods return scopes instead of records, where possible. Additional ordering,
139
+ conditions, limits, etc. can be applied and the results can be retrieved, counted, or checked for existence:
220
140
 
221
141
  ```ruby
222
142
  node.children.where(:name => 'Mary').exists?
@@ -224,7 +144,7 @@ node.subtree.order(:name).limit(10).each { ... }
224
144
  node.descendants.count
225
145
  ```
226
146
 
227
- For convenience, a couple of named scopes are included at the class level:
147
+ A couple of class-level named scopes are included:
228
148
 
229
149
  roots Root nodes
230
150
  ancestors_of(node) Ancestors of node, node can be either a record or an id
@@ -234,8 +154,7 @@ For convenience, a couple of named scopes are included at the class level:
234
154
  subtree_of(node) Subtree of node, node can be either a record or an id
235
155
  siblings_of(node) Siblings of node, node can be either a record or an id
236
156
 
237
- Thanks to some convenient rails magic, it is even possible to create nodes
238
- through the children and siblings scopes:
157
+ It is possible thanks to some convenient rails magic to create nodes through the children and siblings scopes:
239
158
 
240
159
  node.children.create
241
160
  node.siblings.create!
@@ -244,8 +163,8 @@ through the children and siblings scopes:
244
163
 
245
164
  # Selecting nodes by depth
246
165
 
247
- When depth caching is enabled (see has_ancestry options), five more named
248
- scopes can be used to select nodes on their depth:
166
+ With depth caching enabled (see has_ancestry options), an additional five named
167
+ scopes can be used to select nodes by depth:
249
168
 
250
169
  before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
251
170
  to_depth(depth) Return nodes up to a certain depth (node.depth <= depth)
@@ -253,42 +172,27 @@ scopes can be used to select nodes on their depth:
253
172
  from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
254
173
  after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
255
174
 
256
- The depth scopes are also available through calls to descendants,
257
- descendant_ids, subtree, subtree_ids, path and ancestors. In this case, depth
258
- values are interpreted relatively. Some examples:
259
-
260
- node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
261
- node.subtree.to_depth(5) Subtree of node to an absolute depth of 5
262
- node.descendants(:at_depth => 2) Descendant of node, at depth node.depth + 2 (grandchildren)
263
- node.descendants.at_depth(10) Descendants of node at an absolute depth of 10
264
- node.ancestors.to_depth(3) The oldest 4 ancestors of node (its root and 3 more)
265
- node.path(:from_depth => -2) The node's grandparent, parent and the node itself
175
+ Depth scopes are also available through calls to `descendants`,
176
+ `descendant_ids`, `subtree`, `subtree_ids`, `path` and `ancestors` (with relative depth).
177
+ Note that depth constraints cannot be passed to `ancestor_ids` or `path_ids` as both relations
178
+ can be fetched directly from the ancestry column without needing a query. Use
179
+ `ancestors(depth_options).map(&:id)` or `ancestor_ids.slice(min_depth..max_depth)` instead.
266
180
 
267
181
  node.ancestors(:from_depth => -6, :to_depth => -4)
268
182
  node.path.from_depth(3).to_depth(4)
269
183
  node.descendants(:from_depth => 2, :to_depth => 4)
270
184
  node.subtree.from_depth(10).to_depth(12)
271
185
 
272
- Please note that depth constraints cannot be passed to ancestor_ids and
273
- path_ids. The reason for this is that both these relations can be fetched
274
- directly from the ancestry column without performing a database query. It
275
- would require an entirely different method of applying the depth constraints
276
- which isn't worth the effort of implementing. You can use
277
- ancestors(depth_options).map(&:id) or ancestor_ids.slice(min_depth..max_depth)
278
- instead.
279
-
280
186
  # STI support
281
187
 
282
- Ancestry works fine with STI. Just create a STI inheritance hierarchy and
283
- build an Ancestry tree from the different classes/models. All Ancestry
284
- relations that were described above will return nodes of any model type. If
285
- you do only want nodes of a specific subclass you'll have to add a condition
286
- on type for that.
188
+ To use with STI: create a STI inheritance hierarchy and build a tree from the different
189
+ classes/models. All Ancestry relations that were described above will return nodes of any model type. If
190
+ you do only want nodes of a specific subclass, a type condition is required.
287
191
 
288
192
  # Arrangement
289
193
 
290
- Ancestry can arrange an entire subtree into nested hashes for easy navigation
291
- after retrieval from the database. `TreeNode.arrange` could for example return:
194
+ A subtree can be arranged into nested hashes for easy navigation after database retrieval.
195
+ `TreeNode.arrange` could, for instance, return:
292
196
 
293
197
  ```ruby
294
198
  {
@@ -301,43 +205,20 @@ after retrieval from the database. `TreeNode.arrange` could for example return:
301
205
  }
302
206
  ```
303
207
 
304
- The `arrange` method also works on a scoped class, for example:
305
-
306
- ```ruby
307
- TreeNode.find_by_name('Crunchy').subtree.arrange
308
- ```
309
-
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
312
- scope. example:
208
+ The `arrange` method can work on a scoped class (`TreeNode.find_by(:name => 'Crunchy').subtree.arrange`),
209
+ and can take ActiveRecord find options. If you want ordered hashes, pass the order to the method instead of
210
+ the scope as follows:
313
211
 
314
- ```ruby
315
- TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
316
- ```
212
+ `TreeNode.find_by(:name => 'Crunchy').subtree.arrange(:order => :name)`.
317
213
 
318
- To get the arranged nodes as a nested array of hashes for serialization:
214
+ The `arrange_serializable` method returns the arranged nodes as a nested array of hashes. Order
215
+ can be passed in the same fashion as to the `arrange` method:
216
+ `TreeNode.arrange_serializable(:order => :name)` The result can easily be serialized to json with `to_json`
217
+ or other formats. You can also supply your own serialization logic with blocks.
319
218
 
320
- `TreeNode.arrange_serializable`
219
+ Using `ActiveModel` serializers:
321
220
 
322
- ```ruby
323
- [
324
- {
325
- "ancestry" => nil, "id" => 1, "children" => [
326
- { "ancestry" => "1", "id" => 2, "children" => [] }
327
- ]
328
- }
329
- ]
330
- ```
331
-
332
- You can also supply your own serialization logic using blocks:
333
-
334
- For example, using `ActiveModel` Serializers:
335
-
336
- ```ruby
337
- TreeNode.arrange_serializable do |parent, children|
338
- MySerializer.new(parent, children: children)
339
- end
340
- ```
221
+ `TreeNode.arrange_serializable { |parent, children| MySerializer.new(parent, children: children) }`.
341
222
 
342
223
  Or plain hashes:
343
224
 
@@ -350,39 +231,22 @@ TreeNode.arrange_serializable do |parent, children|
350
231
  end
351
232
  ```
352
233
 
353
- The result of `arrange_serializable` can easily be serialized to json with
354
- `to_json`, or some other format:
355
-
356
- ```
357
- TreeNode.arrange_serializable.to_json
358
- ```
359
-
360
- You can also pass the order to the `arrange_serializable` method just as you can
361
- pass it to the `arrange` method:
362
-
363
- ```
364
- TreeNode.arrange_serializable(:order => :name)
365
- ```
366
-
367
234
  # Sorting
368
235
 
369
- If you just want to sort an array of nodes as if you were traversing them in
370
- preorder, you can use the sort_by_ancestry class method:
371
-
372
- ```
373
- TreeNode.sort_by_ancestry(array_of_nodes)
374
- ```
375
-
376
- Note that since materialised path trees don't support ordering within a rank,
377
- the order of siblings depends on their order in the original array.
236
+ The `sort_by_ancestry` class method: `TreeNode.sort_by_ancestry(array_of_nodes)` can be used
237
+ to sort an array of nodes as if traversing in preorder. (Note that since materialised path
238
+ trees do not support ordering within a rank, the order of siblings is
239
+ dependant upon their original array order.)
378
240
 
379
241
  # Migrating from plugin that uses parent_id column
380
242
 
381
243
  Most current tree plugins use a parent_id column (has_ancestry,
382
- awesome_nested_set, better_nested_set, acts_as_nested_set). With ancestry it is
244
+ awesome_nested_set, better_nested_set, acts_as_nested_set). With Ancestry it is
383
245
  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
385
- provide a more detailed explanation:
246
+ `build_ancestry_from_parent_ids!` method on your ancestry model.
247
+
248
+ <details>
249
+ <summary>Details</summary>
386
250
 
387
251
  1. Add ancestry column to your table
388
252
  * Create migration: **rails g migration [add_ancestry_to_](table)
@@ -392,7 +256,7 @@ provide a more detailed explanation:
392
256
  * Migrate your database: **rake db:migrate**
393
257
 
394
258
 
395
- 2. Remove old tree gem and add in Ancestry to `Gemfile`
259
+ 2. Remove old tree gem and add in Ancestry to Gemfile
396
260
  * See 'Installation' for more info on installing and configuring gems
397
261
 
398
262
 
@@ -403,7 +267,7 @@ provide a more detailed explanation:
403
267
 
404
268
 
405
269
  4. Generate ancestry columns
406
- * In 'rails console': **[model].build_ancestry_from_parent_ids!**
270
+ * In rails console: **[model].build_ancestry_from_parent_ids!**
407
271
  * Make sure it worked ok: **[model].check_ancestry_integrity!**
408
272
 
409
273
 
@@ -417,42 +281,7 @@ provide a more detailed explanation:
417
281
  * Create migration: `rails g migration [remove_parent_id_from_](table)`
418
282
  * Add to migration: `remove_column [table], :parent_id`
419
283
  * Migrate your database: `rake db:migrate`
420
-
421
- # Integrity checking and restoration
422
-
423
- I don't see any way Ancestry tree integrity could get compromised without
424
- explicitly setting cyclic parents or invalid ancestry and circumventing
425
- validation with update_attribute. If you do, please let me know.
426
-
427
- Ancestry includes some methods for detecting integrity problems and restoring
428
- integrity just to be sure. To check integrity, use:
429
- `[Model].check_ancestry_integrity!`. An AncestryIntegrityException will be
430
- raised if there are any problems. You can also specify :report => :list to
431
- return an array of exceptions or :report => :echo to echo any error messages.
432
- To restore integrity use: `[Model].restore_ancestry_integrity!`.
433
-
434
- For example, from IRB:
435
-
436
- ```
437
- >> stinky = TreeNode.create :name => 'Stinky'
438
- $ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
439
- >> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
440
- $ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
441
- >> stinky.update_attribute :parent, squeeky
442
- $ true
443
- >> TreeNode.all
444
- $ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
445
- >> TreeNode.check_ancestry_integrity!
446
- !! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
447
- >> TreeNode.restore_ancestry_integrity!
448
- $ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
449
- ```
450
-
451
- Additionally, if you think something is wrong with your depth cache:
452
-
453
- ```
454
- >> TreeNode.rebuild_depth_cache!
455
- ```
284
+ </details>
456
285
 
457
286
  # Running Tests
458
287
 
@@ -7,12 +7,12 @@ Gem::Specification.new do |s|
7
7
  s.summary = 'Organize ActiveRecord model into a tree structure'
8
8
  s.description = <<-EOF
9
9
  Ancestry allows the records of a ActiveRecord model to be organized in a tree
10
- structure, using a single, intuitively formatted database column. It exposes
11
- all the standard tree structure relations (ancestors, parent, root, children,
12
- siblings, descendants) and all of them can be fetched in a single sql query.
13
- Additional features are named_scopes, integrity checking, integrity restoration,
14
- arrangement of (sub)tree into hashes and different strategies for dealing with
15
- orphaned records.
10
+ structure, using the materialized path pattern. It exposes the standard
11
+ relations (ancestors, parent, root, children, siblings, descendants)
12
+ and allows them to be fetched in a single query. Additional features include
13
+ named scopes, integrity checking, integrity restoration, arrangement
14
+ of (sub)tree into hashes and different strategies for dealing with orphaned
15
+ records.
16
16
  EOF
17
17
  s.metadata = {
18
18
  "homepage_uri" => "https://github.com/stefankroes/ancestry",
@@ -4,6 +4,34 @@ require_relative 'ancestry/instance_methods'
4
4
  require_relative 'ancestry/exceptions'
5
5
  require_relative 'ancestry/has_ancestry'
6
6
  require_relative 'ancestry/materialized_path'
7
+ require_relative 'ancestry/materialized_path_pg'
8
+
9
+ I18n.load_path += Dir[File.join(File.expand_path(File.dirname(__FILE__)),
10
+ 'ancestry', 'locales', '*.{rb,yml}').to_s]
7
11
 
8
12
  module Ancestry
13
+ @@default_update_strategy = :ruby
14
+
15
+ # @!default_update_strategy
16
+ # @return [Symbol] the default strategy for updating ancestry
17
+ #
18
+ # The value changes the default way that ancestry is updated for associated records
19
+ #
20
+ # :ruby (default and legacy value)
21
+ #
22
+ # Child records will be loaded into memory and updated. callbacks will get called
23
+ # The callbacks of interest are those that cache values based upon the ancestry value
24
+ #
25
+ # :sql (currently only valid in postgres)
26
+ #
27
+ # Child records are updated in sql and callbacks will not get called.
28
+ # Associated records in memory will have the wrong ancestry value
29
+
30
+ def self.default_update_strategy
31
+ @@default_update_strategy
32
+ end
33
+
34
+ def self.default_update_strategy=(value)
35
+ @@default_update_strategy = value
36
+ end
9
37
  end
@@ -16,7 +16,7 @@ module Ancestry
16
16
  if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
17
17
  scope.send scope_name, depth + relative_depth
18
18
  else
19
- raise Ancestry::AncestryException.new("Unknown depth option: #{scope_name}.")
19
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_depth_option", {:scope_name => scope_name}))
20
20
  end
21
21
  end
22
22
  end
@@ -27,11 +27,17 @@ module Ancestry
27
27
  if [:rootify, :adopt, :restrict, :destroy].include? orphan_strategy
28
28
  class_variable_set :@@orphan_strategy, orphan_strategy
29
29
  else
30
- raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify,:adopt, :restrict and :destroy.")
30
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.invalid_orphan_strategy"))
31
31
  end
32
32
  end
33
33
 
34
- # Get all nodes and sorting them into an empty hash
34
+
35
+ # these methods arrange an entire subtree into nested hashes for easy navigation after database retrieval
36
+ # the arrange method also works on a scoped class
37
+ # the arrange method takes ActiveRecord find options
38
+ # To order your hashes pass the order to the arrange method instead of to the scope
39
+
40
+ # Get all nodes and sort them into an empty hash
35
41
  def arrange options = {}
36
42
  if (order = options.delete(:order))
37
43
  arrange_nodes self.ancestry_base_class.order(order).where(options)
@@ -54,7 +60,9 @@ module Ancestry
54
60
  end
55
61
  end
56
62
 
57
- # Arrangement to nested array
63
+ # Arrangement to nested array for serialization
64
+ # You can also supply your own serialization logic using blocks
65
+ # also allows you to pass the order just as you can pass it to the arrange method
58
66
  def arrange_serializable options={}, nodes=nil, &block
59
67
  nodes = arrange(options) if nodes.nil?
60
68
  nodes.map do |parent, children|
@@ -67,7 +75,6 @@ module Ancestry
67
75
  end
68
76
 
69
77
  # Pseudo-preordered array of nodes. Children will always follow parents,
70
- # for ordering nodes within a rank provide block, eg. Node.sort_by_ancestry(Node.all) {|a, b| a.rank <=> b.rank}.
71
78
  def sort_by_ancestry(nodes, &block)
72
79
  arranged = nodes if nodes.is_a?(Hash)
73
80
 
@@ -94,6 +101,9 @@ module Ancestry
94
101
  end
95
102
 
96
103
  # Integrity checking
104
+ # compromised tree integrity is unlikely without explicitly setting cyclic parents or invalid ancestry and circumventing validation
105
+ # just in case, raise an AncestryIntegrityException if issues are detected
106
+ # specify :report => :list to return an array of exceptions or :report => :echo to echo any error messages
97
107
  def check_ancestry_integrity! options = {}
98
108
  parents = {}
99
109
  exceptions = [] if options[:report] == :list
@@ -103,20 +113,30 @@ module Ancestry
103
113
  scope.find_each do |node|
104
114
  begin
105
115
  # ... check validity of ancestry column
106
- if !node.valid? and !node.errors[node.class.ancestry_column].blank?
107
- raise Ancestry::AncestryIntegrityException.new("Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_column}.")
116
+ if !node.sane_ancestor_ids?
117
+ raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.invalid_ancestry_column",
118
+ :node_id => node.id,
119
+ :ancestry_column => "#{node.read_attribute node.ancestry_column}"
120
+ ))
108
121
  end
109
122
  # ... check that all ancestors exist
110
123
  node.ancestor_ids.each do |ancestor_id|
111
124
  unless exists? ancestor_id
112
- raise Ancestry::AncestryIntegrityException.new("Reference to non-existent node in node #{node.id}: #{ancestor_id}.")
125
+ raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.reference_nonexistent_node",
126
+ :node_id => node.id,
127
+ :ancestor_id => ancestor_id
128
+ ))
113
129
  end
114
130
  end
115
131
  # ... check that all node parents are consistent with values observed earlier
116
132
  node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
117
133
  parents[node_id] = parent_id unless parents.has_key? node_id
118
134
  unless parents[node_id] == parent_id
119
- raise Ancestry::AncestryIntegrityException.new("Conflicting parent id found in node #{node.id}: #{parent_id || 'nil'} for node #{node_id} while expecting #{parents[node_id] || 'nil'}")
135
+ raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.conflicting_parent_id",
136
+ :node_id => node_id,
137
+ :parent_id => parent_id || 'nil',
138
+ :expected => parents[node_id] || 'nil'
139
+ ))
120
140
  end
121
141
  end
122
142
  rescue Ancestry::AncestryIntegrityException => integrity_exception
@@ -140,7 +160,7 @@ module Ancestry
140
160
  # For each node ...
141
161
  scope.find_each do |node|
142
162
  # ... set its ancestry to nil if invalid
143
- if !node.valid? and !node.errors[node.class.ancestry_column].blank?
163
+ if !node.sane_ancestor_ids?
144
164
  node.without_ancestry_callbacks do
145
165
  node.update_attribute :ancestor_ids, []
146
166
  end
@@ -171,7 +191,7 @@ module Ancestry
171
191
  end
172
192
  end
173
193
 
174
- # Build ancestry from parent id's for migration purposes
194
+ # Build ancestry from parent ids for migration purposes
175
195
  def build_ancestry_from_parent_ids! column=:parent_id, parent_id = nil, ancestor_ids = []
176
196
  unscoped_where do |scope|
177
197
  scope.where(column => parent_id).find_each do |node|
@@ -185,7 +205,7 @@ module Ancestry
185
205
 
186
206
  # Rebuild depth cache if it got corrupted or if depth caching was just turned on
187
207
  def rebuild_depth_cache!
188
- raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column
208
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.cannot_rebuild_depth_cache")) unless respond_to? :depth_cache_column
189
209
 
190
210
  self.ancestry_base_class.transaction do
191
211
  unscoped_where do |scope|
@@ -2,10 +2,10 @@ module Ancestry
2
2
  module HasAncestry
3
3
  def has_ancestry options = {}
4
4
  # Check options
5
- raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash
5
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.option_must_be_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, :primary_key_format].include? key
8
- raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.")
7
+ unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format, :update_strategy].include? key
8
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_option", {:key => key.inspect, :value => value.inspect}))
9
9
  end
10
10
  end
11
11
 
@@ -30,6 +30,9 @@ module Ancestry
30
30
  validates_format_of self.ancestry_column, :with => derive_ancestry_pattern(options[:primary_key_format]), :allow_nil => true
31
31
  extend Ancestry::MaterializedPath
32
32
 
33
+ update_strategy = options[:update_strategy] || Ancestry.default_update_strategy
34
+ include Ancestry::MaterializedPathPg if update_strategy == :sql
35
+
33
36
  # Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
34
37
  cattr_reader :orphan_strategy
35
38
  self.orphan_strategy = options[:orphan_strategy] || :destroy
@@ -75,7 +78,9 @@ module Ancestry
75
78
  # Create named scopes for depth
76
79
  {:before_depth => '<', :to_depth => '<=', :at_depth => '=', :from_depth => '>=', :after_depth => '>'}.each do |scope_name, operator|
77
80
  scope scope_name, lambda { |depth|
78
- raise Ancestry::AncestryException.new("Named scope '#{scope_name}' is only available when depth caching is enabled.") unless options[:cache_depth]
81
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.named_scope_depth_cache",
82
+ :scope_name => scope_name
83
+ )) unless options[:cache_depth]
79
84
  where("#{depth_cache_column} #{operator} ?", depth)
80
85
  }
81
86
  end
@@ -2,7 +2,7 @@ module Ancestry
2
2
  module InstanceMethods
3
3
  # Validate that the ancestors don't include itself
4
4
  def ancestry_exclude_self
5
- errors.add(:base, "#{self.class.model_name.human} cannot be a descendant of itself.") if ancestor_ids.include? self.id
5
+ errors.add(:base, I18n.t("ancestry.exclude_self", {:class_name => self.class.name.humanize})) if ancestor_ids.include? self.id
6
6
  end
7
7
 
8
8
  # Update descendants with new ancestry (before save)
@@ -43,7 +43,7 @@ module Ancestry
43
43
  end
44
44
  end
45
45
  when :restrict # throw an exception if it has children
46
- raise Ancestry::AncestryException.new('Cannot delete record because it has descendants.') unless is_childless?
46
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.cannot_delete_descendants")) unless is_childless?
47
47
  end
48
48
  end
49
49
  end
@@ -116,6 +116,10 @@ module Ancestry
116
116
  end
117
117
  end
118
118
 
119
+ def sane_ancestor_ids?
120
+ valid? || errors[self.ancestry_base_class.ancestry_column].blank?
121
+ end
122
+
119
123
  def ancestors depth_options = {}
120
124
  return self.ancestry_base_class.none unless ancestors?
121
125
  self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
@@ -71,14 +71,14 @@ module Ancestry
71
71
  def siblings_of(object)
72
72
  t = arel_table
73
73
  node = to_node(object)
74
- where(t[ancestry_column].eq(node[ancestry_column]))
74
+ where(t[ancestry_column].eq(node[ancestry_column].presence))
75
75
  end
76
76
 
77
77
  def ordered_by_ancestry(order = nil)
78
78
  if %w(mysql mysql2 sqlite sqlite3).include?(connection.adapter_name.downcase)
79
79
  reorder(arel_table[ancestry_column], order)
80
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)
81
+ reorder(Arel::Nodes::Ascending.new(arel_table[ancestry_column]).nulls_first)
82
82
  else
83
83
  reorder(
84
84
  Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
@@ -139,7 +139,7 @@ module Ancestry
139
139
  # This is technically child_ancestry_was
140
140
  def child_ancestry
141
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?
142
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
143
143
  path_was = self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}")
144
144
  path_was.blank? ? id.to_s : "#{path_was}/#{id}"
145
145
  end
@@ -1,3 +1,3 @@
1
1
  module Ancestry
2
- VERSION = "3.1.0"
2
+ VERSION = "3.2.0"
3
3
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ancestry
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Kroes
8
8
  - Keenan Brock
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-08-03 00:00:00.000000000 Z
12
+ date: 2020-09-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -83,12 +83,12 @@ dependencies:
83
83
  version: '0'
84
84
  description: |2
85
85
  Ancestry allows the records of a ActiveRecord model to be organized in a tree
86
- structure, using a single, intuitively formatted database column. It exposes
87
- all the standard tree structure relations (ancestors, parent, root, children,
88
- siblings, descendants) and all of them can be fetched in a single sql query.
89
- Additional features are named_scopes, integrity checking, integrity restoration,
90
- arrangement of (sub)tree into hashes and different strategies for dealing with
91
- orphaned records.
86
+ structure, using the materialized path pattern. It exposes the standard
87
+ relations (ancestors, parent, root, children, siblings, descendants)
88
+ and allows them to be fetched in a single query. Additional features include
89
+ named scopes, integrity checking, integrity restoration, arrangement
90
+ of (sub)tree into hashes and different strategies for dealing with orphaned
91
+ records.
92
92
  email: keenan@thebrocks.net
93
93
  executables: []
94
94
  extensions: []
@@ -114,7 +114,7 @@ metadata:
114
114
  changelog_uri: https://github.com/stefankroes/ancestry/blob/master/CHANGELOG.md
115
115
  source_code_uri: https://github.com/stefankroes/ancestry/
116
116
  bug_tracker_uri: https://github.com/stefankroes/ancestry/issues
117
- post_install_message:
117
+ post_install_message:
118
118
  rdoc_options: []
119
119
  require_paths:
120
120
  - lib
@@ -129,9 +129,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
129
  - !ruby/object:Gem::Version
130
130
  version: '0'
131
131
  requirements: []
132
- rubyforge_project:
132
+ rubyforge_project:
133
133
  rubygems_version: 2.7.6.2
134
- signing_key:
134
+ signing_key:
135
135
  specification_version: 4
136
136
  summary: Organize ActiveRecord model into a tree structure
137
137
  test_files: []