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.
- data/History.rdoc +46 -0
- data/Manifest.txt +2 -2
- data/README.rdoc +220 -0
- data/Rakefile +2 -3
- data/lib/dm-is-list/is/list.rb +493 -78
- data/lib/dm-is-list/is/version.rb +1 -1
- data/lib/dm-is-list.rb +1 -9
- data/spec/integration/list_spec.rb +981 -140
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +11 -8
- data/tasks/install.rb +1 -1
- data/tasks/spec.rb +4 -4
- metadata +14 -31
- data/History.txt +0 -17
- data/README.txt +0 -57
data/lib/dm-is-list/is/list.rb
CHANGED
@@ -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
|
-
|
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
|
33
|
-
#
|
34
|
-
|
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
|
-
|
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
|
-
|
79
|
-
|
80
|
-
all
|
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
|
-
|
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
|
-
|
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
|
368
|
+
list_scope.merge(:order => [ :position ])
|
97
369
|
end
|
98
370
|
|
99
|
-
|
100
|
-
|
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
|
-
|
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
|
-
|
407
|
+
model.repair_list(list_scope.merge(:order => order))
|
115
408
|
end
|
116
409
|
|
117
|
-
|
118
|
-
|
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
|
135
|
-
# * node.move :lower
|
136
|
-
# * node.move :below =>
|
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>
|
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<
|
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
|
-
|
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
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|