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.
@@ -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 root_conditions
9
- arel_table[ancestry_column].eq(nil)
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 ancestor_conditions(object)
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 path_conditions(object)
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 child_conditions(object)
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 indirect_conditions(object)
38
+ def indirects_of(object)
32
39
  t = arel_table
33
40
  node = to_node(object)
34
- # rails has case sensitive matching.
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
- # rails has case sensitive matching.
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 subtree_conditions(object)
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 sibling_conditions(object)
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? || (ancestry_value.to_s =~ Ancestry::ANCESTRY_PATTERN && !ancestor_ids.include?(self.id))
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
@@ -1,3 +1,3 @@
1
1
  module Ancestry
2
- VERSION = "3.0.6"
2
+ VERSION = "4.0.0"
3
3
  end
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: 3.0.6
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: 2019-04-24 00:00:00.000000000 Z
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: 3.2.0
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: 3.2.0
27
+ version: 5.2.4.5
28
28
  - !ruby/object:Gem::Dependency
29
- name: rdoc
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: yard
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: '10.0'
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: '10.0'
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: sqlite3
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 a single, intuitively formatted database column. It exposes
115
- all the standard tree structure relations (ancestors, parent, root, children,
116
- siblings, descendants) and all of them can be fetched in a single sql query.
117
- Additional features are named_scopes, integrity checking, integrity restoration,
118
- arrangement of (sub)tree into hashes and different strategies for dealing with
119
- orphaned records.
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: 1.8.7
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.0.2
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."