dm-is-awesome_set 0.10.2 → 0.11.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.
- data/LICENSE +1 -1
- data/Rakefile +5 -3
- data/VERSION +1 -1
- data/dm-is-awesome_set.gemspec +11 -4
- data/lib/dm-is-awesome_set.rb +428 -5
- data/spec/dm-is-awesome_set_spec.rb +1 -1
- data/spec/rcov.opts +6 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +7 -4
- data/tasks/changelog.rake +20 -0
- data/tasks/ci.rake +1 -0
- data/tasks/metrics.rake +36 -0
- data/tasks/spec.rake +25 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +10 -3
- data/lib/dm-is-awesome_set/is/awesome_set.rb +0 -428
data/LICENSE
CHANGED
data/Rakefile
CHANGED
@@ -6,6 +6,8 @@ begin
|
|
6
6
|
gem 'jeweler', '>= 1.4'
|
7
7
|
require 'jeweler'
|
8
8
|
|
9
|
+
FileList['tasks/**/*.rake'].each { |task| load task }
|
10
|
+
|
9
11
|
Jeweler::Tasks.new do |gem|
|
10
12
|
|
11
13
|
gem.name = "dm-is-awesome_set"
|
@@ -15,9 +17,9 @@ begin
|
|
15
17
|
gem.homepage = "http://github.com/snusnu/dm-is-awesome_set"
|
16
18
|
gem.authors = ["Jeremy Nicoll", "David Haslem", "Martin Gamsjaeger (snusnu)"]
|
17
19
|
|
18
|
-
gem.add_dependency 'dm-core',
|
19
|
-
gem.add_dependency 'dm-adjust',
|
20
|
-
gem.add_dependency 'dm-aggregates',
|
20
|
+
gem.add_dependency 'dm-core', '~> 0.10'
|
21
|
+
gem.add_dependency 'dm-adjust', '~> 0.10'
|
22
|
+
gem.add_dependency 'dm-aggregates', '~> 0.10'
|
21
23
|
|
22
24
|
gem.add_development_dependency 'rspec', '~> 1.2.9'
|
23
25
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.11.0
|
data/dm-is-awesome_set.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{dm-is-awesome_set}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.11.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Jeremy Nicoll", "David Haslem", "Martin Gamsjaeger (snusnu)"]
|
12
|
-
s.date = %q{2010-02-
|
12
|
+
s.date = %q{2010-02-17}
|
13
13
|
s.description = %q{A library that lets any datamapper model act like a nested set}
|
14
14
|
s.email = %q{jnicoll@gnexp.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -26,9 +26,16 @@ Gem::Specification.new do |s|
|
|
26
26
|
"VERSION",
|
27
27
|
"dm-is-awesome_set.gemspec",
|
28
28
|
"lib/dm-is-awesome_set.rb",
|
29
|
-
"lib/dm-is-awesome_set/is/awesome_set.rb",
|
30
29
|
"spec/dm-is-awesome_set_spec.rb",
|
31
|
-
"spec/
|
30
|
+
"spec/rcov.opts",
|
31
|
+
"spec/spec.opts",
|
32
|
+
"spec/spec_helper.rb",
|
33
|
+
"tasks/changelog.rake",
|
34
|
+
"tasks/ci.rake",
|
35
|
+
"tasks/metrics.rake",
|
36
|
+
"tasks/spec.rake",
|
37
|
+
"tasks/yard.rake",
|
38
|
+
"tasks/yardstick.rake"
|
32
39
|
]
|
33
40
|
s.homepage = %q{http://github.com/snusnu/dm-is-awesome_set}
|
34
41
|
s.rdoc_options = ["--charset=UTF-8"]
|
data/lib/dm-is-awesome_set.rb
CHANGED
@@ -1,5 +1,428 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module DataMapper
|
2
|
+
module Is
|
3
|
+
##
|
4
|
+
# Thanks for looking into dm-is-awesome_set. What makes it so awesome? Well,
|
5
|
+
# the fact that it actually works. At least I think it does. Give it a whirl,
|
6
|
+
# and if you come across any bugs let me know (check the readme file for
|
7
|
+
# information). Most of what you will be concerned with is the move method,
|
8
|
+
# though there are some other helper methods for selecting nodes.
|
9
|
+
# The way you use it in your model is like so:
|
10
|
+
#
|
11
|
+
# def ModelName
|
12
|
+
# include DataMapper::Resource
|
13
|
+
# # ... set up your properties ...
|
14
|
+
# is :awesome_set, :scope => [:col1, :col2], :child_key => [:parent_id]
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Note that scope is optional, and :child_key's default is [:parent_id]
|
18
|
+
|
19
|
+
module AwesomeSet
|
20
|
+
# Available options for is awesome_set:
|
21
|
+
# :scope => array of keys for scope (default is [])
|
22
|
+
# :child_key => array of keys for setting the parent-child relationship (default is [:parent_id])
|
23
|
+
|
24
|
+
def is_awesome_set(options={})
|
25
|
+
extend DataMapper::Is::AwesomeSet::ClassMethods
|
26
|
+
include DataMapper::Is::AwesomeSet::InstanceMethods
|
27
|
+
|
28
|
+
opts = set_options(options)
|
29
|
+
[:child_key, :scope].each {|var| raise "#{var} must be an Array" unless opts[var].is_a?(Array)}
|
30
|
+
|
31
|
+
property :parent_id, Integer, :min => 0, :writer => :protected
|
32
|
+
|
33
|
+
property :lft, Integer, :writer => :private, :index => true
|
34
|
+
property :rgt, Integer, :writer => :private, :index => true
|
35
|
+
|
36
|
+
class_opts = {:model => self.name, :child_key => opts[:child_key], :order => [:lft.asc] }
|
37
|
+
belongs_to :parent, class_opts
|
38
|
+
has n, :children, class_opts
|
39
|
+
|
40
|
+
before :save_self do
|
41
|
+
move_without_saving(:root) if lft.nil? #You don't want to use new_record? here. Trust me, you don't.
|
42
|
+
end
|
43
|
+
|
44
|
+
end # def is_awesome_set
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
def set_options(options) #:nodoc:
|
48
|
+
@ias_options = { :child_key => [:parent_id], :scope => [] }.merge(options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def ias_options; @ias_options || superclass.ias_options end #:nodoc:
|
52
|
+
|
53
|
+
def child_keys; ias_options[:child_key]; end
|
54
|
+
def scope_keys; ias_options[:scope]; end
|
55
|
+
def is_nested_set? #:nodoc:
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
# Checks to see if the hash or object contains a valid scope by checking attributes or keys
|
60
|
+
def valid_scope?(hash)
|
61
|
+
return true if hash.is_a?(self)
|
62
|
+
return false unless hash.is_a?(Hash)
|
63
|
+
scope_keys.each { |sk| return false unless hash.keys.include?(sk) }
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
# Raises an error if the scope is not valid
|
68
|
+
def check_scope(hash)
|
69
|
+
raise 'Invalid scope: ' + hash.inspect if !valid_scope?(hash)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Return only the attributes that deal with the scope, will raise an error on invalid scope
|
73
|
+
def extract_scope(hash)
|
74
|
+
check_scope(hash)
|
75
|
+
ret = {}
|
76
|
+
send_to_obj = hash.is_a?(self)
|
77
|
+
scope_keys.each { |sk| ret[sk] = send_to_obj ? hash.attribute_get(sk) : hash[sk] }
|
78
|
+
ret
|
79
|
+
end
|
80
|
+
|
81
|
+
def adjust_gap!(scoped_set, at, adjustment) #:nodoc:
|
82
|
+
scoped_set.all(:rgt.gt => at).adjust!({:rgt => adjustment},true)
|
83
|
+
scoped_set.all(:lft.gt => at).adjust!({:lft => adjustment},true)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Return a hash that gets the roots
|
87
|
+
def root_hash
|
88
|
+
ret = {}
|
89
|
+
child_keys.each { |ck| ret[ck] = nil }
|
90
|
+
ret
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# Get the root with no args if there is no scope
|
95
|
+
# Pass the scope or an object with scope to get the first root
|
96
|
+
def root(scope = {})
|
97
|
+
scope = extract_scope(scope)
|
98
|
+
get_class.first(scope.merge(root_hash.merge(:order => [:lft.asc])))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Same as @root, but gets all roots
|
102
|
+
def roots(scope = {})
|
103
|
+
scope = extract_scope(scope)
|
104
|
+
get_class.all(scope.merge(root_hash.merge(:order => [:lft.asc])))
|
105
|
+
end
|
106
|
+
|
107
|
+
# Gets the full set with scope behavior like @root
|
108
|
+
def full_set(scope = {})
|
109
|
+
scope = extract_scope(scope)
|
110
|
+
get_class.all(scope.merge(:order => [:lft.asc]))
|
111
|
+
end
|
112
|
+
|
113
|
+
# Retrieves all nodes that do not have children.
|
114
|
+
# This needs to be refactored for more of a DM style, if possible.
|
115
|
+
def leaves(scope = {})
|
116
|
+
scope = extract_scope(scope)
|
117
|
+
get_class.all(scope.merge(:order => [:lft.asc], :conditions => ["`rgt` - `lft` = 1"]))
|
118
|
+
end
|
119
|
+
|
120
|
+
# Since DataMapper looks for all records in a table when using discriminators
|
121
|
+
# when using the parent model , we'll look for the earliest ancestor class
|
122
|
+
# that is a nested set.
|
123
|
+
def get_class #:nodoc:
|
124
|
+
klass = self
|
125
|
+
klass = klass.superclass while klass.superclass.respond_to?(:is_nested_set?) && klass.superclass.is_nested_set?
|
126
|
+
klass
|
127
|
+
end
|
128
|
+
end # mod ClassMethods
|
129
|
+
|
130
|
+
module InstanceMethods
|
131
|
+
##
|
132
|
+
# move self / node to a position in the set. position can _only_ be changed through this
|
133
|
+
#
|
134
|
+
# @example [Usage]
|
135
|
+
# * node.move :higher # moves node higher unless it is at the top of parent
|
136
|
+
# * node.move :lower # moves node lower unless it is at the bottom of parent
|
137
|
+
# * node.move :below => other # moves this node below other resource in the set
|
138
|
+
# * node.move :into => other # same as setting a parent-relationship
|
139
|
+
#
|
140
|
+
# @param vector <Symbol, Hash> A symbol, or a key-value pair that describes the requested movement
|
141
|
+
#
|
142
|
+
# @option :higher<Symbol> move node higher
|
143
|
+
# @option :highest<Symbol> move node to the top of the list (within its parent)
|
144
|
+
# @option :lower<Symbol> move node lower
|
145
|
+
# @option :lowest<Symbol> move node to the bottom of the list (within its parent)
|
146
|
+
# @option :indent<Symbol> move node into sibling above
|
147
|
+
# @option :outdent<Symbol> move node out below its current parent
|
148
|
+
# @option :root<Symbol|Hash|Resource> move node to root. If passed an object / hash, it uses the scope of that. Otherwise, it uses currently set scope.
|
149
|
+
# @option :into<Resource> move node into another node
|
150
|
+
# @option :above<Resource> move node above other node
|
151
|
+
# @option :below<Resource> move node below other node
|
152
|
+
# @option :to<Integer> move node to a specific location in the nested set
|
153
|
+
# @see move_without_saving
|
154
|
+
|
155
|
+
def move(vector)
|
156
|
+
transaction do
|
157
|
+
move_without_saving(vector)
|
158
|
+
save!
|
159
|
+
end
|
160
|
+
reload
|
161
|
+
end
|
162
|
+
|
163
|
+
def level
|
164
|
+
ancestors.length
|
165
|
+
end
|
166
|
+
|
167
|
+
# Gets the root of this node
|
168
|
+
def root
|
169
|
+
get_class.first(root_hash.merge(:lft.lt => lft, :rgt.gt => rgt))
|
170
|
+
end
|
171
|
+
|
172
|
+
# Gets all the roots of this node's tree
|
173
|
+
def roots
|
174
|
+
get_class.all(root_hash.merge(:order => [:lft.asc]))
|
175
|
+
end
|
176
|
+
|
177
|
+
# Gets all ancestors of this node
|
178
|
+
def ancestors
|
179
|
+
get_class.all(scope_hash.merge(:lft.lt => lft, :rgt.gt => rgt, :order => [:lft.asc]))
|
180
|
+
end
|
181
|
+
|
182
|
+
# Same as ancestors, but also including this node
|
183
|
+
def self_and_ancestors
|
184
|
+
get_class.all(scope_hash.merge(:lft.lte => lft, :rgt.gte => rgt, :order => [:lft.asc]))
|
185
|
+
end
|
186
|
+
|
187
|
+
# Gets all nodes that share the same parent node, except for this node
|
188
|
+
def siblings
|
189
|
+
get_class.all(scope_and_parent_hash.merge(:order => [:lft.asc], :lft.not => lft))
|
190
|
+
end
|
191
|
+
|
192
|
+
# Same as siblings, but returns this node as well
|
193
|
+
def self_and_siblings
|
194
|
+
get_class.all(scope_and_parent_hash.merge(:order => [:lft.asc]))
|
195
|
+
end
|
196
|
+
|
197
|
+
# Returns next node with same parent, or nil
|
198
|
+
def next_sibling
|
199
|
+
get_class.first(scope_and_parent_hash.merge(:lft.gt => rgt, :order => [:lft.asc]))
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns previous node with same parent, or nil
|
203
|
+
def previous_sibling
|
204
|
+
get_class.first(scope_and_parent_hash.merge(:rgt.lt => lft, :order => [:rgt.desc]))
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns the full set within this scope
|
208
|
+
def full_set
|
209
|
+
get_class.all(scope_hash)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Gets all descendents of this node
|
213
|
+
def descendents
|
214
|
+
get_class.all(scope_hash.merge(:rgt.lt => rgt, :lft.gt => lft, :order => [:lft.asc]))
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
# Same as descendents, but returns self as well
|
219
|
+
def self_and_descendents
|
220
|
+
get_class.all(scope_hash.merge(:rgt.lte => rgt, :lft.gte => lft, :order => [:lft.asc]))
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
# Fixed spelling for when English majors are peering over your shoulder
|
225
|
+
def descendants; descendents; end
|
226
|
+
def self_and_descendants; self_and_descendents; end
|
227
|
+
|
228
|
+
# Retrieves the nodes without any children.
|
229
|
+
def leaves
|
230
|
+
get_class.leaves(self)
|
231
|
+
end
|
232
|
+
|
233
|
+
def attributes_set(hash) #:nodoc:
|
234
|
+
hash = hash || {}
|
235
|
+
hash.each { |k,v| attribute_set(k,v) }
|
236
|
+
end
|
237
|
+
|
238
|
+
# Destroys the current node and all children nodes, running their before and after hooks
|
239
|
+
# Returns the destroyed objects
|
240
|
+
def destroy
|
241
|
+
sads = self_and_descendants
|
242
|
+
hooks = get_class.const_get('INSTANCE_HOOKS')
|
243
|
+
before_methods = hooks[:destroy][:before].map { |hash| hash[:name] }
|
244
|
+
after_methods = hooks[:destroy][:after].map { |hash| hash[:name] }
|
245
|
+
# Trigger all the before :destroy methods
|
246
|
+
sads.each { |sad| before_methods.each { |bf| sad.send(bf) } }
|
247
|
+
# dup is called here because destroy! likes to clear out the array, understandably.
|
248
|
+
transaction do
|
249
|
+
sads.dup.destroy!
|
250
|
+
adjust_gap!(full_set, lft, -(rgt - lft + 1))
|
251
|
+
end
|
252
|
+
# Now go through after all the after :destroy methods.
|
253
|
+
sads.each { |sad| after_methods.each { |bf| sad.send(bf) } }
|
254
|
+
end
|
255
|
+
|
256
|
+
# Same as @destroy, but does not run the hooks
|
257
|
+
def destroy!
|
258
|
+
sad = self_and_descendants
|
259
|
+
transaction do
|
260
|
+
sad.dup.destroy!
|
261
|
+
adjust_gap!(full_set, lft, -(rgt - lft + 1))
|
262
|
+
end
|
263
|
+
sad
|
264
|
+
end
|
265
|
+
|
266
|
+
protected
|
267
|
+
def skip_adjust=(var) #:nodoc:
|
268
|
+
@skip_adjust = true
|
269
|
+
end
|
270
|
+
|
271
|
+
def adjust_gap!(*args) #:nodoc:
|
272
|
+
get_class.adjust_gap!(*args)
|
273
|
+
end
|
274
|
+
|
275
|
+
def get_finder_hash(*args)
|
276
|
+
ret = {}
|
277
|
+
args.each { |arg| get_class.ias_options[arg].each { |s| ret[s] = send(s) } }
|
278
|
+
ret
|
279
|
+
end
|
280
|
+
|
281
|
+
def root_hash
|
282
|
+
ret = {}
|
283
|
+
get_class.child_keys.each { |ck| ret[ck] = nil }
|
284
|
+
scope_hash.merge(ret)
|
285
|
+
end
|
286
|
+
|
287
|
+
def scope_and_parent_hash
|
288
|
+
get_finder_hash(:child_key, :scope)
|
289
|
+
end
|
290
|
+
|
291
|
+
def extract_scope(hash)
|
292
|
+
get_class.extract_scope(hash)
|
293
|
+
end
|
294
|
+
|
295
|
+
def scope_hash
|
296
|
+
get_finder_hash(:scope)
|
297
|
+
end
|
298
|
+
|
299
|
+
def parent_hash
|
300
|
+
get_finder_hash(:child_key)
|
301
|
+
end
|
302
|
+
|
303
|
+
def same_scope?(obj)
|
304
|
+
case obj
|
305
|
+
when get_class : scope_hash == obj.send(:scope_hash)
|
306
|
+
when Hash : scope_hash == obj
|
307
|
+
when nil : true
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def valid_scope?(hash)
|
312
|
+
get_class.valid_scope?(hash)
|
313
|
+
end
|
314
|
+
|
315
|
+
def move_without_saving(vector)
|
316
|
+
# Do some checking of the variable...
|
317
|
+
if vector.respond_to?(:'[]') && vector.respond_to?(:size) && vector.size == 1
|
318
|
+
action = vector.keys[0]
|
319
|
+
obj = vector[action]
|
320
|
+
elsif vector.is_a?(Symbol)
|
321
|
+
obj = nil
|
322
|
+
action = vector
|
323
|
+
else
|
324
|
+
raise 'You must pass either a symbol or a hash with one property to the method "move".'
|
325
|
+
end
|
326
|
+
|
327
|
+
|
328
|
+
# Convenience methods
|
329
|
+
ret_value = case action
|
330
|
+
when :higher : previous_sibling ? move_without_saving(:above => previous_sibling) : false
|
331
|
+
when :highest : move_without_saving(:to => parent ? (parent.lft + 1) : 1)
|
332
|
+
when :lower : next_sibling ? move_without_saving(:below => next_sibling) : false
|
333
|
+
when :lowest : parent ? move_without_saving(:to => parent.rgt - 1) : move_without_saving(:root)
|
334
|
+
when :indent : previous_sibling ? move_without_saving(:into => previous_sibling) : false
|
335
|
+
when :outdent : parent ? move_without_saving(:below => parent) : false
|
336
|
+
else :no_action
|
337
|
+
end
|
338
|
+
return ret_value unless ret_value == :no_action
|
339
|
+
|
340
|
+
this_gap = lft.to_i > 0 && rgt.to_i > 0 ? rgt - lft : 1
|
341
|
+
old_parent = parent
|
342
|
+
new_scope = nil
|
343
|
+
max = nil
|
344
|
+
|
345
|
+
# Here's where the real heavy lifting happens. Any action can be taken
|
346
|
+
# care of by :root, :above, :below, or :to
|
347
|
+
pos, adjust_at, p_obj = case action
|
348
|
+
when :root
|
349
|
+
new_scope = obj ? extract_scope(obj) : scope_hash
|
350
|
+
max = (get_class.max(:rgt, new_scope) || 0) + 1
|
351
|
+
when :into : [obj.rgt, obj.rgt - 1, obj]
|
352
|
+
when :above : [obj.lft, obj.lft - 1, obj.parent]
|
353
|
+
when :below : [obj.rgt + 1, obj.rgt, obj.parent]
|
354
|
+
when :to
|
355
|
+
pos = obj.to_i
|
356
|
+
p_obj = get_class.first(scope_hash.merge(:lft.lt => pos, :rgt.gt => pos, :order => [:lft.desc]))
|
357
|
+
[pos, pos - 1, p_obj]
|
358
|
+
else raise 'Invalid action sent to the method "move": ' + action.to_s
|
359
|
+
end
|
360
|
+
|
361
|
+
old_scope = nil
|
362
|
+
new_scope ||= extract_scope(p_obj) if p_obj
|
363
|
+
|
364
|
+
max ||= (get_class.max(:rgt, new_scope || scope_hash) || 0) + 1
|
365
|
+
if pos == 0 || pos > max
|
366
|
+
raise "You cannot move a node outside of the bounds of the tree. You passed: #{pos}. Acceptable numbers are 1 through #{max}"
|
367
|
+
end
|
368
|
+
|
369
|
+
raise 'You are trying to move a node into one that has not been saved yet.' if p_obj && p_obj.lft.nil?
|
370
|
+
|
371
|
+
if lft
|
372
|
+
adjustment = pos < lft ? this_gap + 1 : 0
|
373
|
+
raise 'Illegal move: you are trying to move a node within itself' if pos.between?(lft+adjustment,rgt+adjustment) && same_scope?(new_scope)
|
374
|
+
end
|
375
|
+
|
376
|
+
# make a new hole and assign parent
|
377
|
+
#
|
378
|
+
# Note: with identity map on an already saved object, making a hole
|
379
|
+
# will alter lft & rgt values immediately, so we need to keep copies
|
380
|
+
old_lft, old_rgt = lft, rgt if lft && rgt
|
381
|
+
adjust_gap!(get_class.full_set(new_scope || scope_hash) , adjust_at, this_gap + 1) if adjust_at
|
382
|
+
|
383
|
+
# Do we need to move the node (already present in the tree), or just save the attributes?
|
384
|
+
if lft && (pos != old_lft || !same_scope?(new_scope))
|
385
|
+
# Move elements
|
386
|
+
if same_scope?(new_scope)
|
387
|
+
move_by = pos - (old_lft + adjustment)
|
388
|
+
full_set.all(:lft.gte => old_lft + adjustment, :rgt.lte => old_rgt + adjustment).adjust!({:lft => move_by, :rgt => move_by}, true)
|
389
|
+
else # Things have to be done a little differently if moving scope
|
390
|
+
move_by = pos - old_lft
|
391
|
+
old_scope = extract_scope(self)
|
392
|
+
sads = self_and_descendants
|
393
|
+
sads.adjust!({:lft => move_by, :rgt => move_by}, true)
|
394
|
+
# Update the attributes to match how they are in the database now.
|
395
|
+
# Be sure to do this between adjust! and setting the new scope
|
396
|
+
attribute_set(:rgt, old_rgt + move_by)
|
397
|
+
attribute_set(:lft, old_lft + move_by)
|
398
|
+
|
399
|
+
sads.each { |d| d.update!(new_scope)}
|
400
|
+
end
|
401
|
+
|
402
|
+
# Close hole
|
403
|
+
if old_scope
|
404
|
+
adjust_gap!(get_class.full_set(old_scope), old_lft, -(this_gap + 1))
|
405
|
+
else
|
406
|
+
adjustment += 1 if parent == old_parent
|
407
|
+
adjust_gap!(full_set, old_lft + adjustment, -(this_gap + 1))
|
408
|
+
end
|
409
|
+
else # just save the attributes
|
410
|
+
attribute_set(:lft, pos)
|
411
|
+
attribute_set(:rgt, lft + this_gap)
|
412
|
+
attributes_set(p_obj.send(:scope_hash)) if p_obj
|
413
|
+
end
|
414
|
+
# We set parent here because we don't want to throw errors with dirty
|
415
|
+
# tracking during all of the adjust! and update!(scope) calls
|
416
|
+
self.parent = p_obj
|
417
|
+
|
418
|
+
end
|
419
|
+
|
420
|
+
def get_class #:no_doc:
|
421
|
+
self.class.get_class
|
422
|
+
end
|
423
|
+
end # mod InstanceMethods
|
424
|
+
|
425
|
+
Model.send(:include, self)
|
426
|
+
end # mod AwesomeSet
|
427
|
+
end # mod Is
|
428
|
+
end # mod DM
|
data/spec/rcov.opts
ADDED
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
$:.push File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
-
require 'dm-is-awesome_set'
|
4
2
|
|
5
|
-
|
6
|
-
|
3
|
+
require 'dm-core'
|
4
|
+
require 'dm-adjust'
|
5
|
+
require 'dm-aggregates'
|
7
6
|
require 'dm-types'
|
8
7
|
require 'dm-validations'
|
9
8
|
|
9
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
10
|
+
require 'dm-is-awesome_set'
|
11
|
+
|
12
|
+
|
10
13
|
ENV["SQLITE3_SPEC_URI"] ||= 'sqlite3::memory:'
|
11
14
|
ENV["MYSQL_SPEC_URI"] ||= 'mysql://localhost/dm-is_awesome_set_test'
|
12
15
|
ENV["POSTGRES_SPEC_URI"] ||= 'postgres://postgres@localhost/dm-is_awesome_set_test'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
desc 'update changelog'
|
4
|
+
task :changelog do
|
5
|
+
File.open('CHANGELOG', 'w+') do |changelog|
|
6
|
+
`git log -z --abbrev-commit`.split("\0").each do |commit|
|
7
|
+
next if commit =~ /^Merge: \d*/
|
8
|
+
ref, author, time, _, title, _, message = commit.split("\n", 7)
|
9
|
+
ref = ref[/commit ([0-9a-f]+)/, 1]
|
10
|
+
author = author[/Author: (.*)/, 1].strip
|
11
|
+
time = Time.parse(time[/Date: (.*)/, 1]).utc
|
12
|
+
title.strip!
|
13
|
+
|
14
|
+
changelog.puts "[#{ref} | #{time}] #{author}"
|
15
|
+
changelog.puts '', " * #{title}"
|
16
|
+
changelog.puts '', message.rstrip if message
|
17
|
+
changelog.puts
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/tasks/ci.rake
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
task :ci => [ :verify_measurements, 'metrics:all' ]
|
data/tasks/metrics.rake
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
begin
|
2
|
+
require 'metric_fu'
|
3
|
+
rescue LoadError
|
4
|
+
namespace :metrics do
|
5
|
+
task :all do
|
6
|
+
abort 'metric_fu is not available. In order to run metrics:all, you must: gem install metric_fu'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'reek/adapters/rake_task'
|
13
|
+
|
14
|
+
Reek::RakeTask.new do |t|
|
15
|
+
t.fail_on_error = true
|
16
|
+
t.verbose = false
|
17
|
+
t.source_files = 'lib/**/*.rb'
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
task :reek do
|
21
|
+
abort 'Reek is not available. In order to run reek, you must: gem install reek'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
begin
|
26
|
+
require 'roodi'
|
27
|
+
require 'roodi_task'
|
28
|
+
|
29
|
+
RoodiTask.new do |t|
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
rescue LoadError
|
33
|
+
task :roodi do
|
34
|
+
abort 'Roodi is not available. In order to run roodi, you must: gem install roodi'
|
35
|
+
end
|
36
|
+
end
|
data/tasks/spec.rake
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec/rake/spectask'
|
2
|
+
require 'spec/rake/verify_rcov'
|
3
|
+
|
4
|
+
spec_defaults = lambda do |spec|
|
5
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
6
|
+
spec.libs << 'lib' << 'spec'
|
7
|
+
spec.spec_opts << '--options' << 'spec/spec.opts'
|
8
|
+
end
|
9
|
+
|
10
|
+
Spec::Rake::SpecTask.new(:spec, &spec_defaults)
|
11
|
+
|
12
|
+
Spec::Rake::SpecTask.new(:rcov) do |rcov|
|
13
|
+
spec_defaults.call(rcov)
|
14
|
+
rcov.rcov = true
|
15
|
+
rcov.rcov_opts = File.read('spec/rcov.opts').split(/\s+/)
|
16
|
+
end
|
17
|
+
|
18
|
+
RCov::VerifyTask.new(:verify_rcov => :rcov) do |rcov|
|
19
|
+
rcov.threshold = 100
|
20
|
+
end
|
21
|
+
|
22
|
+
task :spec => :check_dependencies
|
23
|
+
task :rcov => :check_dependencies
|
24
|
+
|
25
|
+
task :default => :spec
|
data/tasks/yard.rake
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
begin
|
2
|
+
require 'pathname'
|
3
|
+
require 'yardstick/rake/measurement'
|
4
|
+
require 'yardstick/rake/verify'
|
5
|
+
|
6
|
+
# yardstick_measure task
|
7
|
+
Yardstick::Rake::Measurement.new
|
8
|
+
|
9
|
+
# verify_measurements task
|
10
|
+
Yardstick::Rake::Verify.new do |verify|
|
11
|
+
verify.threshold = 100
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
%w[ yardstick_measure verify_measurements ].each do |name|
|
15
|
+
task name.to_s do
|
16
|
+
abort "Yardstick is not available. In order to run #{name}, you must: gem install yardstick"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dm-is-awesome_set
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Nicoll
|
@@ -11,7 +11,7 @@ autorequire:
|
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
13
|
|
14
|
-
date: 2010-02-
|
14
|
+
date: 2010-02-17 00:00:00 -05:00
|
15
15
|
default_executable:
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
@@ -73,9 +73,16 @@ files:
|
|
73
73
|
- VERSION
|
74
74
|
- dm-is-awesome_set.gemspec
|
75
75
|
- lib/dm-is-awesome_set.rb
|
76
|
-
- lib/dm-is-awesome_set/is/awesome_set.rb
|
77
76
|
- spec/dm-is-awesome_set_spec.rb
|
77
|
+
- spec/rcov.opts
|
78
|
+
- spec/spec.opts
|
78
79
|
- spec/spec_helper.rb
|
80
|
+
- tasks/changelog.rake
|
81
|
+
- tasks/ci.rake
|
82
|
+
- tasks/metrics.rake
|
83
|
+
- tasks/spec.rake
|
84
|
+
- tasks/yard.rake
|
85
|
+
- tasks/yardstick.rake
|
79
86
|
has_rdoc: true
|
80
87
|
homepage: http://github.com/snusnu/dm-is-awesome_set
|
81
88
|
licenses: []
|
@@ -1,428 +0,0 @@
|
|
1
|
-
module DataMapper
|
2
|
-
module Is
|
3
|
-
##
|
4
|
-
# Thanks for looking into dm-is-awesome_set. What makes it so awesome? Well,
|
5
|
-
# the fact that it actually works. At least I think it does. Give it a whirl,
|
6
|
-
# and if you come across any bugs let me know (check the readme file for
|
7
|
-
# information). Most of what you will be concerned with is the move method,
|
8
|
-
# though there are some other helper methods for selecting nodes.
|
9
|
-
# The way you use it in your model is like so:
|
10
|
-
#
|
11
|
-
# def ModelName
|
12
|
-
# include DataMapper::Resource
|
13
|
-
# # ... set up your properties ...
|
14
|
-
# is :awesome_set, :scope => [:col1, :col2], :child_key => [:parent_id]
|
15
|
-
# end
|
16
|
-
#
|
17
|
-
# Note that scope is optional, and :child_key's default is [:parent_id]
|
18
|
-
|
19
|
-
module AwesomeSet
|
20
|
-
# Available options for is awesome_set:
|
21
|
-
# :scope => array of keys for scope (default is [])
|
22
|
-
# :child_key => array of keys for setting the parent-child relationship (default is [:parent_id])
|
23
|
-
|
24
|
-
def is_awesome_set(options={})
|
25
|
-
extend DataMapper::Is::AwesomeSet::ClassMethods
|
26
|
-
include DataMapper::Is::AwesomeSet::InstanceMethods
|
27
|
-
|
28
|
-
opts = set_options(options)
|
29
|
-
[:child_key, :scope].each {|var| raise "#{var} must be an Array" unless opts[var].is_a?(Array)}
|
30
|
-
|
31
|
-
property :parent_id, Integer, :min => 0, :writer => :protected
|
32
|
-
|
33
|
-
property :lft, Integer, :writer => :private, :index => true
|
34
|
-
property :rgt, Integer, :writer => :private, :index => true
|
35
|
-
|
36
|
-
class_opts = {:model => self.name, :child_key => opts[:child_key], :order => [:lft.asc] }
|
37
|
-
belongs_to :parent, class_opts
|
38
|
-
has n, :children, class_opts
|
39
|
-
|
40
|
-
before :save_self do
|
41
|
-
move_without_saving(:root) if lft.nil? #You don't want to use new_record? here. Trust me, you don't.
|
42
|
-
end
|
43
|
-
|
44
|
-
end # def is_awesome_set
|
45
|
-
|
46
|
-
module ClassMethods
|
47
|
-
def set_options(options) #:nodoc:
|
48
|
-
@ias_options = { :child_key => [:parent_id], :scope => [] }.merge(options)
|
49
|
-
end
|
50
|
-
|
51
|
-
def ias_options; @ias_options || superclass.ias_options end #:nodoc:
|
52
|
-
|
53
|
-
def child_keys; ias_options[:child_key]; end
|
54
|
-
def scope_keys; ias_options[:scope]; end
|
55
|
-
def is_nested_set? #:nodoc:
|
56
|
-
true
|
57
|
-
end
|
58
|
-
|
59
|
-
# Checks to see if the hash or object contains a valid scope by checking attributes or keys
|
60
|
-
def valid_scope?(hash)
|
61
|
-
return true if hash.is_a?(self)
|
62
|
-
return false unless hash.is_a?(Hash)
|
63
|
-
scope_keys.each { |sk| return false unless hash.keys.include?(sk) }
|
64
|
-
true
|
65
|
-
end
|
66
|
-
|
67
|
-
# Raises an error if the scope is not valid
|
68
|
-
def check_scope(hash)
|
69
|
-
raise 'Invalid scope: ' + hash.inspect if !valid_scope?(hash)
|
70
|
-
end
|
71
|
-
|
72
|
-
# Return only the attributes that deal with the scope, will raise an error on invalid scope
|
73
|
-
def extract_scope(hash)
|
74
|
-
check_scope(hash)
|
75
|
-
ret = {}
|
76
|
-
send_to_obj = hash.is_a?(self)
|
77
|
-
scope_keys.each { |sk| ret[sk] = send_to_obj ? hash.attribute_get(sk) : hash[sk] }
|
78
|
-
ret
|
79
|
-
end
|
80
|
-
|
81
|
-
def adjust_gap!(scoped_set, at, adjustment) #:nodoc:
|
82
|
-
scoped_set.all(:rgt.gt => at).adjust!({:rgt => adjustment},true)
|
83
|
-
scoped_set.all(:lft.gt => at).adjust!({:lft => adjustment},true)
|
84
|
-
end
|
85
|
-
|
86
|
-
# Return a hash that gets the roots
|
87
|
-
def root_hash
|
88
|
-
ret = {}
|
89
|
-
child_keys.each { |ck| ret[ck] = nil }
|
90
|
-
ret
|
91
|
-
end
|
92
|
-
|
93
|
-
|
94
|
-
# Get the root with no args if there is no scope
|
95
|
-
# Pass the scope or an object with scope to get the first root
|
96
|
-
def root(scope = {})
|
97
|
-
scope = extract_scope(scope)
|
98
|
-
get_class.first(scope.merge(root_hash.merge(:order => [:lft.asc])))
|
99
|
-
end
|
100
|
-
|
101
|
-
# Same as @root, but gets all roots
|
102
|
-
def roots(scope = {})
|
103
|
-
scope = extract_scope(scope)
|
104
|
-
get_class.all(scope.merge(root_hash.merge(:order => [:lft.asc])))
|
105
|
-
end
|
106
|
-
|
107
|
-
# Gets the full set with scope behavior like @root
|
108
|
-
def full_set(scope = {})
|
109
|
-
scope = extract_scope(scope)
|
110
|
-
get_class.all(scope.merge(:order => [:lft.asc]))
|
111
|
-
end
|
112
|
-
|
113
|
-
# Retrieves all nodes that do not have children.
|
114
|
-
# This needs to be refactored for more of a DM style, if possible.
|
115
|
-
def leaves(scope = {})
|
116
|
-
scope = extract_scope(scope)
|
117
|
-
get_class.all(scope.merge(:order => [:lft.asc], :conditions => ["`rgt` - `lft` = 1"]))
|
118
|
-
end
|
119
|
-
|
120
|
-
# Since DataMapper looks for all records in a table when using discriminators
|
121
|
-
# when using the parent model , we'll look for the earliest ancestor class
|
122
|
-
# that is a nested set.
|
123
|
-
def get_class #:nodoc:
|
124
|
-
klass = self
|
125
|
-
klass = klass.superclass while klass.superclass.respond_to?(:is_nested_set?) && klass.superclass.is_nested_set?
|
126
|
-
klass
|
127
|
-
end
|
128
|
-
end # mod ClassMethods
|
129
|
-
|
130
|
-
module InstanceMethods
|
131
|
-
##
|
132
|
-
# move self / node to a position in the set. position can _only_ be changed through this
|
133
|
-
#
|
134
|
-
# @example [Usage]
|
135
|
-
# * node.move :higher # moves node higher unless it is at the top of parent
|
136
|
-
# * node.move :lower # moves node lower unless it is at the bottom of parent
|
137
|
-
# * node.move :below => other # moves this node below other resource in the set
|
138
|
-
# * node.move :into => other # same as setting a parent-relationship
|
139
|
-
#
|
140
|
-
# @param vector <Symbol, Hash> A symbol, or a key-value pair that describes the requested movement
|
141
|
-
#
|
142
|
-
# @option :higher<Symbol> move node higher
|
143
|
-
# @option :highest<Symbol> move node to the top of the list (within its parent)
|
144
|
-
# @option :lower<Symbol> move node lower
|
145
|
-
# @option :lowest<Symbol> move node to the bottom of the list (within its parent)
|
146
|
-
# @option :indent<Symbol> move node into sibling above
|
147
|
-
# @option :outdent<Symbol> move node out below its current parent
|
148
|
-
# @option :root<Symbol|Hash|Resource> move node to root. If passed an object / hash, it uses the scope of that. Otherwise, it uses currently set scope.
|
149
|
-
# @option :into<Resource> move node into another node
|
150
|
-
# @option :above<Resource> move node above other node
|
151
|
-
# @option :below<Resource> move node below other node
|
152
|
-
# @option :to<Integer> move node to a specific location in the nested set
|
153
|
-
# @see move_without_saving
|
154
|
-
|
155
|
-
def move(vector)
|
156
|
-
transaction do
|
157
|
-
move_without_saving(vector)
|
158
|
-
save!
|
159
|
-
end
|
160
|
-
reload
|
161
|
-
end
|
162
|
-
|
163
|
-
def level
|
164
|
-
ancestors.length
|
165
|
-
end
|
166
|
-
|
167
|
-
# Gets the root of this node
|
168
|
-
def root
|
169
|
-
get_class.first(root_hash.merge(:lft.lt => lft, :rgt.gt => rgt))
|
170
|
-
end
|
171
|
-
|
172
|
-
# Gets all the roots of this node's tree
|
173
|
-
def roots
|
174
|
-
get_class.all(root_hash.merge(:order => [:lft.asc]))
|
175
|
-
end
|
176
|
-
|
177
|
-
# Gets all ancestors of this node
|
178
|
-
def ancestors
|
179
|
-
get_class.all(scope_hash.merge(:lft.lt => lft, :rgt.gt => rgt, :order => [:lft.asc]))
|
180
|
-
end
|
181
|
-
|
182
|
-
# Same as ancestors, but also including this node
|
183
|
-
def self_and_ancestors
|
184
|
-
get_class.all(scope_hash.merge(:lft.lte => lft, :rgt.gte => rgt, :order => [:lft.asc]))
|
185
|
-
end
|
186
|
-
|
187
|
-
# Gets all nodes that share the same parent node, except for this node
|
188
|
-
def siblings
|
189
|
-
get_class.all(scope_and_parent_hash.merge(:order => [:lft.asc], :lft.not => lft))
|
190
|
-
end
|
191
|
-
|
192
|
-
# Same as siblings, but returns this node as well
|
193
|
-
def self_and_siblings
|
194
|
-
get_class.all(scope_and_parent_hash.merge(:order => [:lft.asc]))
|
195
|
-
end
|
196
|
-
|
197
|
-
# Returns next node with same parent, or nil
|
198
|
-
def next_sibling
|
199
|
-
get_class.first(scope_and_parent_hash.merge(:lft.gt => rgt, :order => [:lft.asc]))
|
200
|
-
end
|
201
|
-
|
202
|
-
# Returns previous node with same parent, or nil
|
203
|
-
def previous_sibling
|
204
|
-
get_class.first(scope_and_parent_hash.merge(:rgt.lt => lft, :order => [:rgt.desc]))
|
205
|
-
end
|
206
|
-
|
207
|
-
# Returns the full set within this scope
|
208
|
-
def full_set
|
209
|
-
get_class.all(scope_hash)
|
210
|
-
end
|
211
|
-
|
212
|
-
# Gets all descendents of this node
|
213
|
-
def descendents
|
214
|
-
get_class.all(scope_hash.merge(:rgt.lt => rgt, :lft.gt => lft, :order => [:lft.asc]))
|
215
|
-
end
|
216
|
-
|
217
|
-
|
218
|
-
# Same as descendents, but returns self as well
|
219
|
-
def self_and_descendents
|
220
|
-
get_class.all(scope_hash.merge(:rgt.lte => rgt, :lft.gte => lft, :order => [:lft.asc]))
|
221
|
-
end
|
222
|
-
|
223
|
-
|
224
|
-
# Fixed spelling for when English majors are peering over your shoulder
|
225
|
-
def descendants; descendents; end
|
226
|
-
def self_and_descendants; self_and_descendents; end
|
227
|
-
|
228
|
-
# Retrieves the nodes without any children.
|
229
|
-
def leaves
|
230
|
-
get_class.leaves(self)
|
231
|
-
end
|
232
|
-
|
233
|
-
def attributes_set(hash) #:nodoc:
|
234
|
-
hash = hash || {}
|
235
|
-
hash.each { |k,v| attribute_set(k,v) }
|
236
|
-
end
|
237
|
-
|
238
|
-
# Destroys the current node and all children nodes, running their before and after hooks
|
239
|
-
# Returns the destroyed objects
|
240
|
-
def destroy
|
241
|
-
sads = self_and_descendants
|
242
|
-
hooks = get_class.const_get('INSTANCE_HOOKS')
|
243
|
-
before_methods = hooks[:destroy][:before].map { |hash| hash[:name] }
|
244
|
-
after_methods = hooks[:destroy][:after].map { |hash| hash[:name] }
|
245
|
-
# Trigger all the before :destroy methods
|
246
|
-
sads.each { |sad| before_methods.each { |bf| sad.send(bf) } }
|
247
|
-
# dup is called here because destroy! likes to clear out the array, understandably.
|
248
|
-
transaction do
|
249
|
-
sads.dup.destroy!
|
250
|
-
adjust_gap!(full_set, lft, -(rgt - lft + 1))
|
251
|
-
end
|
252
|
-
# Now go through after all the after :destroy methods.
|
253
|
-
sads.each { |sad| after_methods.each { |bf| sad.send(bf) } }
|
254
|
-
end
|
255
|
-
|
256
|
-
# Same as @destroy, but does not run the hooks
|
257
|
-
def destroy!
|
258
|
-
sad = self_and_descendants
|
259
|
-
transaction do
|
260
|
-
sad.dup.destroy!
|
261
|
-
adjust_gap!(full_set, lft, -(rgt - lft + 1))
|
262
|
-
end
|
263
|
-
sad
|
264
|
-
end
|
265
|
-
|
266
|
-
protected
|
267
|
-
def skip_adjust=(var) #:nodoc:
|
268
|
-
@skip_adjust = true
|
269
|
-
end
|
270
|
-
|
271
|
-
def adjust_gap!(*args) #:nodoc:
|
272
|
-
get_class.adjust_gap!(*args)
|
273
|
-
end
|
274
|
-
|
275
|
-
def get_finder_hash(*args)
|
276
|
-
ret = {}
|
277
|
-
args.each { |arg| get_class.ias_options[arg].each { |s| ret[s] = send(s) } }
|
278
|
-
ret
|
279
|
-
end
|
280
|
-
|
281
|
-
def root_hash
|
282
|
-
ret = {}
|
283
|
-
get_class.child_keys.each { |ck| ret[ck] = nil }
|
284
|
-
scope_hash.merge(ret)
|
285
|
-
end
|
286
|
-
|
287
|
-
def scope_and_parent_hash
|
288
|
-
get_finder_hash(:child_key, :scope)
|
289
|
-
end
|
290
|
-
|
291
|
-
def extract_scope(hash)
|
292
|
-
get_class.extract_scope(hash)
|
293
|
-
end
|
294
|
-
|
295
|
-
def scope_hash
|
296
|
-
get_finder_hash(:scope)
|
297
|
-
end
|
298
|
-
|
299
|
-
def parent_hash
|
300
|
-
get_finder_hash(:child_key)
|
301
|
-
end
|
302
|
-
|
303
|
-
def same_scope?(obj)
|
304
|
-
case obj
|
305
|
-
when get_class then scope_hash == obj.send(:scope_hash)
|
306
|
-
when Hash then scope_hash == obj
|
307
|
-
when nil then true
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
def valid_scope?(hash)
|
312
|
-
get_class.valid_scope?(hash)
|
313
|
-
end
|
314
|
-
|
315
|
-
def move_without_saving(vector)
|
316
|
-
# Do some checking of the variable...
|
317
|
-
if vector.respond_to?(:'[]') && vector.respond_to?(:size) && vector.size == 1
|
318
|
-
action = vector.keys[0]
|
319
|
-
obj = vector[action]
|
320
|
-
elsif vector.is_a?(Symbol)
|
321
|
-
obj = nil
|
322
|
-
action = vector
|
323
|
-
else
|
324
|
-
raise 'You must pass either a symbol or a hash with one property to the method "move".'
|
325
|
-
end
|
326
|
-
|
327
|
-
|
328
|
-
# Convenience methods
|
329
|
-
ret_value = case action
|
330
|
-
when :higher then previous_sibling ? move_without_saving(:above => previous_sibling) : false
|
331
|
-
when :highest then move_without_saving(:to => parent ? (parent.lft + 1) : 1)
|
332
|
-
when :lower then next_sibling ? move_without_saving(:below => next_sibling) : false
|
333
|
-
when :lowest then parent ? move_without_saving(:to => parent.rgt - 1) : move_without_saving(:root)
|
334
|
-
when :indent then previous_sibling ? move_without_saving(:into => previous_sibling) : false
|
335
|
-
when :outdent then parent ? move_without_saving(:below => parent) : false
|
336
|
-
else :no_action
|
337
|
-
end
|
338
|
-
return ret_value unless ret_value == :no_action
|
339
|
-
|
340
|
-
this_gap = lft.to_i > 0 && rgt.to_i > 0 ? rgt - lft : 1
|
341
|
-
old_parent = parent
|
342
|
-
new_scope = nil
|
343
|
-
max = nil
|
344
|
-
|
345
|
-
# Here's where the real heavy lifting happens. Any action can be taken
|
346
|
-
# care of by :root, :above, :below, or :to
|
347
|
-
pos, adjust_at, p_obj = case action
|
348
|
-
when :root
|
349
|
-
new_scope = obj ? extract_scope(obj) : scope_hash
|
350
|
-
max = (get_class.max(:rgt, new_scope) || 0) + 1
|
351
|
-
when :into then [obj.rgt, obj.rgt - 1, obj]
|
352
|
-
when :above then [obj.lft, obj.lft - 1, obj.parent]
|
353
|
-
when :below then [obj.rgt + 1, obj.rgt, obj.parent]
|
354
|
-
when :to
|
355
|
-
pos = obj.to_i
|
356
|
-
p_obj = get_class.first(scope_hash.merge(:lft.lt => pos, :rgt.gt => pos, :order => [:lft.desc]))
|
357
|
-
[pos, pos - 1, p_obj]
|
358
|
-
else raise 'Invalid action sent to the method "move": ' + action.to_s
|
359
|
-
end
|
360
|
-
|
361
|
-
old_scope = nil
|
362
|
-
new_scope ||= extract_scope(p_obj) if p_obj
|
363
|
-
|
364
|
-
max ||= (get_class.max(:rgt, new_scope || scope_hash) || 0) + 1
|
365
|
-
if pos == 0 || pos > max
|
366
|
-
raise "You cannot move a node outside of the bounds of the tree. You passed: #{pos}. Acceptable numbers are 1 through #{max}"
|
367
|
-
end
|
368
|
-
|
369
|
-
raise 'You are trying to move a node into one that has not been saved yet.' if p_obj && p_obj.lft.nil?
|
370
|
-
|
371
|
-
if lft
|
372
|
-
adjustment = pos < lft ? this_gap + 1 : 0
|
373
|
-
raise 'Illegal move: you are trying to move a node within itself' if pos.between?(lft+adjustment,rgt+adjustment) && same_scope?(new_scope)
|
374
|
-
end
|
375
|
-
|
376
|
-
# make a new hole and assign parent
|
377
|
-
#
|
378
|
-
# Note: with identity map on an already saved object, making a hole
|
379
|
-
# will alter lft & rgt values immediately, so we need to keep copies
|
380
|
-
old_lft, old_rgt = lft, rgt if lft && rgt
|
381
|
-
adjust_gap!(get_class.full_set(new_scope || scope_hash) , adjust_at, this_gap + 1) if adjust_at
|
382
|
-
|
383
|
-
# Do we need to move the node (already present in the tree), or just save the attributes?
|
384
|
-
if lft && (pos != old_lft || !same_scope?(new_scope))
|
385
|
-
# Move elements
|
386
|
-
if same_scope?(new_scope)
|
387
|
-
move_by = pos - (old_lft + adjustment)
|
388
|
-
full_set.all(:lft.gte => old_lft + adjustment, :rgt.lte => old_rgt + adjustment).adjust!({:lft => move_by, :rgt => move_by}, true)
|
389
|
-
else # Things have to be done a little differently if moving scope
|
390
|
-
move_by = pos - old_lft
|
391
|
-
old_scope = extract_scope(self)
|
392
|
-
sads = self_and_descendants
|
393
|
-
sads.adjust!({:lft => move_by, :rgt => move_by}, true)
|
394
|
-
# Update the attributes to match how they are in the database now.
|
395
|
-
# Be sure to do this between adjust! and setting the new scope
|
396
|
-
attribute_set(:rgt, old_rgt + move_by)
|
397
|
-
attribute_set(:lft, old_lft + move_by)
|
398
|
-
|
399
|
-
sads.each { |d| d.update!(new_scope)}
|
400
|
-
end
|
401
|
-
|
402
|
-
# Close hole
|
403
|
-
if old_scope
|
404
|
-
adjust_gap!(get_class.full_set(old_scope), old_lft, -(this_gap + 1))
|
405
|
-
else
|
406
|
-
adjustment += 1 if parent == old_parent
|
407
|
-
adjust_gap!(full_set, old_lft + adjustment, -(this_gap + 1))
|
408
|
-
end
|
409
|
-
else # just save the attributes
|
410
|
-
attribute_set(:lft, pos)
|
411
|
-
attribute_set(:rgt, lft + this_gap)
|
412
|
-
attributes_set(p_obj.send(:scope_hash)) if p_obj
|
413
|
-
end
|
414
|
-
# We set parent here because we don't want to throw errors with dirty
|
415
|
-
# tracking during all of the adjust! and update!(scope) calls
|
416
|
-
self.parent = p_obj
|
417
|
-
|
418
|
-
end
|
419
|
-
|
420
|
-
def get_class #:no_doc:
|
421
|
-
self.class.get_class
|
422
|
-
end
|
423
|
-
end # mod InstanceMethods
|
424
|
-
|
425
|
-
Model.send(:include, self)
|
426
|
-
end # mod AwesomeSet
|
427
|
-
end # mod Is
|
428
|
-
end # mod DM
|