ancestry 3.1.0 → 3.2.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 +4 -4
- data/README.md +74 -245
- data/ancestry.gemspec +6 -6
- data/lib/ancestry.rb +28 -0
- data/lib/ancestry/class_methods.rb +32 -12
- data/lib/ancestry/has_ancestry.rb +9 -4
- data/lib/ancestry/instance_methods.rb +6 -2
- data/lib/ancestry/materialized_path.rb +3 -3
- data/lib/ancestry/version.rb +1 -1
- metadata +12 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d11437b15407107e0d4690084bc462a50d97d4fe9615445ff295a96d74943c67
|
4
|
+
data.tar.gz: e2bdffb78316b524828c0a6d38c0c0e4101ef71c57bda836272d41e0381b44c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
7
|
-
|
8
|
-
relations (ancestors, parent, root, children, siblings, descendants)
|
9
|
-
of them to be fetched in a single SQL query. Additional features
|
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
|
18
|
-
- Ancestry 3.x supports Rails 5.0
|
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
|
-
|
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
|
-
|
86
|
-
TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
|
87
|
-
```
|
84
|
+
`TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')`.
|
88
85
|
|
89
|
-
|
86
|
+
Children can be created through the children relation on a node: `node.children.create :name => 'Stinky'`.
|
90
87
|
|
91
|
-
|
92
|
-
|
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
|
+
| | | |
|
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
|
+
| | | |
|
103
|
+
| `child_ids` |`descendant_ids` |`indirect_ids` |
|
104
|
+
| `has_children?` | | |
|
105
|
+
| `child_of?` |`descendant_of?` |`indirect_of?` |
|
106
|
+
|**siblings** |**subtree** |**path** |
|
107
|
+
| | | |
|
108
|
+
| includes self |self..indirects |root..self |
|
109
|
+
|`sibling_ids` |`subtree_ids` |`path_ids` |
|
110
|
+
|`has_siblings?` | | |
|
111
|
+
|`sibling_of?(node)` | | |
|
94
112
|
|
95
|
-
#
|
96
|
-
|
97
|
-
|
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
|
-
|
217
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
248
|
-
scopes can be used to select nodes
|
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
|
-
|
257
|
-
descendant_ids
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
283
|
-
|
284
|
-
|
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
|
-
|
291
|
-
|
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
|
305
|
-
|
306
|
-
|
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
|
-
|
315
|
-
TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
|
316
|
-
```
|
212
|
+
`TreeNode.find_by(:name => 'Crunchy').subtree.arrange(:order => :name)`.
|
317
213
|
|
318
|
-
|
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
|
-
`
|
219
|
+
Using `ActiveModel` serializers:
|
321
220
|
|
322
|
-
|
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
|
-
|
370
|
-
|
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
|
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.
|
385
|
-
|
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
|
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
|
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
|
|
data/ancestry.gemspec
CHANGED
@@ -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
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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",
|
data/lib/ancestry.rb
CHANGED
@@ -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("
|
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("
|
30
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.invalid_orphan_strategy"))
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
|
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.
|
107
|
-
raise Ancestry::AncestryIntegrityException.new("
|
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("
|
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("
|
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.
|
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
|
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("
|
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("
|
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("
|
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("
|
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, "
|
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(
|
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(
|
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
|
data/lib/ancestry/version.rb
CHANGED
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.
|
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-
|
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
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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: []
|