ancestry 2.2.2 → 3.0.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
  SHA1:
3
- metadata.gz: 005c1204eb8018577673e708da755c1e8b0ae388
4
- data.tar.gz: 7f55187582a5d94339dd86e972ec9070c9cf1fed
3
+ metadata.gz: 10ddb8d5c0746b6c319e51a71a665257612bd6a1
4
+ data.tar.gz: 5da3981850ca1133b3c4318ba1934b492011c235
5
5
  SHA512:
6
- metadata.gz: 568208af2c3eb95eca4b71ceed135c72743370ff60e7d1209f5b1a5ef7557ef379680ccdf5a83dc5b80323a403c422d3b14501b41fc0d01557a53d4d04170a51
7
- data.tar.gz: 1b8c1b91c87573b7d9e611df92bb185bbb9712964a3dc0f64ccda3bde6c17fe11dd358212bdbb2f76645915723a66d179570ee4eb1ef630f96558f1f1ea043d9
6
+ metadata.gz: 44241ad80d6e7e8392c53a25f2396c101f9bf99dc22c1fd10b49b672af1e38f199a74afc8a5f638ccdb469ee821ae953b95160ed64caaabeb52dbc735addff81
7
+ data.tar.gz: e49f276175f2265d9af857eb64c72431d34b78ab49e977d2e0b4985095991b86f8da434926787ceaeccbd2a55719cfc4b2c308770ece7fb9cba47e32f11a66e6
@@ -0,0 +1,430 @@
1
+ [![Build Status](http://travis-ci.org/stefankroes/ancestry.svg?branch=master)](http://travis-ci.org/stefankroes/ancestry) [![Coverage Status](http://coveralls.io/repos/stefankroes/ancestry/badge.svg)](http://coveralls.io/r/stefankroes/ancestry) [![Gitter](http://badges.gitter.im/Join+Chat.svg)](http://gitter.im/stefankroes/ancestry?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Security](http://hakiri.io/github/stefankroes/ancestry/master.svg)](http://hakiri.io/github/stefankroes/ancestry/master)
2
+
3
+ # Ancestry
4
+
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, 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
10
+ support, scopes, depth caching, depth constraints, easy migration from older
11
+ gems, integrity checking, integrity restoration, arrangement of
12
+ (sub)tree into hashes and different strategies for dealing with orphaned
13
+ records.
14
+
15
+ # Installation
16
+
17
+ To apply Ancestry to any `ActiveRecord` model, follow these simple steps:
18
+
19
+ ## Install
20
+
21
+ * Add to Gemfile:
22
+ ```ruby
23
+ # Gemfile
24
+
25
+ gem 'ancestry'
26
+ ```
27
+
28
+ * Install required gems:
29
+ ```bash
30
+ $ bundle install
31
+ ```
32
+
33
+
34
+ ## Add ancestry column to your table
35
+ * Create migration:
36
+ ```bash
37
+ $ rails g migration add_ancestry_to_[table] ancestry:string
38
+ ```
39
+
40
+ * Add index to migration:
41
+ ```ruby
42
+ # db/migrate/[date]_add_ancestry_to_[table].rb
43
+
44
+ class AddAncestryTo[Table] < ActiveRecord::Migration
45
+ def change
46
+ add_column [table], :ancestry, :string
47
+ add_index [table], :ancestry
48
+ end
49
+ end
50
+ ```
51
+
52
+ * Migrate your database:
53
+ ```bash
54
+ $ rake db:migrate
55
+ ```
56
+
57
+
58
+ ## Add ancestry to your model
59
+ * Add to [app/models/](model).rb:
60
+
61
+ ```ruby
62
+ # app/models/[model.rb]
63
+
64
+ class [Model] < ActiveRecord::Base
65
+ has_ancestry
66
+ end
67
+ ```
68
+
69
+ Your model is now a tree!
70
+
71
+ # Using acts_as_tree instead of has_ancestry
72
+
73
+ In version 1.2.0 the **acts_as_tree** method was **renamed to has_ancestry**
74
+ in order to allow usage of both the acts_as_tree gem and the ancestry gem in a
75
+ single application. method `acts_as_tree` will continue to be supported in the future.
76
+
77
+ # Organising records into a tree
78
+
79
+ You can use the parent attribute to organise your records into a tree. If you
80
+ have the id of the record you want to use as a parent and don't want to fetch
81
+ it, you can also use parent_id. Like any virtual model attributes, parent and
82
+ parent_id can be set using parent= and parent_id= on a record or by including
83
+ them in the hash passed to new, create, create!, update_attributes and
84
+ update_attributes!. For example:
85
+
86
+ ```ruby
87
+ TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
88
+ ```
89
+
90
+ You can also create children through the children relation on a node:
91
+
92
+ ```ruby
93
+ node.children.create :name => 'Stinky'
94
+ ```
95
+
96
+ # Navigating your tree
97
+
98
+ To navigate an Ancestry model, use the following methods on any instance /
99
+ record:
100
+
101
+ parent Returns the parent of the record, nil for a root node
102
+ parent_id Returns the id of the parent of the record, nil for a root node
103
+ root Returns the root of the tree the record is in, self for a root node
104
+ root_id Returns the id of the root of the tree the record is in
105
+ root?, is_root? Returns true if the record is a root node, false otherwise
106
+ ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
107
+ ancestors Scopes the model on ancestors of the record
108
+ path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
109
+ path Scopes model on path records of the record
110
+ children Scopes the model on children of the record
111
+ child_ids Returns a list of child ids
112
+ has_children? Returns true if the record has any children, false otherwise
113
+ is_childless? Returns true is the record has no children, false otherwise
114
+ siblings Scopes the model on siblings of the record, the record itself is included*
115
+ sibling_ids Returns a list of sibling ids
116
+ has_siblings? Returns true if the record's parent has more than one child
117
+ is_only_child? Returns true if the record is the only child of its parent
118
+ descendants Scopes the model on direct and indirect children of the record
119
+ descendant_ids Returns a list of a descendant ids
120
+ subtree Scopes the model on descendants and itself
121
+ subtree_ids Returns a list of all ids in the record's subtree
122
+ depth Return the depth of the node, root nodes are at depth 0
123
+
124
+ * If the record is a root, other root records are considered siblings
125
+
126
+
127
+ # Options for `has_ancestry`
128
+
129
+ The has_ancestry methods supports the following options:
130
+
131
+ :ancestry_column Pass in a symbol to store ancestry in a different column
132
+ :orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
133
+ :destroy All children are destroyed as well (default)
134
+ :rootify The children of the destroyed node become root nodes
135
+ :restrict An AncestryException is raised if any children exist
136
+ :adopt The orphan subtree is added to the parent of the deleted node.
137
+ If the deleted node is Root, then rootify the orphan subtree.
138
+ :cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
139
+ If you turn depth_caching on for an existing model:
140
+ - Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
141
+ - Build cache: TreeNode.rebuild_depth_cache!
142
+ :depth_cache_column Pass in a symbol to store depth cache in a different column
143
+ :primary_key_format Supply a regular expression that matches the format of your primary key.
144
+ By default, primary keys only match integers ([0-9]+).
145
+ :touch Instruct Ancestry to touch the ancestors of a node when it changes, to
146
+ invalidate nested key-based caches. (default: false)
147
+
148
+ # (Named) Scopes
149
+
150
+ Where possible, the navigation methods return scopes instead of records, this
151
+ means additional ordering, conditions, limits, etc. can be applied and that
152
+ the result can be either retrieved, counted or checked for existence. For
153
+ example:
154
+
155
+ ```ruby
156
+ node.children.where(:name => 'Mary').exists?
157
+ node.subtree.order(:name).limit(10).each do; ...; end
158
+ node.descendants.count
159
+ ```
160
+
161
+ For convenience, a couple of named scopes are included at the class level:
162
+
163
+ roots Root nodes
164
+ ancestors_of(node) Ancestors of node, node can be either a record or an id
165
+ children_of(node) Children of node, node can be either a record or an id
166
+ descendants_of(node) Descendants of node, node can be either a record or an id
167
+ subtree_of(node) Subtree of node, node can be either a record or an id
168
+ siblings_of(node) Siblings of node, node can be either a record or an id
169
+
170
+ Thanks to some convenient rails magic, it is even possible to create nodes
171
+ through the children and siblings scopes:
172
+
173
+ node.children.create
174
+ node.siblings.create!
175
+ TestNode.children_of(node_id).new
176
+ TestNode.siblings_of(node_id).create
177
+
178
+ # Selecting nodes by depth
179
+
180
+ When depth caching is enabled (see has_ancestry options), five more named
181
+ scopes can be used to select nodes on their depth:
182
+
183
+ before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
184
+ to_depth(depth) Return nodes up to a certain depth (node.depth <= depth)
185
+ at_depth(depth) Return nodes that are at depth (node.depth == depth)
186
+ from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
187
+ after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
188
+
189
+ The depth scopes are also available through calls to descendants,
190
+ descendant_ids, subtree, subtree_ids, path and ancestors. In this case, depth
191
+ values are interpreted relatively. Some examples:
192
+
193
+ node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
194
+ node.subtree.to_depth(5) Subtree of node to an absolute depth of 5
195
+ node.descendants(:at_depth => 2) Descendant of node, at depth node.depth + 2 (grandchildren)
196
+ node.descendants.at_depth(10) Descendants of node at an absolute depth of 10
197
+ node.ancestors.to_depth(3) The oldest 4 ancestors of node (its root and 3 more)
198
+ node.path(:from_depth => -2) The node's grandparent, parent and the node itself
199
+
200
+ node.ancestors(:from_depth => -6, :to_depth => -4)
201
+ node.path.from_depth(3).to_depth(4)
202
+ node.descendants(:from_depth => 2, :to_depth => 4)
203
+ node.subtree.from_depth(10).to_depth(12)
204
+
205
+ Please note that depth constraints cannot be passed to ancestor_ids and
206
+ path_ids. The reason for this is that both these relations can be fetched
207
+ directly from the ancestry column without performing a database query. It
208
+ would require an entirely different method of applying the depth constraints
209
+ which isn't worth the effort of implementing. You can use
210
+ ancestors(depth_options).map(&:id) or ancestor_ids.slice(min_depth..max_depth)
211
+ instead.
212
+
213
+ # STI support
214
+
215
+ Ancestry works fine with STI. Just create a STI inheritance hierarchy and
216
+ build an Ancestry tree from the different classes/models. All Ancestry
217
+ relations that where described above will return nodes of any model type. If
218
+ you do only want nodes of a specific subclass you'll have to add a condition
219
+ on type for that.
220
+
221
+ # Arrangement
222
+
223
+ Ancestry can arrange an entire subtree into nested hashes for easy navigation
224
+ after retrieval from the database. TreeNode.arrange could for example return:
225
+
226
+ ```ruby
227
+ { #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
228
+ => { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
229
+ => { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
230
+ => {}
231
+ }
232
+ }
233
+ }
234
+ ```
235
+
236
+ The arrange method also works on a scoped class, for example:
237
+
238
+ ```ruby
239
+ TreeNode.find_by_name('Crunchy').subtree.arrange
240
+ ```
241
+
242
+ The arrange method takes `ActiveRecord` find options. If you want your hashes to
243
+ be ordered, you should pass the order to the arrange method instead of to the
244
+ scope. example:
245
+
246
+ ```ruby
247
+ TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
248
+ ```
249
+
250
+ To get the arranged nodes as a nested array of hashes for serialization:
251
+
252
+ TreeNode.arrange_serializable
253
+
254
+ ```ruby
255
+ [
256
+ {
257
+ "ancestry" => nil, "id" => 1, "children" => [
258
+ { "ancestry" => "1", "id" => 2, "children" => [] }
259
+ ]
260
+ }
261
+ ]
262
+ ```
263
+
264
+ You can also supply your own serialization logic using blocks:
265
+
266
+ For example, using `ActiveModel` Serializers:
267
+
268
+ ```ruby
269
+ TreeNode.arrange_serializable do |parent, children|
270
+ MySerializer.new(parent, children: children)
271
+ end
272
+ ```
273
+
274
+ Or plain hashes:
275
+
276
+ ```ruby
277
+ TreeNode.arrange_serializable do |parent, children|
278
+ {
279
+ my_id: parent.id
280
+ my_children: children
281
+ }
282
+ end
283
+ ```
284
+
285
+ The result of arrange_serializable can easily be serialized to json with
286
+ `to_json`, or some other format:
287
+
288
+ ```
289
+ TreeNode.arrange_serializable.to_json
290
+ ```
291
+
292
+ You can also pass the order to the arrange_serializable method just as you can
293
+ pass it to the arrange method:
294
+
295
+ ```
296
+ TreeNode.arrange_serializable(:order => :name)
297
+ ```
298
+
299
+ # Sorting
300
+
301
+ If you just want to sort an array of nodes as if you were traversing them in
302
+ preorder, you can use the sort_by_ancestry class method:
303
+
304
+ ```
305
+ TreeNode.sort_by_ancestry(array_of_nodes)
306
+ ```
307
+
308
+ Note that since materialised path trees don't support ordering within a rank,
309
+ the order of siblings depends on their order in the original array.
310
+
311
+ # Migrating from plugin that uses parent_id column
312
+
313
+ Most current tree plugins use a parent_id column (has_ancestry,
314
+ awesome_nested_set, better_nested_set, acts_as_nested_set). With ancestry its
315
+ easy to migrate from any of these plugins, to do so, use the
316
+ build_ancestry_from_parent_ids! method on your ancestry model. These steps
317
+ provide a more detailed explanation:
318
+
319
+ 1. Add ancestry column to your table
320
+ * Create migration: **rails g migration [add_ancestry_to_](table)
321
+ ancestry:string**
322
+ * Add index to migration: **add_index [table], :ancestry** (UP) /
323
+ **remove_index [table], :ancestry** (DOWN)
324
+ * Migrate your database: **rake db:migrate**
325
+
326
+
327
+ 2. Remove old tree gem and add in Ancestry to `Gemfile`
328
+ * See 'Installation' for more info on installing and configuring gems
329
+
330
+
331
+ 3. Change your model
332
+ * Remove any macros required by old plugin/gem from
333
+ `[app/models/](model).rb`
334
+ * Add to `[app/models/](model).rb`: `has_ancestry`
335
+
336
+
337
+ 4. Generate ancestry columns
338
+ * In './script.console': **[model].build_ancestry_from_parent_ids!**
339
+ * Make sure it worked ok: **[model].check_ancestry_integrity!**
340
+
341
+
342
+ 5. Change your code
343
+ * Most tree calls will probably work fine with ancestry
344
+ * Others must be changed or proxied
345
+ * Check if all your data is intact and all tests pass
346
+
347
+
348
+ 6. Drop parent_id column:
349
+ * Create migration: `rails g migration [remove_parent_id_from_](table)`
350
+ * Add to migration: `remove_column [table], :parent_id`
351
+ * Migrate your database: `rake db:migrate`
352
+
353
+ # Integrity checking and restoration
354
+
355
+ I don't see any way Ancestry tree integrity could get compromised without
356
+ explicitly setting cyclic parents or invalid ancestry and circumventing
357
+ validation with update_attribute, if you do, please let me know.
358
+
359
+ Ancestry includes some methods for detecting integrity problems and restoring
360
+ integrity just to be sure. To check integrity use:
361
+ [Model].check_ancestry_integrity!. An AncestryIntegrityException will be
362
+ raised if there are any problems. You can also specify :report => :list to
363
+ return an array of exceptions or :report => :echo to echo any error messages.
364
+ To restore integrity use: [Model].restore_ancestry_integrity!.
365
+
366
+ For example, from IRB:
367
+
368
+ ```
369
+ >> stinky = TreeNode.create :name => 'Stinky'
370
+ $ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
371
+ >> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
372
+ $ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
373
+ >> stinky.update_attribute :parent, squeeky
374
+ $ true
375
+ >> TreeNode.all
376
+ $ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
377
+ >> TreeNode.check_ancestry_integrity!
378
+ !! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
379
+ >> TreeNode.restore_ancestry_integrity!
380
+ $ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
381
+ ```
382
+
383
+ Additionally, if you think something is wrong with your depth cache:
384
+
385
+ ```
386
+ >> TreeNode.rebuild_depth_cache!
387
+ ```
388
+
389
+ # Running Tests
390
+
391
+ ```bash
392
+ git clone git@github.com:stefankroes/ancestry.git
393
+ cd ancestry
394
+ cp test/database.example.yml test/database.yml
395
+ bundle
396
+ appraisal install
397
+ # all tests
398
+ appraisal rake test
399
+ # single test version (sqlite and rails 5.0)
400
+ appraisal sqlite3-ar-50 rake test
401
+ ```
402
+
403
+ # Internals
404
+
405
+ Ancestry stores a path from the root to the parent for every node.
406
+ This is a variation on the materialised path database pattern.
407
+ It allows Ancestry to fetch any relation (siblings,
408
+ descendants, etc.) in a single SQL query without the complicated algorithms
409
+ and incomprehensibility associated with left and right values. Additionally,
410
+ any inserts, deletes and updates only affect nodes within the affected node's
411
+ own subtree.
412
+
413
+ In the example above, the `ancestry` column is created as a `string`. This puts a
414
+ limitation on the depth of the tree of about 40 or 50 levels. To increase the
415
+ maximum depth of the tree, increase the size of the `string` or use `text` to
416
+ remove the limitation entirely. Changing it to a text will however decrease
417
+ performance because an index cannot be put on the column in that case.
418
+
419
+ The materialised path pattern requires Ancestry to use a 'like' condition in
420
+ order to fetch descendants. The wild character (`%`) is on the left of the
421
+ query, so indexes should be used.
422
+
423
+ # Contributing and license
424
+
425
+ Question? Bug report? Faulty/incomplete documentation? Feature request? Please
426
+ post an issue on 'http://github.com/stefankroes/ancestry/issues'. Make sure
427
+ you have read the documentation and you have included tests and documentation
428
+ with any pull request.
429
+
430
+ Copyright (c) 2016 Stefan Kroes, released under the MIT license
@@ -17,8 +17,8 @@ EOF
17
17
 
18
18
  s.version = Ancestry::VERSION
19
19
 
20
- s.author = 'Stefan Kroes'
21
- s.email = 's.a.kroes@gmail.com'
20
+ s.authors = ['Stefan Kroes', 'Keenan Brock']
21
+ s.email = 'keenan@thebrocks.net'
22
22
  s.homepage = 'http://github.com/stefankroes/ancestry'
23
23
  s.license = 'MIT'
24
24
 
@@ -31,12 +31,13 @@ EOF
31
31
  'lib/ancestry/exceptions.rb',
32
32
  'lib/ancestry/class_methods.rb',
33
33
  'lib/ancestry/instance_methods.rb',
34
+ 'lib/ancestry/materialized_path.rb',
34
35
  'MIT-LICENSE',
35
- 'README.rdoc'
36
+ 'README.md'
36
37
  ]
37
38
 
38
39
  s.required_ruby_version = '>= 1.8.7'
39
- s.add_runtime_dependency 'activerecord', '>= 3.0.0'
40
+ s.add_runtime_dependency 'activerecord', '>= 3.2.0'
40
41
  s.add_development_dependency 'yard'
41
42
  s.add_development_dependency 'rake', '~> 10.0'
42
43
  s.add_development_dependency 'test-unit'