mongoid_orderable 4.1.0 → 4.1.1
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.
- 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
|