ancestry 1.3.0 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,8 +8,7 @@ To apply Ancestry to any ActiveRecord model, follow these simple steps:
8
8
 
9
9
  1. Install
10
10
  - <b>Rails 2</b>
11
- - Add to config/environment.rb: <b>config.gem 'ancestry'</b>
12
- - Install required gems: <b>sudo rake gems:install</b>
11
+ - See 1-3-stable branch
13
12
  - <b>Rails 3</b>
14
13
  - Add to Gemfile: <b>gem 'ancestry'</b>
15
14
  - Install required gems: <b>bundle install</b>
@@ -46,7 +45,7 @@ To navigate an Ancestry model, use the following methods on any instance / recor
46
45
  parent_id Returns the id of the parent of the record, nil for a root node
47
46
  root Returns the root of the tree the record is in, self for a root node
48
47
  root_id Returns the id of the root of the tree the record is in
49
- is_root? Returns true if the record is a root node, false otherwise
48
+ root?, is_root? Returns true if the record is a root node, false otherwise
50
49
  ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
51
50
  ancestors Scopes the model on ancestors of the record
52
51
  path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
@@ -74,6 +73,7 @@ The has_ancestry methods supports the following options:
74
73
  :destroy All children are destroyed as well (default)
75
74
  :rootify The children of the destroyed node become root nodes
76
75
  :restrict An AncestryException is raised if any children exist
76
+ :adopt The orphan subtree is added to the parent of the deleted node.If the deleted node is Root, then rootify the orphan subtree.
77
77
  :cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
78
78
  If you turn depth_caching on for an existing model:
79
79
  - Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
@@ -224,7 +224,18 @@ Additionally, if you think something is wrong with your depth cache:
224
224
 
225
225
  = Tests
226
226
 
227
- The Ancestry gem comes with a unit test suite consisting of about 1800 assertions in about 30 tests. It takes about 10 seconds to run on sqlite. To run it yourself check out the repository from GitHub, copy test/database.example.yml to test/database.yml and type 'rake'. You can pass rake style options for ActiveRecord version to test against (e.g. ar=3.0.1) and database to test against (e.g. db=mysql). The test suite is located in test/has_ancestry_test.rb.
227
+ The Ancestry gem comes with a unit test suite consisting of about 1800 assertions in about 30 tests. It takes about 10 seconds to run on sqlite. To run it yourself:
228
+ - check out the repository from GitHub
229
+ - copy test/database.example.yml to test/database.yml
230
+ - run <tt>bundle</tt>
231
+ - run <tt>rake [test]</tt>
232
+
233
+ You can pass an environment variable for the database to test against (e.g. db=mysql). By default, the test suite runs against the latest activerecord version. You can run agains activerecord 3-0 or 3-1 as follows:
234
+ - run <tt>bundle --gemfile Gemfile.rails-<version></tt>
235
+ - run <tt>rake [test] BUNDLE_GEMFILE=Gemfile.rails-<version></tt>
236
+
237
+ To run the test suite multiple times against several databases and all supported activerecord versions, run <tt>rake test_all</tt>
238
+ The test suite is located in test/has_ancestry_test.rb.
228
239
 
229
240
  = Internals
230
241
 
@@ -238,6 +249,14 @@ The materialised path pattern requires Ancestry to use a 'like' condition in ord
238
249
 
239
250
  The latest version of ancestry is recommended. The three numbers of each version numbers are respectively the major, minor and patch versions. We started with major version 1 because it looks so much better and ancestry was already quite mature and complete when it was published. The major version is only bumped when backwards compatibility is broken. The minor version is bumped when new features are added. The patch version is bumped when bugs are fixed.
240
251
 
252
+ - Version 2.0.0.rc1 (2013-05-07)
253
+ - Removed rails 2 compatibility
254
+ - Fixed deprecation warnings for rails 4
255
+ - New adopt strategy
256
+ - Many more fixes & minor changes
257
+ - Version 1.3.0 (2012-05-04)
258
+ - Ancestry now ignores default scopes when moving or destroying nodes, ensuring tree consistency
259
+ - Changed ActiveRecord dependency to 2.3.14
241
260
  - Version 1.2.5 (2012-03-15)
