acts_as_ranked_list 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -2
- data/README.md +63 -24
- 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 +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb250e95788139cb03e2e54560819ed5841904faebd13a8ecb5902f46d0d0a35
|
4
|
+
data.tar.gz: 36ad4c7c01156f08be41395994359fd9ddbae0280c821348086703da3b0138a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ef434d2c8b9dd945c6cafae76b6398b54cc2db33e2c96f518c3abf237d789a5da413322067627f1931e6acff9f0730fd61d077025530d85e94818e2a1583260
|
7
|
+
data.tar.gz: d225b605fa64c2af642895e7075e373edc1eab17944f2a49e508d80be5fdf2571bc0b69f58d5a0001a054e1447864c4acb0ba60169146ce48a431213cb6637a1
|
data/CHANGELOG.md
CHANGED
@@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
## [0.2.2] - 2023-04-24
|
11
|
+
|
12
|
+
- ~~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.
|
13
|
+
- Allows any number of different scopes on the items by referencing another column, of type:
|
14
|
+
- a relationship.
|
15
|
+
- a string, symbol, boolean or number.
|
16
|
+
- a custom-defined scope.
|
17
|
+
- Fixes class method `spread_ranks` to ignore unranked items.
|
12
18
|
|
13
19
|
## [0.2.1] - 2023-04-21
|
14
20
|
|
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)
|
@@ -221,7 +260,7 @@ You can spread ranks so that the difference between each rank and the next is se
|
|
221
260
|
|
222
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
|
|
@@ -233,7 +272,7 @@ Items are spread in, ascending order of each, by:
|
|
233
272
|
|
234
273
|
To spread ranks:
|
235
274
|
|
236
|
-
```
|
275
|
+
```ruby
|
237
276
|
TodoItem.spread_ranks
|
238
277
|
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
278
|
```
|
@@ -246,8 +285,8 @@ Imagine the scenario where you have 199 records to rank, and you must rank them
|
|
246
285
|
|
247
286
|
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
287
|
|
249
|
-
```
|
250
|
-
class CrammedTodoItem
|
288
|
+
```ruby
|
289
|
+
class CrammedTodoItem < ::ActiveRecord::Base
|
251
290
|
acts_as_ranked_list step_increment: 0.5
|
252
291
|
end
|
253
292
|
|
@@ -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", ">= 6.0.3.6"
|
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,14 +1,14 @@
|
|
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.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Farbafe
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-04-
|
11
|
+
date: 2023-04-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -159,6 +159,7 @@ files:
|
|
159
159
|
- LICENSE.txt
|
160
160
|
- README.md
|
161
161
|
- Rakefile
|
162
|
+
- acts_as_ranked_list.gemspec
|
162
163
|
- lib/acts_as_ranked_list.rb
|
163
164
|
- lib/acts_as_ranked_list/active_record/active_record.rb
|
164
165
|
- lib/acts_as_ranked_list/active_record/avoid_collisions.rb
|