acts_as_ranked_list 0.2.1 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -2
- data/README.md +68 -28
- data/acts_as_ranked_list.gemspec +47 -0
- data/lib/acts_as_ranked_list/active_record/rank_column.rb +99 -29
- data/lib/acts_as_ranked_list/active_record/service.rb +41 -6
- data/lib/acts_as_ranked_list/version.rb +1 -1
- metadata +22 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55fb6b1014091f8df47866752d9aedc4cdac1204955852b09c84d480e2769486
|
4
|
+
data.tar.gz: 34bc12ea565e8fd881a6401d9b5cc3ea4e83b86106abb6ee1ec1b7d4a19c1fac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b283316886e10b9d469843c3814fda1143ea619feacd8f2415e8d323c1a21c41281b91b9407b53bdd43425192ccc5dd5aa29ec905030788404490dba844a8765
|
7
|
+
data.tar.gz: dd72d4345b6d37ee33f8f4513200c793865f52c8468949f9935962c77ab898ded2b7fffc43ad9b1616e6e3a55517dd72964bed168069de9c79c04699f61c8b9e
|
data/CHANGELOG.md
CHANGED
@@ -7,8 +7,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
## [0.2.3] - 2023-12-31
|
11
|
+
|
12
|
+
- Updates gem dependencies.
|
13
|
+
|
14
|
+
## [0.2.2] - 2023-04-24
|
15
|
+
|
16
|
+
- ~~Allows unranked items in list by having `nil` values.~~ Implicitly allowed behaviour since v0.2.0, also able to add new items can also be added as unranked. Adds documentation to this feature.
|
17
|
+
- Allows any number of different scopes on the items by referencing another column, of type:
|
18
|
+
- a relationship.
|
19
|
+
- a string, symbol, boolean or number.
|
20
|
+
- a custom-defined scope.
|
21
|
+
- Fixes class method `spread_ranks` to ignore unranked items.
|
12
22
|
|
13
23
|
## [0.2.1] - 2023-04-21
|
14
24
|
|
data/README.md
CHANGED
@@ -22,15 +22,15 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
22
22
|
|
23
23
|
This gem allows you to easily rank `::ActiveRecord` items without worrying about the underlying logic. After installing the gem, add the following `acts_as_ranked_list` to the `::ActiveRecord` model, for example:
|
24
24
|
|
25
|
-
```
|
26
|
-
class MyModelName
|
25
|
+
```ruby
|
26
|
+
class MyModelName < ::ActiveRecord::Base
|
27
27
|
acts_as_ranked_list
|
28
28
|
end
|
29
29
|
```
|
30
30
|
|
31
31
|
When you create a new `MyModelName` item, it will be ranked among the list of existing `MyModelName` items. You can increase/decrease the item's rank by the following methods:
|
32
32
|
|
33
|
-
```
|
33
|
+
```ruby
|
34
34
|
item_a = MyModelName.create!
|
35
35
|
item_a.increase_rank
|
36
36
|
item_a.decrease_rank
|
@@ -38,7 +38,7 @@ item_a.decrease_rank
|
|
38
38
|
|
39
39
|
You can get the current rank of the item by the following:
|
40
40
|
|
41
|
-
```
|
41
|
+
```ruby
|
42
42
|
item_a = MyModelName.create! # is at the bottom of the list, highest rank item
|
43
43
|
item_b = MyModelName.create! # is at the bottom of the list, lowest rank item
|
44
44
|
item_a.current_rank # 1
|
@@ -49,7 +49,7 @@ Note that the list is viewed as ascending order of `rank, last updated at, id`.
|
|
49
49
|
|
50
50
|
You may get the highest items in the list sorted by rank by the following methods:
|
51
51
|
|
52
|
-
```
|
52
|
+
```ruby
|
53
53
|
my_existing_item = MyModelName.create! # placed top in the list
|
54
54
|
my_new_item = MyModelName.create! # placed bottom in the list
|
55
55
|
MyModelName.get_highest_items # ActiveRecord::Relation with results [my_existing_item, my_new_item]
|
@@ -57,7 +57,7 @@ MyModelName.get_highest_items # ActiveRecord::Relation with results [my_existing
|
|
57
57
|
|
58
58
|
You may get the highest/lowest item by specifying a number as the first argument to the `get_highest_items`/`get_lowest_items` methods, such as:
|
59
59
|
|
60
|
-
```
|
60
|
+
```ruby
|
61
61
|
my_existing_item = MyModelName.create! # placed top in the list
|
62
62
|
my_new_item = MyModelName.create! # placed bottom in the list
|
63
63
|
MyModelName.get_highest_items(1) # ActiveRecord::Relation with results [my_existing_item] # Note the result is an array
|
@@ -70,8 +70,8 @@ MyModelName.get_highest_items(50000) # ActiveRecord::Relation with results [my_e
|
|
70
70
|
|
71
71
|
For the next examples, each will be initialized to the following:
|
72
72
|
|
73
|
-
```
|
74
|
-
class TodoItem
|
73
|
+
```ruby
|
74
|
+
class TodoItem < ::ApplicationRecord
|
75
75
|
# the rank column is named "priority" (without quotation marks) for this table
|
76
76
|
# new items are added as highest priority
|
77
77
|
acts_as_ranked_list column: "priority", adds_new_at: :highest, step_increment: 1.0
|
@@ -87,11 +87,11 @@ print_on_shirt = TodoItem.create!(title: "Print a prototype design on the shrit
|
|
87
87
|
# the lowest prioritised item is "design_reusable_plastic_bag_graphic"
|
88
88
|
```
|
89
89
|
|
90
|
-
|
90
|
+
#### Query position of current item
|
91
91
|
|
92
92
|
You can check if an item is the highest item or the lowest item in the list by using the `highest_item?` or `lowest_item?` instance methods.
|
93
93
|
|
94
|
-
```
|
94
|
+
```ruby
|
95
95
|
design_reusable_plastic_bag_graphic.lowest_item? # true
|
96
96
|
design_reusable_plastic_bag_graphic.highest_item? # false
|
97
97
|
print_on_shirt.highest_item? # true
|
@@ -101,27 +101,66 @@ print_on_shirt.highest_item? # true
|
|
101
101
|
|
102
102
|
You can get the higher/lower items by using the instance methods `get_higher_items` or `get_lower_items`:
|
103
103
|
|
104
|
-
```
|
104
|
+
```ruby
|
105
105
|
exercise.get_higher_items # items and their priorities (note the order): [["health_check", 0.25], ["print_on_shirt", 0.125]]
|
106
106
|
```
|
107
107
|
|
108
108
|
You may pass in optional arguments to control how the results are returned. If the first argument is `0`, it will return all higher/lower items.
|
109
109
|
|
110
|
-
```
|
110
|
+
```ruby
|
111
111
|
design_reusable_plastic_bag_graphic.get_higher_items(2, "ASC") # [["health_check", 0.25], ["exercise", 0.5]]
|
112
112
|
```
|
113
113
|
|
114
|
+
#### Scoping items
|
115
|
+
|
116
|
+
You may use scopes to constrain your items to a condition. This means you can group items together, and keep them separate from irrelevant items. For example, you may have 2 todo lists, one each for personal or work related tasks. You may fetch each list of todo items separately, and interact with them separetely using scopes.
|
117
|
+
|
118
|
+
We'll add a new table, and `::ActiveRecord` model to help demonstrate this feature. For possible options on `scope`, refer to the docs on `service.rb`.
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class TodoList < ::ActiveRecord::Base
|
122
|
+
has_many :todo_items
|
123
|
+
end
|
124
|
+
|
125
|
+
class ScopedTodoItem < ::ActiveRecord::Base
|
126
|
+
belongs_to :todo_list # add to table an integer foreign key column `todo_list_id`
|
127
|
+
|
128
|
+
acts_as_ranked_list scopes: { todo_list: nil }
|
129
|
+
end
|
130
|
+
|
131
|
+
work_todo_list = ::TodoList.create!(title: "Chores at work :eye_roll:")
|
132
|
+
personal_todo_list = ::TodoList.create!(title: "Hobbies to enjoy!!! :partying_face:")
|
133
|
+
scoped_work_todo_item = ::ScopedTodoItem.create!(title: "Create a new table to store temporary data", list: work_todo_list, rank: 1)
|
134
|
+
scoped_personal_todo_item = ::ScopedTodoItem.create!(title: "Assemble the new couch to enjoy sitting on", list: personal_todo_list, rank: 1)
|
135
|
+
```
|
136
|
+
|
137
|
+
In the above example, `scoped_personal_todo_item` and `scoped_work_todo_item` have the same rank but are in different scopes. So they do not have colliding ranks, and can be fetched/mutated independently of each other.
|
138
|
+
|
139
|
+
You may also use a string, boolean, integer, symbol, named scopes, custom scopes (via a `::Proc`) or all of them together on the same model.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class WorkTodoItem < ScopedTodoItem # Note the parent class
|
143
|
+
scope :work, -> { where(todo_list: ::TodoList.find_by(...)) }
|
144
|
+
default_scope { :work }
|
145
|
+
acts_as_ranked_list scopes: { todo_list: :work } # or
|
146
|
+
acts_as_ranked_list scopes: { todo_list: :work, week_day: ::Proc.new { "day_of_week = 'tuesday'" } } # or
|
147
|
+
acts_as_ranked_list scopes: { todo_list: :work, manager: "Mr. Disney", priority_bracket: :top, related_to_a_paying_customer: true, feature_ticket_number: 42 }
|
148
|
+
# these scopes will require the relevant column names on the table
|
149
|
+
# such as `manager (string), priority_bracket (string), related_to_a_paying_customer (boolean), feature_ticket_number (integer), week_day (preferred enum on integer, or string)
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
114
153
|
#### Check if the current item is ranked
|
115
154
|
|
116
155
|
If the value of the rank column for the instance is `nil` then the item is not ranked. This item will still interact with the list when running queries such as `highest_item?` and so on. This item will be given a rank when [spreading ranks](spread-ranks).
|
117
156
|
|
118
|
-
```
|
157
|
+
```ruby
|
119
158
|
design_reusable_plastic_bag_graphic.is_ranked? # true
|
120
159
|
```
|
121
160
|
|
122
161
|
You may create a new item with nil rank as follows:
|
123
162
|
|
124
|
-
```
|
163
|
+
```ruby
|
125
164
|
## this persists the record, but skips callbacks
|
126
165
|
::TodoItem.with_skip_persistence { ::TodoItem.create!(rank: nil) }
|
127
166
|
```
|
@@ -130,13 +169,13 @@ You may create a new item with nil rank as follows:
|
|
130
169
|
|
131
170
|
Instead of updating rank one or down one position at a time, you can move above/below another item, using `set_rank_above` or `set_rank_below` instance methods:
|
132
171
|
|
133
|
-
```
|
172
|
+
```ruby
|
134
173
|
design_reusable_plastic_bag_graphic.set_rank_above(health_check)
|
135
174
|
```
|
136
175
|
|
137
176
|
This can be used together with the `get_highest_items(1)` class method to move item to the top of the list.
|
138
177
|
|
139
|
-
```
|
178
|
+
```ruby
|
140
179
|
design_reusable_plastic_bag_graphic.set_rank_above(TodoItem.get_highest_items(1).first)
|
141
180
|
```
|
142
181
|
|
@@ -148,7 +187,7 @@ Skipping persistence is useful if you want to mass update items, and persist onc
|
|
148
187
|
|
149
188
|
You can use the class method `with_skip_persistence` as follows:
|
150
189
|
|
151
|
-
```
|
190
|
+
```ruby
|
152
191
|
TodoItem.with_skip_persistence do
|
153
192
|
design_reusable_plastic_bag_graphic.update(rank: 20.3)
|
154
193
|
exercise.update(rank: 5.2)
|
@@ -166,7 +205,7 @@ end
|
|
166
205
|
|
167
206
|
You may also pass in an array of classes to the `with_skip_persistence` method to skip persistence for these `::ActiveRecord` models which use the `acts_as_ranked_list` concern.
|
168
207
|
|
169
|
-
```
|
208
|
+
```ruby
|
170
209
|
TodoItem.with_skip_persistence([FootballTeam]) do # the calling class is added by default, in this case: `TodoItem`
|
171
210
|
design_reusable_plastic_bag_graphic.increase_rank
|
172
211
|
exercise.increase_rank
|
@@ -197,7 +236,7 @@ end
|
|
197
236
|
|
198
237
|
You can control whether to spread ranks or not on collisions by using the option `avoid_collisions: true` (by default) on using the concern in your `::ActiveRecord` model. You can change this setting on a per-block per-class basis by using the following class method:
|
199
238
|
|
200
|
-
```
|
239
|
+
```ruby
|
201
240
|
# disallows collisions
|
202
241
|
TodoItem.with_avoid_collisions(true) do # do spread ranks on collisions
|
203
242
|
TodoItem.find(1).update(rank: 1)
|
@@ -205,7 +244,7 @@ TodoItem.with_avoid_collisions(true) do # do spread ranks on collisions
|
|
205
244
|
end # items with their new ranks [["health_check", 1.0], ["exercise", 2.0], ["print_on_shirt", 3.0], ["design_reusable_plastic_bag_graphic", 4.0]]
|
206
245
|
```
|
207
246
|
|
208
|
-
```
|
247
|
+
```ruby
|
209
248
|
# allows collisions
|
210
249
|
TodoItem.with_avoid_collisions(false) do # do not spread ranks on collisions
|
211
250
|
TodoItem.find(1).update(rank: 1)
|
@@ -219,21 +258,22 @@ You can spread ranks so that the difference between each rank and the next is se
|
|
219
258
|
- Being able to rerank items again without overflowing column's max precision.
|
220
259
|
- Human-readable viewing purposes. This is not recommended. The rank should be human-readable (or not viewable) at the view (presentation) layer.
|
221
260
|
|
222
|
-
If `avoid_collisions = true (by default)`. Then you do not have to use spread ranks manually. If the database raises an overflow error when mutating a rank, then it could be time to recalibrate the ranks in the table. You should handle this case in your code, and spread ranks. This is very infrequent
|
261
|
+
If `avoid_collisions = true (by default)`. Then you do not have to use spread ranks manually. If the database raises an overflow error when mutating a rank, then it could be time to recalibrate the ranks in the table. You should handle this case in your code, and spread ranks. **This is very infrequent.** For example, in postgres this error will be raised when using the default precision of `16_383` digits after the decimal of a `decimal` column type in postgres versions `9.1+`:
|
223
262
|
|
224
|
-
```
|
263
|
+
```ruby
|
225
264
|
ActiveRecord::RangeError: PG::NumericValueOutOfRange: ERROR: value overflows numeric format
|
226
265
|
```
|
227
266
|
|
228
267
|
Items are spread in, ascending order of each, by:
|
229
268
|
|
230
|
-
1.
|
231
|
-
2.
|
232
|
-
3.
|
269
|
+
1. scopes
|
270
|
+
2. rank
|
271
|
+
3. time updated columns (`updated_at` for example)
|
272
|
+
4. primary key (`id` for example)
|
233
273
|
|
234
274
|
To spread ranks:
|
235
275
|
|
236
|
-
```
|
276
|
+
```ruby
|
237
277
|
TodoItem.spread_ranks
|
238
278
|
TodoItem.get_highest_items # items with their new ranks [["print_on_shirt", 1.0], ["health_check", 2.0], ["exercise", 3.0], ["design_reusable_plastic_bag_graphic", 4.0]]
|
239
279
|
```
|
@@ -246,8 +286,8 @@ Imagine the scenario where you have 199 records to rank, and you must rank them
|
|
246
286
|
|
247
287
|
If you use a `step_increment` of 1 you will be faced with a max float/integer overflow error by the database, as some of the ranks are more than 2 digits before the decimal point. You may use a decimal `step_increment` that is less than 1, and suitable for this scenario, and ranking will work as expected, without errors.
|
248
288
|
|
249
|
-
```
|
250
|
-
class CrammedTodoItem
|
289
|
+
```ruby
|
290
|
+
class CrammedTodoItem < ::ActiveRecord::Base
|
251
291
|
acts_as_ranked_list step_increment: 0.5
|
252
292
|
end
|
253
293
|
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/acts_as_ranked_list/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "acts_as_ranked_list"
|
7
|
+
spec.version = ActsAsRankedList::VERSION
|
8
|
+
spec.authors = ["Farbafe"]
|
9
|
+
spec.email = ["yehyapal@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Orders ActiveRecord items by ranks. Influenced by gem ActsAsList."
|
12
|
+
spec.description = "Orders ActiveRecord items by floating ranks for spaces in-between items. Influenced by gem ActsAsList. The floating rank allows inserting items at arbitrary positions without reordering items. Thus, reducing the number of WRITE queries."
|
13
|
+
spec.homepage = "https://github.com/Farbafe/acts_as_ranked_list"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.6.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/Farbafe/acts_as_ranked_list/tree/main"
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/Farbafe/acts_as_ranked_list/tree/main/CHANGELOG.md"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
28
|
+
end
|
29
|
+
end
|
30
|
+
spec.bindir = "exe"
|
31
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
32
|
+
spec.require_paths = ["lib"]
|
33
|
+
|
34
|
+
# Uncomment to register a new dependency of your gem
|
35
|
+
spec.add_dependency "rails", ">= 6.0.3"
|
36
|
+
spec.add_dependency "rake", ">= 13.0"
|
37
|
+
spec.add_dependency "rspec", ">= 3.0"
|
38
|
+
spec.add_dependency "standard", ">= 1.3"
|
39
|
+
spec.add_dependency "rspec-rails", ">= 5.1.2"
|
40
|
+
spec.add_development_dependency "byebug", ">= 11.1.3"
|
41
|
+
spec.add_development_dependency "yard", ">= 0.9.34"
|
42
|
+
spec.add_development_dependency "redcarpet", ">= 3.6.0"
|
43
|
+
spec.add_development_dependency "sqlite3", ">= 1.6.2"
|
44
|
+
|
45
|
+
# For more information and examples about making a new gem, check out our
|
46
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
47
|
+
end
|
@@ -5,11 +5,47 @@ module ActsAsRankedList #:nodoc:
|
|
5
5
|
module RankColumn
|
6
6
|
# Sets the methods to rank `::ActiveRecord` objects. Please refer to
|
7
7
|
# the {file:README.md} file for usage and examples.
|
8
|
-
def self.call(caller_class, rank_column, touch_on_update, step_increment, avoid_collisions, new_item_at)
|
8
|
+
def self.call(caller_class, rank_column, touch_on_update, step_increment, avoid_collisions, new_item_at, scopes)
|
9
9
|
caller_class.class_eval do
|
10
10
|
|
11
11
|
private
|
12
12
|
|
13
|
+
define_singleton_method :scope_query do
|
14
|
+
@scope_query ||=
|
15
|
+
begin
|
16
|
+
@named_scopes = []
|
17
|
+
scopes.map do |key, value|
|
18
|
+
case value
|
19
|
+
when ::Symbol
|
20
|
+
if respond_to?(value)
|
21
|
+
@named_scopes << value
|
22
|
+
next
|
23
|
+
end
|
24
|
+
"#{caller_class.quoted_table_name}.#{connection.quote_column_name(key)} = \"#{value}\""
|
25
|
+
when ::String, ::Integer, ::TrueClass, ::FalseClass, ::Float
|
26
|
+
"#{caller_class.quoted_table_name}.#{connection.quote_column_name(key)} = \"#{value}\""
|
27
|
+
when nil
|
28
|
+
casted_key = key.to_s
|
29
|
+
key_column_name = column_names.include?(casted_key) ? casted_key : "#{casted_key}_id"
|
30
|
+
|
31
|
+
@grouped_scopes ||= []
|
32
|
+
@grouped_scopes << key_column_name.to_sym
|
33
|
+
next
|
34
|
+
when ::Proc
|
35
|
+
called_value = value.call
|
36
|
+
if called_value.is_a?(::String)
|
37
|
+
next called_value
|
38
|
+
else
|
39
|
+
@anonymous_scopes ||= all
|
40
|
+
@anonymous_scopes.merge!(called_value)
|
41
|
+
next
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end.compact.join(" AND ") || "1 = 1"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
scope_query # initializes variables
|
48
|
+
|
13
49
|
define_singleton_method :new_item_at do
|
14
50
|
new_item_at
|
15
51
|
end
|
@@ -28,7 +64,17 @@ module ActsAsRankedList #:nodoc:
|
|
28
64
|
end
|
29
65
|
|
30
66
|
define_singleton_method :acts_as_ranked_list_query do
|
31
|
-
|
67
|
+
@acts_as_ranked_list_query ||=
|
68
|
+
begin
|
69
|
+
query = @named_scopes.inject(default_scoped.unscope(:select, :where).where("#{scope_query}")) do |chain, scope|
|
70
|
+
chain.send(*scope)
|
71
|
+
end
|
72
|
+
query.merge!(@anonymous_scopes) if @anonymous_scopes.present?
|
73
|
+
@named_scopes = nil
|
74
|
+
@anonymous_scopes = nil
|
75
|
+
|
76
|
+
query
|
77
|
+
end
|
32
78
|
end
|
33
79
|
|
34
80
|
define_singleton_method :quoted_rank_column do
|
@@ -43,28 +89,34 @@ module ActsAsRankedList #:nodoc:
|
|
43
89
|
@order_by_columns ||= {}
|
44
90
|
@order_by_columns[order] ||= <<~ORDER_BY_COLUMNS.squish
|
45
91
|
#{
|
46
|
-
[
|
92
|
+
[ *@grouped_scopes,
|
93
|
+
quoted_rank_column,
|
47
94
|
*quoted_timestamp_attributes_for_update_in_model,
|
48
95
|
primary_key
|
49
|
-
].join(" #{order}, ")
|
96
|
+
].compact.join(" #{order}, ")
|
50
97
|
} #{order}
|
51
98
|
ORDER_BY_COLUMNS
|
52
99
|
end
|
53
100
|
|
54
101
|
define_singleton_method :spread_ranks do
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
102
|
+
@spread_ranks_sql ||=
|
103
|
+
begin
|
104
|
+
scope_query_presence = scope_query.present? ? "AND #{scope_query}" : ""
|
105
|
+
<<~SQL.squish
|
106
|
+
WITH ORDERED_ROW_NUMBER_CTE AS (
|
107
|
+
SELECT #{caller_class.primary_key},
|
108
|
+
ROW_NUMBER() OVER (ORDER BY #{order_by_columns("ASC")}) AS rn
|
109
|
+
FROM #{caller_class.quoted_table_name}
|
110
|
+
)
|
111
|
+
UPDATE #{caller_class.quoted_table_name}
|
112
|
+
SET #{quoted_rank_column} = ORDERED_ROW_NUMBER_CTE.rn * #{step_increment} #{with_touch}
|
113
|
+
FROM ORDERED_ROW_NUMBER_CTE
|
114
|
+
WHERE #{caller_class.quoted_table_name}.#{caller_class.primary_key} = ORDERED_ROW_NUMBER_CTE.#{caller_class.primary_key}
|
115
|
+
AND #{quoted_rank_column_with_table_name} IS NOT NULL #{scope_query_presence}
|
116
|
+
SQL
|
117
|
+
end
|
118
|
+
|
119
|
+
connection.execute(@spread_ranks_sql)
|
68
120
|
end
|
69
121
|
|
70
122
|
define_singleton_method :with_touch do
|
@@ -90,19 +142,23 @@ module ActsAsRankedList #:nodoc:
|
|
90
142
|
end
|
91
143
|
|
92
144
|
define_singleton_method :get_highest_items do |limit = 0|
|
93
|
-
|
145
|
+
@get_highest_items_query ||= acts_as_ranked_list_query
|
146
|
+
.where("#{quoted_rank_column} IS NOT NULL")
|
147
|
+
.order(order_by_columns)
|
94
148
|
|
95
|
-
return
|
96
|
-
|
97
|
-
|
149
|
+
return @get_highest_items_query if limit == 0
|
150
|
+
|
151
|
+
@get_highest_items_query.limit(limit)
|
98
152
|
end
|
99
153
|
|
100
154
|
define_singleton_method :get_lowest_items do |limit = 0|
|
101
|
-
|
155
|
+
@get_lowest_items_query ||= acts_as_ranked_list_query
|
156
|
+
.where("#{quoted_rank_column} IS NOT NULL")
|
157
|
+
.order(order_by_columns("DESC"))
|
102
158
|
|
103
|
-
return
|
104
|
-
|
105
|
-
|
159
|
+
return @get_lowest_items_query if limit == 0
|
160
|
+
|
161
|
+
@get_lowest_items_query.limit(limit)
|
106
162
|
end
|
107
163
|
end
|
108
164
|
|
@@ -200,12 +256,19 @@ module ActsAsRankedList #:nodoc:
|
|
200
256
|
end
|
201
257
|
|
202
258
|
define_method :get_higher_items do |limit = 0, order = "DESC", rank = nil, distinct = false, include_self_rank = false|
|
259
|
+
return if current_rank.nil?
|
260
|
+
|
203
261
|
operator = include_self_rank ? "<=" : "<"
|
204
262
|
rank ||= current_rank
|
205
263
|
|
206
264
|
query = self.class.acts_as_ranked_list_query
|
207
265
|
.where("#{self.class.quoted_rank_column} #{operator} #{rank}")
|
208
|
-
|
266
|
+
|
267
|
+
self.class.instance_variable_get(:@grouped_scopes)&.each do |grouped_scope|
|
268
|
+
query = query.where(grouped_scope => self.send(grouped_scope))
|
269
|
+
end
|
270
|
+
|
271
|
+
query = query.order(self.class.order_by_columns(order))
|
209
272
|
|
210
273
|
query = query.distinct(self.class.quoted_rank_column) if distinct
|
211
274
|
|
@@ -215,12 +278,19 @@ module ActsAsRankedList #:nodoc:
|
|
215
278
|
end
|
216
279
|
|
217
280
|
define_method :get_lower_items do |limit = 0, order = "ASC", rank = nil, distinct = false, include_self_rank = false|
|
281
|
+
return if current_rank.nil?
|
282
|
+
|
218
283
|
operator = include_self_rank ? ">=" : ">"
|
219
284
|
rank ||= current_rank
|
220
285
|
|
221
286
|
query = self.class.acts_as_ranked_list_query
|
222
287
|
.where("#{self.class.quoted_rank_column} #{operator} #{rank}")
|
223
|
-
|
288
|
+
|
289
|
+
self.class.instance_variable_get(:@grouped_scopes)&.each do |grouped_scope|
|
290
|
+
query = query.where(grouped_scope => self.send(grouped_scope))
|
291
|
+
end
|
292
|
+
|
293
|
+
query = query.order(self.class.order_by_columns(order))
|
224
294
|
|
225
295
|
query = query.distinct(self.class.quoted_rank_column) if distinct
|
226
296
|
|
@@ -230,11 +300,11 @@ module ActsAsRankedList #:nodoc:
|
|
230
300
|
end
|
231
301
|
|
232
302
|
define_method :highest_item? do
|
233
|
-
|
303
|
+
!get_higher_items.exists?
|
234
304
|
end
|
235
305
|
|
236
306
|
define_method :lowest_item? do
|
237
|
-
|
307
|
+
!get_lower_items.exists?
|
238
308
|
end
|
239
309
|
|
240
310
|
private
|
@@ -17,6 +17,22 @@ module ActsAsRankedList
|
|
17
17
|
# acts_as_ranked_list column: "position", touch_on_update: false, step_increment: 0.5, avoid_collisions: false, new_item_at: :highest
|
18
18
|
# end
|
19
19
|
#
|
20
|
+
# @example when using scope
|
21
|
+
# class TodoItem << ::ActiveRecord::Base
|
22
|
+
# belongs_to :todo_list
|
23
|
+
# scope :recently_added, -> { where(created_at: ::Time.now - 12.hours..) }
|
24
|
+
# acts_as_ranked_list scopes: do # you may use as many scopes together
|
25
|
+
# column_name_one: "work", # items that have value `work` in column `column_name_one` are scoped together
|
26
|
+
# column_name_two: :personal, # using a symbol is overloaded, check parameters and example for more info
|
27
|
+
# column_name_three: 0, # this could be used to sub-rank items within a rank
|
28
|
+
# column_name_four: true,
|
29
|
+
# column_name_five: :recently_added, # ranks items within the `recently_added` scope
|
30
|
+
# column_name_six: ::Proc.new { "column_name = 'value'" }, # useful for complex scopes, you may pass any valid SQL scopes
|
31
|
+
# column_name_seven: ::Proc.new { where(column_name: value) }, # anonymous scopes
|
32
|
+
# todo_list(_id): nil # ranks items scoped to their respective todo lists, works with or without `(_id)`
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
20
36
|
# @since 0.2.0
|
21
37
|
# @scope class
|
22
38
|
# @param [Hash] user_options options
|
@@ -26,6 +42,11 @@ module ActsAsRankedList
|
|
26
42
|
# @option user_options [Float/Integer] :step_increment The value to use for spreading ranks
|
27
43
|
# @option user_options [Boolean] :avoid_collisions Controls avoiding rank collisions
|
28
44
|
# @option user_options [Symbol] :new_item_at Controls where to add new items
|
45
|
+
# @option user_options [Hash] :scopes Scopes ranked items based on a column, and optional value
|
46
|
+
# * :any_key [String/Integer/Boolean/Float] Uses passed value in column <any_key> to scope items
|
47
|
+
# * :any_key [Symbol] Uses passed value in column <any_key> to scope items, or a named scope
|
48
|
+
# * :any_key [Proc] Calls passed block's value in column <any_key> to scope items, or an anonymous scope
|
49
|
+
# * :any_key [NilClass] Scopes items to a relationship, by querying non-nil values in column <any_key> or <any_key_id>
|
29
50
|
# @return [void]
|
30
51
|
def acts_as_ranked_list(user_options = {})
|
31
52
|
options = {
|
@@ -33,12 +54,13 @@ module ActsAsRankedList
|
|
33
54
|
touch_on_update: true,
|
34
55
|
step_increment: 1024,
|
35
56
|
avoid_collisions: true,
|
36
|
-
new_item_at: :lowest
|
57
|
+
new_item_at: :lowest,
|
58
|
+
scopes: {}
|
37
59
|
}
|
38
60
|
options.update(user_options)
|
39
61
|
|
40
62
|
::ActsAsRankedList::ActiveRecord::PersistenceCallback.call(self)
|
41
|
-
::ActsAsRankedList::ActiveRecord::RankColumn.call(self, options[:column], options[:touch_on_update], options[:step_increment], options[:avoid_collisions], options[:new_item_at])
|
63
|
+
::ActsAsRankedList::ActiveRecord::RankColumn.call(self, options[:column], options[:touch_on_update], options[:step_increment], options[:avoid_collisions], options[:new_item_at], options[:scopes])
|
42
64
|
|
43
65
|
include ::ActsAsRankedList::ActiveRecord::Service::InstanceMethods
|
44
66
|
include ::ActsAsRankedList::ActiveRecord::SkipPersistence
|
@@ -82,11 +104,24 @@ module ActsAsRankedList
|
|
82
104
|
|
83
105
|
return if rank_changed? == false
|
84
106
|
|
85
|
-
return unless current_rank
|
86
|
-
|
87
|
-
|
107
|
+
return unless current_rank
|
108
|
+
|
109
|
+
query_count = self.class.acts_as_ranked_list_query
|
110
|
+
.where("#{self.class.quoted_rank_column_with_table_name} = #{current_rank}")
|
111
|
+
|
112
|
+
self.class.instance_variable_get(:@grouped_scopes)&.each do |grouped_scope|
|
113
|
+
query_count = query_count.where(grouped_scope => self.send(grouped_scope))
|
114
|
+
end
|
115
|
+
|
116
|
+
query_count = query_count.count
|
88
117
|
|
89
|
-
|
118
|
+
if query_count < 1
|
119
|
+
return
|
120
|
+
end
|
121
|
+
|
122
|
+
self.class.with_skip_persistence do
|
123
|
+
self.class.spread_ranks
|
124
|
+
end
|
90
125
|
end
|
91
126
|
|
92
127
|
def set_new_item_rank
|
metadata
CHANGED
@@ -1,145 +1,139 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_ranked_list
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Farbafe
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-12-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 6.0.3
|
20
17
|
- - ">="
|
21
18
|
- !ruby/object:Gem::Version
|
22
|
-
version: 6.0.3
|
19
|
+
version: 6.0.3
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
|
-
- - "~>"
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: 6.0.3
|
30
24
|
- - ">="
|
31
25
|
- !ruby/object:Gem::Version
|
32
|
-
version: 6.0.3
|
26
|
+
version: 6.0.3
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: rake
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
36
30
|
requirements:
|
37
|
-
- - "
|
31
|
+
- - ">="
|
38
32
|
- !ruby/object:Gem::Version
|
39
33
|
version: '13.0'
|
40
34
|
type: :runtime
|
41
35
|
prerelease: false
|
42
36
|
version_requirements: !ruby/object:Gem::Requirement
|
43
37
|
requirements:
|
44
|
-
- - "
|
38
|
+
- - ">="
|
45
39
|
- !ruby/object:Gem::Version
|
46
40
|
version: '13.0'
|
47
41
|
- !ruby/object:Gem::Dependency
|
48
42
|
name: rspec
|
49
43
|
requirement: !ruby/object:Gem::Requirement
|
50
44
|
requirements:
|
51
|
-
- - "
|
45
|
+
- - ">="
|
52
46
|
- !ruby/object:Gem::Version
|
53
47
|
version: '3.0'
|
54
48
|
type: :runtime
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
51
|
requirements:
|
58
|
-
- - "
|
52
|
+
- - ">="
|
59
53
|
- !ruby/object:Gem::Version
|
60
54
|
version: '3.0'
|
61
55
|
- !ruby/object:Gem::Dependency
|
62
56
|
name: standard
|
63
57
|
requirement: !ruby/object:Gem::Requirement
|
64
58
|
requirements:
|
65
|
-
- - "
|
59
|
+
- - ">="
|
66
60
|
- !ruby/object:Gem::Version
|
67
61
|
version: '1.3'
|
68
62
|
type: :runtime
|
69
63
|
prerelease: false
|
70
64
|
version_requirements: !ruby/object:Gem::Requirement
|
71
65
|
requirements:
|
72
|
-
- - "
|
66
|
+
- - ">="
|
73
67
|
- !ruby/object:Gem::Version
|
74
68
|
version: '1.3'
|
75
69
|
- !ruby/object:Gem::Dependency
|
76
70
|
name: rspec-rails
|
77
71
|
requirement: !ruby/object:Gem::Requirement
|
78
72
|
requirements:
|
79
|
-
- -
|
73
|
+
- - ">="
|
80
74
|
- !ruby/object:Gem::Version
|
81
75
|
version: 5.1.2
|
82
76
|
type: :runtime
|
83
77
|
prerelease: false
|
84
78
|
version_requirements: !ruby/object:Gem::Requirement
|
85
79
|
requirements:
|
86
|
-
- -
|
80
|
+
- - ">="
|
87
81
|
- !ruby/object:Gem::Version
|
88
82
|
version: 5.1.2
|
89
83
|
- !ruby/object:Gem::Dependency
|
90
84
|
name: byebug
|
91
85
|
requirement: !ruby/object:Gem::Requirement
|
92
86
|
requirements:
|
93
|
-
- -
|
87
|
+
- - ">="
|
94
88
|
- !ruby/object:Gem::Version
|
95
89
|
version: 11.1.3
|
96
90
|
type: :development
|
97
91
|
prerelease: false
|
98
92
|
version_requirements: !ruby/object:Gem::Requirement
|
99
93
|
requirements:
|
100
|
-
- -
|
94
|
+
- - ">="
|
101
95
|
- !ruby/object:Gem::Version
|
102
96
|
version: 11.1.3
|
103
97
|
- !ruby/object:Gem::Dependency
|
104
98
|
name: yard
|
105
99
|
requirement: !ruby/object:Gem::Requirement
|
106
100
|
requirements:
|
107
|
-
- -
|
101
|
+
- - ">="
|
108
102
|
- !ruby/object:Gem::Version
|
109
103
|
version: 0.9.34
|
110
104
|
type: :development
|
111
105
|
prerelease: false
|
112
106
|
version_requirements: !ruby/object:Gem::Requirement
|
113
107
|
requirements:
|
114
|
-
- -
|
108
|
+
- - ">="
|
115
109
|
- !ruby/object:Gem::Version
|
116
110
|
version: 0.9.34
|
117
111
|
- !ruby/object:Gem::Dependency
|
118
112
|
name: redcarpet
|
119
113
|
requirement: !ruby/object:Gem::Requirement
|
120
114
|
requirements:
|
121
|
-
- -
|
115
|
+
- - ">="
|
122
116
|
- !ruby/object:Gem::Version
|
123
117
|
version: 3.6.0
|
124
118
|
type: :development
|
125
119
|
prerelease: false
|
126
120
|
version_requirements: !ruby/object:Gem::Requirement
|
127
121
|
requirements:
|
128
|
-
- -
|
122
|
+
- - ">="
|
129
123
|
- !ruby/object:Gem::Version
|
130
124
|
version: 3.6.0
|
131
125
|
- !ruby/object:Gem::Dependency
|
132
126
|
name: sqlite3
|
133
127
|
requirement: !ruby/object:Gem::Requirement
|
134
128
|
requirements:
|
135
|
-
- -
|
129
|
+
- - ">="
|
136
130
|
- !ruby/object:Gem::Version
|
137
131
|
version: 1.6.2
|
138
132
|
type: :development
|
139
133
|
prerelease: false
|
140
134
|
version_requirements: !ruby/object:Gem::Requirement
|
141
135
|
requirements:
|
142
|
-
- -
|
136
|
+
- - ">="
|
143
137
|
- !ruby/object:Gem::Version
|
144
138
|
version: 1.6.2
|
145
139
|
description: Orders ActiveRecord items by floating ranks for spaces in-between items.
|
@@ -159,6 +153,7 @@ files:
|
|
159
153
|
- LICENSE.txt
|
160
154
|
- README.md
|
161
155
|
- Rakefile
|
156
|
+
- acts_as_ranked_list.gemspec
|
162
157
|
- lib/acts_as_ranked_list.rb
|
163
158
|
- lib/acts_as_ranked_list/active_record/active_record.rb
|
164
159
|
- lib/acts_as_ranked_list/active_record/avoid_collisions.rb
|
@@ -192,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
192
187
|
- !ruby/object:Gem::Version
|
193
188
|
version: '0'
|
194
189
|
requirements: []
|
195
|
-
rubygems_version: 3.
|
190
|
+
rubygems_version: 3.3.7
|
196
191
|
signing_key:
|
197
192
|
specification_version: 4
|
198
193
|
summary: Orders ActiveRecord items by ranks. Influenced by gem ActsAsList.
|