dm-is-list 0.9.11 → 0.10.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.
@@ -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