ancestry 2.2.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ module Ancestry
2
+ module MaterializedPath
3
+ def self.extended(base)
4
+ base.validates_format_of base.ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true
5
+ base.send(:include, InstanceMethods)
6
+ end
7
+
8
+ def root_conditions
9
+ arel_table[ancestry_column].eq(nil)
10
+ end
11
+
12
+ def ancestor_conditions(object)
13
+ t = arel_table
14
+ node = to_node(object)
15
+ t[primary_key].in(node.ancestor_ids)
16
+ end
17
+
18
+ def path_conditions(object)
19
+ t = arel_table
20
+ node = to_node(object)
21
+ t[primary_key].in(node.path_ids)
22
+ end
23
+
24
+ def child_conditions(object)
25
+ t = arel_table
26
+ node = to_node(object)
27
+ t[ancestry_column].eq(node.child_ancestry)
28
+ end
29
+
30
+ def descendant_conditions(object)
31
+ t = arel_table
32
+ node = to_node(object)
33
+ # rails has case sensitive matching.
34
+ if ActiveRecord::VERSION::MAJOR >= 5
35
+ t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true).or(t[ancestry_column].eq(node.child_ancestry))
36
+ else
37
+ t[ancestry_column].matches("#{node.child_ancestry}/%").or(t[ancestry_column].eq(node.child_ancestry))
38
+ end
39
+ end
40
+
41
+ def subtree_conditions(object)
42
+ t = arel_table
43
+ node = to_node(object)
44
+ descendant_conditions(object).or(t[primary_key].eq(node.id))
45
+ end
46
+
47
+ def sibling_conditions(object)
48
+ t = arel_table
49
+ node = to_node(object)
50
+ t[ancestry_column].eq(node[ancestry_column])
51
+ end
52
+
53
+ module InstanceMethods
54
+ # Validates the ancestry, but can also be applied if validation is bypassed to determine if children should be affected
55
+ def sane_ancestry?
56
+ ancestry_value = read_attribute(self.ancestry_base_class.ancestry_column)
57
+ ancestry_value.nil? || (ancestry_value.to_s =~ Ancestry::ANCESTRY_PATTERN && !ancestor_ids.include?(self.id))
58
+ end
59
+ end
60
+ end
61
+ end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ancestry
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.2
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Kroes
8
+ - Keenan Brock
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2016-11-01 00:00:00.000000000 Z
12
+ date: 2017-05-20 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: activerecord
@@ -16,14 +17,14 @@ dependencies:
16
17
  requirements:
17
18
  - - ">="
18
19
  - !ruby/object:Gem::Version
19
- version: 3.0.0
20
+ version: 3.2.0
20
21
  type: :runtime
21
22
  prerelease: false
22
23
  version_requirements: !ruby/object:Gem::Requirement
23
24
  requirements:
24
25
  - - ">="
25
26
  - !ruby/object:Gem::Version
26
- version: 3.0.0
27
+ version: 3.2.0
27
28
  - !ruby/object:Gem::Dependency
28
29
  name: yard
29
30
  requirement: !ruby/object:Gem::Requirement
@@ -102,13 +103,13 @@ description: |2
102
103
  Additional features are named_scopes, integrity checking, integrity restoration,
103
104
  arrangement of (sub)tree into hashes and different strategies for dealing with
104
105
  orphaned records.
105
- email: s.a.kroes@gmail.com
106
+ email: keenan@thebrocks.net
106
107
  executables: []
107
108
  extensions: []
108
109
  extra_rdoc_files: []
109
110
  files:
110
111
  - MIT-LICENSE
111
- - README.rdoc
112
+ - README.md
112
113
  - ancestry.gemspec
113
114
  - init.rb
114
115
  - install.rb
@@ -117,6 +118,7 @@ files:
117
118
  - lib/ancestry/exceptions.rb
118
119
  - lib/ancestry/has_ancestry.rb
119
120
  - lib/ancestry/instance_methods.rb
121
+ - lib/ancestry/materialized_path.rb
120
122
  homepage: http://github.com/stefankroes/ancestry
121
123
  licenses:
122
124
  - MIT