242
261
  - Fixed warnings: "parenthesize argument(s) for future version"
243
262
  - Fixed a bug in the restore_ancestry_integrity! method (thx Arthur Holstvoogd)
@@ -3,7 +3,7 @@ Gem::Specification.new do |s|
3
3
  s.description = 'Organise ActiveRecord model into a tree structure'
4
4
  s.summary = 'Ancestry allows the records of a ActiveRecord model to be organised in a tree structure, using a single, intuitively formatted database column. It exposes all the standard tree structure relations (ancestors, parent, root, children, siblings, descendants) and all of them can be fetched in a single sql query. Additional features are named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.'
5
5
 
6
- s.version = '1.3.0'
6
+ s.version = '2.0.0.rc1'
7
7
 
8
8
  s.author = 'Stefan Kroes'
9
9
  s.email = 's.a.kroes@gmail.com'
@@ -22,5 +22,5 @@ Gem::Specification.new do |s|
22
22
  'README.rdoc'
23
23
  ]
24
24
 
25
- s.add_dependency 'activerecord', '>= 2.3.14'
25
+ s.add_dependency 'activerecord', '>= 3.0.0'
26
26
  end
@@ -2,3 +2,7 @@ require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/class_meth
2
2
  require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/instance_methods')
3
3
  require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/exceptions')
4
4
  require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/has_ancestry')
5
+
6
+ module Ancestry
7
+ ANCESTRY_PATTERN = /\A[0-9]+(\/[0-9]+)*\Z/
8
+ end
@@ -2,12 +2,12 @@ module Ancestry
2
2
  module ClassMethods
3
3
  # Fetch tree node if necessary
4
4
  def to_node object
5
- if object.is_a?(self.base_class) then object else find(object) end
5
+ if object.is_a?(self.ancestry_base_class) then object else find(object) end
6
6
  end
7
7
 
8
8
  # Scope on relative depth options
9
9
  def scope_depth depth_options, depth
10
- depth_options.inject(self.base_class) do |scope, option|
10
+ depth_options.inject(self.ancestry_base_class) do |scope, option|
11
11
  scope_name, relative_depth = option
12
12
  if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
13
13
  scope.send scope_name, depth + relative_depth
@@ -19,11 +19,11 @@ module Ancestry
19
19
 
20
20
  # Orphan strategy writer
21
21
  def orphan_strategy= orphan_strategy
22
- # Check value of orphan strategy, only rootify, restrict or destroy is allowed
23
- if [:rootify, :restrict, :destroy].include? orphan_strategy
22
+ # Check value of orphan strategy, only rootify, adopt, restrict or destroy is allowed
23
+ if [:rootify, :adopt, :restrict, :destroy].include? orphan_strategy
24
24
  class_variable_set :@@orphan_strategy, orphan_strategy
25
25
  else
26
- raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify, :restrict and :destroy.")
26
+ raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify,:adopt, :restrict and :destroy.")
27
27
  end
28
28
  end
29
29
 
@@ -31,12 +31,12 @@ module Ancestry
31
31
  def arrange options = {}
32
32
  scope =
33
33
  if options[:order].nil?
34
- self.base_class.ordered_by_ancestry
34
+ self.ancestry_base_class.ordered_by_ancestry
35
35
  else
36
- self.base_class.ordered_by_ancestry_and options.delete(:order)
36
+ self.ancestry_base_class.ordered_by_ancestry_and options.delete(:order)
37
37
  end
38
38
  # Get all nodes ordered by ancestry and start sorting them into an empty hash
39
- arrange_nodes scope.all(options)
39
+ arrange_nodes scope.where(options)
40
40
  end
41
41
 
42
42
  # Arrange array of nodes into a nested hash of the form
@@ -57,14 +57,28 @@ module Ancestry
57
57
  end
58
58
 
59
59
  # Pseudo-preordered array of nodes. Children will always follow parents,
