dm-is-list 0.9.11 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,234 @@
1
1
  module DataMapper
2
2
  module Is
3
+
4
+ # = dm-is-list
5
+ #
6
+ # DataMapper plugin for creating and organizing lists.
7
+ #
8
+ # == Installation
9
+ #
10
+ # === Stable
11
+ #
12
+ # Install the +dm-more+ gem, which will by default install +dm-is-list+ and other required gems.
13
+ #
14
+ # $ (sudo)? gem install dm-more
15
+ #
16
+ # === Edge
17
+ #
18
+ # Download or clone +dm-more+ from Github[http://github.com/datamapper/dm-more/].
19
+ #
20
+ # $ cd /path/to/dm-more
21
+ #
22
+ # $ rake install # will install all the dm-more gems (some of which are required by dm-is-list)
23
+ #
24
+ # # enter your password at the prompt, if required
25
+ # $ password ...
26
+ #
27
+ #
28
+ # == Getting started
29
+ #
30
+ # First of all, for a better understanding of this gem, make sure you study the '<tt>dm-is-list/spec/integration/list_spec.rb</tt>' file.
31
+ #
32
+ # ----
33
+ #
34
+ # Require +dm-is-list+ in your app.
35
+ #
36
+ # require 'dm-core' # must be required first
37
+ # require 'dm-is-list'
38
+ #
39
+ #
40
+ # Lets say we have a User class, and we want to give users the possibility of
41
+ # having their own todo-lists.
42
+ #
43
+ #
44
+ # class User
45
+ # include DataMapper::Resource
46
+ #
47
+ # property :id, Serial
48
+ # property :name, String
49
+ #
50
+ # has n, :todos
51
+ # end
52
+ #
53
+ # class Todo
54
+ # include DataMapper::Resource
55
+ #
56
+ # property :id, Serial
57
+ # property :title, String
58
+ # property :done, DateTime
59
+ #
60
+ # belongs_to :user
61
+ #
62
+ # # here we define that this should be a list, scoped on :user_id
63
+ # is :list, :scope => [:user_id]
64
+ # end
65
+ #
66
+ # Once we have our Users and Lists, we might want to work with...
67
+ #
68
+ # == Movements of list items
69
+ #
70
+ # Any list item can be moved around <b>within the same list</b> easily through the <tt>move</tt> method.
71
+ #
72
+ #
73
+ # === move( vector )
74
+ #
75
+ # There are number of convenient vectors that help you move items around within the list.
76
+ #
77
+ # item = Todo.get(1)
78
+ # other = Todo.get(2)
79
+ #
80
+ # item.move(:highest) # moves to top of list.
81
+ # item.move(:lowest) # moves to bottom of list.
82
+ # item.move(:top) # moves to top of list.
83
+ # item.move(:bottom) # moves to bottom of list.
84
+ # item.move(:up) # moves one up (:higher and :up is the same) within the scope.
85
+ # item.move(:down) # moves one up (:lower and :down is the same) within the scope.
86
+ # item.move(:to => position) # moves item to a specific position.
87
+ # item.move(:above => other) # moves item above the other item.*
88
+ # item.move(:below => other) # moves item above the other item.*
89
+ #
90
+ # # * won't move if the other item is in another scope. (should this be enabled?)
91
+ #
92
+ # The list will act as intelligently as possible and keep positions in a logical running order.
93
+ #
94
+ #
95
+ # === move( Integer )
96
+ #
97
+ # <b>NOTE! VERY IMPORTANT!</b>
98
+ #
99
+ # If you set the position manually, and then save, <b>the list will NOT reorganize itself</b>.
100
+ #
101
+ # item.position = 3 # setting position manually
102
+ # item.save # the item will now have position 3, but the list may have two items with the same position.
103
+ #
104
+ # # alternatively
105
+ # item.update(:position => 3) # sets the position manually, but does not reorganize the list positions.
106
+ #
107
+ #
108
+ # You should therefore <b>always use</b> the <tt>item.move(N)</tt> syntax instead.
109
+ #
110
+ # item.move(3) # does the same as above, but in one call AND *reorganizes* the list.
111
+ #
112
+ # <hr>
113
+ #
114
+ # <b>Hold On!</b>
115
+ #
116
+ # <tt>dm-is-list</tt> used to work with <tt>item.position = 1</tt> type syntax. Why this change?
117
+ #
118
+ # The main reason behind this change was that the previous version of <tt>dm-is-list</tt> created a LOT of
119
+ # extra SQL queries in order to support the manual updating of position, and as a result had a quite a few bugs/issues,
120
+ # which have been fixed in this version.
121
+ #
122
+ # The other reason is that I couldn't work out how to keep the functionality without adding the extra queries. But perhaps you can ?
123
+ #
124
+ # <hr>
125
+ #
126
+ # See "<b>Batch Changing Positions</b>" below for information on how to change the positions on a whole list.
127
+ #
128
+ # == Movements between scopes
129
+ #
130
+ # When you move items between scopes, the list will try to work with your intentions.
131
+ #
132
+ #
133
+ # Move the item from list to new list and add the item to the bottom of that list.
134
+ #
135
+ # item.user_id # => 1
136
+ # item.move_to_list(10) # => the scope id ie User.get(10).id
137
+ #
138
+ # # results in...
139
+ # item.user_id # => 10
140
+ # item.position # => < bottom of the list >
141
+ #
142
+ #
143
+ # Move the item from list to new list and add at the position given.
144
+ #
145
+ # item.user_id # => 1
146
+ # item.move_to_list(10, 2) # => the scope id ie User.get(10).id, position => 2
147
+ #
148
+ # # results in...
149
+ # item.user_id # => 10
150
+ # item.position # => 2
151
+ #
152
+ #
153
+ # == Batch Changing Positions
154
+ #
155
+ # A common scenario when working with lists is the sorting of a whole list via something like JQuery's sortable() functionality.
156
+ # <br>
157
+ # (Think re-arranging the order of Todo's according to priority or something similar)
158
+ #
159
+ #
160
+ # === Optimum scenario
161
+ #
162
+ # The most SQL query efficient way of changing the positions is:
163
+ #
164
+ #
165
+ # sort_order = [5,4,3,2,1] # list from AJAX request..
166
+ #
167
+ # items = Todo.all(:user => @u1) # loads all 5 items in the list
168
+ #
169
+ # items.each{ |item| item.update(:position => sort_order.index(item.id) + 1) } # remember the +1 since array's are indexed from 0
170
+ #
171
+ #
172
+ # The above code will result in something like these queries.
173
+ #
174
+ # # SELECT "id", "title", "position", "user_id" FROM "todos" WHERE "user_id" = 1 ORDER BY "position"
175
+ # # UPDATE "todos" SET "position" = 5 WHERE "id" = 1
176
+ # # UPDATE "todos" SET "position" = 4 WHERE "id" = 2
177
+ # # UPDATE "todos" SET "position" = 2 WHERE "id" = 4
178
+ # # UPDATE "todos" SET "position" = 1 WHERE "id" = 5
179
+ #
180
+ # <b>Remember!</b> Your sort order list has to be the same length as the found items in the list, or your loop will fail.
181
+ #
182
+ #
183
+ # === Wasteful scenario
184
+ #
185
+ # You can also use this version, but it will create upto <b>5 times as many SQL queries</b>. :(
186
+ #
187
+ #
188
+ # sort_order = ['5','4','3','2','1'] # list from AJAX request..
189
+ #
190
+ # items = Todo.all(:user => @u1) # loads all 5 items in the list
191
+ #
192
+ # items.each{ |item| item.move(sort_order.index(item.id).to_i + 1) } # remember the +1 since array's are indexed from 0
193
+ #
194
+ # The above code will result in something like these queries:
195
+ #
196
+ # # SELECT "id", "title", "position", "user_id" FROM "todos" WHERE "user_id" = 1 ORDER BY "position"
197
+ #
198
+ # # SELECT "id", "title", "position", "user_id" FROM "todos" WHERE "user_id" = 1 ORDER BY "position" DESC LIMIT 1
199
+ # # SELECT "id" FROM "todos" WHERE "user_id" = 1 AND "id" IN (1, 2, 3, 4, 5) AND "position" BETWEEN 1 AND 5 ORDER BY "position"
200
+ # # UPDATE "todos" SET "position" = "position" + -1 WHERE "user_id" = 1 AND "position" BETWEEN 1 AND 5
201
+ # # SELECT "id", "position" FROM "todos" WHERE "id" IN (1, 2, 3, 4, 5) ORDER BY "id"
202
+ # # UPDATE "todos" SET "position" = 5 WHERE "id" = 1
203
+ #
204
+ # # SELECT "id", "title", "position", "user_id" FROM "todos" WHERE "user_id" = 1 ORDER BY "position" DESC LIMIT 1
205
+ # # SELECT "id" FROM "todos" WHERE "user_id" = 1 AND "id" IN (1, 2, 3, 4, 5) AND "position" BETWEEN 1 AND 4 ORDER BY "position"
206
+ # # UPDATE "todos" SET "position" = "position" + -1 WHERE "user_id" = 1 AND "position" BETWEEN 1 AND 4
207
+ # # SELECT "id", "position" FROM "todos" WHERE "id" IN (2, 3, 4, 5) ORDER BY "id"
208
+ # # UPDATE "todos" SET "position" = 4 WHERE "id" = 2
209
+ #
210
+ # # ...
211
+ #
212
+ # As you can see it will also do the job, but will be more expensive.
213
+ #
214
+ #
215
+ # == RTFM
216
+ #
217
+ # As I said above, for a better understanding of this gem/plugin, make sure you study the '<tt>dm-is-list/spec/integration/list_spec.rb</tt>' tests.
218
+ #
219
+ #
220
+ # == Errors / Bugs
221
+ #
222
+ # If something is not behaving intuitively, it is a bug, and should be reported.
223
+ # Report it here: http://datamapper.lighthouseapp.com/
224
+ #
225
+
3
226
  module List
