closure_tree 4.1.0 → 4.2.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.
@@ -1,6 +1,6 @@
1
1
  # This module is only included if the order column is an integer.
2
2
  module ClosureTree
3
- module DeterministicNumericOrdering
3
+ module NumericDeterministicOrdering
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  def self_and_descendants_preordered
@@ -62,8 +62,7 @@ module ClosureTree
62
62
 
63
63
  def add_sibling(sibling_node, add_after = true)
64
64
  fail "can't add self as sibling" if self == sibling_node
65
- # issue 40: we need to lock the parent to prevent deadlocks on parallel sibling additions
66
- ct_with_advisory_lock do
65
+ _ct.with_advisory_lock do
67
66
  if self.order_value.nil? || siblings_before.without(sibling_node).empty?
68
67
  update_attribute(:order_value, 0)
69
68
  end
@@ -1,5 +1,10 @@
1
+ require 'closure_tree/support_flags'
2
+ require 'closure_tree/support_attributes'
3
+
1
4
  module ClosureTree
2
5
  class Support
6
+ include ClosureTree::SupportFlags
7
+ include ClosureTree::SupportAttributes
3
8
 
4
9
  attr_reader :model_class
5
10
  attr_reader :options
@@ -16,22 +21,6 @@ module ClosureTree
16
21
  raise IllegalArgumentException, "name_column can't be 'path'" if options[:name_column] == 'path'
17
22
  end
18
23
 
19
- def connection
20
- model_class.connection
21
- end
22
-
23
- def use_attr_accessible?
24
- ActiveRecord::VERSION::MAJOR == 3 &&
25
- defined?(ActiveModel::MassAssignmentSecurity) &&
26
- model_class.ancestors.include?(ActiveModel::MassAssignmentSecurity)
27
- end
28
-
29
- def include_forbidden_attributes_protection?
30
- ActiveRecord::VERSION::MAJOR == 3 &&
31
- defined?(ActiveModel::ForbiddenAttributesProtection) &&
32
- model_class.ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
33
- end
34
-
35
24
  def hierarchy_class_for_model
36
25
  hierarchy_class = model_class.parent.const_set(short_hierarchy_class_name, Class.new(ActiveRecord::Base))
37
26
  use_attr_accessible = use_attr_accessible?
@@ -53,26 +42,6 @@ module ClosureTree
53
42
  hierarchy_class
54
43
  end
55
44
 
56
- def parent_column_name
57
- options[:parent_column_name]
58
- end
59
-
60
- def parent_column_sym
61
- parent_column_name.to_sym
62
- end
63
-
64
- def has_name?
65
- model_class.new.attributes.include? options[:name_column]
66
- end
67
-
68
- def name_column
69
- options[:name_column]
70
- end
71
-
72
- def name_sym
73
- name_column.to_sym
74
- end
75
-
76
45
  def hierarchy_table_name
77
46
  # We need to use the table_name, not something like ct_class.to_s.demodulize + "_hierarchies",
78
47
  # because they may have overridden the table name, which is what we want to be consistent with
@@ -83,45 +52,10 @@ module ClosureTree
83
52
  ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
84
53
  end
85
54
 
86
- def hierarchy_class_name
87
- options[:hierarchy_class_name] || model_class.to_s + "Hierarchy"
88
- end
89
-
90
- # Returns the constant name of the hierarchy_class
91
- #
92
- # @return [String] the constant name
93
- #
94
- # @example
95
- # Namespace::Model.hierarchy_class_name # => "Namespace::ModelHierarchy"
96
- # Namespace::Model.short_hierarchy_class_name # => "ModelHierarchy"
97
- def short_hierarchy_class_name
98
- hierarchy_class_name.split('::').last
99
- end
100
-
101
- def quoted_hierarchy_table_name
102
- connection.quote_table_name hierarchy_table_name
103
- end
104
-
105
- def quoted_id_column_name
106
- connection.quote_column_name model_class.primary_key
107
- end
108
-
109
- def quoted_parent_column_name
110
- connection.quote_column_name parent_column_name
111
- end
112
-
113
- def quoted_name_column
114
- connection.quote_column_name name_column
115
- end
116
-
117
55
  def quote(field)
118
56
  connection.quote(field)
119
57
  end
120
58
 
121
- def order_option?
122
- !options[:order].nil?
123
- end
124
-
125
59
  def with_order_option(opts)
126
60
  if order_option?
127
61
  opts[:order] = [opts[:order], options[:order]].compact.join(",")
