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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb3133201175d913d991f6b0d2184d546bf928439dee69e31b5ae0c3dc4e171b
4
- data.tar.gz: 53d0271e984cc9e85004ac80a0bb25f1f6137dc4b1c3ae2dc4705146e78eed6b
3
+ metadata.gz: 55fb6b1014091f8df47866752d9aedc4cdac1204955852b09c84d480e2769486
4
+ data.tar.gz: 34bc12ea565e8fd881a6401d9b5cc3ea4e83b86106abb6ee1ec1b7d4a19c1fac
5
5
  SHA512:
6
- metadata.gz: 7e93a7f60c55c2e1f9b82a8b432bbfbd52169a175d25ea6f0972704f84335e266b2e599e22209e07d8d3c13601ff3297aecae90f0b5e646b5204803e1a91afc4
7
- data.tar.gz: 79bb3e16a410df16e289747c0dc2dd0e642fef50dfd745e3ec073765c2449d8ebaabaed81430b14c7b1921359ea3e4f63466db98ff8a0071d568866e7aacb878
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
- - Allows unranked items in list by having `nil` values. New items can also be added as unranked.
11
- - Allows different scopes on the items by referencing another column.
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 << ::ActiveRecord::Base
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 << ::ApplicationRecord
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
- ### Query position of current item
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. 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+`:
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. rank
231
- 2. time updated columns (`updated_at` for example)
232
- 3. primary key (`id` for example)
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 << ::ActiveRecord::Base
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
- default_scoped.unscope(:select, :where)
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
- [ quoted_rank_column,
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
- sql = <<~SQL.squish
56
- WITH ORDERED_ROW_NUMBER_CTE AS (
57
- SELECT #{caller_class.primary_key},
58
- ROW_NUMBER() OVER (ORDER BY #{order_by_columns("ASC")}) AS rn
59
- FROM #{caller_class.quoted_table_name}
60
- )
61
- UPDATE #{caller_class.quoted_table_name}
62
- SET #{quoted_rank_column} = ORDERED_ROW_NUMBER_CTE.rn * #{step_increment} #{with_touch}
63
- FROM ORDERED_ROW_NUMBER_CTE
64
- WHERE #{caller_class.quoted_table_name}.#{caller_class.primary_key} = ORDERED_ROW_NUMBER_CTE.#{caller_class.primary_key}
65
- SQL
66
-
67
- connection.execute(sql)
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
- query = acts_as_ranked_list_query.order(order_by_columns)
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 query if limit == 0
96
-
97
- query.limit(limit)
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
- query = acts_as_ranked_list_query.order(order_by_columns("DESC"))
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 query if limit == 0
104
-
105
- query.limit(limit)
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
- .order(self.class.order_by_columns(order))
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
- .order(self.class.order_by_columns(order))
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
- self.class.get_highest_items(1).first == self
303
+ !get_higher_items.exists?
234
304
  end
235
305
 
236
306
  define_method :lowest_item? do
237
- self.class.get_lowest_items(1).first == self
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 && self.class.acts_as_ranked_list_query.where(
86
- "#{self.class.quoted_rank_column_with_table_name} = #{current_rank}"
87
- ).count >= 1
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
- self.class.spread_ranks
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActsAsRankedList
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.3"
5
5
  end
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.1
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-04-21 00:00:00.000000000 Z
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.6
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.6
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.1.6
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.