closure_tree 3.10.2 → 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.
data/README.md CHANGED
@@ -444,6 +444,16 @@ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
444
444
 
445
445
  ## Change log
446
446
 
447
+ ### 4.0.0
448
+
449
+ * Moved all of closure_tree's implementation-detail methods into a ```ClosureTree::Support```
450
+ instance, which removes almost all of the namespace pollution in your models that wasn't
451
+ for normal consumption. If you were using any of these methods, they're now available through
452
+ the "_ct" class and instance member.
453
+
454
+ *This change may break consumers*, so I incremented the major version number, even though no new
455
+ functionality was released.
456
+
447
457
  ### 3.10.2
448
458
 
449
459
  * Prevent faulty SQL statement when ```#siblings``` is called on an unsaved records.
@@ -1,55 +1,26 @@
1
- require 'closure_tree/columns'
2
- require 'closure_tree/deterministic_ordering'
1
+ require 'closure_tree/support'
3
2
  require 'closure_tree/model'
3
+ require 'closure_tree/deterministic_ordering'
4
4
  require 'closure_tree/numeric_deterministic_ordering'
5
5
  require 'closure_tree/with_advisory_lock'
6
6
 
7
7
  module ClosureTree
8
8
  module ActsAsTree
9
9
  def acts_as_tree(options = {})
10
-
11
- class_attribute :closure_tree_options
12
-
13
- self.closure_tree_options = {
14
- :ct_base_class => self,
15
- :parent_column_name => 'parent_id',
16
- :dependent => :nullify, # or :destroy or :delete_all -- see the README
17
- :name_column => 'name',
18
- :with_advisory_lock => true
19
- }.merge(options)
20
-
21
- raise IllegalArgumentException, "name_column can't be 'path'" if closure_tree_options[:name_column] == 'path'
22
-
23
- include ClosureTree::Columns
24
- extend ClosureTree::Columns
25
-
26
- include ClosureTree::WithAdvisoryLock
27
- extend ClosureTree::WithAdvisoryLock
10
+ class_attribute :_ct
11
+ self._ct = ClosureTree::Support.new(self, options)
28
12
 
29
13
  # Auto-inject the hierarchy table
30
14
  # See https://github.com/patshaughnessy/class_factory/blob/master/lib/class_factory/class_factory.rb
31
15
  class_attribute :hierarchy_class
32
- self.hierarchy_class = ct_class.parent.const_set short_hierarchy_class_name, Class.new(ActiveRecord::Base)
33
-
34
- self.hierarchy_class.class_eval <<-RUBY
35
- belongs_to :ancestor, :class_name => "#{ct_class.to_s}"
36
- belongs_to :descendant, :class_name => "#{ct_class.to_s}"
37
- attr_accessible :ancestor, :descendant, :generations
38
- def ==(other)
39
- self.class == other.class && ancestor == other.ancestor && descendant == other.descendant
40
- end
41
- alias :eql? :==
42
- def hash
43
- ancestor_id.hash << 31 ^ descendant_id.hash
44
- end
45
- RUBY
46
-
47
- self.hierarchy_class.table_name = hierarchy_table_name
16
+ self.hierarchy_class = _ct.hierarchy_class_for_model
48
17
 
49
18
  include ClosureTree::Model
50
- unless order_option.nil?
19
+ include ClosureTree::WithAdvisoryLock
20
+
21
+ if _ct.order_option
51
22
  include ClosureTree::DeterministicOrdering
52
- include ClosureTree::DeterministicNumericOrdering if order_is_numeric
23
+ include ClosureTree::DeterministicNumericOrdering if _ct.order_is_numeric?
53
24
  end
54
25
  end
55
26
  end
@@ -1,49 +1,19 @@
1
1
  module ClosureTree
2
2
  module DeterministicOrdering
3
- extend ActiveSupport::Concern
4
-
5
- module ClassAndInstanceMethods
6
- def order_column
7
- o = order_option
8
- o.split(' ', 2).first if o
9
- end
10
-
11
- def require_order_column
12
- raise ":order value, '#{order_option}', isn't a column" if order_column.nil?
13
- end
14
-
15
- def order_column_sym
16
- require_order_column
17
- order_column.to_sym
18
- end
19
-
20
- def quoted_order_column(include_table_name = true)
21
- require_order_column
22
- prefix = include_table_name ? "#{quoted_table_name}." : ""
23
- "#{prefix}#{connection.quote_column_name(order_column)}"
24
- end
25
- end
26
-
27
- include ClassAndInstanceMethods
28
-
29
- module ClassMethods
30
- include ClassAndInstanceMethods
31
- end
32
-
33
3
  def order_value
34
- read_attribute(order_column_sym)
4
+ read_attribute(_ct.order_column_sym)
35
5
  end
36
6
 
37
7
  def order_value=(new_order_value)
38
- write_attribute(order_column_sym, new_order_value)
8
+ write_attribute(_ct.order_column_sym, new_order_value)
39
9
  end
40
10
 
41
11
  def siblings_before
42
- siblings.where(["#{quoted_order_column} < ?", order_value])
12
+ siblings.where(["#{_ct.quoted_order_column} < ?", order_value])
43
13
  end
44
14
 
45
15
  def siblings_after
46
- siblings.where(["#{quoted_order_column} > ?", order_value])
16
+ siblings.where(["#{_ct.quoted_order_column} > ?", order_value])
47
17
  end
48
18
  end
49
- end
19
+ end
@@ -11,45 +11,50 @@ module ClosureTree
11
11
  before_destroy :ct_before_destroy
12
12
 
13
13
  belongs_to :parent,
14
- :class_name => ct_class.to_s,
15
- :foreign_key => parent_column_name
14
+ :class_name => _ct.model_class.to_s,
15
+ :foreign_key => _ct.parent_column_name
16
16
 
17
17
  unless defined?(ActiveModel::ForbiddenAttributesProtection) && ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
18
18
  attr_accessible :parent
19
19
  end
20
20
 
21
- has_many :children, with_order_option(
22
- :class_name => ct_class.to_s,
23
- :foreign_key => parent_column_name,
24
- :dependent => closure_tree_options[:dependent]
25
- )
21
+ has_many :children, _ct.with_order_option(
22
+ :class_name => _ct.model_class.to_s,
23
+ :foreign_key => _ct.parent_column_name,
24
+ :dependent => _ct.options[:dependent])
26
25
 
27
26
  has_many :ancestor_hierarchies,
28
- :class_name => hierarchy_class_name,
27
+ :class_name => _ct.hierarchy_class_name,
29
28
  :foreign_key => "descendant_id",
30
- :order => "#{quoted_hierarchy_table_name}.generations asc"
29
+ :order => "#{_ct.quoted_hierarchy_table_name}.generations asc"
31
30
 
32
31
  has_many :self_and_ancestors,
33
32
  :through => :ancestor_hierarchies,
34
33
  :source => :ancestor,
35
- :order => "#{quoted_hierarchy_table_name}.generations asc"
34
+ :order => "#{_ct.quoted_hierarchy_table_name}.generations asc"
36
35
 
37
36
  has_many :descendant_hierarchies,
38
- :class_name => hierarchy_class_name,
37
+ :class_name => _ct.hierarchy_class_name,
39
38
  :foreign_key => "ancestor_id",
40
- :order => "#{quoted_hierarchy_table_name}.generations asc"
39
+ :order => "#{_ct.quoted_hierarchy_table_name}.generations asc"
40
+
41
41
  # TODO: FIXME: this collection currently ignores sort_order
42
42
  # (because the quoted_table_named would need to be joined in to get to the order column)
43
43
 
44
- has_many :self_and_descendants,
44
+ has_many :self_and_descendants, _ct.with_order_option(
45
45
  :through => :descendant_hierarchies,
46
46
  :source => :descendant,
47
- :order => append_order("#{quoted_hierarchy_table_name}.generations asc")
47
+ :order => "#{_ct.quoted_hierarchy_table_name}.generations asc")
48
+ end
49
+
50
+ # Delegate to the Support instance on the class:
51
+ def _ct
52
+ self.class._ct
48
53
  end
49
54
 
50
55
  # Returns true if this node has no parents.
51
56
  def root?
52
- ct_parent_id.nil?
57
+ parent_id.nil?
53
58
  end
54
59
 
55
60
  # Returns true if this node has a parent, and is not a root.
@@ -64,7 +69,7 @@ module ClosureTree
64
69
 
65
70
  # Returns the farthest ancestor, or self if +root?+
66
71
  def root
67
- self_and_ancestors.where(parent_column_name.to_sym => nil).first
72
+ self_and_ancestors.where(_ct.parent_column_name.to_sym => nil).first
68
73
  end
69
74
 
70
75
  def leaves
@@ -82,18 +87,18 @@ module ClosureTree
82
87
  end
83
88
 
84
89
  def ancestor_ids
85
- ids_from(ancestors)
90
+ _ct.ids_from(ancestors)
86
91
  end
87
92
 
88
93
  # Returns an array, root first, of self_and_ancestors' values of the +to_s_column+, which defaults
89
94
  # to the +name_column+.
90
95
  # (so child.ancestry_path == +%w{grandparent parent child}+
91
- def ancestry_path(to_s_column = name_column)
96
+ def ancestry_path(to_s_column = _ct.name_column)
92
97
  self_and_ancestors.reverse.collect { |n| n.send to_s_column.to_sym }
93
98
  end
94
99
 
95
100
  def child_ids
96
- ids_from(children)
101
+ _ct.ids_from(children)
97
102
  end
98
103
 
99
104
  def descendants
@@ -101,12 +106,12 @@ module ClosureTree
101
106
  end
102
107
 
103
108
  def descendant_ids
104
- ids_from(descendants)
109
+ _ct.ids_from(descendants)
105
110
  end
106
111
 
107
112
  def self_and_siblings
108
- s = ct_base_class.where(parent_column_sym => parent)
109
- order_option.present? ? s.order(quoted_order_column) : s
113
+ s = _ct.base_class.where(_ct.parent_column_sym => parent)
114
+ _ct.order_option.present? ? s.order(_ct.quoted_order_column) : s
110
115
  end
111
116
 
112
117
  def siblings
@@ -114,7 +119,7 @@ module ClosureTree
114
119
  end
115
120
 
116
121
  def sibling_ids
117
- ids_from(siblings)
122
+ _ct.ids_from(siblings)
118
123
  end
119
124
 
120
125
  # Alias for appending to the children collection.
@@ -127,8 +132,8 @@ module ClosureTree
127
132
  # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
128
133
  def find_by_path(path)
129
134
  return self if path.empty?