4
227
 
5
228
  ##
6
229
  # method for making your model a list.
230
+ # TODO:: this explanation is confusing. Need to translate into literal code
231
+ #
7
232
  # it will define a :position property if it does not exist, so be sure to have a
8
233
  # position-column in your database (will be added automatically on auto_migrate)
9
234
  # if the column has a different name, simply make a :position-property and set a
@@ -18,42 +243,52 @@ module DataMapper
18
243
  #
19
244
  # @option :scope<Array> an array of attributes that should be used to scope lists
20
245
  #
246
+ # @api public
21
247
  def is_list(options={})
22
248
  options = { :scope => [], :first => 1 }.merge(options)
23
249
 
24
250
  extend DataMapper::Is::List::ClassMethods
25
251
  include DataMapper::Is::List::InstanceMethods
26
252
 
27
- property :position, Integer unless properties.detect{|p| p.name == :position && p.type == Integer}
253
+ unless properties.any? { |p| p.name == :position && p.type == Integer }
254
+ property :position, Integer
255
+ end
28
256
 
29
257
  @list_options = options
30
258
 
31
259
  before :create do
32
- # a position has been set before save => open up and make room for item
33
- # no position has been set => move to bottom of my scope-list (or keep detached?)
34
- self.send(:move_without_saving, (self.position || :lowest))
260
+ # if a position has been set before save, then insert it at the position and
261
+ # move the other items in the list accordingly, else if no position has been set
262
+ # then set position to bottom of list
263
+ send(:move_without_saving, position || :lowest)
264
+
265
+ # on create, set moved to false so we can move the list item after creating it
266
+ # self.moved = false
35
267
  end
