ancestry 3.0.6 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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."