ancestry 3.0.6 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +286 -0
- data/README.md +86 -202
- data/lib/ancestry.rb +28 -1
- data/lib/ancestry/class_methods.rb +60 -52
- data/lib/ancestry/has_ancestry.rb +39 -33
- data/lib/ancestry/instance_methods.rb +62 -106
- data/lib/ancestry/locales/en.yml +16 -0
- data/lib/ancestry/materialized_path.rb +100 -27
- data/lib/ancestry/materialized_path_pg.rb +23 -0
- data/lib/ancestry/version.rb +1 -1
- metadata +24 -51
- data/ancestry.gemspec +0 -53
- data/init.rb +0 -1
- data/install.rb +0 -1
@@ -0,0 +1,16 @@
|
|
1
|
+
en:
|
2
|
+
ancestry:
|
3
|
+
unknown_depth_option: "Unknown depth option: %{scope_name}."
|
4
|
+
invalid_orphan_strategy: "Invalid orphan strategy, valid ones are :rootify, :adopt, :restrict and :destroy."
|
5
|
+
invalid_ancestry_column: "Invalid format for ancestry column of node %{node_id}: %{ancestry_column}."
|
6
|
+
reference_nonexistent_node: "Reference to nonexistent node in node %{node_id}: %{ancestor_id}."
|
7
|
+
conflicting_parent_id: "Conflicting parent id found in node %{node_id}: %{parent_id} for node %{node_id} while expecting %{expected}"
|
8
|
+
cannot_rebuild_depth_cache: "Cannot rebuild depth cache for model without depth caching."
|
9
|
+
|
10
|
+
option_must_be_hash: "Options for has_ancestry must be in a hash."
|
11
|
+
unknown_option: "Unknown option for has_ancestry: %{key} => %{value}."
|
12
|
+
named_scope_depth_cache: "Named scope '%{scope_name}' is only available when depth caching is enabled."
|
13
|
+
|
14
|
+
exclude_self: "%{class_name} cannot be a descendant of itself."
|
15
|
+
cannot_delete_descendants: "Cannot delete record because it has descendants."
|
16
|
+
no_child_for_new_record: "No child ancestry for new record. Save record before performing tree operations."
|
@@ -1,72 +1,145 @@
|
|
1
1
|
module Ancestry
|
2
2
|
module MaterializedPath
|
3
|
+
BEFORE_LAST_SAVE_SUFFIX = '_before_last_save'.freeze
|
4
|
+
IN_DATABASE_SUFFIX = '_in_database'.freeze
|
5
|
+
ANCESTRY_DELIMITER='/'.freeze
|
6
|
+
|
3
7
|
def self.extended(base)
|
4
|
-
base.validates_format_of base.ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true
|
5
8
|
base.send(:include, InstanceMethods)
|
6
9
|
end
|
7
10
|
|
8
|
-
def
|
9
|
-
|
11
|
+
def path_of(object)
|
12
|
+
to_node(object).path
|
13
|
+
end
|
14
|
+
|
15
|
+
def roots
|
16
|
+
where(arel_table[ancestry_column].eq(nil))
|
10
17
|
end
|
11
18
|
|
12
|
-
def
|
19
|
+
def ancestors_of(object)
|
13
20
|
t = arel_table
|
14
21
|
node = to_node(object)
|
15
|
-
t[primary_key].in(node.ancestor_ids)
|
22
|
+
where(t[primary_key].in(node.ancestor_ids))
|
16
23
|
end
|
17
24
|
|
18
|
-
def
|
25
|
+
def inpath_of(object)
|
19
26
|
t = arel_table
|
20
27
|
node = to_node(object)
|
21
|
-
t[primary_key].in(node.path_ids)
|
28
|
+
where(t[primary_key].in(node.path_ids))
|
22
29
|
end
|
23
30
|
|
24
|
-
def
|
31
|
+
def children_of(object)
|
25
32
|
t = arel_table
|
26
33
|
node = to_node(object)
|
27
|
-
t[ancestry_column].eq(node.child_ancestry)
|
34
|
+
where(t[ancestry_column].eq(node.child_ancestry))
|
28
35
|
end
|
29
36
|
|
30
37
|
# indirect = anyone who is a descendant, but not a child
|
31
|
-
def
|
38
|
+
def indirects_of(object)
|
32
39
|
t = arel_table
|
33
40
|
node = to_node(object)
|
34
|
-
#
|
35
|
-
if ActiveRecord::VERSION::MAJOR >= 5
|
36
|
-
t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true)
|
37
|
-
else
|
38
|
-
t[ancestry_column].matches("#{node.child_ancestry}/%")
|
39
|
-
end
|
41
|
+
where(t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true))
|
40
42
|
end
|
41
43
|
|
44
|
+
def descendants_of(object)
|
45
|
+
where(descendant_conditions(object))
|
46
|
+
end
|
47
|
+
|
48
|
+
# deprecated
|
42
49
|
def descendant_conditions(object)
|
43
50
|
t = arel_table
|
44
51
|
node = to_node(object)
|
45
|
-
#
|
46
|
-
if ActiveRecord::VERSION::MAJOR >= 5
|
47
|
-
t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true).or(t[ancestry_column].eq(node.child_ancestry))
|
48
|
-
else
|
49
|
-
t[ancestry_column].matches("#{node.child_ancestry}/%").or(t[ancestry_column].eq(node.child_ancestry))
|
50
|
-
end
|
52
|
+
t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true).or(t[ancestry_column].eq(node.child_ancestry))
|
51
53
|
end
|
52
54
|
|
53
|
-
def
|
55
|
+
def subtree_of(object)
|
54
56
|
t = arel_table
|
55
57
|
node = to_node(object)
|
56
|
-
descendant_conditions(node).or(t[primary_key].eq(node.id))
|
58
|
+
where(descendant_conditions(node).or(t[primary_key].eq(node.id)))
|
57
59
|
end
|
58
60
|
|
59
|
-
def
|
61
|
+
def siblings_of(object)
|
60
62
|
t = arel_table
|
61
63
|
node = to_node(object)
|
62
|
-
t[ancestry_column].eq(node[ancestry_column])
|
64
|
+
where(t[ancestry_column].eq(node[ancestry_column].presence))
|
65
|
+
end
|
66
|
+
|
67
|
+
def ordered_by_ancestry(order = nil)
|
68
|
+
if %w(mysql mysql2 sqlite sqlite3).include?(connection.adapter_name.downcase)
|
69
|
+
reorder(arel_table[ancestry_column], order)
|
70
|
+
elsif %w(postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::STRING >= "6.1"
|
71
|
+
reorder(Arel::Nodes::Ascending.new(arel_table[ancestry_column]).nulls_first, order)
|
72
|
+
else
|
73
|
+
reorder(
|
74
|
+
Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
|
75
|
+
order
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def ordered_by_ancestry_and(order)
|
81
|
+
ordered_by_ancestry(order)
|
63
82
|
end
|
64
83
|
|
65
84
|
module InstanceMethods
|
85
|
+
|
66
86
|
# Validates the ancestry, but can also be applied if validation is bypassed to determine if children should be affected
|
67
87
|
def sane_ancestry?
|
68
88
|
ancestry_value = read_attribute(self.ancestry_base_class.ancestry_column)
|
69
|
-
ancestry_value.nil? ||
|
89
|
+
(ancestry_value.nil? || !ancestor_ids.include?(self.id)) && valid?
|
90
|
+
end
|
91
|
+
|
92
|
+
# optimization - better to go directly to column and avoid parsing
|
93
|
+
def ancestors?
|
94
|
+
read_attribute(self.ancestry_base_class.ancestry_column).present?
|
95
|
+
end
|
96
|
+
alias :has_parent? :ancestors?
|
97
|
+
|
98
|
+
def ancestor_ids=(value)
|
99
|
+
col = self.ancestry_base_class.ancestry_column
|
100
|
+
value.present? ? write_attribute(col, value.join(ANCESTRY_DELIMITER)) : write_attribute(col, nil)
|
101
|
+
end
|
102
|
+
|
103
|
+
def ancestor_ids
|
104
|
+
parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
|
105
|
+
end
|
106
|
+
|
107
|
+
def ancestor_ids_in_database
|
108
|
+
parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"))
|
109
|
+
end
|
110
|
+
|
111
|
+
def ancestor_ids_before_last_save
|
112
|
+
parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
|
113
|
+
end
|
114
|
+
|
115
|
+
def parent_id_before_last_save
|
116
|
+
ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
|
117
|
+
return unless ancestry_was.present?
|
118
|
+
|
119
|
+
parse_ancestry_column(ancestry_was).last
|
120
|
+
end
|
121
|
+
|
122
|
+
# optimization - better to go directly to column and avoid parsing
|
123
|
+
def sibling_of?(node)
|
124
|
+
self.read_attribute(self.ancestry_base_class.ancestry_column) == node.read_attribute(self.ancestry_base_class.ancestry_column)
|
125
|
+
end
|
126
|
+
|
127
|
+
# private (public so class methods can find it)
|
128
|
+
# The ancestry value for this record's children (before save)
|
129
|
+
# This is technically child_ancestry_was
|
130
|
+
def child_ancestry
|
131
|
+
# New records cannot have children
|
132
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
|
133
|
+
path_was = self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}")
|
134
|
+
path_was.blank? ? id.to_s : "#{path_was}/#{id}"
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def parse_ancestry_column obj
|
140
|
+
return [] unless obj
|
141
|
+
obj_ids = obj.split(ANCESTRY_DELIMITER)
|
142
|
+
self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
|
70
143
|
end
|
71
144
|
end
|
72
145
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Ancestry
|
2
|
+
module MaterializedPathPg
|
3
|
+
# Update descendants with new ancestry (before save)
|
4
|
+
def update_descendants_with_new_ancestry
|
5
|
+
# If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
|
6
|
+
if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestry?
|
7
|
+
ancestry_column = ancestry_base_class.ancestry_column
|
8
|
+
old_ancestry = path_ids_in_database.join(Ancestry::MaterializedPath::ANCESTRY_DELIMITER)
|
9
|
+
new_ancestry = path_ids.join(Ancestry::MaterializedPath::ANCESTRY_DELIMITER)
|
10
|
+
update_clause = [
|
11
|
+
"#{ancestry_column} = regexp_replace(#{ancestry_column}, '^#{old_ancestry}', '#{new_ancestry}')"
|
12
|
+
]
|
13
|
+
|
14
|
+
if ancestry_base_class.respond_to?(:depth_cache_column) && respond_to?(ancestry_base_class.depth_cache_column)
|
15
|
+
depth_cache_column = ancestry_base_class.depth_cache_column.to_s
|
16
|
+
update_clause << "#{depth_cache_column} = length(regexp_replace(regexp_replace(ancestry, '^#{old_ancestry}', '#{new_ancestry}'), '\\d', '', 'g')) + 1"
|
17
|
+
end
|
18
|
+
|
19
|
+
unscoped_descendants.update_all update_clause.join(', ')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/ancestry/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ancestry
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Kroes
|
8
8
|
- Keenan Brock
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2021-04-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -17,16 +17,16 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version:
|
20
|
+
version: 5.2.4.5
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version:
|
27
|
+
version: 5.2.4.5
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
|
-
name:
|
29
|
+
name: appraisal
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
32
|
- - ">="
|
@@ -40,7 +40,7 @@ dependencies:
|
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '0'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
|
-
name:
|
43
|
+
name: minitest
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
46
|
- - ">="
|
@@ -59,44 +59,16 @@ dependencies:
|
|
59
59
|
requirements:
|
60
60
|
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '13.0'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
67
|
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
70
|
-
- !ruby/object:Gem::Dependency
|
71
|
-
name: test-unit
|
72
|
-
requirement: !ruby/object:Gem::Requirement
|
73
|
-
requirements:
|
74
|
-
- - ">="
|
75
|
-
- !ruby/object:Gem::Version
|
76
|
-
version: '0'
|
77
|
-
type: :development
|
78
|
-
prerelease: false
|
79
|
-
version_requirements: !ruby/object:Gem::Requirement
|
80
|
-
requirements:
|
81
|
-
- - ">="
|
82
|
-
- !ruby/object:Gem::Version
|
83
|
-
version: '0'
|
84
|
-
- !ruby/object:Gem::Dependency
|
85
|
-
name: minitest
|
86
|
-
requirement: !ruby/object:Gem::Requirement
|
87
|
-
requirements:
|
88
|
-
- - ">="
|
89
|
-
- !ruby/object:Gem::Version
|
90
|
-
version: '0'
|
91
|
-
type: :development
|
92
|
-
prerelease: false
|
93
|
-
version_requirements: !ruby/object:Gem::Requirement
|
94
|
-
requirements:
|
95
|
-
- - ">="
|
96
|
-
- !ruby/object:Gem::Version
|
97
|
-
version: '0'
|
69
|
+
version: '13.0'
|
98
70
|
- !ruby/object:Gem::Dependency
|
99
|
-
name:
|
71
|
+
name: yard
|
100
72
|
requirement: !ruby/object:Gem::Requirement
|
101
73
|
requirements:
|
102
74
|
- - ">="
|
@@ -111,28 +83,28 @@ dependencies:
|
|
111
83
|
version: '0'
|
112
84
|
description: |2
|
113
85
|
Ancestry allows the records of a ActiveRecord model to be organized in a tree
|
114
|
-
structure, using
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
86
|
+
structure, using the materialized path pattern. It exposes the standard
|
87
|
+
relations (ancestors, parent, root, children, siblings, descendants)
|
88
|
+
and allows them to be fetched in a single query. Additional features include
|
89
|
+
named scopes, integrity checking, integrity restoration, arrangement
|
90
|
+
of (sub)tree into hashes and different strategies for dealing with orphaned
|
91
|
+
records.
|
120
92
|
email: keenan@thebrocks.net
|
121
93
|
executables: []
|
122
94
|
extensions: []
|
123
95
|
extra_rdoc_files: []
|
124
96
|
files:
|
97
|
+
- CHANGELOG.md
|
125
98
|
- MIT-LICENSE
|
126
99
|
- README.md
|
127
|
-
- ancestry.gemspec
|
128
|
-
- init.rb
|
129
|
-
- install.rb
|
130
100
|
- lib/ancestry.rb
|
131
101
|
- lib/ancestry/class_methods.rb
|
132
102
|
- lib/ancestry/exceptions.rb
|
133
103
|
- lib/ancestry/has_ancestry.rb
|
134
104
|
- lib/ancestry/instance_methods.rb
|
105
|
+
- lib/ancestry/locales/en.yml
|
135
106
|
- lib/ancestry/materialized_path.rb
|
107
|
+
- lib/ancestry/materialized_path_pg.rb
|
136
108
|
- lib/ancestry/version.rb
|
137
109
|
homepage: https://github.com/stefankroes/ancestry
|
138
110
|
licenses:
|
@@ -142,7 +114,8 @@ metadata:
|
|
142
114
|
changelog_uri: https://github.com/stefankroes/ancestry/blob/master/CHANGELOG.md
|
143
115
|
source_code_uri: https://github.com/stefankroes/ancestry/
|
144
116
|
bug_tracker_uri: https://github.com/stefankroes/ancestry/issues
|
145
|
-
post_install_message:
|
117
|
+
post_install_message: Thank you for installing Ancestry. You can visit http://github.com/stefankroes/ancestry
|
118
|
+
to read the documentation.
|
146
119
|
rdoc_options: []
|
147
120
|
require_paths:
|
148
121
|
- lib
|
@@ -150,15 +123,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
150
123
|
requirements:
|
151
124
|
- - ">="
|
152
125
|
- !ruby/object:Gem::Version
|
153
|
-
version:
|
126
|
+
version: 2.0.0
|
154
127
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
128
|
requirements:
|
156
129
|
- - ">="
|
157
130
|
- !ruby/object:Gem::Version
|
158
131
|
version: '0'
|
159
132
|
requirements: []
|
160
|
-
rubygems_version: 3.
|
161
|
-
signing_key:
|
133
|
+
rubygems_version: 3.1.2
|
134
|
+
signing_key:
|
162
135
|
specification_version: 4
|
163
136
|
summary: Organize ActiveRecord model into a tree structure
|
164
137
|
test_files: []
|
data/ancestry.gemspec
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
lib = File.expand_path('../lib/', __FILE__)
|
2
|
-
$:.unshift lib unless $:.include?(lib)
|
3
|
-
require 'ancestry/version'
|
4
|
-
|
5
|
-
Gem::Specification.new do |s|
|
6
|
-
s.name = 'ancestry'
|
7
|
-
s.summary = 'Organize ActiveRecord model into a tree structure'
|
8
|
-
s.description = <<-EOF
|
9
|
-
Ancestry allows the records of a ActiveRecord model to be organized in a tree
|
10
|
-
structure, using a single, intuitively formatted database column. It exposes
|
11
|
-
all the standard tree structure relations (ancestors, parent, root, children,
|
12
|
-
siblings, descendants) and all of them can be fetched in a single sql query.
|
13
|
-
Additional features are named_scopes, integrity checking, integrity restoration,
|
14
|
-
arrangement of (sub)tree into hashes and different strategies for dealing with
|
15
|
-
orphaned records.
|
16
|
-
EOF
|
17
|
-
s.metadata = {
|
18
|
-
"homepage_uri" => "https://github.com/stefankroes/ancestry",
|
19
|
-
"changelog_uri" => "https://github.com/stefankroes/ancestry/blob/master/CHANGELOG.md",
|
20
|
-
"source_code_uri" => "https://github.com/stefankroes/ancestry/",
|
21
|
-
"bug_tracker_uri" => "https://github.com/stefankroes/ancestry/issues",
|
22
|
-
}
|
23
|
-
s.version = Ancestry::VERSION
|
24
|
-
|
25
|
-
s.authors = ['Stefan Kroes', 'Keenan Brock']
|
26
|
-
s.email = 'keenan@thebrocks.net'
|
27
|
-
s.homepage = 'https://github.com/stefankroes/ancestry'
|
28
|
-
s.license = 'MIT'
|
29
|
-
|
30
|
-
s.files = [
|
31
|
-
'ancestry.gemspec',
|
32
|
-
'init.rb',
|
33
|
-
'install.rb',
|
34
|
-
'lib/ancestry.rb',
|
35
|
-
'lib/ancestry/has_ancestry.rb',
|
36
|
-
'lib/ancestry/exceptions.rb',
|
37
|
-
'lib/ancestry/class_methods.rb',
|
38
|
-
'lib/ancestry/instance_methods.rb',
|
39
|
-
'lib/ancestry/materialized_path.rb',
|
40
|
-
'lib/ancestry/version.rb',
|
41
|
-
'MIT-LICENSE',
|
42
|
-
'README.md'
|
43
|
-
]
|
44
|
-
|
45
|
-
s.required_ruby_version = '>= 1.8.7'
|
46
|
-
s.add_runtime_dependency 'activerecord', '>= 3.2.0'
|
47
|
-
s.add_development_dependency 'rdoc'
|
48
|
-
s.add_development_dependency 'yard'
|
49
|
-
s.add_development_dependency 'rake', '~> 10.0'
|
50
|
-
s.add_development_dependency 'test-unit'
|
51
|
-
s.add_development_dependency 'minitest'
|
52
|
-
s.add_development_dependency 'sqlite3'
|
53
|
-
end
|
data/init.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
require 'ancestry'
|
data/install.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
puts "Thank you for installing Ancestry. You can visit http://github.com/stefankroes/ancestry to read the documentation."
|