36
268
 
37
269
  before :update do
38
- # if the scope has changed, we need to detach our item from the old list
39
- if self.list_scope != self.original_list_scope
40
- newpos = self.position
41
- self.detach(self.original_list_scope) # removing from old list
42
- self.send(:move_without_saving, newpos || :lowest) # moving to pos or bottom of new list
43
- elsif self.attribute_dirty?(:position) && !self.moved
44
- self.send(:move_without_saving, self.position)
45
- end
46
-
47
- # on update, clean moved to prepare for the next change
48
- self.moved = false
49
-
50
270
  # a (new) position has been set => move item to this position (only if position has been set manually)
51
271
  # the scope has changed => detach from old list, and possibly move into position
52
272
  # the scope and position has changed => detach from old, move to pos in new
273
+
274
+ # if the scope has changed, we need to detach our item from the old list
275
+ if list_scope != original_list_scope
276
+ newpos = position
277
+ detach(original_list_scope) # removing from old list
278
+ send(:move_without_saving, newpos || :lowest) # moving to pos or bottom of new list
279
+ end
280
+
281
+ # NOTE:: uncommenting the following creates a large number of extra un-wanted SQL queries
282
+ # hence the commenting out of it.
283
+ # if attribute_dirty?(:position) && !moved
284
+ # send(:move_without_saving, position)
285
+ # end
286
+ # # on update, clean moved to prepare for the next change
287
+ # self.moved = false
53
288
  end
