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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb3133201175d913d991f6b0d2184d546bf928439dee69e31b5ae0c3dc4e171b
4
- data.tar.gz: 53d0271e984cc9e85004ac80a0bb25f1f6137dc4b1c3ae2dc4705146e78eed6b
3
+ metadata.gz: fb250e95788139cb03e2e54560819ed5841904faebd13a8ecb5902f46d0d0a35
4
+ data.tar.gz: 36ad4c7c01156f08be41395994359fd9ddbae0280c821348086703da3b0138a7
5
5
  SHA512:
6
- metadata.gz: 7e93a7f60c55c2e1f9b82a8b432bbfbd52169a175d25ea6f0972704f84335e266b2e599e22209e07d8d3c13601ff3297aecae90f0b5e646b5204803e1a91afc4
7
- data.tar.gz: 79bb3e16a410df16e289747c0dc2dd0e642fef50dfd745e3ec073765c2449d8ebaabaed81430b14c7b1921359ea3e4f63466db98ff8a0071d568866e7aacb878
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
- - 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.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 << ::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)
@@ -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 << ::ActiveRecord::Base
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
- 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.2"
5
5
  end
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.1
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-21 00:00:00.000000000 Z
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