acts_as_many_trees 0.0.3 → 0.0.5
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 +4 -4
- data/lib/acts_as_many_trees/base.rb +54 -21
- data/lib/acts_as_many_trees/hierarchy_table.rb +179 -67
- data/lib/acts_as_many_trees/version.rb +1 -1
- metadata +14 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2968bbb9d336c54e6d67fbcfa2693a7340dd73bf
|
4
|
+
data.tar.gz: b97258ba8b2bd548d842345d6f5640ee3aa8c412
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 793df7daa449ca0ac8bfd911710314749ae1b7c6ee3b2b3a1f5331ed4ad76b6db2f92edd672175abc8099329afe1db1eba36dab4c0c87e3081bbf3d06c9b5fac
|
7
|
+
data.tar.gz: b0bff842b040b01c21d9d8019f9524b9e148caf0e6a58177f06ccb8e8932a5a4f2dca19c7a29f7ef66e58261f005ec0e8b8d4f7f295dbbaf732e68e9d747f1f7
|
@@ -26,7 +26,7 @@ module ActsAsManyTrees
|
|
26
26
|
included do
|
27
27
|
has_many :unscoped_descendant_links,
|
28
28
|
->{order(:position)},
|
29
|
-
class_name:hierarchy_class.to_s,
|
29
|
+
class_name: hierarchy_class.to_s,
|
30
30
|
foreign_key: 'ancestor_id',
|
31
31
|
dependent: :delete_all,
|
32
32
|
inverse_of: :unscoped_ancestor
|
@@ -39,24 +39,43 @@ module ActsAsManyTrees
|
|
39
39
|
inverse_of: :unscoped_descendant
|
40
40
|
|
41
41
|
has_many :unscoped_ancestors,through: :unscoped_ancestor_links
|
42
|
-
has_many :unscoped_descendants,
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
42
|
+
has_many :unscoped_descendants, {:through=>:unscoped_descendant_links, :source=>:unscoped_descendant}
|
43
|
+
has_many :self_and_siblings,
|
44
|
+
{:through=>:unscoped_ancestor_links,
|
45
|
+
:source=>:item_siblings
|
46
|
+
}
|
47
|
+
has_many :siblings_before,
|
48
|
+
->{where('unscoped_ancestor_links_siblings_before_join.position > item_hierarchies.position').where('unscoped_ancestor_links_siblings_before_join.generation=1')},
|
49
|
+
{:through=>:unscoped_ancestor_links,
|
50
|
+
:source=>:item_siblings
|
51
|
+
}
|
52
|
+
has_many :siblings_after,
|
53
|
+
->{where('unscoped_ancestor_links_siblings_after_join.position < item_hierarchies.position').where('unscoped_ancestor_links_siblings_after_join.generation=1')},
|
54
|
+
{:through=>:unscoped_ancestor_links,
|
55
|
+
:source=>:item_siblings
|
56
|
+
}
|
57
|
+
|
58
|
+
scope :roots , ->(hierarchy=''){
|
59
|
+
on = Arel::Nodes::On.new(Arel::Nodes::Equality.new(arel_table[:id],hierarchy_class.arel_table[:descendant_id])
|
60
|
+
.and(hierarchy_class.arel_table[:hierarchy_scope].eq(hierarchy))
|
61
|
+
.and(hierarchy_class.arel_table[:generation].not_eq(0))
|
62
|
+
)
|
63
|
+
outer_join = Arel::Nodes::OuterJoin.new(hierarchy_class.arel_table,on)
|
64
|
+
joins(outer_join).merge(hierarchy_class.where(ancestor_id: nil))
|
65
|
+
}
|
66
|
+
scope :not_this,->(this_id) { where.not(id: this_id)}
|
50
67
|
end
|
51
68
|
delegate :hierarchy_class, to: :class
|
52
69
|
def parent=(inpt_parent)
|
53
70
|
if inpt_parent.is_a?(Hash)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
71
|
+
new_parent=inpt_parent[:new_parent]
|
72
|
+
after_node=inpt_parent[:after_node]
|
73
|
+
before_node=inpt_parent[:before_node]
|
74
|
+
hierarchy_scope=inpt_parent[:hierarchy_scope] || ''
|
58
75
|
else
|
59
76
|
new_parent=inpt_parent
|
77
|
+
after_node=inpt_parent.children.last unless inpt_parent.nil?
|
78
|
+
before_node=inpt_parent.next_sibling unless inpt_parent.nil?
|
60
79
|
hierarchy_scope = ''
|
61
80
|
end
|
62
81
|
hierarchy_class.set_parent_of(self,new_parent,hierarchy_scope,after_node,before_node)
|
@@ -78,23 +97,37 @@ module ActsAsManyTrees
|
|
78
97
|
descendants(hierarchy_scope).where('generation=1')
|
79
98
|
end
|
80
99
|
|
81
|
-
def
|
82
|
-
|
100
|
+
def self_and_ancestors(hierarchy='')
|
101
|
+
unscoped_ancestors.merge(hierarchy_class.scope_hierarchy(hierarchy))
|
83
102
|
end
|
84
103
|
|
85
|
-
def
|
86
|
-
|
104
|
+
def ancestors(hierarchy='')
|
105
|
+
self_and_ancestors(hierarchy).not_this(self.id)
|
87
106
|
end
|
88
107
|
|
108
|
+
def self_and_descendants(hierarchy='')
|
109
|
+
unscoped_descendants.merge(hierarchy_class.scope_hierarchy(hierarchy))
|
110
|
+
end
|
89
111
|
|
90
|
-
def
|
91
|
-
|
112
|
+
def siblings
|
113
|
+
self_and_siblings.where.not(id: id)
|
114
|
+
end
|
115
|
+
|
116
|
+
def previous_sibling
|
117
|
+
siblings_before.last
|
118
|
+
end
|
119
|
+
|
120
|
+
def next_sibling
|
121
|
+
siblings_after.first
|
92
122
|
end
|
93
123
|
|
94
124
|
def descendants(hierarchy='')
|
95
|
-
|
125
|
+
self_and_descendants(hierarchy).not_this(self.id)
|
96
126
|
end
|
97
|
-
end
|
98
127
|
|
128
|
+
def position(hierarchy='')
|
129
|
+
unscoped_ancestor_links.where(ancestor_id: id,hierarchy_scope: hierarchy).first.position
|
130
|
+
end
|
131
|
+
end
|
99
132
|
end
|
100
133
|
ActiveRecord::Base.send :include, ActsAsManyTrees::Base
|
@@ -4,6 +4,7 @@ module ActsAsManyTrees
|
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
included do
|
7
|
+
UPPER_BOUND=10**20
|
7
8
|
class_attribute :item_class_name
|
8
9
|
self.item_class_name = self.to_s.gsub('Hierarchy','')
|
9
10
|
class_attribute :item_class
|
@@ -19,6 +20,44 @@ module ActsAsManyTrees
|
|
19
20
|
foreign_key: 'descendant_id',
|
20
21
|
inverse_of: :unscoped_ancestor_links
|
21
22
|
|
23
|
+
has_many :self_and_ancestors,
|
24
|
+
->(rec){where(hierarchy_scope: rec.hierarchy_scope)},
|
25
|
+
class_name: self.name,
|
26
|
+
foreign_key: 'descendant_id',
|
27
|
+
primary_key: 'ancestor_id'
|
28
|
+
|
29
|
+
has_many :ancestors,
|
30
|
+
->(rec){where(hierarchy_scope: rec.hierarchy_scope).where.not(generation:0)},
|
31
|
+
class_name: self.name,
|
32
|
+
foreign_key: 'descendant_id',
|
33
|
+
primary_key: 'ancestor_id'
|
34
|
+
|
35
|
+
has_many :self_and_descendants,
|
36
|
+
->(rec){where(hierarchy_scope: rec.hierarchy_scope).where.order(:position)},
|
37
|
+
class_name: self.name,
|
38
|
+
foreign_key: 'ancestor_id',
|
39
|
+
primary_key: 'descendant_id'
|
40
|
+
|
41
|
+
has_many :descendants,
|
42
|
+
->(rec){where(hierarchy_scope: rec.hierarchy_scope).where.not(generation:0).order(:position)},
|
43
|
+
class_name: self.name,
|
44
|
+
foreign_key: 'ancestor_id',
|
45
|
+
primary_key: 'descendant_id'
|
46
|
+
|
47
|
+
has_many :siblings,
|
48
|
+
->{where(generation: 1)},
|
49
|
+
class_name: self.name,
|
50
|
+
foreign_key: 'ancestor_id',
|
51
|
+
primary_key: 'ancestor_id'
|
52
|
+
|
53
|
+
has_many :children,
|
54
|
+
->(rec){where(hierarchy_scope: rec.hierarchy_scope,generation: 1).order(:position)},
|
55
|
+
class_name: self.name,
|
56
|
+
foreign_key: 'ancestor_id',
|
57
|
+
primary_key: 'descendant_id'
|
58
|
+
|
59
|
+
has_many :item_siblings,{through: :siblings, source: :unscoped_descendant}
|
60
|
+
|
22
61
|
scope :scope_hierarchy,->(scope_hierarchy=''){ where hierarchy_scope: scope_hierarchy}
|
23
62
|
# select t1.* from item_trees t1 left outer join item_trees t2 on t1.ancestor_id = t2.descendant_id and t1.tree_scope = t2.tree_scope where t2.ancestor_id is null
|
24
63
|
scope :roots,->do
|
@@ -32,30 +71,122 @@ module ActsAsManyTrees
|
|
32
71
|
)
|
33
72
|
.where(t2[:ancestor_id].eq(nil)
|
34
73
|
)
|
35
|
-
|
74
|
+
end
|
75
|
+
scope :self_and_siblings, ->(item,hierarchy_scope='')do
|
76
|
+
joins(:siblings)
|
77
|
+
end
|
78
|
+
scope :siblings_before_this,->(rec) do
|
79
|
+
joins(:siblings).where(:position,lt(rec.position))
|
36
80
|
end
|
37
81
|
|
38
82
|
def self.set_parent_of(item,new_parent,hierarchy_scope='',after_node=nil,before_node=nil)
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
83
|
+
if new_parent
|
84
|
+
wrk_parent = self.find_by(descendant_id:new_parent.id,ancestor_id:new_parent.id,generation: 0,hierarchy_scope: hierarchy_scope)
|
85
|
+
unless wrk_parent
|
86
|
+
position = (after_this(nil,nil,hierarchy_scope)+before_this(nil,hierarchy_scope))/2.0
|
87
|
+
wrk_parent=self.create(descendant_id:new_parent.id,ancestor_id:new_parent.id,generation: 0,hierarchy_scope: hierarchy_scope,position: position)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
if item
|
91
|
+
after_position = after_this(wrk_parent,after_node,hierarchy_scope)
|
92
|
+
before_position = before_this(before_node,hierarchy_scope)
|
93
|
+
position = (after_position+before_position)/2.0
|
94
|
+
wrk_item = self.find_by(descendant_id:item.id,ancestor_id:item.id,generation: 0,hierarchy_scope: hierarchy_scope)
|
95
|
+
if wrk_item
|
96
|
+
wrk_item.position = position
|
97
|
+
else
|
98
|
+
wrk_item=self.create(descendant_id:item.id,ancestor_id:item.id,generation: 0,hierarchy_scope: hierarchy_scope,position: position)
|
99
|
+
end
|
100
|
+
temp_name = SecureRandom.hex
|
101
|
+
create_tree(wrk_item,wrk_parent,temp_name)
|
102
|
+
delete_item_ancestors(wrk_item)
|
103
|
+
delete_ancestors_of_item_children(wrk_item,hierarchy_scope)
|
104
|
+
reset_descendant_position(wrk_item,before_position,temp_name)
|
105
|
+
rename_tree(temp_name,hierarchy_scope)
|
106
|
+
end
|
44
107
|
end
|
45
108
|
|
46
109
|
private
|
110
|
+
# the new position is after the maximum of the after_node, the parent, the current maximum of all
|
111
|
+
def self.after_this(wrk_parent,after_node,hierarchy_scope)
|
112
|
+
if after_node
|
113
|
+
position = after_node.position(hierarchy_scope)
|
114
|
+
elsif wrk_parent
|
115
|
+
position = wrk_parent.position
|
116
|
+
else
|
117
|
+
position = self.where(hierarchy_scope: hierarchy_scope).maximum(:position) || 0
|
118
|
+
end
|
119
|
+
position
|
120
|
+
end
|
121
|
+
|
122
|
+
# and before the minimum of the before_node, the parent's next sibling or 10**20
|
123
|
+
def self.before_this(before_node,hierarchy_scope)
|
124
|
+
if before_node
|
125
|
+
position = before_node.position(hierarchy_scope)
|
126
|
+
else
|
127
|
+
position = UPPER_BOUND
|
128
|
+
end
|
129
|
+
position
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.create_tree(wrk_item,wrk_parent,temp_name)
|
133
|
+
if wrk_parent
|
134
|
+
sql=<<-SQL
|
135
|
+
insert into #{table_name}(ancestor_id,descendant_id,generation,hierarchy_scope,position)
|
136
|
+
select a.ancestor_id,b.descendant_id,a.generation+b.generation+1,'#{temp_name}',b.position
|
137
|
+
from #{table_name} a, #{table_name} b
|
138
|
+
where a.descendant_id=#{wrk_parent.descendant_id}
|
139
|
+
and b.ancestor_id=#{wrk_item.ancestor_id}
|
140
|
+
and a.hierarchy_scope = b.hierarchy_scope
|
141
|
+
and a.hierarchy_scope = '#{wrk_item.hierarchy_scope}'
|
142
|
+
union
|
143
|
+
select c.ancestor_id,c.descendant_id,c.generation,'#{temp_name}',c.position
|
144
|
+
from #{table_name} c
|
145
|
+
where c.ancestor_id = #{wrk_item.descendant_id}
|
146
|
+
and c.hierarchy_scope = '#{wrk_item.hierarchy_scope}'
|
147
|
+
SQL
|
148
|
+
else
|
149
|
+
sql=<<-SQL
|
150
|
+
insert into #{table_name}(ancestor_id,descendant_id,generation,hierarchy_scope,position)
|
151
|
+
select c.ancestor_id,c.descendant_id,c.generation,'#{temp_name}',c.position
|
152
|
+
from #{table_name} c
|
153
|
+
where c.ancestor_id = #{wrk_item.descendant_id}
|
154
|
+
and c.hierarchy_scope = '#{wrk_item.hierarchy_scope}'
|
155
|
+
SQL
|
156
|
+
end
|
157
|
+
connection.execute(sql)
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.delete_item_ancestors(wrk_item)
|
161
|
+
sql=<<-SQL
|
162
|
+
delete from #{table_name}
|
163
|
+
where hierarchy_scope='#{wrk_item.hierarchy_scope}'
|
164
|
+
and descendant_id=#{wrk_item.descendant_id}
|
165
|
+
SQL
|
166
|
+
connection.execute(sql)
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.rename_tree(old_name,new_name)
|
170
|
+
sql=<<-SQL
|
171
|
+
update #{table_name}
|
172
|
+
set hierarchy_scope='#{new_name}'
|
173
|
+
where hierarchy_scope='#{old_name}'
|
174
|
+
SQL
|
175
|
+
connection.execute(sql)
|
176
|
+
end
|
177
|
+
|
47
178
|
def self.delete_ancestors(item,hierarchy_scope)
|
48
|
-
|
179
|
+
delete.where(descendant_id: item.id,hierarchy_scope: hierarchy_scope ).where.not(generation: 0)
|
49
180
|
end
|
50
181
|
|
51
182
|
def self.delete_ancestors_of_item_children(item,hierarchy_scope)
|
52
183
|
sql = <<-SQL
|
53
184
|
delete from #{table_name} as p using #{table_name} as p1
|
54
185
|
where p.descendant_id = p1.descendant_id
|
55
|
-
and p1.ancestor_id = #{item.
|
56
|
-
and p.generation > p1.generation
|
186
|
+
and p1.ancestor_id = #{item.descendant_id}
|
57
187
|
and p.hierarchy_scope = p1.hierarchy_scope
|
58
188
|
and p1.hierarchy_scope = '#{hierarchy_scope}'
|
189
|
+
and p.generation > 0
|
59
190
|
SQL
|
60
191
|
connection.execute(sql)
|
61
192
|
end
|
@@ -74,65 +205,46 @@ module ActsAsManyTrees
|
|
74
205
|
connection.execute(sql)
|
75
206
|
end
|
76
207
|
|
77
|
-
def self.
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
def self.fill_in_ancestors_for(new_parent,item,hierarchy_scope)
|
121
|
-
if new_parent
|
122
|
-
sql=<<-SQL
|
123
|
-
insert into #{table_name}(ancestor_id,descendant_id,generation,hierarchy_scope,position)
|
124
|
-
select it.ancestor_id,new_itm.descendant_id,it.generation+1,it.hierarchy_scope,new_itm.position
|
125
|
-
from #{table_name} it
|
126
|
-
join #{table_name} new_itm on it.descendant_id = new_itm.ancestor_id and it.hierarchy_scope=new_itm.hierarchy_scope
|
127
|
-
where new_itm.ancestor_id=#{new_parent.id}
|
128
|
-
and new_itm.descendant_id=#{item.id}
|
129
|
-
and (it.ancestor_id <> it.descendant_id)
|
130
|
-
and it.hierarchy_scope = '#{hierarchy_scope}'
|
131
|
-
SQL
|
132
|
-
ActiveRecord::Base.connection.execute(sql)
|
133
|
-
end
|
208
|
+
def self.reset_descendant_position(parent,before_position,hierarchy_scope='')
|
209
|
+
after_position = parent.position
|
210
|
+
gap = before_position - after_position
|
211
|
+
# p "before position: #{before_position}, after_position: #{after_position} gap: #{gap}"
|
212
|
+
# sql = <<-SQL
|
213
|
+
# select ancestor_id,descendant_id,hierarchy_scope,(#{after_position} + (
|
214
|
+
# (CAST ((rank() over (partition by ancestor_id order by position)-1) AS numeric))
|
215
|
+
# /( CAST (count(*) over (partition by ancestor_id) AS numeric)) * #{gap})) as position
|
216
|
+
# from #{table_name}
|
217
|
+
# where ancestor_id=#{parent.descendant_id}
|
218
|
+
# and hierarchy_scope='#{hierarchy_scope}'
|
219
|
+
# SQL
|
220
|
+
# res = connection.execute(sql)
|
221
|
+
# res.each_row do |row|
|
222
|
+
# p row
|
223
|
+
# end
|
224
|
+
sql = <<-SQL
|
225
|
+
with new_position as (select ancestor_id,descendant_id,hierarchy_scope,(#{after_position} + (
|
226
|
+
(CAST ((rank() over (partition by ancestor_id order by position)-1) AS numeric))
|
227
|
+
/( CAST (count(*) over (partition by ancestor_id) AS numeric)) * #{gap})) as position
|
228
|
+
from #{table_name}
|
229
|
+
where ancestor_id=#{parent.descendant_id}
|
230
|
+
and hierarchy_scope='#{hierarchy_scope}'
|
231
|
+
)
|
232
|
+
update
|
233
|
+
#{table_name} as t
|
234
|
+
set position = new_position.position
|
235
|
+
from new_position
|
236
|
+
where t.descendant_id = new_position.descendant_id
|
237
|
+
and t.hierarchy_scope = new_position.hierarchy_scope
|
238
|
+
SQL
|
239
|
+
connection.execute(sql)
|
240
|
+
# sql=<<-SQL
|
241
|
+
# select * from #{table_name} where hierarchy_scope='#{hierarchy_scope}' order by position
|
242
|
+
# SQL
|
243
|
+
# res = connection.execute(sql)
|
244
|
+
# res.each_row do |row|
|
245
|
+
# p row
|
246
|
+
# end
|
134
247
|
end
|
135
|
-
|
136
248
|
end
|
137
249
|
end
|
138
250
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_many_trees
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Small
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-02-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -28,44 +28,44 @@ dependencies:
|
|
28
28
|
name: pg
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.17.1
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.17.1
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec-rails
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 3.1.0
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 3.1.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: factory_girl_rails
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 4.5.0
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 4.5.0
|
69
69
|
description: Uses the closure tree pattern with a scope field to maintain separate
|
70
70
|
hierarchies
|
71
71
|
email:
|