/
list.rb
208 lines (186 loc) · 7.73 KB
/
list.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# frozen-string-literal: true
module Sequel
module Plugins
# The list plugin allows for model instances to be part of an ordered list,
# based on a position field in the database. It can either consider all
# rows in the table as being from the same list, or you can specify scopes
# so that multiple lists can be kept in the same table.
#
# Basic Example:
#
# class Item < Sequel::Model(:items)
# plugin :list # will use :position field for position
# plugin :list, field: :pos # will use :pos field for position
# end
#
# item = Item[1]
#
# # Get the next or previous item in the list
#
# item.next
# item.prev
#
# # Modify the item's position, which may require modifying other items in
# # the same list
#
# item.move_to(3)
# item.move_to_top
# item.move_to_bottom
# item.move_up
# item.move_down
#
# You can provide a <tt>:scope</tt> option to scope the list. This option
# can be a symbol or array of symbols specifying column name(s), or a proc
# that accepts a model instance and returns a dataset representing the list
# the object is in. You will need to provide a <tt>:scope</tt> option if
# the model's dataset uses a subquery (such as when using the class_table_inheritance
# plugin).
#
# For example, if each item has a +user_id+ field, and you want every user
# to have their own list:
#
# Item.plugin :list, scope: :user_id
#
# Note that using this plugin modifies the order of the model's dataset to
# sort by the position and scope fields. Also note that this plugin is subject to
# race conditions, and is not safe when concurrent modifications are made
# to the same list.
#
# Note that by default, unlike ruby arrays, the list plugin assumes the first
# entry in the list has position 1, not position 0.
#
# You can change this by providing an integer <tt>:top</tt> option:
#
# Item.plugin :list, top: 0
#
# Copyright (c) 2007-2010 Sharon Rosner, Wayne E. Seguin, Aman Gupta, Adrian Madrid, Jeremy Evans
module List
# Set the +position_field+, +scope_proc+ and +top_of_list+ attributes for the model,
# using the <tt>:field</tt>, <tt>:scope</tt>, and <tt>:top</tt> options, respectively.
# The <tt>:scope</tt> option can be a symbol, array of symbols, or a proc that
# accepts a model instance and returns a dataset representing the list.
# Also, modify the model dataset's order to order by the position and scope fields.
def self.configure(model, opts = OPTS)
model.position_field = opts[:field] || :position
model.dataset = model.dataset.order_prepend(model.position_field)
model.instance_exec do
@top_of_list = opts[:top] || 1
end
model.scope_proc = case scope = opts[:scope]
when Symbol
model.dataset = model.dataset.order_prepend(scope)
proc{|obj| obj.model.where(scope=>obj.public_send(scope))}
when Array
model.dataset = model.dataset.order_prepend(*scope)
proc{|obj| obj.model.where(scope.map{|s| [s, obj.get_column_value(s)]})}
else
scope
end
end
module ClassMethods
# The column name holding the position in the list, as a symbol.
attr_accessor :position_field
# A proc that scopes the dataset, so that there can be multiple positions
# in the list, but the positions are unique with the scoped dataset. This
# proc should accept an instance and return a dataset representing the list.
attr_accessor :scope_proc
# An Integer to use as the position of the top of the list. Defaults to 1.
attr_reader :top_of_list
Plugins.inherited_instance_variables(self, :@position_field=>nil, :@scope_proc=>nil, :@top_of_list=>nil)
end
module InstanceMethods
# The model object at the given position in the list containing this instance.
def at_position(p)
list_dataset.first(position_field => p)
end
# When destroying an instance, move all entries after the instance down
# one position, so that there aren't any gaps
def after_destroy
super
f = Sequel[position_field]
list_dataset.where(f > position_value).update(f => f - 1)
end
# Find the last position in the list containing this instance.
def last_position
list_dataset.max(position_field).to_i
end
# A dataset that represents the list containing this instance.
def list_dataset
model.scope_proc ? model.scope_proc.call(self) : model.dataset
end
# Move this instance down the given number of places in the list,
# or 1 place if no argument is specified.
def move_down(n = 1)
move_to(position_value + n)
end
# Move this instance to the given place in the list. If lp is not
# given or greater than the last list position, uses the last list
# position. If lp is less than the top list position, uses the
# top list position.
def move_to(target, lp = nil)
current = position_value
if target != current
checked_transaction do
ds = list_dataset
op, ds = if target < current
target = model.top_of_list if target < model.top_of_list
[:+, ds.where(position_field=>target...current)]
else
lp ||= last_position
target = lp if target > lp
[:-, ds.where(position_field=>(current + 1)..target)]
end
ds.update(position_field => Sequel::SQL::NumericExpression.new(op, position_field, 1))
update(position_field => target)
end
end
self
end
# Move this instance to the bottom (last position) of the list.
def move_to_bottom
lp = last_position
move_to(lp, lp)
end
# Move this instance to the top (first position, usually position 1) of the list.
def move_to_top
move_to(model.top_of_list)
end
# Move this instance the given number of places up in the list, or 1 place
# if no argument is specified.
def move_up(n = 1)
move_to(position_value - n)
end
# The model instance the given number of places below this model instance
# in the list, or 1 place below if no argument is given.
def next(n = 1)
n == 0 ? self : at_position(position_value + n)
end
# The value of the model's position field for this instance.
def position_value
get_column_value(position_field)
end
# The model instance the given number of places below this model instance
# in the list, or 1 place below if no argument is given.
def prev(n = 1)
self.next(n * -1)
end
# Set the value of the position_field to the maximum value plus 1 unless the
# position field already has a value. If the list is empty, the position will
# be set to the model's +top_of_list+ value.
def before_validation
unless get_column_value(position_field)
current_max = list_dataset.max(position_field)
value = current_max.nil? ? model.top_of_list : current_max.to_i + 1
set_column_value("#{position_field}=", value)
end
super
end
private
# The model's position field, an instance method for ease of use.
def position_field
model.position_field
end
end
end
end
end