54
289
 
55
290
  before :destroy do
56
- self.detach
291
+ detach
57
292
  end
58
293
 
59
294
  # we need to make sure that STI-models will inherit the list_scope.
@@ -61,7 +296,7 @@ module DataMapper
61
296
  target.instance_variable_set(:@list_options, @list_options.dup)
62
297
  end
63
298
 
64
- end
299
+ end # is_list
65
300
 
66
301
  module ClassMethods
67
302
  attr_reader :list_options
@@ -75,126 +310,306 @@ module DataMapper
75
310
  #
76
311
  # @param scope [Hash]
77
312
  #
78
- def repair_list(scope={})
79
- return false unless scope.keys.all?{|s| list_options[:scope].include?(s) || s == :order }
80
- all({:order => [:position.asc]}.merge(scope)).each_with_index{ |item,i| item.position = i+1; item.save }
313
+ # @api public
314
+ def repair_list(scope = {})
315
+ return false unless scope.keys.all?{ |s| list_options[:scope].include?(s) || s == :order }
316
+ all({ :order => [ :position ] }.merge(scope)).each_with_index{ |item, i| item.update(:position => i + 1) }
317
+ true
81
318
  end
319
+
82
320
  end
83
321
 
84
322
  module InstanceMethods
323
+
324
+ # @api semipublic
85
325
  attr_accessor :moved
86
326
 
327
+ ##
328
+ # returns the scope of the current list item
329
+ #
330
+ # @return <Hash> ...?
331
+ #
332
+ # @example [Usage]
333
+ # Todo.get(2).list_scope => { :user_id => 1 }
334
+ #
335
+ #
336
+ # @api semipublic
87
337
  def list_scope
88
- self.class.list_options[:scope].map{|p| [p,attribute_get(p)]}.to_hash
338
+ model.list_options[:scope].map{ |p| [ p, attribute_get(p) ] }.to_hash
89
339
  end
90
340
 
341
+ ##
342
+ # returns the _original_ scope of the current list item
343
+ #
344
+ # @return <Hash> ...?
345
+ #
346
+ # @example [Usage]
347
+ # item = Todo.get(2) # with user_id 1
348
+ # item.user_id = 2
349
+ # item.original_list_scope => { :user_id => 1 }
350
+ #
351
+ # @api semipublic
91
352
  def original_list_scope
92
- self.class.list_options[:scope].map{|p| [p,original_values.key?(p) ? original_values[p] : attribute_get(p)]}.to_hash
353
+ model.list_options[:scope].map{
354
+ |p| [ p, (property = properties[p]) && original_attributes.key?(property) ? original_attributes[property] : attribute_get(p) ]
355
+ }.to_hash
93
356
  end
94
357
 
358
+ ##
359
+ # returns the query conditions
360
+ #
361
+ # @return <Hash> ...?
362
+ #
363
+ # @example [Usage]
364
+ # Todo.get(2).list_query => { :user_id => 1, :order => [:position] }
365
+ #
366
+ # @api semipublic
95
367
  def list_query
96
- list_scope.merge(:order => [:position.asc])
368
+ list_scope.merge(:order => [ :position ])
97
369
  end
98
370
 
