ancestry 3.0.5 → 3.2.1

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,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."