60
- # but the ordering of nodes within a rank depends on their order in the
61
- # array that gets passed in
62
- def sort_by_ancestry(nodes)
63
- arranged = nodes.is_a?(Hash) ? nodes : arrange_nodes(nodes.sort_by{|n| n.ancestry || '0'})
60
+ # for ordering nodes within a rank provide block, eg. Node.sort_by_ancestry(Node.all) {|a, b| a.rank <=> b.rank}.
61
+ def sort_by_ancestry(nodes, &block)
62
+ arranged = nodes if nodes.is_a?(Hash)
63
+
64
+ unless arranged
65
+ presorted_nodes = nodes.sort do |a, b|
66
+ a_cestry, b_cestry = a.ancestry || '0', b.ancestry || '0'
67
+
68
+ if block_given? && a_cestry == b_cestry
69
+ yield a, b
70
+ else
71
+ a_cestry <=> b_cestry
72
+ end
73
+ end
74
+
75
+ arranged = arrange_nodes(presorted_nodes)
76
+ end
77
+
64
78
  arranged.inject([]) do |sorted_nodes, pair|
65
79
  node, children = pair
66
80
  sorted_nodes << node
67
- sorted_nodes += sort_by_ancestry(children) unless children.blank?
81
+ sorted_nodes += sort_by_ancestry(children, &block) unless children.blank?
68
82
  sorted_nodes
69
83
  end
70
84
  end
@@ -74,9 +88,9 @@ module Ancestry
74
88
  parents = {}
75
89
  exceptions = [] if options[:report] == :list
76
90
 
77
- self.base_class.send(:with_exclusive_scope) do
91
+ self.ancestry_base_class.unscoped do
78
92
  # For each node ...
79
- self.base_class.find_each do |node|
93
+ self.ancestry_base_class.find_each do |node|
80
94
  begin
81
95
  # ... check validity of ancestry column
82
96
  if !node.valid? and !node.errors[node.class.ancestry_column].blank?
@@ -111,10 +125,10 @@ module Ancestry
111
125
  def restore_ancestry_integrity!
112
126
  parents = {}
113
127
  # Wrap the whole thing in a transaction ...
114
- self.base_class.transaction do
115
- self.base_class.send(:with_exclusive_scope) do
128
+ self.ancestry_base_class.transaction do
129
+ self.ancestry_base_class.unscoped do
116
130
  # For each node ...
117
- self.base_class.find_each do |node|
131
+ self.ancestry_base_class.find_each do |node|
118
132
  # ... set its ancestry to nil if invalid
119
133
  if !node.valid? and !node.errors[node.class.ancestry_column].blank?
120
134
  node.without_ancestry_callbacks do
@@ -133,7 +147,7 @@ module Ancestry
133
147
  end
134
148
 
135
149
  # For each node ...
136
- self.base_class.find_each do |node|
150
+ self.ancestry_base_class.find_each do |node|
137
151
  # ... rebuild ancestry from parents array
138
152
  ancestry, parent = nil, parents[node.id]
139
153
  until parent.nil?
@@ -149,8 +163,8 @@ module Ancestry
149
163
 
150
164
  # Build ancestry from parent id's for migration purposes
151
165
  def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
152
- self.base_class.send(:with_exclusive_scope) do
153
- self.base_class.find_each(:conditions => {:parent_id => parent_id}) do |node|
166
+ self.ancestry_base_class.unscoped do
167
+ self.ancestry_base_class.where(:parent_id => parent_id).find_each do |node|
154
168
  node.without_ancestry_callbacks do
155
169
  node.update_attribute ancestry_column, ancestry
156
170
  end
@@ -163,8 +177,8 @@ module Ancestry
163
177
  def rebuild_depth_cache!
164
178
  raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column
165
179
 
166
- self.base_class.send(:with_exclusive_scope) do
167
- self.base_class.find_each do |node|
180
+ self.ancestry_base_class.unscoped do
181
+ self.ancestry_base_class.find_each do |node|
168
182
  node.update_attribute depth_cache_column, node.depth
169
183
  end
170
184
  end
@@ -3,7 +3,7 @@ class << ActiveRecord::Base
3
3
  # Check options
4
4
  raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash
5
5
  options.each do |key, value|
6
- unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :primary_key_format].include? key
6
+ unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column].include? key
7
7
  raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.")
8
8
  end
9
9
  end
@@ -23,32 +23,24 @@ class << ActiveRecord::Base
23
23
  self.orphan_strategy = options[:orphan_strategy] || :destroy
24
24
 
25
25
  # Save self as base class (for STI)
26
- cattr_accessor :base_class
27
- self.base_class = self
26
+ cattr_accessor :ancestry_base_class
27
+ self.ancestry_base_class = self
28
28
 
29
29
  # Validate format of ancestry column value
30
- primary_key_format = options[:primary_key_format] || /[0-9]+/
31
- validates_format_of ancestry_column, :with => /\A#{primary_key_format.source}(\/#{primary_key_format.source})*\Z/, :allow_nil => true
30
+ validates_format_of ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true
32
31
 
33
32
  # Validate that the ancestor ids don't include own id
34
33
  validate :ancestry_exclude_self
35
34
 
36
- # Save ActiveRecord version
37
- self.cattr_accessor :rails_3
38
- self.rails_3 = defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::MAJOR >= 3
39
-
40
- # Workaround to support Rails 2
41
- scope_method = if rails_3 then :scope else :named_scope end
42
-
43
35
  # Named scopes
44
- send scope_method, :roots, :conditions => {ancestry_column => nil}
45
- send scope_method, :ancestors_of, lambda { |object| {:conditions => to_node(object).ancestor_conditions} }
46
- send scope_method, :children_of, lambda { |object| {:conditions => to_node(object).child_conditions} }
47
- send scope_method, :descendants_of, lambda { |object| {:conditions => to_node(object).descendant_conditions} }
48
- send scope_method, :subtree_of, lambda { |object| {:conditions => to_node(object).subtree_conditions} }
49
- send scope_method, :siblings_of, lambda { |object| {:conditions => to_node(object).sibling_conditions} }
50
- send scope_method, :ordered_by_ancestry, :order => "(case when #{table_name}.#{ancestry_column} is null then 0 else 1 end), #{table_name}.#{ancestry_column}"
51
- send scope_method, :ordered_by_ancestry_and, lambda { |order| {:order => "(case when #{table_name}.#{ancestry_column} is null then 0 else 1 end), #{table_name}.#{ancestry_column}, #{order}"} }
36
+ scope :roots, lambda { where(ancestry_column => nil) }
37
+ scope :ancestors_of, lambda { |object| where(to_node(object).ancestor_conditions) }
38
+ scope :children_of, lambda { |object| where(to_node(object).child_conditions) }
39
+ scope :descendants_of, lambda { |object| where(to_node(object).descendant_conditions) }
40
+ scope :subtree_of, lambda { |object| where(to_node(object).subtree_conditions) }
41
+ scope :siblings_of, lambda { |object| where(to_node(object).sibling_conditions) }
42
+ scope :ordered_by_ancestry, lambda { reorder("(case when #{table_name}.#{ancestry_column} is null then 0 else 1 end), #{table_name}.#{ancestry_column}") }
43
+ scope :ordered_by_ancestry_and, lambda { |order| reorder("(case when #{table_name}.#{ancestry_column} is null then 0 else 1 end), #{table_name}.#{ancestry_column}, #{order}") }
52
44
 
53
45
  # Update descendants with new ancestry before save
54
46
  before_save :update_descendants_with_new_ancestry
@@ -64,6 +56,7 @@ class << ActiveRecord::Base
64
56
 
65
57
  # Cache depth in depth cache column before save
66
58
  before_validation :cache_depth
59
+ before_save :cache_depth
67
60
 
68
61
  # Validate depth column
69
62
  validates_numericality_of depth_cache_column, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
@@ -71,9 +64,9 @@ class << ActiveRecord::Base
71
64
 
72
65
  # Create named scopes for depth
73
66
  {:before_depth => '<', :to_depth => '<=', :at_depth => '=', :from_depth => '>=', :after_depth => '>'}.each do |scope_name, operator|
