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.
- 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
|