@@ -151,64 +85,6 @@ module ClosureTree
151
85
  end
152
86
  end
153
87
 
154
- def order_is_numeric?
155
- # The table might not exist yet (in the case of ActiveRecord::Observer use, see issue 32)
156
- return false if !order_option? || !model_class.table_exists?
157
- c = model_class.columns_hash[order_column]
158
- c && c.type == :integer
159
- end
160
-
161
- def order_column
162
- o = options[:order]
163
- if o.nil?
164
- nil
165
- elsif o.is_a?(String)
166
- o.split(' ', 2).first
167
- else
168
- o.to_s
169
- end
170
- end
171
-
172
- def require_order_column
173
- raise ":order value, '#{options[:order]}', isn't a column" if order_column.nil?
174
- end
175
-
176
- def order_column_sym
177
- require_order_column
178
- order_column.to_sym
179
- end
180
-
181
- def quoted_order_column(include_table_name = true)
182
- require_order_column
183
- prefix = include_table_name ? "#{quoted_table_name}." : ""
184
- "#{prefix}#{connection.quote_column_name(order_column)}"
185
- end
186
-
187
- # This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
188
- def base_class
189
- options[:base_class]
190
- end
191
-
192
- def subclass?
193
- model_class != model_class.base_class
194
- end
195
-
196
- def attribute_names
197
- @attribute_names ||= model_class.new.attributes.keys - model_class.protected_attributes.to_a
198
- end
199
-
200
- def has_type?
201
- attribute_names.include? 'type'
202
- end
203
-
204
- def table_name
205
- model_class.table_name
206
- end
207
-
208
- def quoted_table_name
209
- connection.quote_table_name table_name
210
- end
211
-
212
88
  def remove_prefix_and_suffix(table_name)
213
89
  prefix = Regexp.escape(ActiveRecord::Base.table_name_prefix)
214
90
  suffix = Regexp.escape(ActiveRecord::Base.table_name_suffix)
@@ -222,5 +98,17 @@ module ClosureTree
222
98
  scope.select(model_class.primary_key).map { |ea| ea._ct_id }
223
99
  end
224
100
  end
101
+
102
+ def with_advisory_lock(&block)
103
+ if options[:with_advisory_lock]
104
+ model_class.with_advisory_lock("closure_tree") do
105
+ model_class.transaction do
106
+ yield
107
+ end
108
+ end
109
+ else
110
+ yield
111
+ end
112
+ end
225
113
  end
226
114
  end