74
- send scope_method, scope_name, lambda { |depth|
67
+ scope scope_name, lambda { |depth|
75
68
  raise Ancestry::AncestryException.new("Named scope '#{scope_name}' is only available when depth caching is enabled.") unless options[:cache_depth]
76
- {:conditions => ["#{depth_cache_column} #{operator} ?", depth]}
69
+ where("#{depth_cache_column} #{operator} ?", depth)
77
70
  }
78
71
  end
79
72
  end
@@ -2,21 +2,21 @@ module Ancestry
2
2
  module InstanceMethods
3
3
  # Validate that the ancestors don't include itself
4
4
  def ancestry_exclude_self
5
- add_error_to_base "#{self.class.name.humanize} cannot be a descendant of itself." if ancestor_ids.include? self.id
5
+ errors.add(:base, "#{self.class.name.humanize} cannot be a descendant of itself.") if ancestor_ids.include? self.id
6
6
  end
7
7
 
8
8
  # Update descendants with new ancestry
9
9
  def update_descendants_with_new_ancestry
10
10
  # Skip this if callbacks are disabled
11
11
  unless ancestry_callbacks_disabled?
12
- # If node is valid, not a new record and ancestry was updated ...
13
- if changed.include?(self.base_class.ancestry_column.to_s) && !new_record? && valid?
12
+ # If node is not a new record and ancestry was updated and the new ancestry is sane ...
13
+ if ancestry_changed? && !new_record? && sane_ancestry?
14
14
  # ... for each descendant ...
15
15
  unscoped_descendants.each do |descendant|
16
16
  # ... replace old ancestry with new ancestry
17
17
  descendant.without_ancestry_callbacks do
18
18
  descendant.update_attribute(
19
- self.base_class.ancestry_column,
19
+ self.ancestry_base_class.ancestry_column,
20
20
  descendant.read_attribute(descendant.class.ancestry_column).gsub(
21
21
  /^#{self.child_ancestry}/,
22
22
  if read_attribute(self.class.ancestry_column).blank? then id.to_s else "#{read_attribute self.class.ancestry_column }/#{id}" end
@@ -35,21 +35,29 @@ module Ancestry
35
35
  # If this isn't a new record ...
36
36
  unless new_record?
37
37
  # ... make all children root if orphan strategy is rootify
38
- if self.base_class.orphan_strategy == :rootify
38
+ if self.ancestry_base_class.orphan_strategy == :rootify
39
39
  unscoped_descendants.each do |descendant|
40
40
  descendant.without_ancestry_callbacks do
41
41
  descendant.update_attribute descendant.class.ancestry_column, (if descendant.ancestry == child_ancestry then nil else descendant.ancestry.gsub(/^#{child_ancestry}\//, '') end)
42
42
  end
43
43
  end
44
44
  # ... destroy all descendants if orphan strategy is destroy
45
- elsif self.base_class.orphan_strategy == :destroy
45
+ elsif self.ancestry_base_class.orphan_strategy == :destroy
46
46
  unscoped_descendants.each do |descendant|
47
47
  descendant.without_ancestry_callbacks do
48
48
  descendant.destroy
49
49
  end
50
50
  end
51
+ # ... make child elements of this node, child of its parent if orphan strategy is adopt
52
+ elsif self.ancestry_base_class.orphan_strategy == :adopt
53
+ descendants.each do |descendant|
54
+ descendant.without_ancestry_callbacks do
55
+ new_ancestry = descendant.ancestor_ids.delete_if { |x| x == self.id }.join("/")
56
+ descendant.update_attribute descendant.class.ancestry_column, new_ancestry || nil
57
+ end
58
+ end
51
59
  # ... throw an exception if it has children and orphan strategy is restrict
52
- elsif self.base_class.orphan_strategy == :restrict
60
+ elsif self.ancestry_base_class.orphan_strategy == :restrict
53
61
  raise Ancestry::AncestryException.new('Cannot delete record because it has descendants.') unless is_childless?
54
62
  end
55
63
  end
@@ -61,20 +69,24 @@ module Ancestry
61
69
  # New records cannot have children
62
70
  raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
63
71
 
64
- if self.send("#{self.base_class.ancestry_column}_was").blank? then id.to_s else "#{self.send "#{self.base_class.ancestry_column}_was"}/#{id}" end
72
+ if self.send("#{self.ancestry_base_class.ancestry_column}_was").blank? then id.to_s else "#{self.send "#{self.ancestry_base_class.ancestry_column}_was"}/#{id}" end
65
73
  end
66
74
 
67
75
  # Ancestors
76
+ def ancestry_changed?
77
+ changed.include?(self.ancestry_base_class.ancestry_column.to_s)
78
+ end
79
+
68
80
  def ancestor_ids
69
- read_attribute(self.base_class.ancestry_column).to_s.split('/').map { |id| cast_primary_key(id) }
81
+ read_attribute(self.ancestry_base_class.ancestry_column).to_s.split('/').map { |id| cast_primary_key(id) }
70
82
  end
71
83
 
72
84
  def ancestor_conditions
73
- {self.base_class.primary_key => ancestor_ids}
85
+ {primary_key_with_table => ancestor_ids}
74
86
  end
75
87
 
76
88
  def ancestors depth_options = {}
77
- self.base_class.scope_depth(depth_options, depth).ordered_by_ancestry.scoped :conditions => ancestor_conditions
89
+ self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions
78
90
  end
79
91
 
80
92
  def path_ids
@@ -82,11 +94,11 @@ module Ancestry
82
94
  end
83
95
 
84
96
  def path_conditions
85
- {self.base_class.primary_key => path_ids}
97
+ {primary_key_with_table => path_ids}
86
98
  end
87
99
 
88
100
  def path depth_options = {}
89
- self.base_class.scope_depth(depth_options, depth).ordered_by_ancestry.scoped :conditions => path_conditions
101
+ self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where path_conditions
90
102
  end
91
103
 
92
104
  def depth
@@ -94,16 +106,16 @@ module Ancestry
94
106
  end
95
107
 
96
108
  def cache_depth
97
- write_attribute self.base_class.depth_cache_column, depth
109
+ write_attribute self.ancestry_base_class.depth_cache_column, depth
98
110
  end
99
111
 
100
112
  # Parent
101
113
  def parent= parent
102
- write_attribute(self.base_class.ancestry_column, if parent.blank? then nil else parent.child_ancestry end)
114
+ write_attribute(self.ancestry_base_class.ancestry_column, if parent.nil? then nil else parent.child_ancestry end)
103
115
  end
104
116
 
105
117
  def parent_id= parent_id
106
- self.parent = if parent_id.blank? then nil else self.base_class.find(parent_id) end
118
+ self.parent = if parent_id.blank? then nil else unscoped_find(parent_id) end
107
119
  end
108
120
 
109
121
  def parent_id
@@ -111,7 +123,11 @@ module Ancestry
111
123
  end
112
124
 
113
125
  def parent
114
- if parent_id.blank? then nil else self.base_class.find(parent_id) end
126
+ if parent_id.blank? then nil else unscoped_find(parent_id) end
127
+ end
128
+
129
+ def parent_id?
130
+ parent_id.present?
115
131
  end
116
132
 
117
133
  # Root
@@ -120,24 +136,25 @@ module Ancestry
120
136
  end
121
137
 
122
138
  def root
123
- if root_id == id then self else self.base_class.find(root_id) end
139
+ if root_id == id then self else unscoped_find(root_id) end
124
140
  end
125
141
 
126
142
  def is_root?
127
- read_attribute(self.base_class.ancestry_column).blank?
143
+ read_attribute(self.ancestry_base_class.ancestry_column).blank?
128
144
  end
145
+ alias :root? :is_root?
129
146
 
130
147
  # Children
131
148
  def child_conditions
132
- {self.base_class.ancestry_column => child_ancestry}
149
+ {ancestry_column_with_table => child_ancestry}
133
150
  end
134
151
 
135
152
  def children
136
- self.base_class.scoped :conditions => child_conditions
153
+ self.ancestry_base_class.where child_conditions
137
154
  end
138
155
 
139
156
  def child_ids
140
- children.all(:select => self.base_class.primary_key).map(&self.base_class.primary_key.to_sym)
157
+ children.select(self.ancestry_base_class.primary_key).map(&self.ancestry_base_class.primary_key.to_sym)
141
158
  end
142
159
 
143
160
  def has_children?
@@ -150,15 +167,15 @@ module Ancestry
150
167
 
151
168
  # Siblings
152
169
  def sibling_conditions
153
- {self.base_class.ancestry_column => read_attribute(self.base_class.ancestry_column)}
170
+ {ancestry_column_with_table => read_attribute(self.ancestry_base_class.ancestry_column)}
154
171
  end
155
172
 
156
173
  def siblings
157
- self.base_class.scoped :conditions => sibling_conditions
174
+ self.ancestry_base_class.where sibling_conditions
158
175
  end
159
176
 
160
177
  def sibling_ids
161
- siblings.all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
178
+ siblings.select(self.ancestry_base_class.primary_key).collect(&self.ancestry_base_class.primary_key.to_sym)
162
179
  end
163
180
 
164
181
  def has_siblings?
@@ -171,28 +188,28 @@ module Ancestry
171
188
 
172
189
  # Descendants
173
190
  def descendant_conditions
174
- ["#{self.base_class.table_name}.#{self.base_class.ancestry_column} like ? or #{self.base_class.table_name}.#{self.base_class.ancestry_column} = ?", "#{child_ancestry}/%", child_ancestry]
191
+ ["#{ancestry_column_with_table} like ? or #{ancestry_column_with_table} = ?", "#{child_ancestry}/%", child_ancestry]
175
192
  end
176
193
 
177
194
  def descendants depth_options = {}
178
- self.base_class.ordered_by_ancestry.scope_depth(depth_options, depth).scoped :conditions => descendant_conditions
195
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where descendant_conditions
179
196
  end
180
197
 
181
198
  def descendant_ids depth_options = {}
182
- descendants(depth_options).all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
199
+ descendants(depth_options).select(self.ancestry_base_class.primary_key).collect(&self.ancestry_base_class.primary_key.to_sym)
183
200
  end
184
201
 
185
202
  # Subtree
186
203
  def subtree_conditions
187
- ["#{self.base_class.table_name}.#{self.base_class.primary_key} = ? or #{self.base_class.table_name}.#{self.base_class.ancestry_column} like ? or #{self.base_class.table_name}.#{self.base_class.ancestry_column} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
204
+ ["#{primary_key_with_table} = ? or #{ancestry_column_with_table} like ? or #{ancestry_column_with_table} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
188
205
  end
189
206
 
190
207
  def subtree depth_options = {}
191
- self.base_class.ordered_by_ancestry.scope_depth(depth_options, depth).scoped :conditions => subtree_conditions
208
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where subtree_conditions
192
209
  end
193
210
 
194
211
  def subtree_ids depth_options = {}
195
- subtree(depth_options).all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
212
+ subtree(depth_options).select(self.ancestry_base_class.primary_key).collect(&self.ancestry_base_class.primary_key.to_sym)
196
213
  end
197
214
 
198
215
  # Callback disabling
@@ -208,15 +225,6 @@ module Ancestry
208
225
 
209
226
  private
210
227
 
211
- # Workaround to support Rails 2
212
- def add_error_to_base error
213
- if rails_3
214
- errors[:base] << error
215
- else
216
- errors.add_to_base error
217
- end
218
- end
219
-
220
228
  def cast_primary_key(key)
221
229
  if primary_key_type == :string
222
230
  key
@@ -228,11 +236,28 @@ module Ancestry
228
236
  def primary_key_type
229
237
  @primary_key_type ||= column_for_attribute(self.class.primary_key).type
230
238
  end
231
-
232
239
  def unscoped_descendants
233
- self.base_class.send(:with_exclusive_scope) do
234
- self.base_class.all(:conditions => descendant_conditions)
240
+ self.ancestry_base_class.unscoped do
241
+ self.ancestry_base_class.where descendant_conditions
235
242
  end
236
243
  end
244
+
245
+ # basically validates the ancestry, but also applied if validation is
246
+ # bypassed to determine if chidren should be affected
247
+ def sane_ancestry?
248
+ ancestry.nil? || (ancestry.to_s =~ Ancestry::ANCESTRY_PATTERN && !ancestor_ids.include?(self.id))
249
+ end
250
+
251
+ def unscoped_find id
252
+ self.ancestry_base_class.unscoped { self.ancestry_base_class.find(id) }
253
+ end
254
+
255
+ def primary_key_with_table
256
+ "#{self.ancestry_base_class.table_name}.#{self.ancestry_base_class.primary_key}"
257
+ end
258
+
259
+ def ancestry_column_with_table
260
+ "#{self.ancestry_base_class.table_name}.#{self.ancestry_base_class.ancestry_column}"
261
+ end
237
262
  end
238
263
  end
metadata CHANGED
@@ -1,46 +1,38 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: ancestry
3
- version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 1
7
- - 3
8
- - 0
9
- version: 1.3.0
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0.rc1
5
+ prerelease: 6
10
6
  platform: ruby
11
- authors:
7
+ authors:
12
8
  - Stefan Kroes
13
9
  autorequire:
14
10
  bindir: bin
15
11
  cert_chain: []
16
-
17
- date: 2012-05-04 00:00:00 +02:00
18
- default_executable:
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
12
+ date: 2013-05-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
21
15
  name: activerecord
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
24
17
  none: false
25
- requirements:
26
- - - ">="
27
- - !ruby/object:Gem::Version
28
- segments:
29
- - 2
30
- - 3
31
- - 14
32
- version: 2.3.14
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
33
22
  type: :runtime
34
- version_requirements: *id001
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.0
35
30
  description: Organise ActiveRecord model into a tree structure
36
31
  email: s.a.kroes@gmail.com
37
32
  executables: []
38
-
39
33
  extensions: []
40
-
41
34
  extra_rdoc_files: []
42
-
43
- files:
35
+ files:
44
36
  - ancestry.gemspec
45
37
  - init.rb
46
38
  - install.rb
@@ -51,37 +43,34 @@ files:
51
43
  - lib/ancestry/instance_methods.rb
52
44
  - MIT-LICENSE
53
45
  - README.rdoc
54
- has_rdoc: true
55
46
  homepage: http://github.com/stefankroes/ancestry
56
47
  licenses: []
57
-
58
48
  post_install_message:
59
49
  rdoc_options: []
60
-
61
- require_paths:
50
+ require_paths:
62
51
  - lib
63
- required_ruby_version: !ruby/object:Gem::Requirement
52
+ required_ruby_version: !ruby/object:Gem::Requirement
64
53
  none: false
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- segments:
69
- - 0
70
- version: "0"
71
- required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
59
  none: false
73
- requirements:
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- segments:
77
- - 0
78
- version: "0"
60
+ requirements:
61
+ - - ! '>'
62
+ - !ruby/object:Gem::Version
63
+ version: 1.3.1
79
64
  requirements: []
80
-
81
65
  rubyforge_project:
82
- rubygems_version: 1.3.7
66
+ rubygems_version: 1.8.25
83
67
  signing_key:
84
68
  specification_version: 3
85
- summary: Ancestry allows the records of a ActiveRecord model to be organised in a tree structure, using a single, intuitively formatted database column. It exposes all the standard tree structure relations (ancestors, parent, root, children, siblings, descendants) and all of them can be fetched in a single sql query. Additional features are named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree into hashes and different strategies for dealing with orphaned records.
69
+ summary: Ancestry allows the records of a ActiveRecord model to be organised in a
70
+ tree structure, using a single, intuitively formatted database column. It exposes
71
+ all the standard tree structure relations (ancestors, parent, root, children, siblings,
72
+ descendants) and all of them can be fetched in a single sql query. Additional features
73
+ are named_scopes, integrity checking, integrity restoration, arrangement of (sub)tree
74
+ into hashes and different strategies for dealing with orphaned records.
86
75
  test_files: []
87
-
76
+ has_rdoc: