closure_tree 3.7.3 → 3.8.0

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