99
- def list(scope=list_query)
100
- self.class.all(scope)
371
+ ##
372
+ # returns the list the current item belongs to
373
+ #
374
+ # @param scope <Hash> Optional (Default is #list_query)
375
+ #
376
+ # @return <DataMapper::Collection> the list items within the given scope
377
+ #
378
+ # @example [Usage]
379
+ # Todo.get(2).list => [ list of Todo items within the same scope as item]
380
+ # Todo.get(2).list(:user_id => 2 ) => [ list of Todo items with user_id => 2]
381
+ #
382
+ # @api public
383
+ def list(scope = list_query)
384
+ model.all(scope)
101
385
  end
102
386
 
103
387
  ##
104
388
  # repair the list this item belongs to
105
389
  #
390
+ # @api public
106
391
  def repair_list
107
- self.class.repair_list(list_scope)
392
+ model.repair_list(list_scope)
108
393
  end
109
394
 
110
395
  ##
111
396
  # reorder the list this item belongs to
112
397
  #
398
+ # @param order <Array> ...?
399
+ #
400
+ # @return <Boolean> True/False based upon result
401
+ #
402
+ # @example [Usage]
403
+ # Todo.get(2).reorder_list([:title.asc])
404
+ #
405
+ # @api public
113
406
  def reorder_list(order)
114
- self.class.repair_list(list_scope.merge(:order => order))
407
+ model.repair_list(list_scope.merge(:order => order))
115
408
  end
116
409
 
117
- def detach(scope=list_scope)
118
- list(scope).all(:position.gt => position).adjust!({:position => -1},true)
410
+ ##
411
+ # detaches a list item from the list, essentially setting the position as nil
412
+ #
413
+ # @param scope <Hash> Optional (Default is #list_scope)
414
+ #
415
+ # @return <DataMapper::Collection> the list items within the given scope
416
+ #
417
+ # @example [Usage]
418
+ #
419
+ # @api public
420
+ def detach(scope = list_scope)
421
+ list(scope).all(:position.gt => position).adjust!({ :position => -1 },true)
119
422
  self.position = nil
120
423
  end
121
424
 
425
+ ##
426
+ # moves an item from one list to another
427
+ #
428
+ # @param scope <Integer> must be the id value of the scope
429
+ # @param pos <Integer> Optional sets the entry position for the item in the new list
430
+ #
431
+ # @example [Usage]
432
+ # Todo.get(2).move_to_list(2)
433
+ # Todo.get(2).move_to_list(2, 10)
434
+ #
435
+ # @return <Boolean> True/False based upon result
436
+ #
437
+ # @api public
438
+ def move_to_list(scope, pos = nil)
439
+ transaction do |txn|
440
+ self.detach # remove from current list
441
+ self.attribute_set(model.list_options[:scope][0], scope.to_i) # set new scope
442
+ self.save # save progress. Needed to get the positions correct.
443
+ self.reload # get a fresh new start
444
+ self.move(pos) unless pos.nil?
445
+ end
446
+ end
447
+
448
+ ##
449
+ # finds the previous _higher_ item in the list (lower in number position)
450
+ #
451
+ # @return <Model> the previous list item
452
+ #
453
+ # @example [Usage]
454
+ # Todo.get(2).left_sibling => Todo.get(1)
455
+ # Todo.get(2).higher_item => Todo.get(1)
456
+ # Todo.get(2).previous_item => Todo.get(1)
457
+ #
458
+ # @api public
122
459
  def left_sibling
123
460
  list.reverse.first(:position.lt => position)
124
461
  end
462
+ alias_method :higher_item, :left_sibling
463
+ alias_method :previous_item, :left_sibling
125
464
 
465
+ ##
466
+ # finds the next _lower_ item in the list (higher in number position)
467
+ #
468
+ # @return <Model> the next list item
469
+ #
470
+ # @example [Usage]
471
+ # Todo.get(2).right_sibling => Todo.get(3)
472
+ # Todo.get(2).lower_item => Todo.get(3)
473
+ # Todo.get(2).next_item => Todo.get(3)
474
+ #
475
+ # @api public
126
476
  def right_sibling
127
477
  list.first(:position.gt => position)
128
478
  end
479
+ alias_method :lower_item, :right_sibling
480
+ alias_method :next_item, :right_sibling
481
+
129
482
 
130
483
  ##
131
484
  # move item to a position in the list. position should _only_ be changed through this
