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.
- checksums.yaml +15 -0
- data/README.md +15 -3
- data/lib/closure_tree/acts_as_tree.rb +12 -6
- data/lib/closure_tree/digraphs.rb +31 -0
- data/lib/closure_tree/finders.rb +152 -0
- data/lib/closure_tree/hash_tree.rb +59 -0
- data/lib/closure_tree/hierarchy_maintenance.rb +87 -0
- data/lib/closure_tree/model.rb +0 -283
- data/lib/closure_tree/numeric_deterministic_ordering.rb +2 -3
- data/lib/closure_tree/support.rb +17 -129
- data/lib/closure_tree/support_attributes.rb +99 -0
- data/lib/closure_tree/support_flags.rb +41 -0
- data/lib/closure_tree/version.rb +1 -1
- data/spec/label_spec.rb +0 -1
- data/spec/support/models.rb +6 -7
- data/spec/tag_examples.rb +40 -1
- data/spec/user_spec.rb +14 -4
- metadata +161 -167
- data/lib/closure_tree/with_advisory_lock.rb +0 -28
@@ -1,6 +1,6 @@
|
|
1
1
|
# This module is only included if the order column is an integer.
|
2
2
|
module ClosureTree
|
3
|
-
module
|
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
|
-
|
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
|
data/lib/closure_tree/support.rb
CHANGED
@@ -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
|
data/lib/closure_tree/version.rb
CHANGED
data/spec/label_spec.rb
CHANGED
@@ -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"
|
data/spec/support/models.rb
CHANGED
@@ -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
|
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
|
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
|
data/spec/tag_examples.rb
CHANGED
@@ -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
|