ancestry 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +49 -116
- data/ancestry.gemspec +12 -3
- data/lib/ancestry.rb +4 -4
- data/lib/ancestry/class_methods.rb +33 -23
- data/lib/ancestry/has_ancestry.rb +22 -11
- data/lib/ancestry/instance_methods.rb +69 -16
- metadata +15 -17
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NzllMWZkZTQwYWVlNTVlZTdiYzJiMWMxZGZhYTY1YjlkYzNiMzM4Yw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZWRkMWYxOWZhOTJiNjU2MTE2ZTcyYWIwM2FlNmVmNjk0NjBkZGE2ZQ==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZDg4NTk4ODJiZmQ5NjAxMThkMDNhNjg5NTk3ZTJkN2IzN2QxMTY5OWIwNTgz
|
10
|
+
OTg2MzVjY2Q3MWEyNTEzZDUzNjc4OTA5NWVhNTFiNWJjYjFiMTQxNjhkYzM5
|
11
|
+
ODNlY2Y0MmIwZDc2ODQ4YTRiNmYxMWMwOTJiNzE0NDZmMDlhZGQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
N2E1ZjAwY2M1YjkzZTBlNWRkNDQ0ZTVlMDM3OTg3YmE0ZDgwMzNlNTdjOTFh
|
14
|
+
Y2NkYWY0NGUwNDFjZWI3OTZkMTAwMTBmNDI4OWZlOGQyMDRiZmQxOTgyOWFi
|
15
|
+
ZGFmYzk1YTExNjFhYTczNzIxMGE2NGVlZTA4ZGMwYWViMjU3NGE=
|
data/MIT-LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
{<img src="https://travis-ci.org/stefankroes/ancestry.png?branch=master" alt="Build Status" />}[https://travis-ci.org/stefankroes/ancestry]
|
2
|
+
{<img src="https://coveralls.io/repos/stefankroes/ancestry/badge.png" alt="Coverage Status" />}[https://coveralls.io/r/stefankroes/ancestry]
|
3
|
+
|
1
4
|
= Ancestry
|
2
5
|
|
3
6
|
Ancestry is a gem/plugin that allows the records of a Ruby on Rails ActiveRecord model to be organised as a tree structure (or hierarchy). It uses a single, intuitively formatted database column, using a variation on the materialised path pattern. It exposes all the standard tree structure relations (ancestors, parent, root, children, siblings, descendants) and all of them can be fetched in a single SQL query. Additional features are STI support, scopes, depth caching, depth constraints, easy migration from older plugins/gems, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.
|
@@ -53,7 +56,7 @@ To navigate an Ancestry model, use the following methods on any instance / recor
|
|
53
56
|
children Scopes the model on children of the record
|
54
57
|
child_ids Returns a list of child ids
|
55
58
|
has_children? Returns true if the record has any children, false otherwise
|
56
|
-
is_childless? Returns true is the record has no
|
59
|
+
is_childless? Returns true is the record has no children, false otherwise
|
57
60
|
siblings Scopes the model on siblings of the record, the record itself is included
|
58
61
|
sibling_ids Returns a list of sibling ids
|
59
62
|
has_siblings? Returns true if the record's parent has more than one child
|
@@ -64,7 +67,7 @@ To navigate an Ancestry model, use the following methods on any instance / recor
|
|
64
67
|
subtree_ids Returns a list of all ids in the record's subtree
|
65
68
|
depth Return the depth of the node, root nodes are at depth 0
|
66
69
|
|
67
|
-
= Options for has_ancestry
|
70
|
+
= Options for has_ancestry
|
68
71
|
|
69
72
|
The has_ancestry methods supports the following options:
|
70
73
|
|
@@ -73,7 +76,8 @@ The has_ancestry methods supports the following options:
|
|
73
76
|
:destroy All children are destroyed as well (default)
|
74
77
|
:rootify The children of the destroyed node become root nodes
|
75
78
|
:restrict An AncestryException is raised if any children exist
|
76
|
-
:adopt The orphan subtree is added to the parent of the deleted node.
|
79
|
+
:adopt The orphan subtree is added to the parent of the deleted node.
|
80
|
+
If the deleted node is Root, then rootify the orphan subtree.
|
77
81
|
:cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
|
78
82
|
If you turn depth_caching on for an existing model:
|
79
83
|
- Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
|
@@ -81,6 +85,8 @@ The has_ancestry methods supports the following options:
|
|
81
85
|
:depth_cache_column Pass in a symbol to store depth cache in a different column
|
82
86
|
:primary_key_format Supply a regular expression that matches the format of your primary key.
|
83
87
|
By default, primary keys only match integers ([0-9]+).
|
88
|
+
:touch Instruct Ancestry to touch the ancestors of a node when it changes, to
|
89
|
+
invalidate nested key-based caches. (default: false)
|
84
90
|
|
85
91
|
= (Named) Scopes
|
86
92
|
|
@@ -115,7 +121,7 @@ When depth caching is enabled (see has_ancestry options), five more named scopes
|
|
115
121
|
at_depth(depth) Return nodes that are at depth (node.depth == depth)
|
116
122
|
from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
|
117
123
|
after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
|
118
|
-
|
124
|
+
|
119
125
|
The depth scopes are also available through calls to descendants, descendant_ids, subtree, subtree_ids, path and ancestors. In this case, depth values are interpreted relatively. Some examples:
|
120
126
|
|
121
127
|
node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
|
@@ -156,10 +162,30 @@ The arrange method takes ActiveRecord find options. If you want your hashes to b
|
|
156
162
|
|
157
163
|
TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
|
158
164
|
|
165
|
+
To get the arranged nodes as a nested array of hashes for serialization:
|
166
|
+
|
167
|
+
TreeNode.arrange_serializable
|
168
|
+
|
169
|
+
[
|
170
|
+
{
|
171
|
+
"ancestry" => nil, "id" => 1, "children" => [
|
172
|
+
{ "ancestry" => "1", "id" => 2, "children" => [] }
|
173
|
+
]
|
174
|
+
}
|
175
|
+
]
|
176
|
+
|
177
|
+
The result of arrange_serializable can easily be serialized to json with 'to_json', or some other format:
|
178
|
+
|
179
|
+
TreeNode.arrange_serializable.to_json
|
180
|
+
|
181
|
+
You can also pass the order to the arrange_serializable method just as you can pass it to the arrange method:
|
182
|
+
|
183
|
+
TreeNode.arrange_serializable(:order => :name)
|
184
|
+
|
159
185
|
= Sorting
|
160
186
|
|
161
187
|
If you just want to sort an array of nodes as if you were traversing them in preorder, you can use the sort_by_ancestry class method:
|
162
|
-
|
188
|
+
|
163
189
|
TreeNode.sort_by_ancestry(array_of_nodes)
|
164
190
|
|
165
191
|
Note that since materialised path trees don't support ordering within a rank, the order of siblings depends on their order in the original array.
|
@@ -178,7 +204,7 @@ Most current tree plugins use a parent_id column (has_ancestry, awesome_nested_s
|
|
178
204
|
- Remove gem config line from environment.rb: config.gem [old gem]
|
179
205
|
- Add Ancestry to environment.rb: config.gem :ancestry
|
180
206
|
- See 'Installation' for more info on installing and configuring gems
|
181
|
-
|
207
|
+
|
182
208
|
3. Change your model
|
183
209
|
- Remove any macros required by old plugin/gem from app/models/[model].rb
|
184
210
|
- Add to app/models/[model].rb: <b>has_ancestry</b>
|
@@ -199,7 +225,7 @@ Most current tree plugins use a parent_id column (has_ancestry, awesome_nested_s
|
|
199
225
|
|
200
226
|
= Integrity checking and restoration
|
201
227
|
|
202
|
-
I don't see any way Ancestry tree integrity could get compromised without explicitly setting cyclic parents or invalid ancestry and circumventing validation with update_attribute, if you do, please let me know.
|
228
|
+
I don't see any way Ancestry tree integrity could get compromised without explicitly setting cyclic parents or invalid ancestry and circumventing validation with update_attribute, if you do, please let me know.
|
203
229
|
|
204
230
|
Ancestry includes some methods for detecting integrity problems and restoring integrity just to be sure. To check integrity use: [Model].check_ancestry_integrity!. An AncestryIntegrityException will be raised if there are any problems. You can also specify :report => :list to return an array of exceptions or :report => :echo to echo any error messages. To restore integrity use: [Model].restore_ancestry_integrity!.
|
205
231
|
|
@@ -224,18 +250,16 @@ Additionally, if you think something is wrong with your depth cache:
|
|
224
250
|
|
225
251
|
= Tests
|
226
252
|
|
227
|
-
The Ancestry gem comes with a unit test suite consisting of about
|
228
|
-
-
|
229
|
-
-
|
230
|
-
-
|
231
|
-
-
|
253
|
+
The Ancestry gem comes with a unit test suite consisting of about 1900 assertions in about 50 tests. It takes about 10 seconds to run on sqlite. It is run against three databases (sqlite3, mysql and postgresql) and four versions of Activerecord (3.0, 3.1, 3.2 and 4.0) using Appraisals. To run it yourself:
|
254
|
+
- Check out the repository from GitHub
|
255
|
+
- Copy test/database.example.yml to test/database.yml
|
256
|
+
- Run <tt>bundle</tt>
|
257
|
+
- Run <tt>appraisal install</tt>
|
258
|
+
- Run <tt>appraisal rake test</tt>
|
232
259
|
|
233
|
-
You can
|
234
|
-
-
|
235
|
-
-
|
236
|
-
|
237
|
-
To run the test suite multiple times against several databases and all supported activerecord versions, run <tt>rake test_all</tt>
|
238
|
-
The test suite is located in test/has_ancestry_test.rb.
|
260
|
+
You can also run against a specific database and specific version of Activerecord:
|
261
|
+
- Run the above commands, except for the last one
|
262
|
+
- Run <tt>appraisal sqlite3-ar-32 rake test</tt> (to test against sqlite3 and Activerecord 3.2)
|
239
263
|
|
240
264
|
= Internals
|
241
265
|
|
@@ -245,101 +269,10 @@ In the example above, the ancestry column is created as a string. This puts a li
|
|
245
269
|
|
246
270
|
The materialised path pattern requires Ancestry to use a 'like' condition in order to fetch descendants. This should not be particularly slow however since the the condition never starts with a wildcard which allows the DBMS to use the column index. If you have any data on performance with a large number of records, please drop me line.
|
247
271
|
|
248
|
-
=
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
- Fix depth_cache not being updated when moving up to ancestors (thx scottatron)
|
256
|
-
- add alias :root? to existing is_root? (thx divineforest)
|
257
|
-
- Add block to sort_by_ancestry (thx Iliya)
|
258
|
-
- Add attribute query method for parent_id (thx sj26)
|
259
|
-
- Fixed and tested for rails 4 (thx adammck, Nihad, Systho, Philippe, e.a.)
|
260
|
-
- Fixed overwriting ActiveRecord::Base.base_class (thx Rozhnov)
|
261
|
-
- New adopt strategy (thx unknown)
|
262
|
-
- Many more improvements
|
263
|
-
- Version 1.3.0 (2012-05-04)
|
264
|
-
- Ancestry now ignores default scopes when moving or destroying nodes, ensuring tree consistency
|
265
|
-
- Changed ActiveRecord dependency to 2.3.14
|
266
|
-
- Version 1.2.5 (2012-03-15)
|
267
|
-
- Fixed warnings: "parenthesize argument(s) for future version"
|
268
|
-
- Fixed a bug in the restore_ancestry_integrity! method (thx Arthur Holstvoogd)
|
269
|
-
- Version 1.2.4 (2011-04-22)
|
270
|
-
- Prepended table names to column names in queries (thx raelik)
|
271
|
-
- Better check to see if acts_as_tree can be overloaded (thx jims)
|
272
|
-
- Performance inprovements (thx kueda)
|
273
|
-
- Version 1.2.3 (2010-10-28)
|
274
|
-
- Fixed error with determining ActiveRecord version
|
275
|
-
- Added option to specify :primary_key_format (thanks goes to rolftimmermans)
|
276
|
-
- Version 1.2.2 (2010-10-24)
|
277
|
-
- Fixed all deprecation warnings for rails 3.0.X
|
278
|
-
- Added :report option to check_ancestry_integrity!
|
279
|
-
- Changed ActiveRecord dependency to 2.2.2
|
280
|
-
- Tested and fixed for ruby 1.8.7 and 1.9.2
|
281
|
-
- Changed usage of update_attributes to update_attribute to allow ancestry column protection
|
282
|
-
- Version 1.2.0 (2009-11-07)
|
283
|
-
- Removed some duplication in has_ancestry
|
284
|
-
- Cleaned up plugin pattern according to http://yehudakatz.com/2009/11/12/better-ruby-idioms/
|
285
|
-
- Moved parts of ancestry into seperate files
|
286
|
-
- Made it possible to pass options into the arrange method
|
287
|
-
- Renamed acts_as_tree to has_ancestry
|
288
|
-
- Aliased has_ancestry as acts_as_tree if acts_as_tree is available
|
289
|
-
- Added subtree_of scope
|
290
|
-
- Updated ordered_by_ancestry scope to support Microsoft SQL Server
|
291
|
-
- Added empty hash as parameter to exists? calls for older ActiveRecord versions
|
292
|
-
- Version 1.1.4 (2009-11-07)
|
293
|
-
- Thanks to a patch from tom taylor, Ancestry now works with different primary keys
|
294
|
-
- Version 1.1.3 (2009-11-01)
|
295
|
-
- Fixed a pretty bad bug where several operations took far too many queries
|
296
|
-
- Version 1.1.2 (2009-10-29)
|
297
|
-
- Added validation for depth cache column
|
298
|
-
- Added STI support (reported broken)
|
299
|
-
- Version 1.1.1 (2009-10-28)
|
300
|
-
- Fixed some parentheses warnings that where reported
|
301
|
-
- Fixed a reported issue with arrangement
|
302
|
-
- Fixed issues with ancestors and path order on postgres
|
303
|
-
- Added ordered_by_ancestry scope (needed to fix issues)
|
304
|
-
- Version 1.1.0 (2009-10-22)
|
305
|
-
- Depth caching (and cache rebuilding)
|
306
|
-
- Depth method for nodes
|
307
|
-
- Named scopes for selecting by depth
|
308
|
-
- Relative depth options for tree navigation methods:
|
309
|
-
- ancestors
|
310
|
-
- path
|
311
|
-
- descendants
|
312
|
-
- descendant_ids
|
313
|
-
- subtree
|
314
|
-
- subtree_ids
|
315
|
-
- Updated README
|
316
|
-
- Easy migration from existing plugins/gems
|
317
|
-
- acts_as_tree checks unknown options
|
318
|
-
- acts_as_tree checks that options are hash
|
319
|
-
- Added a bang (!) to the integrity functions
|
320
|
-
- Since these functions should only be used from ./script/console and not from your application, this change is not considered as breaking backwards compatibility and the major version wasn't bumped.
|
321
|
-
- Updated install script to point to documentation
|
322
|
-
- Removed rails specific init
|
323
|
-
- Removed uninstall script
|
324
|
-
- Version 1.0.0 (2009-10-16)
|
325
|
-
- Initial version
|
326
|
-
- Tree building
|
327
|
-
- Tree navigation
|
328
|
-
- Integrity checking / restoration
|
329
|
-
- Arrangement
|
330
|
-
- Orphan strategies
|
331
|
-
- Subtree movement
|
332
|
-
- Named scopes
|
333
|
-
- Validations
|
334
|
-
|
335
|
-
= Future work
|
336
|
-
|
337
|
-
I will try to keep Ancestry up to date with changing versions of Rails and Ruby and also with any bug reports I might receive. I will implement new features on request as I see fit. One thing I definitely want to do soon is some proper performance testing.
|
338
|
-
|
339
|
-
= Contact and copyright
|
340
|
-
|
341
|
-
Bug report? Faulty/incomplete documentation? Feature request? Please post an issue on 'http://github.com/stefankroes/ancestry/issues'. Please also contact me at s.a.kroes[at]gmail.com if it's urgent.
|
342
|
-
|
343
|
-
Question? Contact me at s.a.kroes[at]gmail.com, make sure you read the documentation.
|
344
|
-
|
345
|
-
Copyright (c) 2009 Stefan Kroes, released under the MIT license
|
272
|
+
= Contributing and licence
|
273
|
+
|
274
|
+
I will try to keep Ancestry up to date with changing versions of Rails and Ruby and also with any bug reports I might receive. I will implement new features on request as I see fit and have time.
|
275
|
+
|
276
|
+
Question? Bug report? Faulty/incomplete documentation? Feature request? Please post an issue on 'http://github.com/stefankroes/ancestry/issues'. Make sure you have read the documentation and you have included tests and documentation with any pull request.
|
277
|
+
|
278
|
+
Copyright (c) 2013 Stefan Kroes, released under the MIT license
|
data/ancestry.gemspec
CHANGED
@@ -1,13 +1,22 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'ancestry'
|
3
|
-
s.
|
4
|
-
s.
|
3
|
+
s.summary = 'Organize ActiveRecord model into a tree structure'
|
4
|
+
s.description = <<-EOF
|
5
|
+
Ancestry allows the records of a ActiveRecord model to be organized in a tree
|
6
|
+
structure, using a single, intuitively formatted database column. It exposes
|
7
|
+
all the standard tree structure relations (ancestors, parent, root, children,
|
8
|
+
siblings, descendants) and all of them can be fetched in a single sql query.
|
9
|
+
Additional features are named_scopes, integrity checking, integrity restoration,
|
10
|
+
arrangement of (sub)tree into hashes and different strategies for dealing with
|
11
|
+
orphaned records.
|
12
|
+
EOF
|
5
13
|
|
6
|
-
s.version = '2.
|
14
|
+
s.version = '2.1.0'
|
7
15
|
|
8
16
|
s.author = 'Stefan Kroes'
|
9
17
|
s.email = 's.a.kroes@gmail.com'
|
10
18
|
s.homepage = 'http://github.com/stefankroes/ancestry'
|
19
|
+
s.license = 'MIT'
|
11
20
|
|
12
21
|
s.files = [
|
13
22
|
'ancestry.gemspec',
|
data/lib/ancestry.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
require_relative 'ancestry/class_methods'
|
2
|
+
require_relative 'ancestry/instance_methods'
|
3
|
+
require_relative 'ancestry/exceptions'
|
4
|
+
require_relative 'ancestry/has_ancestry'
|
5
5
|
|
6
6
|
module Ancestry
|
7
7
|
ANCESTRY_PATTERN = /\A[0-9]+(\/[0-9]+)*\Z/
|
@@ -3,8 +3,8 @@ module Ancestry
|
|
3
3
|
# Fetch tree node if necessary
|
4
4
|
def to_node object
|
5
5
|
if object.is_a?(self.ancestry_base_class) then object else find(object) end
|
6
|
-
end
|
7
|
-
|
6
|
+
end
|
7
|
+
|
8
8
|
# Scope on relative depth options
|
9
9
|
def scope_depth depth_options, depth
|
10
10
|
depth_options.inject(self.ancestry_base_class) do |scope, option|
|
@@ -26,7 +26,7 @@ module Ancestry
|
|
26
26
|
raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify,:adopt, :restrict and :destroy.")
|
27
27
|
end
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
# Arrangement
|
31
31
|
def arrange options = {}
|
32
32
|
scope =
|
@@ -38,8 +38,8 @@ module Ancestry
|
|
38
38
|
# Get all nodes ordered by ancestry and start sorting them into an empty hash
|
39
39
|
arrange_nodes scope.where(options)
|
40
40
|
end
|
41
|
-
|
42
|
-
# Arrange array of nodes into a nested hash of the form
|
41
|
+
|
42
|
+
# Arrange array of nodes into a nested hash of the form
|
43
43
|
# {node => children}, where children = {} if the node has no children
|
44
44
|
def arrange_nodes(nodes)
|
45
45
|
# Get all nodes ordered by ancestry and start sorting them into an empty hash
|
@@ -55,26 +55,34 @@ module Ancestry
|
|
55
55
|
arranged_nodes
|
56
56
|
end
|
57
57
|
end
|
58
|
-
|
59
|
-
|
58
|
+
|
59
|
+
# Arrangement to nested array
|
60
|
+
def arrange_serializable options={}, nodes=nil
|
61
|
+
nodes = arrange(options) if nodes.nil?
|
62
|
+
nodes.map do |parent, children|
|
63
|
+
parent.serializable_hash.merge 'children' => arrange_serializable(options, children)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Pseudo-preordered array of nodes. Children will always follow parents,
|
60
68
|
# for ordering nodes within a rank provide block, eg. Node.sort_by_ancestry(Node.all) {|a, b| a.rank <=> b.rank}.
|
61
69
|
def sort_by_ancestry(nodes, &block)
|
62
70
|
arranged = nodes if nodes.is_a?(Hash)
|
63
|
-
|
71
|
+
|
64
72
|
unless arranged
|
65
73
|
presorted_nodes = nodes.sort do |a, b|
|
66
74
|
a_cestry, b_cestry = a.ancestry || '0', b.ancestry || '0'
|
67
|
-
|
75
|
+
|
68
76
|
if block_given? && a_cestry == b_cestry
|
69
77
|
yield a, b
|
70
78
|
else
|
71
79
|
a_cestry <=> b_cestry
|
72
80
|
end
|
73
81
|
end
|
74
|
-
|
82
|
+
|
75
83
|
arranged = arrange_nodes(presorted_nodes)
|
76
84
|
end
|
77
|
-
|
85
|
+
|
78
86
|
arranged.inject([]) do |sorted_nodes, pair|
|
79
87
|
node, children = pair
|
80
88
|
sorted_nodes << node
|
@@ -87,8 +95,8 @@ module Ancestry
|
|
87
95
|
def check_ancestry_integrity! options = {}
|
88
96
|
parents = {}
|
89
97
|
exceptions = [] if options[:report] == :list
|
90
|
-
|
91
|
-
self.ancestry_base_class.unscoped do
|
98
|
+
|
99
|
+
self.ancestry_base_class.unscoped do
|
92
100
|
# For each node ...
|
93
101
|
self.ancestry_base_class.find_each do |node|
|
94
102
|
begin
|
@@ -126,7 +134,7 @@ module Ancestry
|
|
126
134
|
parents = {}
|
127
135
|
# Wrap the whole thing in a transaction ...
|
128
136
|
self.ancestry_base_class.transaction do
|
129
|
-
self.ancestry_base_class.unscoped do
|
137
|
+
self.ancestry_base_class.unscoped do
|
130
138
|
# For each node ...
|
131
139
|
self.ancestry_base_class.find_each do |node|
|
132
140
|
# ... set its ancestry to nil if invalid
|
@@ -143,9 +151,9 @@ module Ancestry
|
|
143
151
|
until parent.nil? || parent == node.id
|
144
152
|
parent = parents[parent]
|
145
153
|
end
|
146
|
-
parents[node.id] = nil if parent == node.id
|
154
|
+
parents[node.id] = nil if parent == node.id
|
147
155
|
end
|
148
|
-
|
156
|
+
|
149
157
|
# For each node ...
|
150
158
|
self.ancestry_base_class.find_each do |node|
|
151
159
|
# ... rebuild ancestry from parents array
|
@@ -160,10 +168,10 @@ module Ancestry
|
|
160
168
|
end
|
161
169
|
end
|
162
170
|
end
|
163
|
-
|
171
|
+
|
164
172
|
# Build ancestry from parent id's for migration purposes
|
165
173
|
def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
|
166
|
-
self.ancestry_base_class.unscoped do
|
174
|
+
self.ancestry_base_class.unscoped do
|
167
175
|
self.ancestry_base_class.where(:parent_id => parent_id).find_each do |node|
|
168
176
|
node.without_ancestry_callbacks do
|
169
177
|
node.update_attribute ancestry_column, ancestry
|
@@ -172,14 +180,16 @@ module Ancestry
|
|
172
180
|
end
|
173
181
|
end
|
174
182
|
end
|
175
|
-
|
183
|
+
|
176
184
|
# Rebuild depth cache if it got corrupted or if depth caching was just turned on
|
177
185
|
def rebuild_depth_cache!
|
178
186
|
raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column
|
179
|
-
|
180
|
-
self.ancestry_base_class.
|
181
|
-
self.ancestry_base_class.
|
182
|
-
|
187
|
+
|
188
|
+
self.ancestry_base_class.transaction do
|
189
|
+
self.ancestry_base_class.unscoped do
|
190
|
+
self.ancestry_base_class.find_each do |node|
|
191
|
+
node.update_attribute depth_cache_column, node.depth
|
192
|
+
end
|
183
193
|
end
|
184
194
|
end
|
185
195
|
end
|
@@ -3,11 +3,11 @@ class << ActiveRecord::Base
|
|
3
3
|
# Check options
|
4
4
|
raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash
|
5
5
|
options.each do |key, value|
|
6
|
-
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column].include? key
|
6
|
+
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch].include? key
|
7
7
|
raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.")
|
8
8
|
end
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
# Include instance methods
|
12
12
|
include Ancestry::InstanceMethods
|
13
13
|
|
@@ -25,13 +25,17 @@ class << ActiveRecord::Base
|
|
25
25
|
# Save self as base class (for STI)
|
26
26
|
cattr_accessor :ancestry_base_class
|
27
27
|
self.ancestry_base_class = self
|
28
|
-
|
28
|
+
|
29
|
+
# Touch ancestors after updating
|
30
|
+
cattr_accessor :touch_ancestors
|
31
|
+
self.touch_ancestors = options[:touch] || false
|
32
|
+
|
29
33
|
# Validate format of ancestry column value
|
30
34
|
validates_format_of ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true
|
31
35
|
|
32
36
|
# Validate that the ancestor ids don't include own id
|
33
37
|
validate :ancestry_exclude_self
|
34
|
-
|
38
|
+
|
35
39
|
# Named scopes
|
36
40
|
scope :roots, lambda { where(ancestry_column => nil) }
|
37
41
|
scope :ancestors_of, lambda { |object| where(to_node(object).ancestor_conditions) }
|
@@ -41,7 +45,7 @@ class << ActiveRecord::Base
|
|
41
45
|
scope :siblings_of, lambda { |object| where(to_node(object).sibling_conditions) }
|
42
46
|
scope :ordered_by_ancestry, lambda { reorder("(case when #{table_name}.#{ancestry_column} is null then 0 else 1 end), #{table_name}.#{ancestry_column}") }
|
43
47
|
scope :ordered_by_ancestry_and, lambda { |order| reorder("(case when #{table_name}.#{ancestry_column} is null then 0 else 1 end), #{table_name}.#{ancestry_column}, #{order}") }
|
44
|
-
|
48
|
+
|
45
49
|
# Update descendants with new ancestry before save
|
46
50
|
before_save :update_descendants_with_new_ancestry
|
47
51
|
|
@@ -61,7 +65,7 @@ class << ActiveRecord::Base
|
|
61
65
|
# Validate depth column
|
62
66
|
validates_numericality_of depth_cache_column, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
|
63
67
|
end
|
64
|
-
|
68
|
+
|
65
69
|
# Create named scopes for depth
|
66
70
|
{:before_depth => '<', :to_depth => '<=', :at_depth => '=', :from_depth => '>=', :after_depth => '>'}.each do |scope_name, operator|
|
67
71
|
scope scope_name, lambda { |depth|
|
@@ -69,10 +73,17 @@ class << ActiveRecord::Base
|
|
69
73
|
where("#{depth_cache_column} #{operator} ?", depth)
|
70
74
|
}
|
71
75
|
end
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
alias_method :acts_as_tree, :has_ancestry
|
76
|
+
|
77
|
+
after_save :touch_ancestors_callback
|
78
|
+
after_touch :touch_ancestors_callback
|
79
|
+
after_destroy :touch_ancestors_callback
|
77
80
|
end
|
78
81
|
end
|
82
|
+
|
83
|
+
ActiveSupport.on_load :active_record do
|
84
|
+
if not(ActiveRecord::Base.respond_to?(:acts_as_tree))
|
85
|
+
class << ActiveRecord::Base
|
86
|
+
alias_method :acts_as_tree, :has_ancestry
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -64,6 +64,25 @@ module Ancestry
|
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
|
+
# Touch each of this record's ancestors
|
68
|
+
def touch_ancestors_callback
|
69
|
+
|
70
|
+
# Skip this if callbacks are disabled
|
71
|
+
unless ancestry_callbacks_disabled?
|
72
|
+
|
73
|
+
# Only touch if the option is enabled
|
74
|
+
if self.ancestry_base_class.touch_ancestors
|
75
|
+
|
76
|
+
# Touch each of the old *and* new ancestors
|
77
|
+
self.class.where(id: (ancestor_ids + ancestor_ids_was).uniq).each do |ancestor|
|
78
|
+
ancestor.without_ancestry_callbacks do
|
79
|
+
ancestor.touch
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
67
86
|
# The ancestry value for this record's children
|
68
87
|
def child_ancestry
|
69
88
|
# New records cannot have children
|
@@ -73,20 +92,34 @@ module Ancestry
|
|
73
92
|
end
|
74
93
|
|
75
94
|
# Ancestors
|
95
|
+
|
76
96
|
def ancestry_changed?
|
77
97
|
changed.include?(self.ancestry_base_class.ancestry_column.to_s)
|
78
98
|
end
|
79
99
|
|
100
|
+
def parse_ancestry_column obj
|
101
|
+
obj.to_s.split('/').map { |id| cast_primary_key(id) }
|
102
|
+
end
|
103
|
+
|
80
104
|
def ancestor_ids
|
81
|
-
read_attribute(self.ancestry_base_class.ancestry_column)
|
105
|
+
parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
|
82
106
|
end
|
83
107
|
|
84
108
|
def ancestor_conditions
|
85
|
-
|
109
|
+
t = get_arel_table
|
110
|
+
t[get_primary_key_column].in(ancestor_ids)
|
86
111
|
end
|
87
112
|
|
88
113
|
def ancestors depth_options = {}
|
89
|
-
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where
|
114
|
+
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions
|
115
|
+
end
|
116
|
+
|
117
|
+
def ancestor_was_conditions
|
118
|
+
{primary_key_with_table => ancestor_ids_was}
|
119
|
+
end
|
120
|
+
|
121
|
+
def ancestor_ids_was
|
122
|
+
parse_ancestry_column(changed_attributes[self.ancestry_base_class.ancestry_column.to_s])
|
90
123
|
end
|
91
124
|
|
92
125
|
def path_ids
|
@@ -94,11 +127,12 @@ module Ancestry
|
|
94
127
|
end
|
95
128
|
|
96
129
|
def path_conditions
|
97
|
-
|
130
|
+
t = get_arel_table
|
131
|
+
t[get_primary_key_column].in(path_ids)
|
98
132
|
end
|
99
133
|
|
100
134
|
def path depth_options = {}
|
101
|
-
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where
|
135
|
+
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where path_conditions
|
102
136
|
end
|
103
137
|
|
104
138
|
def depth
|
@@ -110,6 +144,7 @@ module Ancestry
|
|
110
144
|
end
|
111
145
|
|
112
146
|
# Parent
|
147
|
+
|
113
148
|
def parent= parent
|
114
149
|
write_attribute(self.ancestry_base_class.ancestry_column, if parent.nil? then nil else parent.child_ancestry end)
|
115
150
|
end
|
@@ -131,6 +166,7 @@ module Ancestry
|
|
131
166
|
end
|
132
167
|
|
133
168
|
# Root
|
169
|
+
|
134
170
|
def root_id
|
135
171
|
if ancestor_ids.empty? then id else ancestor_ids.first end
|
136
172
|
end
|
@@ -145,8 +181,10 @@ module Ancestry
|
|
145
181
|
alias :root? :is_root?
|
146
182
|
|
147
183
|
# Children
|
184
|
+
|
148
185
|
def child_conditions
|
149
|
-
|
186
|
+
t = get_arel_table
|
187
|
+
t[get_ancestry_column].eq(child_ancestry)
|
150
188
|
end
|
151
189
|
|
152
190
|
def children
|
@@ -160,14 +198,18 @@ module Ancestry
|
|
160
198
|
def has_children?
|
161
199
|
self.children.exists?({})
|
162
200
|
end
|
201
|
+
alias_method :children?, :has_children?
|
163
202
|
|
164
203
|
def is_childless?
|
165
204
|
!has_children?
|
166
205
|
end
|
206
|
+
alias_method :childless?, :is_childless?
|
167
207
|
|
168
208
|
# Siblings
|
209
|
+
|
169
210
|
def sibling_conditions
|
170
|
-
|
211
|
+
t = get_arel_table
|
212
|
+
t[get_ancestry_column].eq(read_attribute(self.ancestry_base_class.ancestry_column))
|
171
213
|
end
|
172
214
|
|
173
215
|
def siblings
|
@@ -181,14 +223,18 @@ module Ancestry
|
|
181
223
|
def has_siblings?
|
182
224
|
self.siblings.count > 1
|
183
225
|
end
|
226
|
+
alias_method :siblings?, :has_siblings?
|
184
227
|
|
185
228
|
def is_only_child?
|
186
229
|
!has_siblings?
|
187
230
|
end
|
231
|
+
alias_method :only_child?, :is_only_child?
|
188
232
|
|
189
233
|
# Descendants
|
234
|
+
|
190
235
|
def descendant_conditions
|
191
|
-
|
236
|
+
t = get_arel_table
|
237
|
+
t[get_ancestry_column].matches("#{child_ancestry}/%").or(t[get_ancestry_column].eq(child_ancestry))
|
192
238
|
end
|
193
239
|
|
194
240
|
def descendants depth_options = {}
|
@@ -200,8 +246,10 @@ module Ancestry
|
|
200
246
|
end
|
201
247
|
|
202
248
|
# Subtree
|
249
|
+
|
203
250
|
def subtree_conditions
|
204
|
-
|
251
|
+
t = get_arel_table
|
252
|
+
t[get_primary_key_column].eq(self.id).or(t[get_ancestry_column].matches("#{child_ancestry}/%")).or(t[get_ancestry_column].eq(child_ancestry))
|
205
253
|
end
|
206
254
|
|
207
255
|
def subtree depth_options = {}
|
@@ -213,6 +261,7 @@ module Ancestry
|
|
213
261
|
end
|
214
262
|
|
215
263
|
# Callback disabling
|
264
|
+
|
216
265
|
def without_ancestry_callbacks
|
217
266
|
@disable_ancestry_callbacks = true
|
218
267
|
yield
|
@@ -226,7 +275,7 @@ module Ancestry
|
|
226
275
|
private
|
227
276
|
|
228
277
|
def cast_primary_key(key)
|
229
|
-
if
|
278
|
+
if [:string, :uuid].include? primary_key_type
|
230
279
|
key
|
231
280
|
else
|
232
281
|
key.to_i
|
@@ -236,14 +285,14 @@ module Ancestry
|
|
236
285
|
def primary_key_type
|
237
286
|
@primary_key_type ||= column_for_attribute(self.class.primary_key).type
|
238
287
|
end
|
288
|
+
|
239
289
|
def unscoped_descendants
|
240
290
|
self.ancestry_base_class.unscoped do
|
241
291
|
self.ancestry_base_class.where descendant_conditions
|
242
292
|
end
|
243
293
|
end
|
244
294
|
|
245
|
-
#
|
246
|
-
# bypassed to determine if chidren should be affected
|
295
|
+
# Validates the ancestry, but can also be applied if validation is bypassed to determine if chidren should be affected
|
247
296
|
def sane_ancestry?
|
248
297
|
ancestry.nil? || (ancestry.to_s =~ Ancestry::ANCESTRY_PATTERN && !ancestor_ids.include?(self.id))
|
249
298
|
end
|
@@ -252,12 +301,16 @@ module Ancestry
|
|
252
301
|
self.ancestry_base_class.unscoped { self.ancestry_base_class.find(id) }
|
253
302
|
end
|
254
303
|
|
255
|
-
def
|
256
|
-
|
304
|
+
def get_arel_table
|
305
|
+
self.ancestry_base_class.arel_table
|
306
|
+
end
|
307
|
+
|
308
|
+
def get_primary_key_column
|
309
|
+
self.ancestry_base_class.primary_key.to_sym
|
257
310
|
end
|
258
311
|
|
259
|
-
def
|
260
|
-
|
312
|
+
def get_ancestry_column
|
313
|
+
self.ancestry_base_class.ancestry_column.to_sym
|
261
314
|
end
|
262
315
|
end
|
263
316
|
end
|
metadata
CHANGED
@@ -1,20 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ancestry
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
5
|
-
prerelease:
|
4
|
+
version: 2.1.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Stefan Kroes
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2014-04-16 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: activerecord
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
17
|
- - ! '>='
|
20
18
|
- !ruby/object:Gem::Version
|
@@ -22,12 +20,17 @@ dependencies:
|
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
24
|
- - ! '>='
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: 3.0.0
|
30
|
-
description:
|
27
|
+
description: ! " Ancestry allows the records of a ActiveRecord model to be organized
|
28
|
+
in a tree\n structure, using a single, intuitively formatted database column. It
|
29
|
+
exposes\n all the standard tree structure relations (ancestors, parent, root, children,\n
|
30
|
+
\ siblings, descendants) and all of them can be fetched in a single sql query.\n
|
31
|
+
\ Additional features are named_scopes, integrity checking, integrity restoration,\n
|
32
|
+
\ arrangement of (sub)tree into hashes and different strategies for dealing with\n
|
33
|
+
\ orphaned records.\n"
|
31
34
|
email: s.a.kroes@gmail.com
|
32
35
|
executables: []
|
33
36
|
extensions: []
|
@@ -44,33 +47,28 @@ files:
|
|
44
47
|
- MIT-LICENSE
|
45
48
|
- README.rdoc
|
46
49
|
homepage: http://github.com/stefankroes/ancestry
|
47
|
-
licenses:
|
50
|
+
licenses:
|
51
|
+
- MIT
|
52
|
+
metadata: {}
|
48
53
|
post_install_message:
|
49
54
|
rdoc_options: []
|
50
55
|
require_paths:
|
51
56
|
- lib
|
52
57
|
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
-
none: false
|
54
58
|
requirements:
|
55
59
|
- - ! '>='
|
56
60
|
- !ruby/object:Gem::Version
|
57
61
|
version: '0'
|
58
62
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
-
none: false
|
60
63
|
requirements:
|
61
64
|
- - ! '>='
|
62
65
|
- !ruby/object:Gem::Version
|
63
66
|
version: '0'
|
64
67
|
requirements: []
|
65
68
|
rubyforge_project:
|
66
|
-
rubygems_version: 1.
|
69
|
+
rubygems_version: 2.1.11
|
67
70
|
signing_key:
|
68
|
-
specification_version:
|
69
|
-
summary:
|
70
|
-
tree structure, using a single, intuitively formatted database column. It exposes
|
71
|
-
all the standard tree structure relations (ancestors, parent, root, children, siblings,
|
72
|
-
descendants) and all of them can be fetched in a single sql query. Additional features
|
73
|
-
are named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree
|
74
|
-
into hashes and different strategies for dealing with orphaned records.
|
71
|
+
specification_version: 4
|
72
|
+
summary: Organize ActiveRecord model into a tree structure
|
75
73
|
test_files: []
|
76
74
|
has_rdoc:
|