132
485
  #
133
486
  # @example [Usage]
134
- # * node.move :higher # moves node higher unless it is at the top of parent
135
- # * node.move :lower # moves node lower unless it is at the bottom of parent
136
- # * node.move :below => other # moves this node below other resource in the set
487
+ # * node.move :higher # moves node higher unless it is at the top of list
488
+ # * node.move :lower # moves node lower unless it is at the bottom of list
489
+ # * node.move :below => other_node # moves this node below the other resource in the list
490
+ # * node.move :above => Node.get(2) # moves this node above the other resource in the list
491
+ # * node.move :to => 2 # moves this node to the position given in the list
492
+ # * node.move(2) # moves this node to the position given in the list
137
493
  #
138
- # @param vector <Symbol, Hash> A symbol, or a key-value pair that describes the requested movement
494
+ # @param vector <Symbol, Hash, Integer> An integer, a symbol, or a key-value pair that describes the requested movement
139
495
  #
140
496
  # @option :higher<Symbol> move item higher
141
- # @option :up<Symbol> move item higher
142
- # @option :highest<Symbol> move item to the top of the list
143
497
  # @option :lower<Symbol> move item lower
498
+ # @option :up<Symbol> move item higher
144
499
  # @option :down<Symbol> move item lower
500
+ # @option :highest<Symbol> move item to the top of the list
145
501
  # @option :lowest<Symbol> move item to the bottom of the list
502
+ # @option :top<Symbol> move item to the top of the list
503
+ # @option :bottom<Symbol> move item to the bottom of the list
146
504
  # @option :above<Resource> move item above other item. must be in same scope
147
505
  # @option :below<Resource> move item below other item. must be in same scope
148
- # @option :to<Fixnum> move item to a specific location in the list
506
+ # @option :to<Hash{Symbol => Integer/String}> move item to a specific position in the list
507
+ # @option <Integer> move item to a specific position in the list
149
508
  #
150
509
  # @return <TrueClass, FalseClass> returns false if it cannot move to the position, otherwise true
151
510
  # @see move_without_saving
511
+ #
512
+ # @api public
152
513
  def move(vector)
153
- move_without_saving(vector) && save
514
+ transaction do |txn|
515
+ move_without_saving(vector) && save
516
+ end
154
517
  end
155
518
 
156
- ##
157
- # does all the actual movement in #move, but does not save afterwards. this is used internally in
158
- # before :save, and will probably be marked private. should not be used by organic beings.
159
- #
160
- # @see move
161
- private
162
- def move_without_saving(vector)
163
- if vector.is_a? Hash then action,object = vector.keys[0],vector.values[0] else action = vector end
164
-
165
- minpos = self.class.list_options[:first]
166
- prepos = self.original_values[:position] || self.position
167
- maxpos = list.last ? (list.last == self ? prepos : list.last.position + 1) : minpos
168
-
169
- newpos = case action
170
- when :highest then minpos
171
- when :lowest then maxpos
172
- when :higher,:up then [position-1,minpos].max
173
- when :lower,:down then [position+1,maxpos].min
174
- when :above then object.position
175
- when :below then object.position+1
176
- when :to then [minpos,[object.to_i,maxpos].min].max
177
- else [action.to_i,maxpos].min
178
- end
179
519
 
180
- return false if [:lower, :higher].include?(action) && newpos == prepos
181
- return false if !newpos || ([:above,:below].include?(action) && list_scope != object.list_scope)
182
- return true if newpos == position && position == prepos || (newpos == maxpos && position == maxpos-1)
183
-
184
- if !position
185
- list.all(:position.gte => newpos).adjust!({:position => +1},true) unless action == :lowest
186
- elsif newpos > prepos
187
- newpos -= 1 if [:lowest,:above,:below,:to].include?(action)
188
- list.all(:position => prepos..newpos).adjust!({:position => -1},true)
189
- elsif newpos < prepos
190
- list.all(:position => newpos..prepos).adjust!({:position => +1},true)
191
- end
520
+ private
192
521
 
