snusnu-dm-is-awesome_set 0.7.1
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 +23 -0
- data/README +14 -0
- data/Rakefile +45 -0
- data/TODO +4 -0
- data/lib/dm-is-awesome_set/is/awesome_set.rb +419 -0
- data/lib/dm-is-awesome_set.rb +6 -0
- data/spec/dm-is-awesome_set_spec.rb +256 -0
- data/spec/spec_helper.rb +90 -0
- metadata +99 -0
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2008 Jeremy Nicoll (http://gnexp.com, jnicoll@gnexp.com)
|
2
|
+
|
3
|
+
Some code and documentation janked from dm-is-nested_set by Sindre Aarsaether
|
4
|
+
(somebee.com)
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
dm-is-awesome_set
|
2
|
+
=================
|
3
|
+
|
4
|
+
Yes, finally! A nested_set for Datamapper that actually works! .... at least
|
5
|
+
I _think_ it does. Please test this out and let me know at jnicoll@gnexp.com
|
6
|
+
if you run into any problems. This readme will eventually have examples. Until
|
7
|
+
then, check the RDoc's and you should be fine.
|
8
|
+
|
9
|
+
A quick note about discriminators:
|
10
|
+
|
11
|
+
This version enables scoping that can either include or ignore discriminators.
|
12
|
+
If you wish to scope by a discriminator, please include that column name in
|
13
|
+
the scope option. Otherwise this plugin will work with all rows regardless of
|
14
|
+
the discriminator column.
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
|
4
|
+
GEM_NAME = 'dm-is-awesome_set'
|
5
|
+
|
6
|
+
spec = Gem::Specification.new do |s|
|
7
|
+
s.rubyforge_project = 'merb'
|
8
|
+
s.name = GEM_NAME
|
9
|
+
s.version = "0.7.1"
|
10
|
+
s.platform = Gem::Platform::RUBY
|
11
|
+
s.has_rdoc = true
|
12
|
+
s.extra_rdoc_files = ["README", "LICENSE", 'TODO']
|
13
|
+
s.summary = "DataMapper nested set plugin that works"
|
14
|
+
s.description = s.summary
|
15
|
+
s.author = "Jeremy Nicoll"
|
16
|
+
s.email = "jnicoll@gnexp.com"
|
17
|
+
s.homepage = "http://gnexp.com/"
|
18
|
+
s.add_dependency('dm-core', '>= 0.9.7')
|
19
|
+
s.add_dependency('dm-adjust', '>= 0.9.7')
|
20
|
+
s.add_dependency('dm-aggregates', '>= 0.9.7')
|
21
|
+
s.add_dependency('dm-validations', '>= 0.9.10')
|
22
|
+
s.require_path = 'lib'
|
23
|
+
s.files = %w(LICENSE README Rakefile TODO) + Dir.glob("{lib,spec}/**/*")
|
24
|
+
end
|
25
|
+
|
26
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
27
|
+
pkg.gem_spec = spec
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "install the plugin as a gem"
|
31
|
+
task :install => [:package] do
|
32
|
+
sh %{sudo gem install pkg/#{spec.name}-#{spec.version}}
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Uninstall the gem"
|
36
|
+
task :uninstall do
|
37
|
+
sh %{sudo gem uninstall #{spec.name} --version #{spec.version}}
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "Create a gemspec file"
|
41
|
+
task :gemspec do
|
42
|
+
File.open("#{GEM_NAME}.gemspec", "w") do |file|
|
43
|
+
file.puts spec.to_ruby
|
44
|
+
end
|
45
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,419 @@
|
|
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 :lft, Integer, :writer => :private, :index => true
|
32
|
+
property :rgt, Integer, :writer => :private, :index => true
|
33
|
+
|
34
|
+
class_opts = {:class_name => self.name, :child_key => opts[:child_key], :order => [:lft.asc], :writer => :protected}
|
35
|
+
belongs_to :parent, class_opts
|
36
|
+
has n, :children, class_opts
|
37
|
+
|
38
|
+
before :save do
|
39
|
+
move_without_saving(:root) if lft.nil? #You don't want to use new_record? here. Trust me, you don't.
|
40
|
+
end
|
41
|
+
|
42
|
+
end # def is_awesome_set
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
def set_options(options) #:nodoc:
|
46
|
+
@ias_options = { :child_key => [:parent_id], :scope => [] }.merge(options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def ias_options; @ias_options || superclass.ias_options end #:nodoc:
|
50
|
+
|
51
|
+
def child_keys; ias_options[:child_key]; end
|
52
|
+
def scope_keys; ias_options[:scope]; end
|
53
|
+
def is_nested_set? #:nodoc:
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
# Checks to see if the hash or object contains a valid scope by checking attributes or keys
|
58
|
+
def valid_scope?(hash)
|
59
|
+
return true if hash.is_a?(self)
|
60
|
+
return false unless hash.is_a?(Hash)
|
61
|
+
scope_keys.each { |sk| return false unless hash.keys.include?(sk) }
|
62
|
+
true
|
63
|
+
end
|
64
|
+
|
65
|
+
# Raises an error if the scope is not valid
|
66
|
+
def check_scope(hash)
|
67
|
+
raise 'Invalid scope: ' + hash.inspect if !valid_scope?(hash)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Return only the attributes that deal with the scope, will raise an error on invalid scope
|
71
|
+
def extract_scope(hash)
|
72
|
+
check_scope(hash)
|
73
|
+
ret = {}
|
74
|
+
send_to_obj = hash.is_a?(self)
|
75
|
+
scope_keys.each { |sk| ret[sk] = send_to_obj ? hash.attribute_get(sk) : hash[sk] }
|
76
|
+
ret
|
77
|
+
end
|
78
|
+
|
79
|
+
def adjust_gap!(scoped_set, at, adjustment) #:nodoc:
|
80
|
+
scoped_set.all(:rgt.gt => at).adjust!({:rgt => adjustment},true)
|
81
|
+
scoped_set.all(:lft.gt => at).adjust!({:lft => adjustment},true)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return a hash that gets the roots
|
85
|
+
def root_hash
|
86
|
+
ret = {}
|
87
|
+
child_keys.each { |ck| ret[ck] = nil }
|
88
|
+
ret
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
# Get the root with no args if there is no scope
|
93
|
+
# Pass the scope or an object with scope to get the first root
|
94
|
+
def root(scope = {})
|
95
|
+
scope = extract_scope(scope)
|
96
|
+
get_class.first(scope.merge(root_hash.merge(:order => [:lft.asc])))
|
97
|
+
end
|
98
|
+
|
99
|
+
# Same as @root, but gets all roots
|
100
|
+
def roots(scope = {})
|
101
|
+
scope = extract_scope(scope)
|
102
|
+
get_class.all(scope.merge(root_hash.merge(:order => [:lft.asc])))
|
103
|
+
end
|
104
|
+
|
105
|
+
# Gets the full set with scope behavior like @root
|
106
|
+
def full_set(scope = {})
|
107
|
+
scope = extract_scope(scope)
|
108
|
+
get_class.all(scope.merge(:order => [:lft.asc]))
|
109
|
+
end
|
110
|
+
|
111
|
+
# Retrieves all nodes that do not have children.
|
112
|
+
# This needs to be refactored for more of a DM style, if possible.
|
113
|
+
def leaves(scope = {})
|
114
|
+
scope = extract_scope(scope)
|
115
|
+
get_class.all(scope.merge(:order => [:lft.asc], :conditions => ["`rgt` - `lft` = 1"]))
|
116
|
+
end
|
117
|
+
|
118
|
+
# Since DataMapper looks for all records in a table when using discriminators
|
119
|
+
# when using the parent model , we'll look for the earliest ancestor class
|
120
|
+
# that is a nested set.
|
121
|
+
def get_class #:nodoc:
|
122
|
+
klass = self
|
123
|
+
klass = klass.superclass while klass.superclass.respond_to?(:is_nested_set?) && klass.superclass.is_nested_set?
|
124
|
+
klass
|
125
|
+
end
|
126
|
+
end # mod ClassMethods
|
127
|
+
|
128
|
+
module InstanceMethods
|
129
|
+
##
|
130
|
+
# move self / node to a position in the set. position can _only_ be changed through this
|
131
|
+
#
|
132
|
+
# @example [Usage]
|
133
|
+
# * node.move :higher # moves node higher unless it is at the top of parent
|
134
|
+
# * node.move :lower # moves node lower unless it is at the bottom of parent
|
135
|
+
# * node.move :below => other # moves this node below other resource in the set
|
136
|
+
# * node.move :into => other # same as setting a parent-relationship
|
137
|
+
#
|
138
|
+
# @param vector <Symbol, Hash> A symbol, or a key-value pair that describes the requested movement
|
139
|
+
#
|
140
|
+
# @option :higher<Symbol> move node higher
|
141
|
+
# @option :highest<Symbol> move node to the top of the list (within its parent)
|
142
|
+
# @option :lower<Symbol> move node lower
|
143
|
+
# @option :lowest<Symbol> move node to the bottom of the list (within its parent)
|
144
|
+
# @option :indent<Symbol> move node into sibling above
|
145
|
+
# @option :outdent<Symbol> move node out below its current parent
|
146
|
+
# @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.
|
147
|
+
# @option :into<Resource> move node into another node
|
148
|
+
# @option :above<Resource> move node above other node
|
149
|
+
# @option :below<Resource> move node below other node
|
150
|
+
# @option :to<Integer> move node to a specific location in the nested set
|
151
|
+
# @see move_without_saving
|
152
|
+
|
153
|
+
def move(vector)
|
154
|
+
transaction do
|
155
|
+
move_without_saving(vector)
|
156
|
+
save!
|
157
|
+
end
|
158
|
+
reload
|
159
|
+
end
|
160
|
+
|
161
|
+
def level
|
162
|
+
ancestors.length
|
163
|
+
end
|
164
|
+
|
165
|
+
# Gets the root of this node
|
166
|
+
def root
|
167
|
+
get_class.first(root_hash.merge(:lft.lt => lft, :rgt.gt => rgt))
|
168
|
+
end
|
169
|
+
|
170
|
+
# Gets all the roots of this node's tree
|
171
|
+
def roots
|
172
|
+
get_class.all(root_hash.merge(:order => [:lft.asc]))
|
173
|
+
end
|
174
|
+
|
175
|
+
# Gets all ancestors of this node
|
176
|
+
def ancestors
|
177
|
+
get_class.all(scope_hash.merge(:lft.lt => lft, :rgt.gt => rgt, :order => [:lft.asc]))
|
178
|
+
end
|
179
|
+
|
180
|
+
# Same as ancestors, but also including this node
|
181
|
+
def self_and_ancestors
|
182
|
+
get_class.all(scope_hash.merge(:lft.lte => lft, :rgt.gte => rgt, :order => [:lft.asc]))
|
183
|
+
end
|
184
|
+
|
185
|
+
# Gets all nodes that share the same parent node, except for this node
|
186
|
+
def siblings
|
187
|
+
get_class.all(scope_and_parent_hash.merge(:order => [:lft.asc], :lft.not => lft))
|
188
|
+
end
|
189
|
+
|
190
|
+
# Same as siblings, but returns this node as well
|
191
|
+
def self_and_siblings
|
192
|
+
get_class.all(scope_and_parent_hash.merge(:order => [:lft.asc]))
|
193
|
+
end
|
194
|
+
|
195
|
+
# Returns next node with same parent, or nil
|
196
|
+
def next_sibling
|
197
|
+
get_class.first(scope_and_parent_hash.merge(:lft.gt => rgt, :order => [:lft.asc]))
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns previous node with same parent, or nil
|
201
|
+
def previous_sibling
|
202
|
+
get_class.first(scope_and_parent_hash.merge(:rgt.lt => lft, :order => [:rgt.desc]))
|
203
|
+
end
|
204
|
+
|
205
|
+
# Returns the full set within this scope
|
206
|
+
def full_set
|
207
|
+
get_class.all(scope_hash)
|
208
|
+
end
|
209
|
+
|
210
|
+
# Gets all descendents of this node
|
211
|
+
def descendents
|
212
|
+
get_class.all(scope_hash.merge(:lft.lt => rgt, :lft.gt => lft, :order => [:lft.asc]))
|
213
|
+
end
|
214
|
+
|
215
|
+
# Same as descendents, but returns self as well
|
216
|
+
def self_and_descendents
|
217
|
+
get_class.all(scope_hash.merge(:rgt.lte => rgt, :lft.gte => lft, :order => [:lft.asc]))
|
218
|
+
end
|
219
|
+
|
220
|
+
# Retrieves the nodes without any children.
|
221
|
+
def leaves
|
222
|
+
get_class.leaves(self)
|
223
|
+
end
|
224
|
+
|
225
|
+
def attributes_set(hash) #:nodoc:
|
226
|
+
hash = hash || {}
|
227
|
+
hash.each { |k,v| attribute_set(k,v) }
|
228
|
+
end
|
229
|
+
|
230
|
+
def update!(hash) #:nodoc#
|
231
|
+
attributes_set(hash)
|
232
|
+
save!
|
233
|
+
end
|
234
|
+
|
235
|
+
# Destroys the current node and all children nodes, running their before and after hooks
|
236
|
+
# Returns the destroyed objects
|
237
|
+
def destroy
|
238
|
+
sads = self_and_descendents
|
239
|
+
hooks = get_class.const_get('INSTANCE_HOOKS')
|
240
|
+
before_methods = hooks[:destroy][:before].map { |hash| hash[:name] }
|
241
|
+
after_methods = hooks[:destroy][:after].map { |hash| hash[:name] }
|
242
|
+
# Trigger all the before :destroy methods
|
243
|
+
sads.each { |sad| before_methods.each { |bf| sad.send(bf) } }
|
244
|
+
# dup is called here because destroy! likes to clear out the array, understandably.
|
245
|
+
transaction do
|
246
|
+
sads.dup.destroy!
|
247
|
+
adjust_gap!(full_set, lft, -(rgt - lft + 1))
|
248
|
+
end
|
249
|
+
# Now go through after all the after :destroy methods.
|
250
|
+
sads.each { |sad| after_methods.each { |bf| sad.send(bf) } }
|
251
|
+
end
|
252
|
+
|
253
|
+
# Same as @destroy, but does not run the hooks
|
254
|
+
def destroy!
|
255
|
+
sad = self_and_descendents
|
256
|
+
transaction do
|
257
|
+
sad.dup.destroy!
|
258
|
+
adjust_gap!(full_set, lft, -(rgt - lft + 1))
|
259
|
+
end
|
260
|
+
sad
|
261
|
+
end
|
262
|
+
|
263
|
+
protected
|
264
|
+
def skip_adjust=(var) #:nodoc:
|
265
|
+
@skip_adjust = true
|
266
|
+
end
|
267
|
+
|
268
|
+
def adjust_gap!(*args) #:nodoc:
|
269
|
+
get_class.adjust_gap!(*args)
|
270
|
+
end
|
271
|
+
|
272
|
+
def get_finder_hash(*args)
|
273
|
+
ret = {}
|
274
|
+
args.each { |arg| get_class.ias_options[arg].each { |s| ret[s] = send(s) } }
|
275
|
+
ret
|
276
|
+
end
|
277
|
+
|
278
|
+
def root_hash
|
279
|
+
ret = {}
|
280
|
+
get_class.child_keys.each { |ck| ret[ck] = nil }
|
281
|
+
scope_hash.merge(ret)
|
282
|
+
end
|
283
|
+
|
284
|
+
def scope_and_parent_hash
|
285
|
+
get_finder_hash(:child_key, :scope)
|
286
|
+
end
|
287
|
+
|
288
|
+
def extract_scope(hash)
|
289
|
+
get_class.extract_scope(hash)
|
290
|
+
end
|
291
|
+
|
292
|
+
def scope_hash
|
293
|
+
get_finder_hash(:scope)
|
294
|
+
end
|
295
|
+
|
296
|
+
def parent_hash
|
297
|
+
get_finder_hash(:child_key)
|
298
|
+
end
|
299
|
+
|
300
|
+
def same_scope?(obj)
|
301
|
+
case obj
|
302
|
+
when get_class : scope_hash == obj.send(:scope_hash)
|
303
|
+
when Hash : scope_hash == obj
|
304
|
+
when nil : true
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def valid_scope?(hash)
|
309
|
+
get_class.valid_scope?(hash)
|
310
|
+
end
|
311
|
+
|
312
|
+
def move_without_saving(vector)
|
313
|
+
# Do some checking of the variable...
|
314
|
+
if vector.respond_to?(:'[]') && vector.respond_to?(:size) && vector.size == 1
|
315
|
+
action = vector.keys[0]
|
316
|
+
obj = vector[action]
|
317
|
+
elsif vector.is_a?(Symbol)
|
318
|
+
obj = nil
|
319
|
+
action = vector
|
320
|
+
else
|
321
|
+
raise 'You must pass either a symbol or a hash with one property to the method "move".'
|
322
|
+
end
|
323
|
+
|
324
|
+
|
325
|
+
# Convenience methods
|
326
|
+
ret_value = case action
|
327
|
+
when :higher : previous_sibling ? move_without_saving(:above => previous_sibling) : false
|
328
|
+
when :highest : move_without_saving(:to => parent ? (parent.lft + 1) : 1)
|
329
|
+
when :lower : next_sibling ? move_without_saving(:below => next_sibling) : false
|
330
|
+
when :lowest : parent ? move_without_saving(:to => parent.rgt - 1) : move_without_saving(:root)
|
331
|
+
when :indent : previous_sibling ? move_without_saving(:into => previous_sibling) : false
|
332
|
+
when :outdent : parent ? move_without_saving(:below => parent) : false
|
333
|
+
else :no_action
|
334
|
+
end
|
335
|
+
return ret_value unless ret_value == :no_action
|
336
|
+
|
337
|
+
this_gap = lft.to_i > 0 && rgt.to_i > 0 ? rgt - lft : 1
|
338
|
+
old_parent = parent
|
339
|
+
new_scope = nil
|
340
|
+
max = nil
|
341
|
+
|
342
|
+
# Here's where the real heavy lifting happens. Any action can be taken
|
343
|
+
# care of by :root, :above, :below, or :to
|
344
|
+
pos, adjust_at, p_obj = case action
|
345
|
+
when :root
|
346
|
+
new_scope = obj ? extract_scope(obj) : scope_hash
|
347
|
+
max = (get_class.max(:rgt, new_scope) || 0) + 1
|
348
|
+
when :into : [obj.rgt, obj.rgt - 1, obj]
|
349
|
+
when :above : [obj.lft, obj.lft - 1, obj.parent]
|
350
|
+
when :below : [obj.rgt + 1, obj.rgt, obj.parent]
|
351
|
+
when :to
|
352
|
+
pos = obj.to_i
|
353
|
+
p_obj = get_class.first(scope_hash.merge(:lft.lt => pos, :rgt.gt => pos, :order => [:lft.desc]))
|
354
|
+
[pos, pos - 1, p_obj]
|
355
|
+
else raise 'Invalid action sent to the method "move": ' + action.to_s
|
356
|
+
end
|
357
|
+
|
358
|
+
old_scope = nil
|
359
|
+
new_scope ||= extract_scope(p_obj) if p_obj
|
360
|
+
|
361
|
+
max ||= (get_class.max(:rgt, new_scope || scope_hash) || 0) + 1
|
362
|
+
if pos == 0 || pos > max
|
363
|
+
raise "You cannot move a node outside of the bounds of the tree. You passed: #{pos}. Acceptable numbers are 1 through #{max}"
|
364
|
+
end
|
365
|
+
|
366
|
+
raise 'You are trying to move a node into one that has not been saved yet.' if p_obj && p_obj.lft.nil?
|
367
|
+
|
368
|
+
if lft
|
369
|
+
adjustment = pos < lft ? this_gap + 1 : 0
|
370
|
+
raise 'Illegal move: you are trying to move a node within itself' if pos.between?(lft+adjustment,rgt+adjustment) && same_scope?(new_scope)
|
371
|
+
end
|
372
|
+
|
373
|
+
# make a new hole and assign parent
|
374
|
+
adjust_gap!(get_class.full_set(new_scope || scope_hash) , adjust_at, this_gap + 1) if adjust_at
|
375
|
+
self.parent = p_obj
|
376
|
+
|
377
|
+
# Do we need to move the node (already present in the tree), or just save the attributes?
|
378
|
+
if lft && (pos != lft || !same_scope?(new_scope))
|
379
|
+
# Move elements
|
380
|
+
if same_scope?(new_scope)
|
381
|
+
move_by = pos - (lft + adjustment)
|
382
|
+
full_set.all(:lft.gte => lft + adjustment, :rgt.lte => rgt + adjustment).adjust!(:lft => move_by, :rgt => move_by)
|
383
|
+
else # Things have to be done a little differently if moving scope
|
384
|
+
old_lft = lft
|
385
|
+
move_by = pos - lft
|
386
|
+
old_scope = extract_scope(self)
|
387
|
+
sads = self_and_descendents
|
388
|
+
sads.adjust!(:lft => move_by, :rgt => move_by)
|
389
|
+
# Update the attributes to match how they are in the database now.
|
390
|
+
# Be sure to do this between adjust! and setting the new scope
|
391
|
+
attribute_set(:rgt, rgt + move_by)
|
392
|
+
attribute_set(:lft, lft + move_by)
|
393
|
+
sads.each { |sad| sad.update!(new_scope) }
|
394
|
+
end
|
395
|
+
|
396
|
+
# Close hole
|
397
|
+
if old_scope
|
398
|
+
adjust_gap!(get_class.full_set(old_scope), old_lft, -(this_gap + 1))
|
399
|
+
else
|
400
|
+
adjustment += 1 if parent == old_parent
|
401
|
+
adjust_gap!(full_set, lft + adjustment, -(this_gap + 1))
|
402
|
+
end
|
403
|
+
else # just save the attributes
|
404
|
+
attribute_set(:lft, pos)
|
405
|
+
attribute_set(:rgt, lft + this_gap)
|
406
|
+
attributes_set(p_obj.send(:scope_hash)) if p_obj
|
407
|
+
end
|
408
|
+
|
409
|
+
end
|
410
|
+
|
411
|
+
def get_class #:no_doc:
|
412
|
+
self.class.get_class
|
413
|
+
end
|
414
|
+
end # mod InstanceMethods
|
415
|
+
|
416
|
+
Model.send(:include, self)
|
417
|
+
end # mod AwesomeSet
|
418
|
+
end # mod Is
|
419
|
+
end # mod DM
|
@@ -0,0 +1,256 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
scope = {:scope => 1, :scope_2 => 2}
|
5
|
+
scope2 = {:scope => 1, :scope_2 => 5}
|
6
|
+
|
7
|
+
describe DataMapper::Is::AwesomeSet do
|
8
|
+
before :each do
|
9
|
+
Category.auto_migrate!
|
10
|
+
Discrim1.auto_migrate!
|
11
|
+
Discrim2.auto_migrate!
|
12
|
+
end
|
13
|
+
|
14
|
+
it "puts itself as the last root in the defined scope on initial save" do
|
15
|
+
|
16
|
+
c1 = Category.create(scope)
|
17
|
+
c2 = Category.create(scope)
|
18
|
+
c1.pos.should eql([1,2])
|
19
|
+
c2.pos.should eql([3,4])
|
20
|
+
|
21
|
+
c3 = Category.create(scope2)
|
22
|
+
c4 = Category.create(scope2)
|
23
|
+
c3.pos.should eql([1,2])
|
24
|
+
c4.pos.should eql([3,4])
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
it "moves itself into a parent" do
|
30
|
+
|
31
|
+
c1 = Category.create(scope)
|
32
|
+
c2 = Category.create(scope)
|
33
|
+
c3 = Category.create(scope)
|
34
|
+
c2.move(:into => c1)
|
35
|
+
|
36
|
+
[c1,c2,c3].each { |c| c.reload }
|
37
|
+
c1.pos.should eql([1,4])
|
38
|
+
c2.pos.should eql([2,3])
|
39
|
+
c3.pos.should eql([5,6])
|
40
|
+
|
41
|
+
|
42
|
+
c2.move(:into => c3)
|
43
|
+
[c1,c2,c3].each { |c| c.reload }
|
44
|
+
c1.pos.should eql([1,2])
|
45
|
+
c3.pos.should eql([3,6])
|
46
|
+
c2.pos.should eql([4,5])
|
47
|
+
|
48
|
+
# This is to ensure that
|
49
|
+
c4 = Category.new
|
50
|
+
c4.move(:into => c1)
|
51
|
+
[c1,c2,c3, c4].each { |c| c.reload }
|
52
|
+
c4.sco.should eql(c1.sco)
|
53
|
+
c1.pos.should eql([1,4])
|
54
|
+
c4.pos.should eql([2,3])
|
55
|
+
c3.pos.should eql([5,8])
|
56
|
+
c2.pos.should eql([6,7])
|
57
|
+
end
|
58
|
+
|
59
|
+
it "moves around properly" do
|
60
|
+
c1 = Category.create(scope)
|
61
|
+
c2 = Category.create(scope)
|
62
|
+
c3 = Category.create(scope)
|
63
|
+
c4 = Category.new(scope)
|
64
|
+
|
65
|
+
c2.move(:to => 1)
|
66
|
+
[c1,c2,c3].each { |c| c.reload }
|
67
|
+
c2.pos.should eql([1,2])
|
68
|
+
c1.pos.should eql([3,4])
|
69
|
+
c3.pos.should eql([5,6])
|
70
|
+
|
71
|
+
c3.move(:higher)
|
72
|
+
[c1,c2,c3].each { |c| c.reload }
|
73
|
+
c2.pos.should eql([1,2])
|
74
|
+
c3.pos.should eql([3,4])
|
75
|
+
c1.pos.should eql([5,6])
|
76
|
+
|
77
|
+
c3.move(:lower)
|
78
|
+
[c1,c2,c3].each { |c| c.reload }
|
79
|
+
c2.pos.should eql([1,2])
|
80
|
+
c1.pos.should eql([3,4])
|
81
|
+
c3.pos.should eql([5,6])
|
82
|
+
|
83
|
+
c1.move(:lowest)
|
84
|
+
[c1,c2,c3].each { |c| c.reload }
|
85
|
+
c2.pos.should eql([1,2])
|
86
|
+
c3.pos.should eql([3,4])
|
87
|
+
c1.pos.should eql([5,6])
|
88
|
+
|
89
|
+
c1.move(:highest)
|
90
|
+
[c1,c2,c3].each { |c| c.reload }
|
91
|
+
c1.pos.should eql([1,2])
|
92
|
+
c2.pos.should eql([3,4])
|
93
|
+
c3.pos.should eql([5,6])
|
94
|
+
|
95
|
+
c4.move(:highest)
|
96
|
+
[c1,c2,c3].each { |c| c.reload }
|
97
|
+
|
98
|
+
c4.pos.should eql([1,2])
|
99
|
+
c1.pos.should eql([3,4])
|
100
|
+
c2.pos.should eql([5,6])
|
101
|
+
c3.pos.should eql([7,8])
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
it "puts in proper places for above and below" do
|
106
|
+
c1 = Category.create(scope)
|
107
|
+
c2 = Category.create(scope)
|
108
|
+
c3 = Category.create(scope)
|
109
|
+
|
110
|
+
c2.move(:into => c1)
|
111
|
+
[c1,c2,c3].each { |c| c.reload }
|
112
|
+
c1.pos.should eql([1,4])
|
113
|
+
c2.pos.should eql([2,3])
|
114
|
+
c3.pos.should eql([5,6])
|
115
|
+
|
116
|
+
|
117
|
+
c3.move(:above => c2)
|
118
|
+
[c1,c2,c3].each { |c| c.reload }
|
119
|
+
c1.pos.should eql([1,6])
|
120
|
+
c2.pos.should eql([4,5])
|
121
|
+
c3.pos.should eql([2,3])
|
122
|
+
|
123
|
+
c3.move(:below => c1)
|
124
|
+
[c1,c2,c3].each { |c| c.reload }
|
125
|
+
c1.pos.should eql([1,4])
|
126
|
+
c2.pos.should eql([2,3])
|
127
|
+
c3.pos.should eql([5,6])
|
128
|
+
|
129
|
+
c2.move(:below => c1)
|
130
|
+
[c1,c2,c3].each { |c| c.reload }
|
131
|
+
c1.pos.should eql([1,2])
|
132
|
+
c2.pos.should eql([3,4])
|
133
|
+
c3.pos.should eql([5,6])
|
134
|
+
end
|
135
|
+
|
136
|
+
it "gets the parent" do
|
137
|
+
c1 = Category.create(scope)
|
138
|
+
c2 = Category.create(scope)
|
139
|
+
c2.move(:into => c1)
|
140
|
+
c2.parent.should_not be_nil
|
141
|
+
c2.parent.id.should eql(c1.id)
|
142
|
+
end
|
143
|
+
|
144
|
+
it "identifies the root" do
|
145
|
+
|
146
|
+
c1 = Category.create(scope)
|
147
|
+
c2 = Category.create(scope)
|
148
|
+
c3 = Category.create(scope)
|
149
|
+
c2.move :into => c1
|
150
|
+
c3.move :into => c2
|
151
|
+
c3.root.should_not be_nil
|
152
|
+
c3.root.id.should eql(c1.id)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "gets all roots in the current scope" do
|
156
|
+
c1 = Category.create(scope)
|
157
|
+
c2 = Category.create(scope)
|
158
|
+
c2.roots.size.should eql(2)
|
159
|
+
|
160
|
+
c3 = Category.create(scope2)
|
161
|
+
c4 = Category.create(scope2)
|
162
|
+
c3.roots.size.should eql(2)
|
163
|
+
end
|
164
|
+
|
165
|
+
it "gets all ancestors" do
|
166
|
+
c1 = Category.create(scope)
|
167
|
+
c2 = Category.create(scope)
|
168
|
+
c3 = Category.create(scope)
|
169
|
+
|
170
|
+
c2.move(:into => c1)
|
171
|
+
c3.move(:into => c2)
|
172
|
+
c3.ancestors.size.should eql(2)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "gets all siblings" do
|
176
|
+
c1 = Category.create(scope)
|
177
|
+
c2 = Category.create(scope)
|
178
|
+
c3 = Category.create(scope)
|
179
|
+
|
180
|
+
c2.move(:into => c1)
|
181
|
+
c3.move(:into => c1)
|
182
|
+
c3.siblings.size.should eql(1)
|
183
|
+
end
|
184
|
+
|
185
|
+
it "moves scope properly" do
|
186
|
+
c1 = Category.create(scope)
|
187
|
+
c2 = Category.create(scope)
|
188
|
+
c3 = Category.create(scope)
|
189
|
+
|
190
|
+
c4 = Category.create(scope2)
|
191
|
+
c5 = Category.create(scope2)
|
192
|
+
c6 = Category.create(scope2)
|
193
|
+
|
194
|
+
c1.move(:into => c4)
|
195
|
+
[c1,c2,c3,c4,c5,c6].each { |c| c.reload }
|
196
|
+
|
197
|
+
c1.sco.should eql(scope2)
|
198
|
+
|
199
|
+
c2.pos.should eql([1,2])
|
200
|
+
c3.pos.should eql([3,4])
|
201
|
+
|
202
|
+
c4.pos.should eql([1,4])
|
203
|
+
c1.pos.should eql([2,3])
|
204
|
+
c5.pos.should eql([5,6])
|
205
|
+
c6.pos.should eql([7,8])
|
206
|
+
|
207
|
+
c4.move(:into => c2)
|
208
|
+
[c1,c2,c3,c4,c5,c6].each { |c| c.reload }
|
209
|
+
c1.sco.should eql(scope)
|
210
|
+
c4.sco.should eql(scope)
|
211
|
+
|
212
|
+
c2.pos.should eql([1,6])
|
213
|
+
c4.pos.should eql([2,5])
|
214
|
+
c1.pos.should eql([3,4])
|
215
|
+
c3.pos.should eql([7,8])
|
216
|
+
|
217
|
+
|
218
|
+
c5.pos.should eql([1,2])
|
219
|
+
c6.pos.should eql([3,4])
|
220
|
+
|
221
|
+
# You can move into the root of a different scope by passing an object from that scope
|
222
|
+
# or a hash that represents that scope
|
223
|
+
c5.move(:root => scope)
|
224
|
+
[c1,c2,c3,c4,c5,c6].each { |c| c.reload }
|
225
|
+
c5.sco.should eql(scope)
|
226
|
+
|
227
|
+
c2.pos.should eql([1,6])
|
228
|
+
c4.pos.should eql([2,5])
|
229
|
+
c1.pos.should eql([3,4])
|
230
|
+
c3.pos.should eql([7,8])
|
231
|
+
c5.pos.should eql([9,10])
|
232
|
+
|
233
|
+
c6.pos.should eql([1,2])
|
234
|
+
|
235
|
+
end
|
236
|
+
|
237
|
+
it "should get all rows in the database if the discrimator is not part of scope" do
|
238
|
+
c1 = CatD11.create(scope)
|
239
|
+
c2 = CatD11.create(scope)
|
240
|
+
c3 = CatD12.create(scope)
|
241
|
+
c4 = CatD12.create(scope)
|
242
|
+
CatD12.roots(scope).size.should eql(4)
|
243
|
+
end
|
244
|
+
|
245
|
+
it "should get only the same object types if discriminator is part of scope" do
|
246
|
+
c1 = CatD21.create(scope)
|
247
|
+
c2 = CatD21.create(scope)
|
248
|
+
c3 = CatD22.create(scope2)
|
249
|
+
c4 = CatD22.create(scope2)
|
250
|
+
Discrim2.roots(scope.merge(:type => 'CatD21')).size.should eql(2)
|
251
|
+
Discrim2.roots(scope2.merge(:type => 'CatD22')).size.should eql(2)
|
252
|
+
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
$:.push File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
require 'dm-is-awesome_set'
|
4
|
+
|
5
|
+
# Needed for the discriminator
|
6
|
+
gem 'dm-types', '>=0.9.7'
|
7
|
+
require 'dm-types'
|
8
|
+
require 'dm-validations'
|
9
|
+
|
10
|
+
|
11
|
+
# classes/vars for tests
|
12
|
+
class Category
|
13
|
+
include DataMapper::Resource
|
14
|
+
|
15
|
+
property :id, Serial
|
16
|
+
property :name, String
|
17
|
+
property :scope, Integer
|
18
|
+
property :scope_2, Integer
|
19
|
+
|
20
|
+
is :awesome_set, :scope => [:scope, :scope_2]
|
21
|
+
|
22
|
+
# convenience methods only for speccing.
|
23
|
+
def pos; [lft,rgt] end
|
24
|
+
def sco; {:scope => scope, :scope_2 => scope_2}; end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Discrim1
|
28
|
+
include DataMapper::Resource
|
29
|
+
|
30
|
+
property :id, Serial
|
31
|
+
property :name, String
|
32
|
+
property :scope, Integer
|
33
|
+
property :scope_2, Integer
|
34
|
+
property :type, Discriminator
|
35
|
+
|
36
|
+
is :awesome_set, :scope => [:scope, :scope_2]
|
37
|
+
|
38
|
+
# convenience methods only for speccing.
|
39
|
+
def pos; [lft,rgt] end
|
40
|
+
def sco; {:scope => scope, :scope_2 => scope_2}; end
|
41
|
+
end
|
42
|
+
|
43
|
+
class CatD11 < Discrim1
|
44
|
+
end
|
45
|
+
|
46
|
+
class CatD12 < Discrim1
|
47
|
+
end
|
48
|
+
|
49
|
+
class Discrim2
|
50
|
+
include DataMapper::Resource
|
51
|
+
|
52
|
+
property :id, Serial
|
53
|
+
property :name, String
|
54
|
+
property :scope, Integer
|
55
|
+
property :scope_2, Integer
|
56
|
+
property :type, Discriminator
|
57
|
+
|
58
|
+
is :awesome_set, :scope => [:scope, :scope_2, :type]
|
59
|
+
|
60
|
+
# convenience methods only for speccing.
|
61
|
+
def pos; [lft,rgt] end
|
62
|
+
def sco; {:scope => scope, :scope_2 => scope_2}; end
|
63
|
+
end
|
64
|
+
|
65
|
+
class CatD21 < Discrim2
|
66
|
+
end
|
67
|
+
|
68
|
+
class CatD22 < Discrim2
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
# Set up database
|
74
|
+
DataMapper.setup(:default, 'sqlite3://awesome_set_test.db')
|
75
|
+
DataMapper.auto_migrate!
|
76
|
+
|
77
|
+
|
78
|
+
# Quick hack for ruby 1.8.6 - really, it's a hack. Don't use this anywhere else.
|
79
|
+
|
80
|
+
class Hash
|
81
|
+
def eql?(obj)
|
82
|
+
if obj.is_a?(Hash)
|
83
|
+
each { |k,v| return false unless self[k].eql?(obj[k]) }
|
84
|
+
true
|
85
|
+
else
|
86
|
+
false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: snusnu-dm-is-awesome_set
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.7.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Nicoll
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-02-12 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: dm-core
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.9.7
|
23
|
+
version:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: dm-adjust
|
26
|
+
version_requirement:
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 0.9.7
|
32
|
+
version:
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: dm-aggregates
|
35
|
+
version_requirement:
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.9.7
|
41
|
+
version:
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: dm-validations
|
44
|
+
version_requirement:
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 0.9.10
|
50
|
+
version:
|
51
|
+
description: DataMapper nested set plugin that works
|
52
|
+
email: jnicoll@gnexp.com
|
53
|
+
executables: []
|
54
|
+
|
55
|
+
extensions: []
|
56
|
+
|
57
|
+
extra_rdoc_files:
|
58
|
+
- README
|
59
|
+
- LICENSE
|
60
|
+
- TODO
|
61
|
+
files:
|
62
|
+
- LICENSE
|
63
|
+
- README
|
64
|
+
- Rakefile
|
65
|
+
- TODO
|
66
|
+
- lib/dm-is-awesome_set
|
67
|
+
- lib/dm-is-awesome_set/is
|
68
|
+
- lib/dm-is-awesome_set/is/awesome_set.rb
|
69
|
+
- lib/dm-is-awesome_set.rb
|
70
|
+
- spec/dm-is-awesome_set_spec.rb
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
has_rdoc: true
|
73
|
+
homepage: http://gnexp.com/
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: "0"
|
84
|
+
version:
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
version:
|
91
|
+
requirements: []
|
92
|
+
|
93
|
+
rubyforge_project: merb
|
94
|
+
rubygems_version: 1.2.0
|
95
|
+
signing_key:
|
96
|
+
specification_version: 2
|
97
|
+
summary: DataMapper nested set plugin that works
|
98
|
+
test_files: []
|
99
|
+
|