@@ -0,0 +1,99 @@
1
+ module ClosureTree
2
+ module SupportAttributes
3
+
4
+ # This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
5
+ def base_class
6
+ options[:base_class]
7
+ end
8
+
9
+ def attribute_names
10
+ @attribute_names ||= model_class.new.attributes.keys - model_class.protected_attributes.to_a
11
+ end
12
+
13
+ def connection
14
+ model_class.connection
15
+ end
16
+
17
+ def table_name
18
+ model_class.table_name
19
+ end
20
+
21
+ def quoted_table_name
22
+ connection.quote_table_name(table_name)
23
+ end
24
+
25
+ def hierarchy_class_name
26
+ options[:hierarchy_class_name] || model_class.to_s + "Hierarchy"
27
+ end
28
+
29
+ def parent_column_name
30
+ options[:parent_column_name]
31
+ end
32
+
33
+ def parent_column_sym
34
+ parent_column_name.to_sym
35
+ end
36
+
37
+ def name_column
38
+ options[:name_column]
39
+ end
40
+
41
+ def name_sym
42
+ name_column.to_sym
43
+ end
44
+
45
+ # Returns the constant name of the hierarchy_class
46
+ #
47
+ # @return [String] the constant name
48
+ #
49
+ # @example
50
+ # Namespace::Model.hierarchy_class_name # => "Namespace::ModelHierarchy"
51
+ # Namespace::Model.short_hierarchy_class_name # => "ModelHierarchy"
52
+ def short_hierarchy_class_name
53
+ hierarchy_class_name.split('::').last
54
+ end
55
+
56
+ def quoted_hierarchy_table_name
57
+ connection.quote_table_name hierarchy_table_name
58
+ end
59
+
60
+ def quoted_id_column_name
61
+ connection.quote_column_name model_class.primary_key
62
+ end
63
+
64
+ def quoted_parent_column_name
65
+ connection.quote_column_name parent_column_name
66
+ end
67
+
68
+ def quoted_name_column
69
+ connection.quote_column_name name_column
70
+ end
71
+
72
+ def order_column
73
+ o = options[:order]
74
+ if o.nil?
75
+ nil
76
+ elsif o.is_a?(String)
77
+ o.split(' ', 2).first
78
+ else
79
+ o.to_s
80
+ end
81
+ end
82
+
83
+ def require_order_column
84
+ raise ":order value, '#{options[:order]}', isn't a column" if order_column.nil?
85
+ end
86
+
87
+ def order_column_sym
88
+ require_order_column
89
+ order_column.to_sym
90
+ end
91
+
92
+ def quoted_order_column(include_table_name = true)
93
+ require_order_column
94
+ prefix = include_table_name ? "#{quoted_table_name}." : ""
95
+ "#{prefix}#{connection.quote_column_name(order_column)}"
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,41 @@
1
+ module ClosureTree
2
+ module SupportFlags
3
+
4
+ def use_attr_accessible?
5
+ ActiveRecord::VERSION::MAJOR == 3 &&
6
+ defined?(ActiveModel::MassAssignmentSecurity) &&
7
+ model_class.respond_to?(:accessible_attributes) &&
8
+ model_class.accessible_attributes.present?
9
+ end
10
+
11
+ def include_forbidden_attributes_protection?
12
+ ActiveRecord::VERSION::MAJOR == 3 &&
13
+ defined?(ActiveModel::ForbiddenAttributesProtection) &&
14
+ model_class.ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
15
+ end
16
+
17
+ def order_option?
18
+ !options[:order].nil?
19
+ end
20
+
21
+ def order_is_numeric?
22
+ # The table might not exist yet (in the case of ActiveRecord::Observer use, see issue 32)
23
+ return false if !order_option? || !model_class.table_exists?
24
+ c = model_class.columns_hash[order_column]
25
+ c && c.type == :integer
26
+ end
27
+
28
+ def subclass?
29
+ model_class != model_class.base_class
30
+ end
31
+
32
+ def has_type?
33
+ attribute_names.include? 'type'
34
+ end
35
+
36
+ def has_name?
37
+ model_class.new.attributes.include? options[:name_column]
38
+ end
39
+
40
+ end
41
+ end
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = Gem::Version.new("4.1.0") unless defined?(::ClosureTree::VERSION)
2
+ VERSION = Gem::Version.new("4.2.0") unless defined?(::ClosureTree::VERSION)
3
3
  end
@@ -88,7 +88,6 @@ describe Label do
88
88
  it "should find or create by path" do
89
89
  date = DateLabel.find_or_create_by_path(%w{2011 November 23})
90
90
  date.ancestry_path.should == %w{2011 November 23}
91
- date.parent
92
91
  date.self_and_ancestors.each { |ea| ea.class.should == DateLabel }
93
92
  date.name.should == "23"
94
93
  date.parent.name.should == "November"
@@ -3,7 +3,7 @@ require 'uuidtools'
3
3
  class Tag < ActiveRecord::Base
4
4
  acts_as_tree :dependent => :destroy, :order => "name"
5
5
  before_destroy :add_destroyed_tag
6
- attr_accessible :name if _ct.use_attr_accessible?
6
+ attr_accessible :name, :title if _ct.use_attr_accessible?
7
7
  def to_s
8
8
  name
9
9
  end
@@ -18,7 +18,7 @@ class UUIDTag < ActiveRecord::Base
18
18
  before_create :set_uuid
19
19
  acts_as_tree :dependent => :destroy, :order => 'name', :parent_column_name => 'parent_uuid'
20
20
  before_destroy :add_destroyed_tag
21
- attr_accessible :name if _ct.use_attr_accessible?
21
+ attr_accessible :name, :title if _ct.use_attr_accessible?
22
22
 
23
23
  def set_uuid
24
24
  self.uuid = UUIDTools::UUID.timestamp_create.to_s
@@ -34,10 +34,8 @@ class UUIDTag < ActiveRecord::Base
34
34
  end
35
35
  end
36
36
 
37
- USE_ATTR_ACCESSIBLE = Tag._ct.use_attr_accessible?
38
-
39
37
  class DestroyedTag < ActiveRecord::Base
40
- attr_accessible :name if USE_ATTR_ACCESSIBLE
38
+ attr_accessible :name if Tag._ct.use_attr_accessible?
41
39
  end
42
40
 
43
41
  class User < ActiveRecord::Base
