ancestry 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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=
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Stefan Kroes
1
+ Copyright (c) 2013 Stefan Kroes
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -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 childen, false otherwise
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.If the deleted node is Root, then rootify the orphan subtree.
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 1800 assertions in about 30 tests. It takes about 10 seconds to run on sqlite. To run it yourself:
228
- - check out the repository from GitHub
229
- - copy test/database.example.yml to test/database.yml
230
- - run <tt>bundle</tt>
231
- - run <tt>rake [test]</tt>
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 pass an environment variable for the database to test against (e.g. db=mysql). By default, the test suite runs against the latest activerecord version. You can run agains activerecord 3-0 or 3-1 as follows:
234
- - run <tt>bundle --gemfile Gemfile.rails-<version></tt>
235
- - run <tt>rake [test] BUNDLE_GEMFILE=Gemfile.rails-<version></tt>
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
- = Version history
249
-
250
- The latest version of ancestry is recommended. The three numbers of each version numbers are respectively the major, minor and patch versions. We started with major version 1 because it looks so much better and ancestry was already quite mature and complete when it was published. The major version is only bumped when backwards compatibility is broken. The minor version is bumped when new features are added. The patch version is bumped when bugs are fixed.
251
-
252
- - Version 2.0.0 (2013-05-17)
253
- - Removed rails 2 compatibility
254
- - Added table name to condition constructing methods (thx aflatter)
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
@@ -1,13 +1,22 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'ancestry'
3
- s.description = 'Organise ActiveRecord model into a tree structure'
4
- s.summary = 'Ancestry allows the records of a ActiveRecord model to be organised in a tree structure, using a single, intuitively formatted database column. 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 named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.'
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.0.0'
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',
@@ -1,7 +1,7 @@
1
- require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/class_methods')
2
- require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/instance_methods')
3
- require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/exceptions')
4
- require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/has_ancestry')
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
- # Pseudo-preordered array of nodes. Children will always follow parents,
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.unscoped do
181
- self.ancestry_base_class.find_each do |node|
182
- node.update_attribute depth_cache_column, node.depth
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
- end
73
-
74
- # Alias has_ancestry with acts_as_tree, if it's available.
75
- if !defined?(ActsAsTree)
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).to_s.split('/').map { |id| cast_primary_key(id) }
105
+ parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
82
106
  end
83
107
 
84
108
  def ancestor_conditions
85
- {primary_key_with_table => ancestor_ids}
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 ancestor_conditions
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
- {primary_key_with_table => path_ids}
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 path_conditions
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
- {ancestry_column_with_table => child_ancestry}
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
- {ancestry_column_with_table => read_attribute(self.ancestry_base_class.ancestry_column)}
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
- ["#{ancestry_column_with_table} like ? or #{ancestry_column_with_table} = ?", "#{child_ancestry}/%", child_ancestry]
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
- ["#{primary_key_with_table} = ? or #{ancestry_column_with_table} like ? or #{ancestry_column_with_table} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
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 primary_key_type == :string
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
- # basically validates the ancestry, but also applied if validation is
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 primary_key_with_table
256
- "#{self.ancestry_base_class.table_name}.#{self.ancestry_base_class.primary_key}"
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 ancestry_column_with_table
260
- "#{self.ancestry_base_class.table_name}.#{self.ancestry_base_class.ancestry_column}"
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.0.0
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: 2013-05-17 00:00:00.000000000 Z
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: Organise ActiveRecord model into a tree structure
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.8.25
69
+ rubygems_version: 2.1.11
67
70
  signing_key:
68
- specification_version: 3
69
- summary: Ancestry allows the records of a ActiveRecord model to be organised in a
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: