closure_tree 3.10.2 → 4.0.0

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