193
- self.position = newpos
194
- self.moved = true
195
- true
196
- end
197
- end
522
+ ##
523
+ # does all the actual movement in #move, but does not save afterwards. this is used internally in
524
+ # before :create / :update. Should not be used by organic beings.
525
+ #
526
+ # @see move
527
+ #
528
+ # @api private
529
+ def move_without_saving(vector)
530
+ if vector.kind_of?(Hash)
531
+ action, object = vector.keys[0], vector.values[0]
532
+ else
533
+ action = vector
534
+ end
535
+
536
+ # set the start position to 1 or, if offset in the list_options is :list, :first => X
537
+ minpos = model.list_options[:first]
538
+
539
+ # the previous position (if changed) else current position
540
+ prepos = original_attributes[properties[:position]] || position
541
+
542
+ # set the last position in the list or previous position if the last item
543
+ maxpos = (last = list.last) ? (last == self ? prepos : last.position + 1) : minpos
544
+
545
+ newpos = case action
546
+ when :highest then minpos
547
+ when :top then minpos
548
+ when :lowest then maxpos
549
+ when :bottom then maxpos
550
+ when :higher,:up then [ position - 1, minpos ].max
551
+ when :lower,:down then [ position + 1, maxpos ].min
552
+ when :above
553
+ # the object given, can either be:
554
+ # -- the same as self
555
+ # -- already below self
556
+ # -- higher up than self (lower number in list)
557
+ ( (self == object) or (object.position > self.position) ) ? self.position : object.position
558
+
559
+ when :below
560
+ # the object given, can either be:
561
+ # -- the same as self
562
+ # -- already above self
563
+ # -- lower than self (higher number in list)
564
+ ( self == object or (object.position < self.position) ) ? self.position : object.position + 1
565
+
566
+ when :to
567
+ # can only move within top and bottom positions of list
568
+ # -- .move(:to => 2 ) Hash with FixNum
569
+ # -- .move(:to => '2' ) Hash with String
570
+
571
+ # NOTE:: sensitive functionality
572
+ # maxpos is incremented above, so decrement by 1 to get true maxpos
573
+ # minpos is fixed, so just take the object position value given
574
+ # else add 1 to object position value
575
+ obj = object.to_i
576
+ if (obj > maxpos)
577
+ [ minpos, [ obj, maxpos - 1 ].min ].max
578
+ else
579
+ [ minpos, [ obj, maxpos ].min ].max
580
+ end
581
+
582
+ else
583
+ raise ArgumentError, "unrecognized vector: [#{action}]. Please check your spelling and/or the docs" if action.is_a?(Symbol)
584
+ # -- .move(2) as FixNum only
585
+ # -- .move('2') as String only
586
+ if action.to_i < minpos
587
+ [ minpos, maxpos - 1 ].min
588
+ else
589
+ [ action.to_i, maxpos - 1 ].min
590
+ end
591
+ end
592
+
593
+ # don't move if already at the position
594
+ return false if [ :lower, :down, :higher, :up, :top, :bottom, :highest, :lowest, :above, :below ].include?(action) && newpos == prepos
595
+ return false if !newpos || ([ :above, :below ].include?(action) && list_scope != object.list_scope)
596
+ return true if newpos == position && position == prepos || (newpos == maxpos && position == maxpos - 1)
597
+
598
+ if !position
599
+ list.all(:position.gte => newpos).adjust!({ :position => 1 }, true) unless action =~ /:(lowest|bottom)/
600
+ elsif newpos > prepos
601
+ newpos -= 1 if [:lowest,:bottom,:above,:below].include?(action)
602
+ list.all(:position => prepos..newpos).adjust!({ :position => -1 }, true)
603
+ elsif newpos < prepos
604
+ list.all(:position => newpos..prepos).adjust!({ :position => 1 }, true)
605
+ end
606
+
607
+ self.position = newpos
608
+ self.moved = true
609
+ true
610
+ end # move_without_saving
611
+
612
+ end # InstanceMethods
198
613
  end # List
199
614
  end # Is
200
615
  end # DataMapper
@@ -1,7 +1,7 @@
1
1
  module DataMapper
2
2
  module Is
3
3
  module List
4
- VERSION = '0.9.11'
4
+ VERSION = '0.10.0'.freeze
5
5
  end
6
6
  end
7
7
  end