ancestry 3.0.6 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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."
|