ancestry 2.2.2 → 3.0.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
  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](https://travis-ci.org/stefankroes/ancestry.svg?branch=master)](https://travis-ci.org/stefankroes/ancestry) [![Coverage Status](https://coveralls.io/repos/stefankroes/ancestry/badge.svg)](https://coveralls.io/r/stefankroes/ancestry) [![Gitter](https://badges.gitter.im/Join+Chat.svg)](https://gitter.im/stefankroes/ancestry?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Security](https://hakiri.io/github/stefankroes/ancestry/master.svg)](https://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'