@@ -1,330 +0,0 @@
1
- {<img src="https://travis-ci.org/stefankroes/ancestry.svg?branch=master" alt="Build Status" />}[https://travis-ci.org/stefankroes/ancestry]
2
- {<img src="https://coveralls.io/repos/stefankroes/ancestry/badge.svg" alt="Coverage Status" />}[https://coveralls.io/r/stefankroes/ancestry]
3
- {<img src="https://badges.gitter.im/Join Chat.svg" alt="Gitter" />}[https://gitter.im/stefankroes/ancestry?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge]
4
- {<img src="https://hakiri.io/github/stefankroes/ancestry/master.svg" alt="Security" />}[https://hakiri.io/github/stefankroes/ancestry/master]
5
-
6
- = Ancestry
7
-
8
- 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.
9
-
10
- = Installation
11
-
12
- To apply Ancestry to any ActiveRecord model, follow these simple steps:
13
-
14
- == Install
15
- === Rails 2
16
- - See 1-3-stable branch
17
- === Rails 3 and 4
18
- - Add to Gemfile:
19
- # Gemfile
20
-
21
- gem 'ancestry'
22
- - Install required gems:
23
- $ bundle install
24
-
25
- == Add ancestry column to your table
26
- - Create migration:
27
- $ rails g migration add_ancestry_to_[table] ancestry:string
28
- - Add index to migration:
29
- # db/migrate/[date]_add_ancestry_to_[table].rb
30
-
31
- class AddAncestryTo[Table] < ActiveRecord::Migration
32
- # Rails 4 Syntax
33
- def change
34
- add_column [table], :ancestry, :string
35
- add_index [table], :ancestry
36
- end
37
-
38
- # Rails 3 Syntax
39
- def up
40
- add_column [table], :ancestry, :string
41
- add_index [table], :ancestry
42
- end
43
-
44
- def down
45
- remove_column [table], :ancestry
46
- remove_index [table], :ancestry
47
- end
48
-
49
- - Migrate your database:
50
- $ rake db:migrate
51
-
52
- == Add ancestry to your model
53
- - Add to app/models/[model].rb:
54
- # app/models/[model.rb]
55
-
56
- class [Model] < ActiveRecord::Base
57
- has_ancestry
58
- end
59
-
60
- Your model is now a tree!
61
-
62
- = Using acts_as_tree instead of has_ancestry
63
-
64
- In version 1.2.0 the <b>acts_as_tree</b> method was <b>renamed to has_ancestry</b> in order to allow usage of both the acts_as_tree gem and the ancestry gem in a single application. To not break backwards compatibility, the has_ancestry method is aliased with acts_as_tree if ActiveRecord::Base does not respond to acts_as_tree. acts_as_tree will continue to be supported in the future as I personally prefer it.
65
-
66
- = Organising records into a tree
67
-
68
- You can use the parent attribute to organise your records into a tree. If you have the id of the record you want to use as a parent and don't want to fetch it, you can also use parent_id. Like any virtual model attributes, parent and parent_id can be set using parent= and parent_id= on a record or by including them in the hash passed to new, create, create!, update_attributes and update_attributes!. For example:
69
-
70
- TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
71
-
72
- You can also create children through the children relation on a node:
73
-
74
- node.children.create :name => 'Stinky'
75
-
76
- = Navigating your tree
77
-
78
- To navigate an Ancestry model, use the following methods on any instance / record:
79
-
80
- parent Returns the parent of the record, nil for a root node
81
- parent_id Returns the id of the parent of the record, nil for a root node
82
- root Returns the root of the tree the record is in, self for a root node
83
- root_id Returns the id of the root of the tree the record is in
84
- root?, is_root? Returns true if the record is a root node, false otherwise
85
- ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
86
- ancestors Scopes the model on ancestors of the record
87
- path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
88
- path Scopes model on path records of the record
89
- children Scopes the model on children of the record
90
- child_ids Returns a list of child ids
91
- has_children? Returns true if the record has any children, false otherwise
92
- is_childless? Returns true is the record has no children, false otherwise
93
- siblings Scopes the model on siblings of the record, the record itself is included*
94
- sibling_ids Returns a list of sibling ids
95
- has_siblings? Returns true if the record's parent has more than one child
96
- is_only_child? Returns true if the record is the only child of its parent
97
- descendants Scopes the model on direct and indirect children of the record
98
- descendant_ids Returns a list of a descendant ids
99
- subtree Scopes the model on descendants and itself
100
- subtree_ids Returns a list of all ids in the record's subtree
101
- depth Return the depth of the node, root nodes are at depth 0
102
-
103
- * If the record is a root, other root records are considered siblings
104
-
105
- = Options for has_ancestry
106
-
107
- The has_ancestry methods supports the following options:
108
-
109
- :ancestry_column Pass in a symbol to store ancestry in a different column
110
- :orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
111
- :destroy All children are destroyed as well (default)
112
- :rootify The children of the destroyed node become root nodes
113
- :restrict An AncestryException is raised if any children exist
114
- :adopt The orphan subtree is added to the parent of the deleted node.
115
- If the deleted node is Root, then rootify the orphan subtree.
116
- :cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
117
- If you turn depth_caching on for an existing model:
118
- - Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
119
- - Build cache: TreeNode.rebuild_depth_cache!
120
- :depth_cache_column Pass in a symbol to store depth cache in a different column
121
- :primary_key_format Supply a regular expression that matches the format of your primary key.
122
- By default, primary keys only match integers ([0-9]+).
123
- :touch Instruct Ancestry to touch the ancestors of a node when it changes, to
124
- invalidate nested key-based caches. (default: false)
125
-
126
- = (Named) Scopes
127
-
128
- Where possible, the navigation methods return scopes instead of records, this means additional ordering, conditions, limits, etc. can be applied and that the result can be either retrieved, counted or checked for existence. For example:
129
-
130
- node.children.exists?(:name => 'Mary')
131
- node.subtree.all(:order => :name, :limit => 10).each do; ...; end
132
- node.descendants.count
133
-
134
- For convenience, a couple of named scopes are included at the class level:
135
-
136
- roots Root nodes
137
- ancestors_of(node) Ancestors of node, node can be either a record or an id
138
- children_of(node) Children of node, node can be either a record or an id
139
- descendants_of(node) Descendants of node, node can be either a record or an id
140
- subtree_of(node) Subtree of node, node can be either a record or an id
141
- siblings_of(node) Siblings of node, node can be either a record or an id
142
-
143
- Thanks to some convenient rails magic, it is even possible to create nodes through the children and siblings scopes:
144
-
145
- node.children.create
146
- node.siblings.create!
147
- TestNode.children_of(node_id).new
148
- TestNode.siblings_of(node_id).create
149
-
150
- = Selecting nodes by depth
151
-
152
- When depth caching is enabled (see has_ancestry options), five more named scopes can be used to select nodes on their depth:
153
-
154
- before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
155
- to_depth(depth) Return nodes up to a certain depth (node.depth <= depth)
156
- at_depth(depth) Return nodes that are at depth (node.depth == depth)
157
- from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
158
- after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
159
-
160
- 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:
161
-
162
- node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
163
- node.subtree.to_depth(5) Subtree of node to an absolute depth of 5
164
- node.descendants(:at_depth => 2) Descendant of node, at depth node.depth + 2 (grandchildren)
165
- node.descendants.at_depth(10) Descendants of node at an absolute depth of 10
166
- node.ancestors.to_depth(3) The oldest 4 ancestors of node (its root and 3 more)
167
- node.path(:from_depth => -2) The node's grandparent, parent and the node itself
168
-
169
- node.ancestors(:from_depth => -6, :to_depth => -4)
170
- node.path.from_depth(3).to_depth(4)
171
- node.descendants(:from_depth => 2, :to_depth => 4)
172
- node.subtree.from_depth(10).to_depth(12)
173
-
174
- Please note that depth constraints cannot be passed to ancestor_ids and path_ids. The reason for this is that both these relations can be fetched directly from the ancestry column without performing a database query. It would require an entirely different method of applying the depth constraints which isn't worth the effort of implementing. You can use ancestors(depth_options).map(&:id) or ancestor_ids.slice(min_depth..max_depth) instead.
175
-
176
- = STI support
177
-
178
- Ancestry works fine with STI. Just create a STI inheritance hierarchy and build an Ancestry tree from the different classes/models. All Ancestry relations that where described above will return nodes of any model type. If you do only want nodes of a specific subclass you'll have to add a condition on type for that.
179
-
180
- = Arrangement
181
-
182
- Ancestry can arrange an entire subtree into nested hashes for easy navigation after retrieval from the database. TreeNode.arrange could for example return:
183
-
184
- { #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
185
- => { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
186
- => { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
187
- => {}
188
- }
189
- }
190
- }
191
-
192
- The arrange method also works on a scoped class, for example:
193
-
194
- TreeNode.find_by_name('Crunchy').subtree.arrange
195
-
196
- The arrange method takes ActiveRecord find options. If you want your hashes to be ordered, you should pass the order to the arrange method instead of to the scope. This also works for Ruby 1.8 since an OrderedHash is returned. For example:
197
-
198
- TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
199
-
200
- To get the arranged nodes as a nested array of hashes for serialization:
201
-
202
- TreeNode.arrange_serializable
203
-
204
- [
205
- {
206
- "ancestry" => nil, "id" => 1, "children" => [
207
- { "ancestry" => "1", "id" => 2, "children" => [] }
208
- ]
209
- }
210
- ]
211
-
212
- You can also supply your own serialization logic using blocks:
213
-
214
- For example, using Active Model Serializers:
215
-
216
- TreeNode.arrange_serializable do |parent, children|
217
- MySerializer.new(parent, children: children)
218
- end
219
-
220
- Or plain hashes:
221
-
222
- TreeNode.arrange_serializable do |parent, children|
223
- {
224
- my_id: parent.id
225
- my_children: children
226
- }
227
- end
228
-
229
- The result of arrange_serializable can easily be serialized to json with 'to_json', or some other format:
230
-
231
- TreeNode.arrange_serializable.to_json
232
-
233
- You can also pass the order to the arrange_serializable method just as you can pass it to the arrange method:
234
-
235
- TreeNode.arrange_serializable(:order => :name)
236
-
237
- = Sorting
238
-
239
- 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:
240
-
241
- TreeNode.sort_by_ancestry(array_of_nodes)
242
-
243
- 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.
244
-
245
- = Migrating from plugin that uses parent_id column
246
-
247
- Most current tree plugins use a parent_id column (has_ancestry, awesome_nested_set, better_nested_set, acts_as_nested_set). With ancestry its easy to migrate from any of these plugins, to do so, use the build_ancestry_from_parent_ids! method on your ancestry model. These steps provide a more detailed explanation:
248
-
249
- 1. Add ancestry column to your table
250
- - Create migration: <b>rails g migration add_ancestry_to_[table] ancestry:string</b>
251
- - Add index to migration: <b>add_index [table], :ancestry</b> (UP) / <b>remove_index [table], :ancestry</b> (DOWN)
252
- - Migrate your database: <b>rake db:migrate</b>
253
-
254
- 2. Remove old tree plugin or gem and add in Ancestry
255
- - Remove plugin: rm -Rf vendor/plugins/[old plugin]
256
- - Remove gem config line from environment.rb: config.gem [old gem]
257
- - Add Ancestry to environment.rb: config.gem :ancestry
258
- - See 'Installation' for more info on installing and configuring gems
259
-
260
- 3. Change your model
261
- - Remove any macros required by old plugin/gem from app/models/[model].rb
262
- - Add to app/models/[model].rb: <b>has_ancestry</b>
263
-
264
- 4. Generate ancestry columns
265
- - In './script.console': <b>[model].build_ancestry_from_parent_ids!</b>
266
- - Make sure it worked ok: <b>[model].check_ancestry_integrity!</b>
267
-
268
- 5. Change your code
269
- - Most tree calls will probably work fine with ancestry
270
- - Others must be changed or proxied
271
- - Check if all your data is intact and all tests pass
272
-
273
- 6. Drop parent_id column:
274
- - Create migration: <b>rails g migration remove_parent_id_from_[table]</b>
275
- - Add to migration: <b>remove_column [table], :parent_id</b> (UP) / <b>add_column [table], :parent_id, :integer</b> (DOWN)
276
- - Migrate your database: <b>rake db:migrate</b>
277
-
278
- = Integrity checking and restoration
279
-
280
- 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.
281
-
282
- 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!.
283
-
284
- For example, from IRB:
285
-
286
- >> stinky = TreeNode.create :name => 'Stinky'
287
- $ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
288
- >> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
289
- $ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
290
- >> stinky.update_attribute :parent, squeeky
291
- $ true
292
- >> TreeNode.all
293
- $ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
294
- >> TreeNode.check_ancestry_integrity!
295
- !! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
296
- >> TreeNode.restore_ancestry_integrity!
297
- $ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
298
-
299
- Additionally, if you think something is wrong with your depth cache:
300
-
301
- >> TreeNode.rebuild_depth_cache!
302
-
303
- = Tests
304
-
305
- 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:
306
- - Check out the repository from GitHub
307
- - Copy test/database.example.yml to test/database.yml
308
- - Run <tt>bundle</tt>
309
- - Run <tt>appraisal install</tt>
310
- - Run <tt>appraisal rake test</tt>
311
-
312
- You can also run against a specific database and specific version of Activerecord:
313
- - Run the above commands, except for the last one
314
- - Run <tt>appraisal sqlite3-ar-32 rake test</tt> (to test against sqlite3 and Activerecord 3.2)
315
-
316
- = Internals
317
-
318
- As can be seen in the previous section, Ancestry stores a path from the root to the parent for every node. This is a variation on the materialised path database pattern. It allows Ancestry to fetch any relation (siblings, descendants, etc.) in a single SQL query without the complicated algorithms and incomprehensibility associated with left and right values. Additionally, any inserts, deletes and updates only affect nodes within the affected node's own subtree.
319
-
320
- In the example above, the ancestry column is created as a string. This puts a limitation on the depth of the tree of about 40 or 50 levels, which I think may be enough for most users. To increase the maximum depth of the tree, increase the size of the string that is being used or change it to a text to remove the limitation entirely. Changing it to a text will however decrease performance because an index cannot be put on the column in that case.
321
-
322
- 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.
323
-
324
- = Contributing and license
325
-
326
- 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.
327
-
328
- 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.
329
-
330
- Copyright (c) 2016 Stefan Kroes, released under the MIT license