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 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,4 @@
1
+ TODO:
2
+ Write documentation that doesn't suck
3
+ Integrate autospec functionality into specs for convenience
4
+ Refactor specs so that they are not ugly.
@@ -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,6 @@
1
+ require 'pathname'
2
+ ['dm-core', 'dm-adjust', 'dm-aggregates', 'dm-validations'].each do |dm_var|
3
+ gem dm_var, '>=0.9.7'
4
+ require dm_var
5
+ end
6
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-awesome_set' / 'is' / 'awesome_set.rb'
@@ -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
@@ -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
+