ancestry 3.0.5 → 3.2.1

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,44 +1,56 @@
1
1
  module Ancestry
2
2
  module MaterializedPath
3
+ BEFORE_LAST_SAVE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_before_last_save'.freeze : '_was'.freeze
4
+ IN_DATABASE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_in_database'.freeze : '_was'.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
41
  # rails has case sensitive matching.
35
42
  if ActiveRecord::VERSION::MAJOR >= 5
36
- t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true)
43
+ where(t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true))
37
44
  else
38
- t[ancestry_column].matches("#{node.child_ancestry}/%")
45
+ where(t[ancestry_column].matches("#{node.child_ancestry}/%"))
39
46
  end
40
47
  end
41
48
 
49
+ def descendants_of(object)
50
+ where(descendant_conditions(object))
51
+ end
52
+
53
+ # deprecated
42
54
  def descendant_conditions(object)
43
55
  t = arel_table
44
56
  node = to_node(object)
@@ -50,23 +62,94 @@ module Ancestry
50
62
  end
51
63
  end
52
64
 
53
- def subtree_conditions(object)
65
+ def subtree_of(object)
54
66
  t = arel_table
55
67
  node = to_node(object)
56
- descendant_conditions(node).or(t[primary_key].eq(node.id))
68
+ where(descendant_conditions(node).or(t[primary_key].eq(node.id)))
57
69
  end
58
70
 
59
- def sibling_conditions(object)
71
+ def siblings_of(object)
60
72
  t = arel_table
61
73
  node = to_node(object)
62
- t[ancestry_column].eq(node[ancestry_column])
74
+ where(t[ancestry_column].eq(node[ancestry_column].presence))
75
+ end
76
+
77
+ def ordered_by_ancestry(order = nil)
78
+ if %w(mysql mysql2 sqlite sqlite3).include?(connection.adapter_name.downcase)
79
+ reorder(arel_table[ancestry_column], order)
80
+ elsif %w(postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::STRING >= "6.1"
81
+ reorder(Arel::Nodes::Ascending.new(arel_table[ancestry_column]).nulls_first)
82
+ else
83
+ reorder(
84
+ Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
85
+ order
86
+ )
87
+ end
88
+ end
89
+
90
+ def ordered_by_ancestry_and(order)
91
+ ordered_by_ancestry(order)
63
92
  end
64
93
 
65
94
  module InstanceMethods
95
+
66
96
  # Validates the ancestry, but can also be applied if validation is bypassed to determine if children should be affected
67
97
  def sane_ancestry?
68
98
  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))
99
+ (ancestry_value.nil? || !ancestor_ids.include?(self.id)) && valid?
100
+ end
101
+
102
+ # optimization - better to go directly to column and avoid parsing
103
+ def ancestors?
104
+ read_attribute(self.ancestry_base_class.ancestry_column).present?
105
+ end
106
+ alias :has_parent? :ancestors?
107
+
108
+ def ancestor_ids=(value)
109
+ col = self.ancestry_base_class.ancestry_column
110
+ value.present? ? write_attribute(col, value.join(ANCESTRY_DELIMITER)) : write_attribute(col, nil)
111
+ end
112
+
113
+ def ancestor_ids
114
+ parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
115
+ end
116
+
117
+ def ancestor_ids_in_database
118
+ parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"))
119
+ end
120
+
121
+ def ancestor_ids_before_last_save
122
+ parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
123
+ end
124
+
125
+ def parent_id_before_last_save
126
+ ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
127
+ return unless ancestry_was.present?
128
+
129
+ parse_ancestry_column(ancestry_was).last
130
+ end
131
+
132
+ # optimization - better to go directly to column and avoid parsing
133
+ def sibling_of?(node)
134
+ self.read_attribute(self.ancestry_base_class.ancestry_column) == node.read_attribute(self.ancestry_base_class.ancestry_column)
135
+ end
136
+
137
+ # private (public so class methods can find it)
138
+ # The ancestry value for this record's children (before save)
139
+ # This is technically child_ancestry_was
140
+ def child_ancestry
141
+ # New records cannot have children
142
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
143
+ path_was = self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}")
144
+ path_was.blank? ? id.to_s : "#{path_was}/#{id}"
145
+ end
146
+
147
+ private
148
+
149
+ def parse_ancestry_column obj
150
+ return [] unless obj
151
+ obj_ids = obj.split(ANCESTRY_DELIMITER)
152
+ self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
70
153
  end
71
154
  end
72
155
  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.5"
2
+ VERSION = "3.2.1"
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.5
4
+ version: 3.2.1
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: 2018-11-06 00:00:00.000000000 Z
12
+ date: 2020-09-23 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: 4.2.0
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: 4.2.0
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,16 +123,16 @@ 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
- rubyforge_project:
161
- rubygems_version: 2.6.14.1
162
- signing_key:
133
+ rubyforge_project:
134
+ rubygems_version: 2.7.6.2
135
+ signing_key:
163
136
  specification_version: 4
164
137
  summary: Organize ActiveRecord model into a tree structure
165
138
  test_files: []
@@ -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."