closure_tree 4.1.0 → 4.2.0

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