@@ -52,7 +50,7 @@ class User < ActiveRecord::Base
52
50
  Contract.where(:user_id => descendant_ids)
53
51
  end
54
52
 
55
- attr_accessible :email, :referrer if USE_ATTR_ACCESSIBLE
53
+ attr_accessible :email, :referrer if _ct.use_attr_accessible?
56
54
 
57
55
  def to_s
58
56
  email
@@ -65,11 +63,12 @@ end
65
63
 
66
64
  class Label < ActiveRecord::Base
67
65
  # make sure order doesn't matter
68
- attr_accessible :name if USE_ATTR_ACCESSIBLE
69
66
  acts_as_tree :order => :sort_order, # <- LOOK IT IS A SYMBOL OMG
70
67
  :parent_column_name => "mother_id",
71
68
  :dependent => :destroy
72
69
 
70
+ attr_accessible :name if _ct.use_attr_accessible?
71
+
73
72
  def to_s
74
73
  "#{self.class}: #{name}"
75
74
  end
@@ -9,7 +9,7 @@ shared_examples_for "Tag (without fixtures)" do
9
9
 
10
10
  it 'has correct accessible_attributes' do
11
11
  if tag_class._ct.use_attr_accessible?
12
- tag_class.accessible_attributes.to_a.should =~ %w(parent name)
12
+ tag_class.accessible_attributes.to_a.should =~ %w(parent name title)
13
13
  end
14
14
  end
15
15
 
@@ -226,6 +226,24 @@ shared_examples_for "Tag (without fixtures)" do
226
226
  end
227
227
  end
228
228
 
229
+ context 'with_ancestor' do
230
+ it 'works with no rows' do
231
+ tag_class.with_ancestor().to_a.should be_empty
232
+ end
233
+ it 'finds only children' do
234
+ c = tag_class.find_or_create_by_path %w(A B C)
235
+ a, b = c.parent.parent, c.parent
236
+ e = tag_class.find_or_create_by_path %w(D E)
237
+ tag_class.with_ancestor(a).to_a.should == [b, c]
238
+ end
239
+ it 'limits subsequent where clauses' do
240
+ a1c = tag_class.find_or_create_by_path %w(A1 B C)
241
+ a2c = tag_class.find_or_create_by_path %w(A2 B C)
242
+ tag_class.where(:name => "C").to_a.should =~ [a1c, a2c]
243
+ tag_class.with_ancestor(a1c.parent.parent).where(:name => "C").to_a.should == [a1c]
244
+ end
245
+ end
246
+
229
247
  context "paths" do
230
248
  before :each do
231
249
  @child = tag_class.find_or_create_by_path(%w(grandparent parent child))
@@ -298,6 +316,26 @@ shared_examples_for "Tag (without fixtures)" do
298
316
  # instance method:
299
317
  a.find_or_create_by_path(%w{b c}).ancestry_path.should == %w{a b c}
300
318
  end
319
+
320
+ it "should respect attribute hashes with both selection and creation" do
321
+ expected_title = 'something else'
322
+ attrs = {:title => expected_title}
323
+ existing_title = @grandparent.title
324
+ new_grandparent = tag_class.find_or_create_by_path(%w{grandparent}, attrs)
325
+ new_grandparent.should_not == @grandparent
326
+ new_grandparent.title.should == expected_title
327
+ @grandparent.reload.title.should == existing_title
328
+ end
329
+
330
+ it "should create a hierarchy with a given attribute" do
331
+ expected_title = 'unicorn rainbows'
332
+ attrs = {:title => expected_title}
333
+ child = tag_class.find_or_create_by_path(%w{grandparent parent child}, attrs)
334
+ child.should_not == @child
335
+ [child, child.parent, child.parent.parent].each do |ea|
336
+ ea.title.should == expected_title
337
+ end
338
+ end
301
339
  end
302
340
 
303
341
  context "hash_tree" do
@@ -311,6 +349,7 @@ shared_examples_for "Tag (without fixtures)" do
311
349
  @d2 = @b.find_or_create_by_path %w(c2 d2)
312
350
  @c2 = @d2.parent
313
351
  @full_tree = {@a => {@b => {@c1 => {@d1 => {}}, @c2 => {@d2 => {}}}, @b2 => {}}}
352
+ #File.open("example.dot", "w") { |f| f.write(tag_class.root.to_dot_digraph) }
314
353
  end
315
354
 
316
355
  context "#hash_tree" do