mongoid_orderable 4.1.0 → 4.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.rvmrc +1 -1
- data/CHANGELOG.md +4 -0
- data/README.md +54 -0
- data/lib/config/locales/en.yml +9 -0
- data/lib/mongoid/orderable.rb +10 -227
- data/lib/mongoid/orderable/callbacks.rb +75 -0
- data/lib/mongoid/orderable/configuration.rb +60 -0
- data/lib/mongoid/orderable/errors.rb +2 -0
- data/lib/mongoid/orderable/errors/invalid_target_position.rb +18 -0
- data/lib/mongoid/orderable/errors/mongoid_orderable_error.rb +14 -0
- data/lib/mongoid/orderable/generator.rb +34 -0
- data/lib/mongoid/orderable/generator/helpers.rb +29 -0
- data/lib/mongoid/orderable/generator/listable.rb +41 -0
- data/lib/mongoid/orderable/generator/movable.rb +62 -0
- data/lib/mongoid/orderable/generator/position.rb +26 -0
- data/lib/mongoid/orderable/generator/scope.rb +17 -0
- data/lib/mongoid/orderable/helpers.rb +50 -0
- data/lib/mongoid/orderable/listable.rb +49 -0
- data/lib/mongoid/orderable/movable.rb +58 -0
- data/lib/mongoid/orderable/orderable_class.rb +51 -0
- data/lib/mongoid_orderable.rb +19 -0
- data/lib/mongoid_orderable/version.rb +1 -1
- data/spec/mongoid/orderable_spec.rb +885 -86
- metadata +27 -11
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZjBmZDk1OTM1MmE1ZGNiMmFiMDU1Y2IxYTAwMzZlZTkzZWFiMjhlYQ==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cea70aa8c9e3b86f49203e6f1bb17b45a994e45a
|
4
|
+
data.tar.gz: 99ad3433535e36e7e4f9f579f1ab4c7640f5552c
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
YzA4NWZmNmZmZTJmNTZmNTdlNDFjZDk2NmJkOGQyNDMyMTM2Mjg2ZjU5NGE1
|
11
|
-
MmI0YzU2MmVhODdmMWY1MGI5ZDhhZTVkNGQ0NjRlNzNlMzI4ZDU=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
NTYyN2E1ZTVlNDQ5YzZhNDY5Y2UyNDMzZWVhZWQ4YWZmMTJkNTI0ZDNhNzIy
|
14
|
-
ZDg0MTkyZjY3YWVkNzdmNWM5ZDY3ZWJiNDMxYzk2NDM0ZWUxOTFiMmMxMWEy
|
15
|
-
ZjE2MmRiZDcyY2U5NTg1MjU3ODkwOGIwMDIxMDY0NDMyMmExMzQ=
|
6
|
+
metadata.gz: 52b38d32f9272161134dd2c4b04d70549e6b8fe7034b0838c12d243e420702889ae36f2b43463c22b0afafc5447817414f0d402d9441b9a6847434c328f505c4
|
7
|
+
data.tar.gz: 1608265030534f0bb65c9911014cb1e3d6a54691a037644cdc31295aec872c58b4edfc84414cf847e2c35497b2629d87606f6fe48b993c3b2ca34a237d97890b
|
data/.rvmrc
CHANGED
@@ -1 +1 @@
|
|
1
|
-
rvm use
|
1
|
+
rvm use 2.0.0@mongoid_orderable --create
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -10,6 +10,7 @@ Mongoid::Orderable is a ordered list implementation for your mongoid models.
|
|
10
10
|
* It supports mutators api
|
11
11
|
* It correctly assigns the position while moving document between scopes
|
12
12
|
* It supports mongoid 2, 3 and 4
|
13
|
+
* It supports specifying multiple orderable columns
|
13
14
|
|
14
15
|
# How?
|
15
16
|
|
@@ -18,6 +19,7 @@ gem 'mongoid_orderable'
|
|
18
19
|
```
|
19
20
|
|
20
21
|
Gem has the same api as others. Just include Mongoid::Orderable into your model and call `orderable` method.
|
22
|
+
Embedded objects are automatically scoped by their parent.
|
21
23
|
|
22
24
|
```
|
23
25
|
class Item
|
@@ -64,6 +66,58 @@ item.next_item # returns the next item in the list
|
|
64
66
|
item.previous_item # returns the previous item in the list
|
65
67
|
```
|
66
68
|
|
69
|
+
# Multiple Columns
|
70
|
+
|
71
|
+
You can also define multiple orderable columns for any class including the Mongoid::Orderable module.
|
72
|
+
|
73
|
+
```
|
74
|
+
class Book
|
75
|
+
include Mongoid::Document
|
76
|
+
include Mongoid::Orderable
|
77
|
+
|
78
|
+
orderable base: 0
|
79
|
+
orderable column: sno, as: :serial_no
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
The above defines two different orderable_columns on Book - *position* and *serial_no*.
|
84
|
+
The following helpers are generated in this case:
|
85
|
+
|
86
|
+
```
|
87
|
+
item.move_#{column_name}_to
|
88
|
+
item.move_#{column_name}_to=
|
89
|
+
item.move_#{column_name}_to!
|
90
|
+
|
91
|
+
item.move_#{column_name}_to_top
|
92
|
+
item.move_#{column_name}_to_bottom
|
93
|
+
item.move_#{column_name}_higher
|
94
|
+
item.move_#{column_name}_lower
|
95
|
+
|
96
|
+
item.next_#{column_name}_items
|
97
|
+
item.previous_#{column_name}_items
|
98
|
+
|
99
|
+
item.next_#{column_name}_item
|
100
|
+
item.previous_#{column_name}_item
|
101
|
+
```
|
102
|
+
|
103
|
+
*where column_name is either **position** or **serial_no**.*
|
104
|
+
|
105
|
+
When a model defines multiple orderable columns, the original helpers are also available and work on the first orderable column.
|
106
|
+
|
107
|
+
```
|
108
|
+
@book1 = Book.create!
|
109
|
+
@book2 = Book.create!
|
110
|
+
@book2 => #<Book _id: 53a16a2ba1bde4f746000001, serial_no: 1, position: 1>
|
111
|
+
@book2.move_to! :top # this will change the :position of the book to 0 (not serial_no)
|
112
|
+
@book2 => #<Book _id: 53a16a2ba1bde4f746000001, serial_no: 1, position: 0>
|
113
|
+
```
|
114
|
+
|
115
|
+
To specify any other orderable column as default pass the **default: true** option with orderable.
|
116
|
+
|
117
|
+
```
|
118
|
+
orderable column: sno, as: :serial_no, default: true
|
119
|
+
```
|
120
|
+
|
67
121
|
# Contributing
|
68
122
|
|
69
123
|
Fork && Patch && Spec && Push && Pull request.
|
@@ -0,0 +1,9 @@
|
|
1
|
+
en:
|
2
|
+
mongoid:
|
3
|
+
errors:
|
4
|
+
messages:
|
5
|
+
invalid_target_position:
|
6
|
+
message: "`%{value}` is not an acceptable value for target position."
|
7
|
+
summary: "Mongoid::Orderable accepts: a Numeric, a numeric string,
|
8
|
+
:top, :bottom, or nil."
|
9
|
+
resolution: "You must give an acceptable value for target position."
|
data/lib/mongoid/orderable.rb
CHANGED
@@ -1,239 +1,22 @@
|
|
1
1
|
module Mongoid::Orderable
|
2
2
|
extend ActiveSupport::Concern
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
:scope => nil,
|
10
|
-
:base => 1
|
11
|
-
}
|
12
|
-
|
13
|
-
configuration.merge! options if options.is_a?(Hash)
|
14
|
-
|
15
|
-
if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
|
16
|
-
scope_relation = self.relations[configuration[:scope].to_s]
|
17
|
-
if scope_relation
|
18
|
-
configuration[:scope] = scope_relation.key.to_sym
|
19
|
-
else
|
20
|
-
configuration[:scope] = "#{configuration[:scope]}_id".to_sym
|
21
|
-
end
|
22
|
-
elsif configuration[:scope].is_a?(String)
|
23
|
-
configuration[:scope] = configuration[:scope].to_sym
|
24
|
-
end
|
25
|
-
|
26
|
-
field configuration[:column], orderable_field_opts(configuration)
|
27
|
-
if configuration[:index]
|
28
|
-
if MongoidOrderable.mongoid2?
|
29
|
-
index configuration[:column]
|
30
|
-
else
|
31
|
-
index(configuration[:column] => 1)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
case configuration[:scope]
|
36
|
-
when Symbol then
|
37
|
-
scope :orderable_scope, lambda { |document|
|
38
|
-
where(configuration[:scope] => document.send(configuration[:scope])) }
|
39
|
-
when Proc then
|
40
|
-
scope :orderable_scope, configuration[:scope]
|
41
|
-
else
|
42
|
-
scope :orderable_scope, lambda { |document| where({}) }
|
43
|
-
end
|
44
|
-
|
45
|
-
define_method :orderable_column do
|
46
|
-
configuration[:column]
|
47
|
-
end
|
48
|
-
|
49
|
-
define_method :orderable_base do
|
50
|
-
configuration[:base]
|
51
|
-
end
|
52
|
-
|
53
|
-
self_class = self
|
54
|
-
define_method :orderable_inherited_class do
|
55
|
-
self_class if configuration[:inherited]
|
56
|
-
end
|
57
|
-
|
58
|
-
before_save :add_to_list
|
59
|
-
after_destroy :remove_from_list
|
60
|
-
end
|
61
|
-
|
62
|
-
private
|
63
|
-
|
64
|
-
def orderable_field_opts(configuration)
|
65
|
-
field_opts = { :type => Integer }
|
66
|
-
field_opts.merge!(configuration.slice(:as))
|
67
|
-
field_opts
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
##
|
72
|
-
# Returns items above the current document.
|
73
|
-
# Items with a position lower than this document's position.
|
74
|
-
def previous_items
|
75
|
-
orderable_scoped.where(orderable_column.lt => self.position)
|
76
|
-
end
|
77
|
-
alias_method :prev_items, :previous_items
|
78
|
-
|
79
|
-
##
|
80
|
-
# Returns items below the current document.
|
81
|
-
# Items with a position greater than this document's position.
|
82
|
-
def next_items
|
83
|
-
orderable_scoped.where(orderable_column.gt => self.position)
|
84
|
-
end
|
85
|
-
|
86
|
-
# returns the previous item in the list
|
87
|
-
def previous_item
|
88
|
-
if previous_items.present?
|
89
|
-
previous_position = self.position - 1
|
90
|
-
orderable_scoped.where(:position => previous_position).first
|
91
|
-
else
|
92
|
-
nil
|
93
|
-
end
|
94
|
-
end
|
95
|
-
alias_method :prev_item, :previous_item
|
96
|
-
|
97
|
-
# returns the next item in the list
|
98
|
-
def next_item
|
99
|
-
if next_items.present?
|
100
|
-
next_position = self.position + 1
|
101
|
-
orderable_scoped.where(:position => next_position).first
|
102
|
-
else
|
103
|
-
nil
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def move_to! target_position
|
108
|
-
@move_to = target_position
|
109
|
-
save
|
110
|
-
end
|
111
|
-
alias_method :insert_at!, :move_to!
|
112
|
-
|
113
|
-
def move_to target_position
|
114
|
-
@move_to = target_position
|
115
|
-
end
|
116
|
-
alias_method :insert_at, :move_to
|
117
|
-
|
118
|
-
def move_to= target_position
|
119
|
-
@move_to = target_position
|
120
|
-
end
|
121
|
-
alias_method :insert_at=, :move_to=
|
122
|
-
|
123
|
-
[:top, :bottom].each do |symbol|
|
124
|
-
define_method "move_to_#{symbol}" do
|
125
|
-
move_to symbol
|
126
|
-
end
|
127
|
-
|
128
|
-
define_method "move_to_#{symbol}!" do
|
129
|
-
move_to! symbol
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
[:higher, :lower].each do |symbol|
|
134
|
-
define_method "move_#{symbol}" do
|
135
|
-
move_to symbol
|
136
|
-
end
|
137
|
-
|
138
|
-
define_method "move_#{symbol}!" do
|
139
|
-
move_to! symbol
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def first?
|
144
|
-
in_list? && orderable_position == orderable_base
|
145
|
-
end
|
146
|
-
|
147
|
-
def last?
|
148
|
-
in_list? && orderable_position == bottom_orderable_position
|
149
|
-
end
|
150
|
-
|
151
|
-
def in_list?
|
152
|
-
!orderable_position.nil?
|
153
|
-
end
|
4
|
+
included do
|
5
|
+
include Mongoid::Orderable::Helpers
|
6
|
+
include Mongoid::Orderable::Callbacks
|
7
|
+
include Mongoid::Orderable::Movable
|
8
|
+
include Mongoid::Orderable::Listable
|
154
9
|
|
155
|
-
|
156
|
-
apply_position @move_to
|
10
|
+
class_attribute :orderable_configurations
|
157
11
|
end
|
158
12
|
|
159
|
-
|
160
|
-
MongoidOrderable.inc orderable_scoped.where(orderable_column.gt => orderable_position), orderable_column, -1
|
161
|
-
end
|
162
|
-
|
163
|
-
private
|
164
|
-
|
165
|
-
def orderable_position
|
166
|
-
send orderable_column
|
167
|
-
end
|
168
|
-
|
169
|
-
def orderable_position= value
|
170
|
-
send "#{orderable_column}=", value
|
171
|
-
end
|
172
|
-
|
173
|
-
def orderable_scoped
|
174
|
-
if embedded?
|
175
|
-
send(MongoidOrderable.metadata(self).inverse).send(MongoidOrderable.metadata(self).name).orderable_scope(self)
|
176
|
-
else
|
177
|
-
(orderable_inherited_class || self.class).orderable_scope(self)
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
def orderable_scope_changed?
|
182
|
-
if Mongoid.respond_to?(:unit_of_work)
|
183
|
-
Mongoid.unit_of_work do
|
184
|
-
orderable_scope_changed_query
|
185
|
-
end
|
186
|
-
else
|
187
|
-
orderable_scope_changed_query
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def orderable_scope_changed_query
|
192
|
-
!orderable_scoped.where(:_id => _id).exists?
|
193
|
-
end
|
194
|
-
|
195
|
-
def apply_position target_position
|
196
|
-
if persisted? && !embedded? && orderable_scope_changed?
|
197
|
-
self.class.unscoped.find(_id).remove_from_list
|
198
|
-
self.orderable_position = nil
|
199
|
-
end
|
200
|
-
|
201
|
-
return if !target_position && in_list?
|
13
|
+
module ClassMethods
|
202
14
|
|
203
|
-
|
15
|
+
def orderable options = {}
|
16
|
+
configuration = Mongoid::Orderable::Configuration.build(self, options)
|
204
17
|
|
205
|
-
|
206
|
-
MongoidOrderable.inc orderable_scoped.where(orderable_column.gte => target_position), orderable_column, 1
|
207
|
-
else
|
208
|
-
MongoidOrderable.inc(orderable_scoped.where(orderable_column.gte => target_position, orderable_column.lt => orderable_position), orderable_column, 1) if target_position < orderable_position
|
209
|
-
MongoidOrderable.inc(orderable_scoped.where(orderable_column.gt => orderable_position, orderable_column.lte => target_position), orderable_column, -1) if target_position > orderable_position
|
18
|
+
Mongoid::Orderable::OrderableClass.setup(self, configuration)
|
210
19
|
end
|
211
20
|
|
212
|
-
self.orderable_position = target_position
|
213
|
-
end
|
214
|
-
|
215
|
-
def target_position_to_position target_position
|
216
|
-
target_position = :bottom unless target_position
|
217
|
-
|
218
|
-
target_position = case target_position.to_sym
|
219
|
-
when :top then orderable_base
|
220
|
-
when :bottom then bottom_orderable_position
|
221
|
-
when :higher then orderable_position.pred
|
222
|
-
when :lower then orderable_position.next
|
223
|
-
end unless target_position.is_a? Numeric
|
224
|
-
|
225
|
-
target_position = orderable_base if target_position < orderable_base
|
226
|
-
target_position = bottom_orderable_position if target_position > bottom_orderable_position
|
227
|
-
target_position
|
228
21
|
end
|
229
|
-
|
230
|
-
def bottom_orderable_position
|
231
|
-
@bottom_orderable_position = begin
|
232
|
-
positions_list = orderable_scoped.distinct(orderable_column)
|
233
|
-
return orderable_base if positions_list.empty?
|
234
|
-
max = positions_list.map(&:to_i).max.to_i
|
235
|
-
in_list? ? max : max.next
|
236
|
-
end
|
237
|
-
end
|
238
|
-
|
239
22
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Orderable
|
3
|
+
module Callbacks
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def add_to_list
|
11
|
+
self.orderable_keys.each do |column|
|
12
|
+
apply_position column, move_all[column]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def remove_from_list
|
17
|
+
self.orderable_keys.each do |column|
|
18
|
+
remove_position_from_list column
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def remove_position_from_list(column)
|
23
|
+
col, pos = orderable_column(column), orderable_position(column)
|
24
|
+
MongoidOrderable.inc orderable_scoped(column).where(col.gt => pos), col, -1
|
25
|
+
end
|
26
|
+
|
27
|
+
def apply_position column, target_position
|
28
|
+
if persisted? && !embedded? && orderable_scope_changed?(column)
|
29
|
+
self.class.unscoped.find(_id).remove_position_from_list(column)
|
30
|
+
self.send("orderable_#{column}_position=", nil)
|
31
|
+
end
|
32
|
+
|
33
|
+
return if !target_position && in_list?(column)
|
34
|
+
|
35
|
+
target_position = target_position_to_position column, target_position
|
36
|
+
scope, col, pos = orderable_scoped(column), orderable_column(column), orderable_position(column)
|
37
|
+
|
38
|
+
unless in_list?(column)
|
39
|
+
MongoidOrderable.inc scope.where(col.gte => target_position), col, 1
|
40
|
+
else
|
41
|
+
MongoidOrderable.inc(scope.where(col.gte => target_position, col.lt => pos), col, 1) if target_position < pos
|
42
|
+
MongoidOrderable.inc(scope.where(col.gt => pos, col.lte => target_position), col, -1) if target_position > pos
|
43
|
+
end
|
44
|
+
|
45
|
+
self.send("orderable_#{column}_position=", target_position)
|
46
|
+
end
|
47
|
+
|
48
|
+
def target_position_to_position column, target_position
|
49
|
+
target_position = :bottom unless target_position
|
50
|
+
|
51
|
+
target_position = case target_position.to_s
|
52
|
+
when 'top' then orderable_base(column)
|
53
|
+
when 'bottom' then bottom_orderable_position(column)
|
54
|
+
when 'higher' then orderable_position(column).pred
|
55
|
+
when 'lower' then orderable_position(column).next
|
56
|
+
when /\A\d+\Z/ then target_position.to_i
|
57
|
+
else raise Mongoid::Orderable::Errors::InvalidTargetPosition.new target_position
|
58
|
+
end unless target_position.is_a? Numeric
|
59
|
+
|
60
|
+
target_position = orderable_base(column) if target_position < orderable_base(column)
|
61
|
+
target_position = bottom_orderable_position(column) if target_position > bottom_orderable_position(column)
|
62
|
+
target_position
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
module ClassMethods
|
68
|
+
def add_orderable_callbacks
|
69
|
+
before_save :add_to_list
|
70
|
+
after_destroy :remove_from_list
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Orderable
|
3
|
+
class Configuration
|
4
|
+
|
5
|
+
CONFIG_OPTIONS = %w(column scope foreign_key inherited base index default).map(&:to_sym)
|
6
|
+
FIELD_OPTIONS = %w(as).map(&:to_sym)
|
7
|
+
VALID_OPTIONS = CONFIG_OPTIONS + FIELD_OPTIONS
|
8
|
+
|
9
|
+
attr_reader :orderable_class, :options
|
10
|
+
|
11
|
+
def initialize(parent, options = {})
|
12
|
+
@orderable_class = parent
|
13
|
+
set_options(options)
|
14
|
+
set_field_options
|
15
|
+
set_orderable_scope
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_configuration
|
19
|
+
{ :column => :position,
|
20
|
+
:index => true,
|
21
|
+
:scope => nil,
|
22
|
+
:base => 1,
|
23
|
+
:field_opts => { :type => Integer }}
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.build(parent, options = {})
|
27
|
+
new(parent, options).options
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def set_options(options)
|
33
|
+
@options = default_configuration
|
34
|
+
return unless options.is_a? Hash
|
35
|
+
@options.merge! options.symbolize_keys.slice(*VALID_OPTIONS)
|
36
|
+
end
|
37
|
+
|
38
|
+
def set_field_options
|
39
|
+
FIELD_OPTIONS.each do |key|
|
40
|
+
next unless options.has_key? key
|
41
|
+
@options[:field_opts][key] = options.delete(key)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_orderable_scope
|
46
|
+
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
|
47
|
+
scope_relation = @orderable_class.relations[options[:scope].to_s]
|
48
|
+
if scope_relation
|
49
|
+
@options[:scope] = scope_relation.key.to_sym
|
50
|
+
else
|
51
|
+
@options[:scope] = "#{options[:scope]}_id".to_sym
|
52
|
+
end
|
53
|
+
elsif options[:scope].is_a?(String)
|
54
|
+
@options[:scope] = options[:scope].to_sym
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|