closure_tree 3.7.3 → 3.8.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.
@@ -0,0 +1,102 @@
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
+ def quoted_hierarchy_table_name
41
+ connection.quote_table_name hierarchy_table_name
42
+ end
43
+
44
+ def quoted_parent_column_name
45
+ connection.quote_column_name parent_column_name
46
+ end
47
+
48
+ def order_option
49
+ closure_tree_options[:order]
50
+ end
51
+
52
+ def with_order_option(options)
53
+ order_option ? options.merge(:order => order_option) : options
54
+ end
55
+
56
+ def append_order(order_by)
57
+ order_option ? "#{order_by}, #{order_option}" : order_by
58
+ end
59
+
60
+ def order_is_numeric
61
+ # The table might not exist yet (in the case of ActiveRecord::Observer use, see issue 32)
62
+ return false if order_option.nil? || !self.table_exists?
63
+ c = ct_class.columns_hash[order_option]
64
+ c && c.type == :integer
65
+ end
66
+
67
+ def ct_class
68
+ (self.is_a?(Class) ? self : self.class)
69
+ end
70
+
71
+ # This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
72
+ def ct_base_class
73
+ ct_class.closure_tree_options[:ct_base_class]
74
+ end
75
+
76
+ def ct_subclass?
77
+ ct_class != ct_class.base_class
78
+ end
79
+
80
+ def ct_attribute_names
81
+ @ct_attr_names ||= ct_class.new.attributes.keys - ct_class.protected_attributes.to_a
82
+ end
83
+
84
+ def ct_has_type?
85
+ ct_attribute_names.include? 'type'
86
+ end
87
+
88
+ def ct_table_name
89
+ ct_class.table_name
90
+ end
91
+
92
+ def quoted_table_name
93
+ connection.quote_table_name ct_table_name
94
+ end
95
+
96
+ def remove_prefix_and_suffix(table_name)
97
+ prefix = Regexp.escape(ActiveRecord::Base.table_name_prefix)
98
+ suffix = Regexp.escape(ActiveRecord::Base.table_name_suffix)
99
+ table_name.gsub(/^#{prefix}(.+)#{suffix}$/, "\\1")
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,39 @@
1
+ module ClosureTree
2
+ module DeterministicOrdering
3
+ def order_column
4
+ o = order_option
5
+ o.split(' ', 2).first if o
6
+ end
7
+
8
+ def require_order_column
9
+ raise ":order value, '#{order_option}', isn't a column" if order_column.nil?
10
+ end
11
+
12
+ def order_column_sym
13
+ require_order_column
14
+ order_column.to_sym
15
+ end
16
+
17
+ def order_value
18
+ read_attribute(order_column_sym)
19
+ end
20
+
21
+ def order_value=(new_order_value)
22
+ write_attribute(order_column_sym, new_order_value)
23
+ end
24
+
25
+ def quoted_order_column(include_table_name = true)
26
+ require_order_column
27
+ prefix = include_table_name ? "#{quoted_table_name}." : ""
28
+ "#{prefix}#{connection.quote_column_name(order_column)}"
29
+ end
30
+
31
+ def siblings_before
32
+ siblings.where(["#{quoted_order_column} < ?", order_value])
33
+ end
34
+
35
+ def siblings_after
36
+ siblings.where(["#{quoted_order_column} > ?", order_value])
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,373 @@
1
+ require 'active_support/concern'
2
+
3
+ module ClosureTree
4
+ module Model
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ validate :ct_validate
9
+ before_save :ct_before_save
10
+ after_save :ct_after_save
11
+ before_destroy :ct_before_destroy
12
+
13
+ belongs_to :parent,
14
+ :class_name => ct_class.to_s,
15
+ :foreign_key => parent_column_name
16
+
17
+ unless defined?(ActiveModel::ForbiddenAttributesProtection) && ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
18
+ attr_accessible :parent
19
+ end
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
+ )
26
+
27
+ has_many :ancestor_hierarchies,
28
+ :class_name => hierarchy_class_name,
29
+ :foreign_key => "descendant_id",
30
+ :order => "#{quoted_hierarchy_table_name}.generations asc",
31
+ :dependent => :destroy
32
+
33
+ has_many :self_and_ancestors,
34
+ :through => :ancestor_hierarchies,
35
+ :source => :ancestor,
36
+ :order => "#{quoted_hierarchy_table_name}.generations asc"
37
+
38
+ has_many :descendant_hierarchies,
39
+ :class_name => hierarchy_class_name,
40
+ :foreign_key => "ancestor_id",
41
+ :order => "#{quoted_hierarchy_table_name}.generations asc",
42
+ :dependent => :destroy
43
+ # TODO: FIXME: this collection currently ignores sort_order
44
+ # (because the quoted_table_named would need to be joined in to get to the order column)
45
+
46
+ has_many :self_and_descendants,
47
+ :through => :descendant_hierarchies,
48
+ :source => :descendant,
49
+ :order => append_order("#{quoted_hierarchy_table_name}.generations asc")
50
+ end
51
+
52
+ # Returns true if this node has no parents.
53
+ def root?
54
+ ct_parent_id.nil?
55
+ end
56
+
57
+ # Returns true if this node has a parent, and is not a root.
58
+ def child?
59
+ !parent.nil?
60
+ end
61
+
62
+ # Returns true if this node has no children.
63
+ def leaf?
64
+ children.empty?
65
+ end
66
+
67
+ # Returns the farthest ancestor, or self if +root?+
68
+ def root
69
+ self_and_ancestors.where(parent_column_name.to_sym => nil).first
70
+ end
71
+
72
+ def leaves
73
+ self_and_descendants.leaves
74
+ end
75
+
76
+ def depth
77
+ ancestors.size
78
+ end
79
+
80
+ alias :level :depth
81
+
82
+ def ancestors
83
+ without_self(self_and_ancestors)
84
+ end
85
+
86
+ def ancestor_ids
87
+ ids_from(ancestors)
88
+ end
89
+
90
+ # Returns an array, root first, of self_and_ancestors' values of the +to_s_column+, which defaults
91
+ # to the +name_column+.
92
+ # (so child.ancestry_path == +%w{grandparent parent child}+
93
+ def ancestry_path(to_s_column = name_column)
94
+ self_and_ancestors.reverse.collect { |n| n.send to_s_column.to_sym }
95
+ end
96
+
97
+ def descendants
98
+ without_self(self_and_descendants)
99
+ end
100
+
101
+ def descendant_ids
102
+ ids_from(descendants)
103
+ end
104
+
105
+ def self_and_siblings
106
+ s = ct_base_class.where(parent_column_sym => parent)
107
+ order_option.present? ? s.order(quoted_order_column) : s
108
+ end
109
+
110
+ def siblings
111
+ without_self(self_and_siblings)
112
+ end
113
+
114
+ def sibling_ids
115
+ ids_from(siblings)
116
+ end
117
+
118
+ # Alias for appending to the children collection.
119
+ # You can also add directly to the children collection, if you'd prefer.
120
+ def add_child(child_node)
121
+ children << child_node
122
+ child_node
123
+ end
124
+
125
+ # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
126
+ def find_by_path(path)
127
+ path = path.is_a?(Enumerable) ? path.dup : [path]
128
+ node = self
129
+ while !path.empty? && node
130
+ node = node.children.where(name_sym => path.shift).first
131
+ end
132
+ node
133
+ end
134
+
135
+ # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
136
+ def find_or_create_by_path(path, attributes = {})
137
+ ct_with_advisory_lock do
138
+ subpath = path.is_a?(Enumerable) ? path.dup : [path]
139
+ child_name = subpath.shift
140
+ return self unless child_name
141
+ child = transaction do
142
+ attrs = {name_sym => child_name}
143
+ attrs[:type] = self.type if ct_subclass? && ct_has_type?
144
+ self.children.where(attrs).first || begin
145
+ child = self.class.new(attributes.merge(attrs))
146
+ self.children << child
147
+ child
148
+ end
149
+ end
150
+ child.find_or_create_by_path(subpath, attributes)
151
+ end
152
+ end
153
+
154
+ def find_all_by_generation(generation_level)
155
+ s = ct_base_class.joins(<<-SQL)
156
+ INNER JOIN (
157
+ SELECT descendant_id
158
+ FROM #{quoted_hierarchy_table_name}
159
+ WHERE ancestor_id = #{ct_quote(self.id)}
160
+ GROUP BY 1
161
+ HAVING MAX(#{quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
162
+ ) AS descendants ON (#{quoted_table_name}.#{ct_base_class.primary_key} = descendants.descendant_id)
163
+ SQL
164
+ order_option ? s.order(order_option) : s
165
+ end
166
+
167
+ def hash_tree_scope(limit_depth = nil)
168
+ scope = self_and_descendants
169
+ if limit_depth
170
+ scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
171
+ else
172
+ scope
173
+ end
174
+ end
175
+
176
+ def hash_tree(options = {})
177
+ self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
178
+ end
179
+
180
+ def ct_parent_id
181
+ read_attribute(parent_column_sym)
182
+ end
183
+
184
+ def ct_validate
185
+ if changes[parent_column_name] &&
186
+ parent.present? &&
187
+ parent.self_and_ancestors.include?(self)
188
+ errors.add(parent_column_sym, "You cannot add an ancestor as a descendant")
189
+ end
190
+ end
191
+
192
+ def ct_before_save
193
+ @was_new_record = new_record?
194
+ true # don't cancel the save
195
+ end
196
+
197
+ def ct_after_save
198
+ rebuild! if changes[parent_column_name] || @was_new_record
199
+ @was_new_record = false # we aren't new anymore.
200
+ true # don't cancel anything.
201
+ end
202
+
203
+ def rebuild!
204
+ ct_with_advisory_lock do
205
+ delete_hierarchy_references unless @was_new_record
206
+ hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
207
+ unless root?
208
+ connection.execute <<-SQL
209
+ INSERT INTO #{quoted_hierarchy_table_name}
210
+ (ancestor_id, descendant_id, generations)
211
+ SELECT x.ancestor_id, #{ct_quote(id)}, x.generations + 1
212
+ FROM #{quoted_hierarchy_table_name} x
213
+ WHERE x.descendant_id = #{ct_quote(self.ct_parent_id)}
214
+ SQL
215
+ end
216
+ children.each { |c| c.rebuild! }
217
+ end
218
+ end
219
+
220
+ def ct_before_destroy
221
+ delete_hierarchy_references
222
+ if closure_tree_options[:dependent] == :nullify
223
+ children.each { |c| c.rebuild! }
224
+ end
225
+ end
226
+
227
+ def delete_hierarchy_references
228
+ # The crazy double-wrapped sub-subselect works around MySQL's limitation of subselects on the same table that is being mutated.
229
+ # It shouldn't affect performance of postgresql.
230
+ # See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
231
+ # Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
232
+ connection.execute <<-SQL
233
+ DELETE FROM #{quoted_hierarchy_table_name}
234
+ WHERE descendant_id IN (
235
+ SELECT DISTINCT descendant_id
236
+ FROM ( SELECT descendant_id
237
+ FROM #{quoted_hierarchy_table_name}
238
+ WHERE ancestor_id = #{ct_quote(id)}
239
+ ) AS x )
240
+ OR descendant_id = #{ct_quote(id)}
241
+ SQL
242
+ end
243
+
244
+ def without_self(scope)
245
+ scope.where(["#{quoted_table_name}.#{ct_base_class.primary_key} != ?", self])
246
+ end
247
+
248
+ def ids_from(scope)
249
+ if scope.respond_to? :pluck
250
+ scope.pluck(:id)
251
+ else
252
+ scope.select(:id).collect(&:id)
253
+ end
254
+ end
255
+
256
+ def ct_quote(field)
257
+ self.class.connection.quote(field)
258
+ end
259
+
260
+ # TODO: _parent_id will be removed in the next major version
261
+ alias :_parent_id :ct_parent_id
262
+
263
+ module ClassMethods
264
+ def roots
265
+ where(parent_column_name => nil)
266
+ end
267
+
268
+ # Returns an arbitrary node that has no parents.
269
+ def root
270
+ roots.first
271
+ end
272
+
273
+ # There is no default depth limit. This might be crazy-big, depending
274
+ # on your tree shape. Hash huge trees at your own peril!
275
+ def hash_tree(options = {})
276
+ build_hash_tree(hash_tree_scope(options[:limit_depth]))
277
+ end
278
+
279
+ def leaves
280
+ s = joins(<<-SQL)
281
+ INNER JOIN (
282
+ SELECT ancestor_id
283
+ FROM #{quoted_hierarchy_table_name}
284
+ GROUP BY 1
285
+ HAVING MAX(#{quoted_hierarchy_table_name}.generations) = 0
286
+ ) AS leaves ON (#{quoted_table_name}.#{primary_key} = leaves.ancestor_id)
287
+ SQL
288
+ order_option ? s.order(order_option) : s
289
+ end
290
+
291
+ # Rebuilds the hierarchy table based on the parent_id column in the database.
292
+ # Note that the hierarchy table will be truncated.
293
+ def rebuild!
294
+ ct_with_advisory_lock do
295
+ hierarchy_class.delete_all # not destroy_all -- we just want a simple truncate.
296
+ roots.each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
297
+ end
298
+ nil
299
+ end
300
+
301
+ def find_all_by_generation(generation_level)
302
+ s = joins(<<-SQL)
303
+ INNER JOIN (
304
+ SELECT #{primary_key} as root_id
305
+ FROM #{quoted_table_name}
306
+ WHERE #{quoted_parent_column_name} IS NULL
307
+ ) AS roots ON (1 = 1)
308
+ INNER JOIN (
309
+ SELECT ancestor_id, descendant_id
310
+ FROM #{quoted_hierarchy_table_name}
311
+ GROUP BY 1, 2
312
+ HAVING MAX(generations) = #{generation_level.to_i}
313
+ ) AS descendants ON (
314
+ #{quoted_table_name}.#{primary_key} = descendants.descendant_id
315
+ AND roots.root_id = descendants.ancestor_id
316
+ )
317
+ SQL
318
+ order_option ? s.order(order_option) : s
319
+ end
320
+
321
+ # Find the node whose +ancestry_path+ is +path+
322
+ def find_by_path(path)
323
+ subpath = path.dup
324
+ root = roots.where(name_sym => subpath.shift).first
325
+ root.find_by_path(subpath) if root
326
+ end
327
+
328
+ # Find or create nodes such that the +ancestry_path+ is +path+
329
+ def find_or_create_by_path(path, attributes = {})
330
+ subpath = path.dup
331
+ root_name = subpath.shift
332
+ ct_with_advisory_lock do
333
+ # shenanigans because find_or_create can't infer we want the same class as this:
334
+ # Note that roots will already be constrained to this subclass (in the case of polymorphism):
335
+ root = roots.where(name_sym => root_name).first
336
+ root ||= create!(attributes.merge(name_sym => root_name))
337
+ root.find_or_create_by_path(subpath, attributes)
338
+ end
339
+ end
340
+
341
+ def hash_tree_scope(limit_depth = nil)
342
+ # Deepest generation, within limit, for each descendant
343
+ # NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
344
+ generation_depth = <<-SQL
345
+ INNER JOIN (
346
+ SELECT descendant_id, MAX(generations) as depth
347
+ FROM #{quoted_hierarchy_table_name}
348
+ GROUP BY descendant_id
349
+ #{limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ""}
350
+ ) AS generation_depth
351
+ ON #{quoted_table_name}.#{primary_key} = generation_depth.descendant_id
352
+ SQL
353
+ scoped.joins(generation_depth).order(append_order("generation_depth.depth"))
354
+ end
355
+
356
+ # Builds nested hash structure using the scope returned from the passed in scope
357
+ def build_hash_tree(tree_scope)
358
+ tree = ActiveSupport::OrderedHash.new
359
+ id_to_hash = {}
360
+
361
+ tree_scope.each do |ea|
362
+ h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
363
+ if ea.root? || tree.empty? # We're at the top of the tree.
364
+ tree[ea] = h
365
+ else
366
+ id_to_hash[ea.ct_parent_id][ea] = h
367
+ end
368
+ end
369
+ tree
370
+ end
371
+ end
372
+ end
373
+ end