130
- parent_constraint = "#{quoted_parent_column_name} = #{ct_quote(id)}"
131
- ct_class.ct_scoped_to_path(path, parent_constraint).first
135
+ parent_constraint = "#{_ct.quoted_parent_column_name} = #{_ct.quote(id)}"
136
+ self.class.ct_scoped_to_path(path, parent_constraint).first
132
137
  end
133
138
 
134
139
  # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
@@ -139,8 +144,8 @@ module ClosureTree
139
144
  child_name = subpath.shift
140
145
  return self unless child_name
141
146
  child = transaction do
142
- attrs = {name_sym => child_name}
143
- attrs[:type] = self.type if ct_subclass? && ct_has_type?
147
+ attrs = {_ct.name_sym => child_name}
148
+ attrs[:type] = self.type if _ct.subclass? && _ct.has_type?
144
149
  self.children.where(attrs).first || begin
145
150
  child = self.class.new(attributes.merge(attrs))
146
151
  self.children << child
@@ -153,22 +158,22 @@ module ClosureTree
153
158
  end
154
159
 
155
160
  def find_all_by_generation(generation_level)
156
- s = ct_base_class.joins(<<-SQL)
161
+ s = _ct.base_class.joins(<<-SQL)
157
162
  INNER JOIN (
158
163
  SELECT descendant_id
159
- FROM #{quoted_hierarchy_table_name}
160
- WHERE ancestor_id = #{ct_quote(self.id)}
164
+ FROM #{_ct.quoted_hierarchy_table_name}
165
+ WHERE ancestor_id = #{_ct.quote(self.id)}
161
166
  GROUP BY 1
162
- HAVING MAX(#{quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
163
- ) AS descendants ON (#{quoted_table_name}.#{ct_base_class.primary_key} = descendants.descendant_id)
167
+ HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
168
+ ) AS descendants ON (#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} = descendants.descendant_id)
164
169
  SQL
165
- order_option ? s.order(order_option) : s
170
+ _ct.scope_with_order(s)
166
171
  end
167
172
 
168
173
  def hash_tree_scope(limit_depth = nil)
169
174
  scope = self_and_descendants
170
175
  if limit_depth
171
- scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
176
+ scope.where("#{_ct.quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
172
177
  else
173
178
  scope
174
179
  end
@@ -178,15 +183,15 @@ module ClosureTree
178
183
  self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
179
184
  end
180
185
 
181
- def ct_parent_id
182
- read_attribute(parent_column_sym)
186
+ def parent_id
187
+ read_attribute(_ct.parent_column_sym)
183
188
  end
184
189
 
185
190
  def ct_validate
186
- if changes[parent_column_name] &&
191
+ if changes[_ct.parent_column_name] &&
187
192
  parent.present? &&
188
193
  parent.self_and_ancestors.include?(self)
189
- errors.add(parent_column_sym, "You cannot add an ancestor as a descendant")
194
+ errors.add(_ct.parent_column_sym, "You cannot add an ancestor as a descendant")
190
195
  end
191
196
  end
192
197
 
@@ -196,7 +201,7 @@ module ClosureTree
196
201
  end
197
202
 
198
203
  def ct_after_save
199
- rebuild! if changes[parent_column_name] || @was_new_record
204
+ rebuild! if changes[_ct.parent_column_name] || @was_new_record
200
205
  @was_new_record = false # we aren't new anymore.
201
206
  true # don't cancel anything.
202
207
  end
@@ -207,11 +212,11 @@ module ClosureTree
207
212
  hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
208
213
  unless root?
209
214
  sql = <<-SQL
210
- INSERT INTO #{quoted_hierarchy_table_name}
215
+ INSERT INTO #{_ct.quoted_hierarchy_table_name}
211
216
  (ancestor_id, descendant_id, generations)
212
- SELECT x.ancestor_id, #{ct_quote(id)}, x.generations + 1
213
- FROM #{quoted_hierarchy_table_name} x
214
- WHERE x.descendant_id = #{ct_quote(self.ct_parent_id)}
217
+ SELECT x.ancestor_id, #{_ct.quote(id)}, x.generations + 1
218
+ FROM #{_ct.quoted_hierarchy_table_name} x
219
+ WHERE x.descendant_id = #{_ct.quote(self.parent_id)}
215
220
  SQL
216
221
  connection.execute sql.strip
217
222
  end
@@ -221,7 +226,7 @@ module ClosureTree
221
226
 
222
227
  def ct_before_destroy
223
228
  delete_hierarchy_references
224
- if closure_tree_options[:dependent] == :nullify
229
+ if _ct.options[:dependent] == :nullify
225
230
  children.each { |c| c.rebuild! }
226
231
  end
227
232
  end
@@ -232,36 +237,25 @@ module ClosureTree
232
237
  # See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
233
238
  # Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
234
239
  connection.execute <<-SQL
235
- DELETE FROM #{quoted_hierarchy_table_name}
240
+ DELETE FROM #{_ct.quoted_hierarchy_table_name}
236
241
  WHERE descendant_id IN (
237
242
  SELECT DISTINCT descendant_id
238
- FROM ( SELECT descendant_id
239
- FROM #{quoted_hierarchy_table_name}
240
- WHERE ancestor_id = #{ct_quote(id)}
243
+ FROM (SELECT descendant_id
244
+ FROM #{_ct.quoted_hierarchy_table_name}
245
+ WHERE ancestor_id = #{_ct.quote(id)}
241
246
  ) AS x )
242
- OR descendant_id = #{ct_quote(id)}
247
+ OR descendant_id = #{_ct.quote(id)}
243
248
  SQL
244
249
  end
245
250
 
246
251
  def without_self(scope)
247
252
  return scope if self.new_record?
248
- scope.where(["#{quoted_table_name}.#{ct_base_class.primary_key} != ?", self])
249
- end
250
-
251
- def ids_from(scope)
252
- if scope.respond_to? :pluck
253
- scope.pluck(:id)
254
- else
255
- scope.select(:id).collect(&:id)
256
- end
253
+ scope.where(["#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} != ?", self])
257
254
  end
258
255
 
259
- # TODO: _parent_id will be removed in the next major version
260
- alias :_parent_id :ct_parent_id
261
-
262
256
  module ClassMethods
263
257
  def roots
264
- scope_with_order(where(parent_column_name => nil))
258
+ _ct.scope_with_order(where(_ct.parent_column_name => nil))
265
259
  end
266
260
 
267
261
  # Returns an arbitrary node that has no parents.
@@ -279,12 +273,12 @@ module ClosureTree
279
273
  s = joins(<<-SQL)
280
274
  INNER JOIN (
281
275
  SELECT ancestor_id
282
- FROM #{quoted_hierarchy_table_name}
276
+ FROM #{_ct.quoted_hierarchy_table_name}
283
277
  GROUP BY 1
284
- HAVING MAX(#{quoted_hierarchy_table_name}.generations) = 0
285
- ) AS leaves ON (#{quoted_table_name}.#{primary_key} = leaves.ancestor_id)
278
+ HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = 0
279
+ ) AS leaves ON (#{_ct.quoted_table_name}.#{primary_key} = leaves.ancestor_id)
286
280
  SQL
287
- order_option ? s.order(order_option) : s
281
+ _ct.scope_with_order(s)
288
282
  end
289
283
 
290
284
  # Rebuilds the hierarchy table based on the parent_id column in the database.
@@ -301,39 +295,39 @@ module ClosureTree
301
295
  s = joins(<<-SQL)
302
296
  INNER JOIN (
303
297
  SELECT #{primary_key} as root_id
304
- FROM #{quoted_table_name}
305
- WHERE #{quoted_parent_column_name} IS NULL
298
+ FROM #{_ct.quoted_table_name}
299
+ WHERE #{_ct.quoted_parent_column_name} IS NULL
306
300
  ) AS roots ON (1 = 1)
307
301
  INNER JOIN (
308
302
  SELECT ancestor_id, descendant_id
309
- FROM #{quoted_hierarchy_table_name}
303
+ FROM #{_ct.quoted_hierarchy_table_name}
310
304
  GROUP BY 1, 2
311
305
  HAVING MAX(generations) = #{generation_level.to_i}
312
306
  ) AS descendants ON (
313
- #{quoted_table_name}.#{primary_key} = descendants.descendant_id
307
+ #{_ct.quoted_table_name}.#{primary_key} = descendants.descendant_id
314
308
  AND roots.root_id = descendants.ancestor_id
315
309
  )
316
310
  SQL
317
- order_option ? s.order(order_option) : s
311
+ _ct.scope_with_order(s)
318
312
  end
319
313
 
320
314
  # Find the node whose +ancestry_path+ is +path+
321
315
  def find_by_path(path)
322
- parent_constraint = "#{quoted_parent_column_name} IS NULL"
316
+ parent_constraint = "#{_ct.quoted_parent_column_name} IS NULL"
323
317
  ct_scoped_to_path(path, parent_constraint).first
324
318
  end
325
319
 
326
320
  def ct_scoped_to_path(path, parent_constraint)
327
321
  path = path.is_a?(Enumerable) ? path.dup : [path]
328
- scope = scoped.where(name_sym => path.last).readonly(false)
322
+ scope = scoped.where(_ct.name_sym => path.last).readonly(false)
329
323
  path[0..-2].reverse.each_with_index do |ea, idx|
330
- subtable = idx == 0 ? quoted_table_name : "p#{idx - 1}"
324
+ subtable = idx == 0 ? _ct.quoted_table_name : "p#{idx - 1}"
331
325
  scope = scope.joins(<<-SQL)
332
- INNER JOIN #{quoted_table_name} AS p#{idx} ON p#{idx}.id = #{subtable}.#{parent_column_name}
326
+ INNER JOIN #{_ct.quoted_table_name} AS p#{idx} ON p#{idx}.id = #{subtable}.#{_ct.parent_column_name}
333
327
  SQL
334
- scope = scope.where("p#{idx}.#{quoted_name_column} = #{ct_quote(ea)}")
328
+ scope = scope.where("p#{idx}.#{_ct.quoted_name_column} = #{_ct.quote(ea)}")
335
329
  end
336
- root_table_name = path.size > 1 ? "p#{path.size - 2}" : quoted_table_name
330
+ root_table_name = path.size > 1 ? "p#{path.size - 2}" : _ct.quoted_table_name
337
331
  scope.where("#{root_table_name}.#{parent_constraint}")
338
332
  end
339
333
 
@@ -345,8 +339,8 @@ module ClosureTree
345
339
  ct_with_advisory_lock do
346
340
  # shenanigans because find_or_create can't infer we want the same class as this:
347
341
  # Note that roots will already be constrained to this subclass (in the case of polymorphism):
348
- root = roots.where(name_sym => root_name).first
349
- root ||= create!(attributes.merge(name_sym => root_name))
342
+ root = roots.where(_ct.name_sym => root_name).first
343
+ root ||= create!(attributes.merge(_ct.name_sym => root_name))
350
344
  root.find_or_create_by_path(subpath, attributes)
351
345
  end
352
346
  end
@@ -358,13 +352,13 @@ module ClosureTree
358
352
  generation_depth = <<-SQL
359
353
  INNER JOIN (
360
354
  SELECT descendant_id, MAX(generations) as depth
361
- FROM #{quoted_hierarchy_table_name}
355
+ FROM #{_ct.quoted_hierarchy_table_name}
362
356
  GROUP BY descendant_id
363
357
  #{limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ""}
364
358
  ) AS generation_depth
365
- ON #{quoted_table_name}.#{primary_key} = generation_depth.descendant_id
359
+ ON #{_ct.quoted_table_name}.#{primary_key} = generation_depth.descendant_id
366
360
  SQL
367
- scoped.joins(generation_depth).order(append_order("generation_depth.depth"))
361
+ _ct.scope_with_order(joins(generation_depth), "generation_depth.depth")
368
362
  end
369
363
 
370
364
  # Builds nested hash structure using the scope returned from the passed in scope
@@ -377,7 +371,7 @@ module ClosureTree
377
371
  if ea.root? || tree.empty? # We're at the top of the tree.
378
372
  tree[ea] = h
379
373
  else
380
- id_to_hash[ea.ct_parent_id][ea] = h
374
+ id_to_hash[ea.parent_id][ea] = h
381
375
  end
382
376
  end
383
377
  tree
@@ -9,21 +9,21 @@ module ClosureTree
9
9
  SELECT
10
10
  count(*) as total_descendants,
11
11
  max(generations) as max_depth
12
- FROM #{quoted_hierarchy_table_name}
13
- WHERE ancestor_id = #{ct_quote(self.id)}
12
+ FROM #{_ct.quoted_hierarchy_table_name}
13
+ WHERE ancestor_id = #{_ct.quote(self.id)}
14
14
  SQL
15
15
  join_sql = <<-SQL
16
- JOIN #{quoted_hierarchy_table_name} anc_hier
17
- ON anc_hier.descendant_id = #{quoted_hierarchy_table_name}.descendant_id
18
- JOIN #{quoted_table_name} anc
16
+ JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
17
+ ON anc_hier.descendant_id = #{_ct.quoted_hierarchy_table_name}.descendant_id
18
+ JOIN #{_ct.quoted_table_name} anc
19
19
  ON anc.id = anc_hier.ancestor_id
20
- JOIN #{quoted_hierarchy_table_name} depths
21
- ON depths.ancestor_id = #{ct_quote(self.id)} AND depths.descendant_id = anc.id
20
+ JOIN #{_ct.quoted_hierarchy_table_name} depths
21
+ ON depths.ancestor_id = #{_ct.quote(self.id)} AND depths.descendant_id = anc.id
22
22
  SQL
23
- node_score = "(1 + anc.#{quoted_order_column(false)}) * " +
23
+ node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
24
24
  "power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - depths.generations)"
25
25
  order_by = "sum(#{node_score})"
26
- self_and_descendants.joins(join_sql).group("#{quoted_table_name}.id").reorder(order_by)
26
+ self_and_descendants.joins(join_sql).group("#{_ct.quoted_table_name}.id").reorder(order_by)
27
27
  end
28
28
 
29
29
  module ClassMethods
@@ -32,23 +32,23 @@ module ClosureTree
32
32
  SELECT
33
33
  count(*) as total_descendants,
34
34
  max(generations) as max_depth
35
- FROM #{quoted_hierarchy_table_name}
35
+ FROM #{_ct.quoted_hierarchy_table_name}
36
36
  SQL
37
37
  join_sql = <<-SQL
38
- JOIN #{quoted_hierarchy_table_name} anc_hier
39
- ON anc_hier.descendant_id = #{quoted_table_name}.id
40
- JOIN #{quoted_table_name} anc
38
+ JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
39
+ ON anc_hier.descendant_id = #{_ct.quoted_table_name}.id
40
+ JOIN #{_ct.quoted_table_name} anc
41
41
  ON anc.id = anc_hier.ancestor_id
42
42
  JOIN (
43
43
  SELECT descendant_id, max(generations) AS max_depth
44
- FROM #{quoted_hierarchy_table_name}
44
+ FROM #{_ct.quoted_hierarchy_table_name}
45
45
  GROUP BY 1
46
46
  ) AS depths ON depths.descendant_id = anc.id
47
47
  SQL
48
- node_score = "(1 + anc.#{quoted_order_column(false)}) * " +
48
+ node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
49
49
  "power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - depths.max_depth)"
50
50
  order_by = "sum(#{node_score})"
51
- joins(join_sql).group("#{quoted_table_name}.id").reorder(order_by)
51
+ joins(join_sql).group("#{_ct.quoted_table_name}.id").reorder(order_by)
52
52
  end
53
53
  end
54
54
 
@@ -69,12 +69,12 @@ module ClosureTree
69
69
  sibling_node.order_value = self.order_value.to_i + (add_after ? 1 : -1)
70
70
  # We need to incr the before_siblings to make room for sibling_node:
71
71
  if use_update_all
72
- col = quoted_order_column(false)
72
+ col = _ct.quoted_order_column(false)
73
73
  # issue 21: we have to use the base class, so STI doesn't get in the way of only updating the child class instances:
74
- ct_base_class.update_all(
74
+ _ct.base_class.update_all(
75
75
  ["#{col} = #{col} #{add_after ? '+' : '-'} 1", "updated_at = now()"],
76
- ["#{quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
77
- ct_parent_id,
76
+ ["#{_ct.quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
77
+ parent_id,
78
78
  sibling_node.order_value])
79
79
  else
80
80
  last_value = sibling_node.order_value.to_i
@@ -90,4 +90,4 @@ module ClosureTree
90
90
  end
91
91
  end
92
92
  end
93
- end
93
+ end
@@ -0,0 +1,193 @@
1
+ module ClosureTree
2
+ class Support
3
+
4
+ attr_reader :model_class
5
+ attr_reader :options
6
+
7
+ def initialize(model_class, options)
8
+ @model_class = model_class
9
+ @options = {
10
+ :base_class => model_class,
11
+ :parent_column_name => 'parent_id',
12
+ :dependent => :nullify, # or :destroy or :delete_all -- see the README
13
+ :name_column => 'name',
14
+ :with_advisory_lock => true
15
+ }.merge(options)
16
+
17
+ raise IllegalArgumentException, "name_column can't be 'path'" if options[:name_column] == 'path'
18
+
19
+ end
20
+
21
+ def connection
22
+ model_class.connection
23
+ end
24
+
25
+ def hierarchy_class_for_model
26
+ hierarchy_class = model_class.parent.const_set(short_hierarchy_class_name, Class.new(ActiveRecord::Base))
27
+ hierarchy_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
28
+ belongs_to :ancestor, :class_name => "#{model_class}"
29
+ belongs_to :descendant, :class_name => "#{model_class}"
30
+ attr_accessible :ancestor, :descendant, :generations
31
+ def ==(other)
32
+ self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
33
+ end
34
+ alias :eql? :==
35
+ def hash
36
+ ancestor_id.hash << 31 ^ descendant_id.hash
37
+ end
38
+ RUBY
39
+ hierarchy_class.table_name = hierarchy_table_name
40
+ hierarchy_class
41
+ end
42
+
43
+ def parent_column_name
44
+ options[:parent_column_name]
45
+ end
46
+
47
+ def parent_column_sym
48
+ parent_column_name.to_sym
49
+ end
50
+
51
+ def has_name?
52
+ model_class.new.attributes.include? options[:name_column]
53
+ end
54
+
55
+ def name_column
56
+ options[:name_column]
57
+ end
58
+
59
+ def name_sym
60
+ name_column.to_sym
61
+ end
62
+
63
+ def hierarchy_table_name
64
+ # We need to use the table_name, not something like ct_class.to_s.demodulize + "_hierarchies",
65
+ # because they may have overridden the table name, which is what we want to be consistent with
66
+ # in order for the schema to make sense.
67
+ tablename = options[:hierarchy_table_name] ||
68
+ remove_prefix_and_suffix(table_name).singularize + "_hierarchies"
69
+
70
+ ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
71
+ end
72
+
73
+ def hierarchy_class_name
74
+ options[:hierarchy_class_name] || model_class.to_s + "Hierarchy"
75
+ end
76
+
77
+ # Returns the constant name of the hierarchy_class
78
+ #
79
+ # @return [String] the constant name
80
+ #
81
+ # @example
82
+ # Namespace::Model.hierarchy_class_name # => "Namespace::ModelHierarchy"
83
+ # Namespace::Model.short_hierarchy_class_name # => "ModelHierarchy"
84
+ def short_hierarchy_class_name
85
+ hierarchy_class_name.split('::').last
86
+ end
87
+
88
+ def quoted_hierarchy_table_name
89
+ connection.quote_table_name hierarchy_table_name
90
+ end
91
+
92
+ def quoted_parent_column_name
93
+ connection.quote_column_name parent_column_name
94
+ end
95
+
96
+ def quoted_name_column
97
+ connection.quote_column_name name_column
98
+ end
99
+
100
+ def quote(field)
101
+ connection.quote(field)
102
+ end
103
+
104
+ def order_option
105
+ options[:order]
106
+ end
107
+
108
+ def with_order_option(options)
109
+ order_option ? options.merge(:order => order_option) : options
110
+ end
111
+
112
+ def scope_with_order(scope, additional_order_by = nil)
113
+ if order_option
114
+ scope.order(*([additional_order_by, order_option].compact))
115
+ else
116
+ scope
117
+ end
118
+ end
119
+
120
+ def with_order_option(options)
121
+ if order_option
122
+ options[:order] = [options[:order], order_option].compact.join(",")
123
+ end
124
+ options
125
+ end
126
+
127
+ def order_is_numeric?
128
+ # The table might not exist yet (in the case of ActiveRecord::Observer use, see issue 32)
129
+ return false if order_option.nil? || !model_class.table_exists?
130
+ c = model_class.columns_hash[order_option]
131
+ c && c.type == :integer
132
+ end
133
+
134
+ def order_column
135
+ o = order_option
136
+ o.split(' ', 2).first if o
137
+ end
138
+
139
+ def require_order_column
140
+ raise ":order value, '#{order_option}', isn't a column" if order_column.nil?
141
+ end
142
+
143
+ def order_column_sym
144
+ require_order_column
145
+ order_column.to_sym
146
+ end
147
+
148
+ def quoted_order_column(include_table_name = true)
149
+ require_order_column
150
+ prefix = include_table_name ? "#{quoted_table_name}." : ""
151
+ "#{prefix}#{connection.quote_column_name(order_column)}"
152
+ end
153
+
154
+ # This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
155
+ def base_class
156
+ options[:base_class]
157
+ end
158
+
159
+ def subclass?
160
+ model_class != model_class.base_class
161
+ end
162
+
163
+ def attribute_names
164
+ @attribute_names ||= model_class.new.attributes.keys - model_class.protected_attributes.to_a
165
+ end
166
+
167
+ def has_type?
168
+ attribute_names.include? 'type'
169
+ end
170
+
171
+ def table_name
172
+ model_class.table_name
173
+ end
174
+
175
+ def quoted_table_name
176
+ connection.quote_table_name table_name
177
+ end
178
+
179
+ def remove_prefix_and_suffix(table_name)
180
+ prefix = Regexp.escape(ActiveRecord::Base.table_name_prefix)
181
+ suffix = Regexp.escape(ActiveRecord::Base.table_name_suffix)
182
+ table_name.gsub(/^#{prefix}(.+)#{suffix}$/, "\\1")
183
+ end
184
+
185
+ def ids_from(scope)
186
+ if scope.respond_to? :pluck
187
+ scope.pluck(:id)
188
+ else
189
+ scope.select(:id).collect(&:id)
190
+ end
191
+ end
192
+ end
193
+ end
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = "3.10.2" unless defined?(::ClosureTree::VERSION)
2
+ VERSION = "4.0.0" unless defined?(::ClosureTree::VERSION)
3
3
  end
@@ -1,18 +1,28 @@
1
1
  require 'with_advisory_lock'
2
+ require 'active_support/concern'
2
3
 
3
4
  module ClosureTree
4
5
  module WithAdvisoryLock
6
+ extend ActiveSupport::Concern
7
+
5
8
  def ct_with_advisory_lock(&block)
6
- if closure_tree_options[:with_advisory_lock]
7
- with_advisory_lock("closure_tree") do
8
- transaction do
9
+ self.class.ct_with_advisory_lock(&block)
10
+ end
11
+
12
+ included do
13
+ class_eval do
14
+ def self.ct_with_advisory_lock(&block)
15
+ if _ct.options[:with_advisory_lock]
16
+ with_advisory_lock("closure_tree") do
17
+ transaction do
18
+ yield
19
+ end
20
+ end
21
+ else
9
22
  yield
10
23
  end
11
24
  end
12
- else
13
- yield
14
25
  end
15
26
  end
16
27
  end
17
28
  end
18
-
@@ -5,8 +5,8 @@ describe Namespace::Type do
5
5
  context "class injection" do
6
6
  it "should build hierarchy classname correctly" do
7
7
  Namespace::Type.hierarchy_class.to_s.should == "Namespace::TypeHierarchy"
8
- Namespace::Type.hierarchy_class_name.should == "Namespace::TypeHierarchy"
9
- Namespace::Type.short_hierarchy_class_name.should == "TypeHierarchy"
8
+ Namespace::Type._ct.hierarchy_class_name.should == "Namespace::TypeHierarchy"
9
+ Namespace::Type._ct.short_hierarchy_class_name.should == "TypeHierarchy"
10
10
  end
11
11
  end
12
12
 
@@ -6,7 +6,7 @@ describe "threadhot" do
6
6
  TagHierarchy.delete_all
7
7
  Tag.delete_all
8
8
  @iterations = 5
9
- @workers = 8
9
+ @workers = 6 # Travis CI workers can't reliably handle larger numbers
10
10
  @parent = nil
11
11
  end
12
12
 
@@ -54,4 +54,4 @@ RSpec.configure do |config|
54
54
  config.after(:all) do
55
55
  FileUtils.remove_entry_secure ENV['FLOCK_DIR']
56
56
  end
57
- end
57
+ end
@@ -259,12 +259,12 @@ shared_examples_for "Tag (2)" do
259
259
  context "class injection" do
260
260
  it "should build hierarchy classname correctly" do
261
261
  Tag.hierarchy_class.to_s.should == "TagHierarchy"
262
- Tag.hierarchy_class_name.should == "TagHierarchy"
263
- Tag.short_hierarchy_class_name.should == "TagHierarchy"
262
+ Tag._ct.hierarchy_class_name.should == "TagHierarchy"
263
+ Tag._ct.short_hierarchy_class_name.should == "TagHierarchy"
264
264
  end
265
265
 
266
266
  it "should have a correct parent column name" do
267
- Tag.parent_column_name.should == "parent_id"
267
+ Tag._ct.parent_column_name.should == "parent_id"
268
268
  end
269
269
  end
270
270
 
@@ -469,7 +469,7 @@ describe "Tag with UUID" do
469
469
  TagHierarchy.reset_column_information
470
470
 
471
471
  # We have to reset a few other caches
472
- Tag.closure_tree_options[:hierarchy_table_name] = 'tag_hierarchies_uuid'
472
+ Tag._ct.options[:hierarchy_table_name] = 'tag_hierarchies_uuid'
473
473
  Tag.reflections.each do |key, ref|
474
474
  ref.instance_variable_set('@table_name', nil)
475
475
  ref.instance_variable_set('@quoted_table_name', nil)
@@ -116,7 +116,7 @@ describe "empty db" do
116
116
  end
117
117
 
118
118
  it "supports siblings" do
119
- User.order_option.should be_nil
119
+ User._ct.order_option.should be_nil
120
120
  a = User.create(:email => "a")
121
121
  b1 = a.children.create(:email => "b1")
122
122
  b2 = a.children.create(:email => "b2")
@@ -127,7 +127,7 @@ describe "empty db" do
127
127
 
128
128
  context "when a user is not yet saved" do
129
129
  it "supports siblings" do
130
- User.order_option.should be_nil
130
+ User._ct.order_option.should be_nil
131
131
  a = User.create(:email => "a")
132
132
  b1 = a.children.new(:email => "b1")
133
133
  b2 = a.children.create(:email => "b2")
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: closure_tree
3
3
  version: !ruby/object:Gem::Version
4
- hash: 43
4
+ hash: 63
5
5
  prerelease:
6
6
  segments:
7
- - 3
8
- - 10
9
- - 2
10
- version: 3.10.2
7
+ - 4
8
+ - 0
9
+ - 0
10
+ version: 4.0.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Matthew McEachen
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2013-04-16 00:00:00 -07:00
18
+ date: 2013-05-18 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -201,10 +201,10 @@ extra_rdoc_files: []
201
201
 
202
202
  files:
203
203
  - lib/closure_tree/acts_as_tree.rb
204
- - lib/closure_tree/columns.rb
205
204
  - lib/closure_tree/deterministic_ordering.rb
206
205
  - lib/closure_tree/model.rb
207
206
  - lib/closure_tree/numeric_deterministic_ordering.rb
207
+ - lib/closure_tree/support.rb
208
208
  - lib/closure_tree/version.rb
209
209
  - lib/closure_tree/with_advisory_lock.rb
210
210
  - lib/closure_tree.rb
@@ -1,127 +0,0 @@
1
- module ClosureTree
2
-
3
- # Mixed into both classes and instances to provide easy access to the column names
4
- module Columns
5
-
6
- def parent_column_name
7
- closure_tree_options[:parent_column_name]
8
- end
9
-
10
- def parent_column_sym
11
- parent_column_name.to_sym
12
- end
13
-
14
- def has_name?
15
- ct_class.new.attributes.include? closure_tree_options[:name_column]
16
- end
17
-
18
- def name_column
19
- closure_tree_options[:name_column]
20
- end
21
-
22
- def name_sym
23
- name_column.to_sym
24
- end
25
-
26
- def hierarchy_table_name
27
- # We need to use the table_name, not something like ct_class.to_s.demodulize + "_hierarchies",
28
- # because they may have overridden the table name, which is what we want to be consistent with
29
- # in order for the schema to make sense.
30
- tablename = closure_tree_options[:hierarchy_table_name] ||
31
- remove_prefix_and_suffix(ct_table_name).singularize + "_hierarchies"
32
-
33
- ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
34
- end
35
-
36
- def hierarchy_class_name
37
- closure_tree_options[:hierarchy_class_name] || ct_class.to_s + "Hierarchy"
38
- end
39
-
40
-
41
- #
42
- # Returns the constant name of the hierarchy_class
43
- #
44
- # @return [String] the constant name
45
- #
46
- # @example
47
- # Namespace::Model.hierarchy_class_name # => "Namespace::ModelHierarchy"
48
- # Namespace::Model.short_hierarchy_class_name # => "ModelHierarchy"
49
- def short_hierarchy_class_name
50
- hierarchy_class_name.split('::').last
51
- end
52
-
53
- def quoted_hierarchy_table_name
54
- connection.quote_table_name hierarchy_table_name
55
- end
56
-
57
- def quoted_parent_column_name
58
- connection.quote_column_name parent_column_name
59
- end
60
-
61
- def quoted_name_column
62
- connection.quote_column_name name_column
63
- end
64
-
65
- def ct_quote(field)
66
- connection.quote(field)
67
- end
68
-
69
- def order_option
70
- closure_tree_options[:order]
71
- end
72
-
73
- def with_order_option(options)
74
- order_option ? options.merge(:order => order_option) : options
75
- end
76
-
77
- def scope_with_order(scope)
78
- order_option ? scope.order(order_option) : scope
79
- end
80
-
81
- def append_order(order_by)
82
- order_option ? "#{order_by}, #{order_option}" : order_by
83
- end
84
-
85
- def order_is_numeric
86
- # The table might not exist yet (in the case of ActiveRecord::Observer use, see issue 32)
87
- return false if order_option.nil? || !self.table_exists?
88
- c = ct_class.columns_hash[order_option]
89
- c && c.type == :integer
90
- end
91
-
92
- def ct_class
93
- (self.is_a?(Class) ? self : self.class)
94
- end
95
-
96
- # This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
97
- def ct_base_class
98
- ct_class.closure_tree_options[:ct_base_class]
99
- end
100
-
101
- def ct_subclass?
102
- ct_class != ct_class.base_class
103
- end
104
-
105
- def ct_attribute_names
106
- @ct_attr_names ||= ct_class.new.attributes.keys - ct_class.protected_attributes.to_a
107
- end
108
-
109
- def ct_has_type?
110
- ct_attribute_names.include? 'type'
111
- end
112
-
113
- def ct_table_name
114
- ct_class.table_name
115
- end
116
-
117
- def quoted_table_name
118
- connection.quote_table_name ct_table_name
119
- end
120
-
121
- def remove_prefix_and_suffix(table_name)
122
- prefix = Regexp.escape(ActiveRecord::Base.table_name_prefix)
123
- suffix = Regexp.escape(ActiveRecord::Base.table_name_suffix)
124
- table_name.gsub(/^#{prefix}(.+)#{suffix}$/, "\\1")
125
- end
126
- end
127
- end