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.
- 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
|