concerns_on_rails 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +99 -14
- data/lib/concerns_on_rails/hashable.rb +77 -0
- data/lib/concerns_on_rails/publishable.rb +12 -15
- data/lib/concerns_on_rails/sluggable.rb +8 -11
- data/lib/concerns_on_rails/soft_deletable.rb +43 -45
- data/lib/concerns_on_rails/sortable.rb +14 -10
- data/lib/concerns_on_rails/version.rb +1 -1
- data/lib/concerns_on_rails.rb +1 -0
- metadata +12 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d4b68b3625ea62546fa003654e7bd0618a56b09b615b406dcaed12f16560338
|
|
4
|
+
data.tar.gz: 4d048584cdf09f1eb9859a4cca0ccd64182b331bfa8a6ffb897ff61ab1c4636a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8797f42f52e32309614fa95bf2a52b0a17e6adff54f07bbdcc9f9f27863a92fdfe0dfec1a14d7ad9b34285e25abc6a12cf37194a4da4532874a29d0fc7a13b01
|
|
7
|
+
data.tar.gz: e60133f68c2b4778cdea6b34fdcddd279eee5076ef060c009734c9e42eda3d379d65c9adfa847dd30dcb47e06e4a39c88313644fa248e407a5d60e242e8e3756
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<!-- CHANGELOG.md -->
|
|
2
2
|
|
|
3
|
+
## 1.3.0 (2026-05-16)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Hashable: Auto-generate a random value on create (`:hex`, `:uuid`, `:integer`, or `:custom` alphabet). Adds `hashable_by` macro and a dynamic `regenerate_<field>!` instance method.
|
|
7
|
+
|
|
3
8
|
## 1.1.0 (2025-04-17)
|
|
4
9
|
|
|
5
10
|
### Added
|
data/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A simple collection of reusable Rails concerns to keep your models clean and DRY
|
|
|
10
10
|
- 🔢 `Sortable`: Sort records based on a field using `acts_as_list`, with flexible sorting field and direction
|
|
11
11
|
- 📤 `Publishable`: Easily manage published/unpublished records using a simple `published_at` field
|
|
12
12
|
- ❌ `SoftDeletable`: Soft delete records using a configurable timestamp field (e.g., `deleted_at`) with automatic scoping
|
|
13
|
+
- 🔐 `Hashable`: Auto-generate a random hex/UUID/integer/custom-alphabet value on create, with a `regenerate_<field>!` helper
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -127,29 +128,113 @@ Soft delete records using a timestamp field (default: `deleted_at`).
|
|
|
127
128
|
class User < ApplicationRecord
|
|
128
129
|
include ConcernsOnRails::SoftDeletable
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
# Optional: customize field and touch behavior
|
|
132
|
+
soft_deletable_by :deleted_at, touch: true
|
|
131
133
|
end
|
|
134
|
+
```
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
user.deleted? # => true
|
|
136
|
-
|
|
136
|
+
#### Scopes
|
|
137
|
+
```ruby
|
|
137
138
|
User.without_deleted # => returns only active users
|
|
138
139
|
User.soft_deleted # => returns soft-deleted users
|
|
140
|
+
User.active # => same as without_deleted
|
|
139
141
|
User.all # => returns only non-deleted by default (default_scope applied)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### Soft delete and restore
|
|
145
|
+
```ruby
|
|
146
|
+
user.soft_delete! # Soft delete the user (sets deleted_at)
|
|
147
|
+
user.deleted? # => true
|
|
148
|
+
user.soft_deleted? # => true (alias)
|
|
149
|
+
user.is_soft_deleted? # => true (alias)
|
|
140
150
|
|
|
141
|
-
user.restore!
|
|
142
|
-
user.deleted?
|
|
151
|
+
user.restore! # Restore the user (sets deleted_at to nil)
|
|
152
|
+
user.deleted? # => false
|
|
143
153
|
```
|
|
144
154
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
#### Permanently delete
|
|
156
|
+
```ruby
|
|
157
|
+
user.really_delete! # Hard delete the record from DB
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Soft delete/hard delete all records
|
|
161
|
+
```ruby
|
|
162
|
+
User.destroy_all # Soft delete all users (sets deleted_at)
|
|
163
|
+
User.really_destroy_all # Hard delete ALL users (removes from DB)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### Callbacks (Hooks)
|
|
167
|
+
You can use the following hooks to run logic before/after soft delete or restore:
|
|
168
|
+
```ruby
|
|
169
|
+
class User < ApplicationRecord
|
|
170
|
+
include ConcernsOnRails::SoftDeletable
|
|
171
|
+
|
|
172
|
+
def before_soft_delete
|
|
173
|
+
# Code to run before soft delete
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def after_soft_delete
|
|
177
|
+
# Code to run after soft delete
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def before_restore
|
|
181
|
+
# Code to run before restore
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def after_restore
|
|
185
|
+
# Code to run after restore
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### Notes
|
|
191
|
+
- Default field is `deleted_at`, can be changed with `soft_deletable_by :your_field`
|
|
192
|
+
- `touch: false` to skip updating updated_at when soft deleting/restoring
|
|
152
193
|
- Aliases for `deleted?`: `soft_deleted?`, `is_soft_deleted?`
|
|
194
|
+
- All scopes and methods work seamlessly with ActiveRecord
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### 5. Hashable
|
|
199
|
+
|
|
200
|
+
Auto-generate a random value (hex, UUID, fixed-digit integer, or custom-alphabet string) on create.
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
class Order < ApplicationRecord
|
|
204
|
+
include ConcernsOnRails::Hashable
|
|
205
|
+
|
|
206
|
+
# Defaults: type: :hex, length: 16 (32-char hex string)
|
|
207
|
+
hashable_by :token
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
order = Order.create!
|
|
211
|
+
order.token # => "a3f7c9b1e2d40859e2f1c9b73d40a857"
|
|
212
|
+
order.regenerate_token! # rolls a new random value and persists it
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### Types
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
hashable_by :token, type: :hex, length: 16
|
|
219
|
+
hashable_by :external_id, type: :uuid
|
|
220
|
+
hashable_by :code, type: :integer, length: 6
|
|
221
|
+
hashable_by :code, type: :custom, length: 8,
|
|
222
|
+
alphabet: "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| Type | `length` means | Example output |
|
|
226
|
+
|------------|-------------------------|--------------------------|
|
|
227
|
+
| `:hex` | byte count (output is `length * 2` hex chars) | `"a3f7c9b1e2d40859"` |
|
|
228
|
+
| `:uuid` | ignored | `"550e8400-e29b-41d4-a716-446655440000"` |
|
|
229
|
+
| `:integer` | digit count | `483921` |
|
|
230
|
+
| `:custom` | output length, samples from `alphabet:` | `"K7M3PQ9A"` |
|
|
231
|
+
|
|
232
|
+
#### Notes
|
|
233
|
+
- Auto-assigns on `before_create` only when the field is blank, so callers can still pass an explicit value.
|
|
234
|
+
- A `regenerate_<field>!` instance method is defined dynamically to match the configured column.
|
|
235
|
+
- No uniqueness retry is built in. For collision-prone configurations (e.g. short integer codes), add a unique index and rescue at the application level.
|
|
236
|
+
- For fixed-width numeric codes (e.g. `000042`), use a string column — integer columns drop leading zeros.
|
|
237
|
+
- If your model has `validates :<field>, presence: true`, switch to a `before_validation` callback in your model since the concern uses `before_create`.
|
|
153
238
|
|
|
154
239
|
---
|
|
155
240
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require "active_support/concern"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
|
|
4
|
+
module ConcernsOnRails
|
|
5
|
+
module Hashable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
VALID_TYPES = %i[hex uuid integer custom].freeze
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
class_attribute :hashable_field, instance_accessor: false
|
|
12
|
+
class_attribute :hashable_type, instance_accessor: false, default: :hex
|
|
13
|
+
class_attribute :hashable_length, instance_accessor: false, default: 16
|
|
14
|
+
class_attribute :hashable_alphabet, instance_accessor: false, default: nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class_methods do
|
|
18
|
+
# Define hashable field and generation options.
|
|
19
|
+
# Example:
|
|
20
|
+
# hashable_by :token
|
|
21
|
+
# hashable_by :token, type: :hex, length: 16
|
|
22
|
+
# hashable_by :external_id, type: :uuid
|
|
23
|
+
# hashable_by :code, type: :integer, length: 6
|
|
24
|
+
# hashable_by :code, type: :custom, length: 8, alphabet: "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
|
|
25
|
+
def hashable_by(field, type: :hex, length: 16, alphabet: nil)
|
|
26
|
+
self.hashable_field = field.to_sym
|
|
27
|
+
self.hashable_type = type.to_sym
|
|
28
|
+
self.hashable_length = length.to_i
|
|
29
|
+
self.hashable_alphabet = alphabet
|
|
30
|
+
|
|
31
|
+
validate_hashable_options!
|
|
32
|
+
before_create :assign_hashable_value
|
|
33
|
+
|
|
34
|
+
define_method("regenerate_#{hashable_field}!") do
|
|
35
|
+
update!(self.class.hashable_field => self.class.generate_hashable_value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class_methods do
|
|
41
|
+
# Generate a new random value using the configured type/length/alphabet.
|
|
42
|
+
def generate_hashable_value
|
|
43
|
+
case hashable_type
|
|
44
|
+
when :hex then SecureRandom.hex(hashable_length)
|
|
45
|
+
when :uuid then SecureRandom.uuid
|
|
46
|
+
when :integer then SecureRandom.random_number(10**hashable_length).to_s.rjust(hashable_length, "0").to_i
|
|
47
|
+
when :custom then Array.new(hashable_length) { hashable_alphabet[SecureRandom.random_number(hashable_alphabet.size)] }.join
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def validate_hashable_options!
|
|
54
|
+
unless column_names.include?(hashable_field.to_s)
|
|
55
|
+
raise ArgumentError, "ConcernsOnRails::Hashable: hashable_field '#{hashable_field}' does not exist in the database"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
unless VALID_TYPES.include?(hashable_type)
|
|
59
|
+
raise ArgumentError, "ConcernsOnRails::Hashable: unknown type '#{hashable_type}'. Valid types: #{VALID_TYPES.join(', ')}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return unless hashable_type == :custom && (!hashable_alphabet.is_a?(String) || hashable_alphabet.empty?)
|
|
63
|
+
|
|
64
|
+
raise ArgumentError, "ConcernsOnRails::Hashable: type :custom requires a non-empty alphabet: String"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Assigns the generated value only when the field is blank,
|
|
69
|
+
# so callers can still pass an explicit value at create time.
|
|
70
|
+
def assign_hashable_value
|
|
71
|
+
field = self.class.hashable_field
|
|
72
|
+
return if self[field].present?
|
|
73
|
+
|
|
74
|
+
self[field] = self.class.generate_hashable_value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -4,28 +4,22 @@ module ConcernsOnRails
|
|
|
4
4
|
module Publishable
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
|
-
# instance methods
|
|
8
7
|
included do
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
class_attribute :publishable_field, instance_accessor: false, default: :published_at
|
|
9
|
+
|
|
10
|
+
scope :published, -> { where(arel_table[publishable_field].lteq(Time.zone.now)) }
|
|
11
|
+
scope :unpublished, lambda {
|
|
12
|
+
where(arel_table[publishable_field].eq(nil).or(arel_table[publishable_field].gt(Time.zone.now)))
|
|
13
|
+
}
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
# class methods
|
|
15
16
|
class_methods do
|
|
16
|
-
# Define publishable field
|
|
17
|
-
# Example:
|
|
18
|
-
# publishable_by :published_at
|
|
19
17
|
def publishable_by(field = nil)
|
|
20
18
|
self.publishable_field = field || :published_at
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
unless column_names.include?(publishable_field.to_s)
|
|
24
|
-
raise ArgumentError, "ConcernsOnRails::Publishable: publishable_field '#{publishable_field}' does not exist in the database"
|
|
25
|
-
end
|
|
20
|
+
return if column_names.include?(publishable_field.to_s)
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
scope :unpublished, -> { where(arel_table[publishable_field].eq(nil)) }
|
|
22
|
+
raise ArgumentError, "ConcernsOnRails::Publishable: publishable_field '#{publishable_field}' does not exist in the database"
|
|
29
23
|
end
|
|
30
24
|
end
|
|
31
25
|
|
|
@@ -48,7 +42,10 @@ module ConcernsOnRails
|
|
|
48
42
|
# Example:
|
|
49
43
|
# record.published?
|
|
50
44
|
def published?
|
|
51
|
-
self[self.class.publishable_field]
|
|
45
|
+
value = self[self.class.publishable_field]
|
|
46
|
+
return false unless value.present?
|
|
47
|
+
|
|
48
|
+
value.respond_to?(:<=) ? value <= Time.zone.now : true
|
|
52
49
|
end
|
|
53
50
|
|
|
54
51
|
# Check if the record is unpublished
|
|
@@ -12,6 +12,7 @@ module ConcernsOnRails
|
|
|
12
12
|
self.sluggable_field ||= :name
|
|
13
13
|
|
|
14
14
|
extend FriendlyId
|
|
15
|
+
|
|
15
16
|
# we need use a lambda to access the instance variable
|
|
16
17
|
# instead of friendly_id :slug_source, use: :slugged
|
|
17
18
|
friendly_id :slug_source, use: :slugged
|
|
@@ -37,11 +38,12 @@ module ConcernsOnRails
|
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
private
|
|
41
|
+
|
|
40
42
|
# Validate sluggable_field exists in database
|
|
41
43
|
def validate_sluggable_field!
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
return if column_names.include?(sluggable_field.to_s)
|
|
45
|
+
|
|
46
|
+
raise ArgumentError, "ConcernsOnRails::Sluggable: sluggable_field '#{sluggable_field}' does not exist in the database"
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
|
|
@@ -51,13 +53,8 @@ module ConcernsOnRails
|
|
|
51
53
|
# Example:
|
|
52
54
|
# record.slug_source
|
|
53
55
|
def slug_source
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
elsif respond_to?(:title)
|
|
57
|
-
title
|
|
58
|
-
else
|
|
59
|
-
to_s
|
|
60
|
-
end
|
|
56
|
+
field = self.class.sluggable_field
|
|
57
|
+
respond_to?(field) ? send(field) : to_s
|
|
61
58
|
end
|
|
62
59
|
end
|
|
63
|
-
end
|
|
60
|
+
end
|
|
@@ -8,17 +8,13 @@ module ConcernsOnRails
|
|
|
8
8
|
# declare class attributes and set default values
|
|
9
9
|
class_attribute :soft_delete_field, instance_accessor: false, default: :deleted_at
|
|
10
10
|
class_attribute :soft_delete_touch, instance_accessor: false, default: true
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
# scopes
|
|
13
13
|
scope :active, -> { unscope(where: soft_delete_field).where(soft_delete_field => nil) }
|
|
14
14
|
scope :without_deleted, -> { unscope(where: soft_delete_field).where(soft_delete_field => nil) }
|
|
15
15
|
scope :soft_deleted, -> { unscope(where: soft_delete_field).where.not(soft_delete_field => nil) }
|
|
16
16
|
# Optionally, uncomment to hide deleted by default:
|
|
17
17
|
default_scope { without_deleted }
|
|
18
|
-
|
|
19
|
-
# define callbacks
|
|
20
|
-
define_model_callbacks :soft_delete
|
|
21
|
-
define_model_callbacks :restore
|
|
22
18
|
end
|
|
23
19
|
|
|
24
20
|
class_methods do
|
|
@@ -29,9 +25,19 @@ module ConcernsOnRails
|
|
|
29
25
|
self.soft_delete_field = field || :deleted_at
|
|
30
26
|
self.soft_delete_touch = touch
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
return if column_names.include?(soft_delete_field.to_s)
|
|
29
|
+
|
|
30
|
+
raise ArgumentError, "ConcernsOnRails::SoftDeletable: soft_delete_field '#{soft_delete_field}' does not exist in the database"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Override destroy_all to perform soft delete on all records
|
|
34
|
+
def destroy_all
|
|
35
|
+
all.each(&:soft_delete!)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Provide really_destroy_all to hard delete all records
|
|
39
|
+
def really_destroy_all
|
|
40
|
+
unscoped.delete_all
|
|
35
41
|
end
|
|
36
42
|
end
|
|
37
43
|
|
|
@@ -41,57 +47,49 @@ module ConcernsOnRails
|
|
|
41
47
|
def before_restore; end
|
|
42
48
|
def after_restore; end
|
|
43
49
|
|
|
44
|
-
# add soft delete methods
|
|
45
50
|
def soft_delete!
|
|
46
51
|
return true if deleted?
|
|
47
|
-
run_callbacks(:soft_delete) do
|
|
48
|
-
before_soft_delete
|
|
49
|
-
if self.class.soft_delete_touch
|
|
50
|
-
update(self.class.soft_delete_field => Time.zone.now).tap do |result|
|
|
51
|
-
touch if respond_to?(:touch)
|
|
52
|
-
after_soft_delete if result
|
|
53
|
-
end
|
|
54
|
-
else
|
|
55
|
-
update_column(self.class.soft_delete_field, Time.zone.now).tap do |result|
|
|
56
|
-
after_soft_delete if result
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
52
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
before_soft_delete
|
|
54
|
+
result = if self.class.soft_delete_touch
|
|
55
|
+
update(self.class.soft_delete_field => Time.zone.now)
|
|
56
|
+
else
|
|
57
|
+
update_column(self.class.soft_delete_field, Time.zone.now)
|
|
58
|
+
end
|
|
59
|
+
after_soft_delete if result
|
|
60
|
+
result
|
|
65
61
|
end
|
|
66
|
-
|
|
62
|
+
|
|
67
63
|
def restore!
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
64
|
+
return true unless deleted?
|
|
65
|
+
|
|
66
|
+
before_restore
|
|
67
|
+
result = if self.class.soft_delete_touch
|
|
68
|
+
update(self.class.soft_delete_field => nil)
|
|
69
|
+
else
|
|
70
|
+
update_column(self.class.soft_delete_field, nil)
|
|
71
|
+
end
|
|
72
|
+
after_restore if result
|
|
73
|
+
result
|
|
81
74
|
end
|
|
82
|
-
|
|
75
|
+
|
|
76
|
+
# bypasses AR callbacks and validations — use when you want a true hard delete
|
|
77
|
+
def really_delete!
|
|
78
|
+
self.class.unscoped.where(self.class.primary_key => id).delete_all
|
|
79
|
+
freeze
|
|
80
|
+
end
|
|
81
|
+
|
|
83
82
|
def deleted?
|
|
84
83
|
self[self.class.soft_delete_field].present?
|
|
85
84
|
end
|
|
86
85
|
|
|
87
86
|
# alias methods
|
|
88
87
|
# define here to avoid issue: undefined method `deleted?' for module `ConcernsOnRails::SoftDeletable'
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
alias is_soft_deleted? deleted?
|
|
89
|
+
alias soft_deleted? deleted?
|
|
91
90
|
|
|
92
|
-
# Is really deleted?
|
|
93
91
|
def is_really_deleted?
|
|
94
|
-
!self.class.exists?(id)
|
|
92
|
+
!self.class.unscoped.exists?(id)
|
|
95
93
|
end
|
|
96
94
|
end
|
|
97
95
|
end
|
|
@@ -22,7 +22,13 @@ module ConcernsOnRails
|
|
|
22
22
|
self.sortable_direction ||= :asc
|
|
23
23
|
|
|
24
24
|
# we cannot use acts_as_list here
|
|
25
|
-
default_scope
|
|
25
|
+
default_scope do
|
|
26
|
+
unless column_names.include?(sortable_field.to_s)
|
|
27
|
+
raise ArgumentError, "#{name}: '#{sortable_field}' column not found. Call `sortable_by :your_column` to configure the sort field."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
order(sortable_field => sortable_direction)
|
|
31
|
+
end
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
# class methods
|
|
@@ -35,7 +41,9 @@ module ConcernsOnRails
|
|
|
35
41
|
# sortable_by position: :desc
|
|
36
42
|
#
|
|
37
43
|
# sortable_by :position, use_acts_as_list: false
|
|
38
|
-
def sortable_by(field_config, use_acts_as_list: true)
|
|
44
|
+
def sortable_by(field_config = nil, use_acts_as_list: true, **field_options)
|
|
45
|
+
field_config = field_options if field_config.nil? && field_options.any?
|
|
46
|
+
|
|
39
47
|
# parse field_config
|
|
40
48
|
field, direction = parse_sortable_config(field_config)
|
|
41
49
|
|
|
@@ -48,15 +56,11 @@ module ConcernsOnRails
|
|
|
48
56
|
|
|
49
57
|
validate_sortable_field!
|
|
50
58
|
|
|
51
|
-
# add acts_as_list and default scope
|
|
52
|
-
# Setup sorting behaviors
|
|
53
59
|
acts_as_list column: sortable_field if use_acts_as_list
|
|
54
|
-
|
|
55
|
-
# add default scope: position => asc
|
|
56
|
-
default_scope { order(sortable_field => sortable_direction) }
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
private
|
|
63
|
+
|
|
60
64
|
def parse_sortable_config(config)
|
|
61
65
|
if config.is_a?(Hash)
|
|
62
66
|
# extract key and value
|
|
@@ -71,9 +75,9 @@ module ConcernsOnRails
|
|
|
71
75
|
|
|
72
76
|
# Validate sortable_field exists in database
|
|
73
77
|
def validate_sortable_field!
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
return if column_names.include?(sortable_field.to_s)
|
|
79
|
+
|
|
80
|
+
raise ArgumentError, "ConcernsOnRails::Sortable: sortable_field '#{sortable_field}' does not exist in the database"
|
|
77
81
|
end
|
|
78
82
|
end
|
|
79
83
|
end
|
data/lib/concerns_on_rails.rb
CHANGED
metadata
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: concerns_on_rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ethan Nguyen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-05-15 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
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: '5.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '9'
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
|
-
- - "
|
|
27
|
+
- - ">="
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
29
|
version: '5.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '9'
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
34
|
name: acts_as_list
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -64,6 +70,7 @@ files:
|
|
|
64
70
|
- CODE_OF_CONDUCT.md
|
|
65
71
|
- README.md
|
|
66
72
|
- lib/concerns_on_rails.rb
|
|
73
|
+
- lib/concerns_on_rails/hashable.rb
|
|
67
74
|
- lib/concerns_on_rails/publishable.rb
|
|
68
75
|
- lib/concerns_on_rails/sluggable.rb
|
|
69
76
|
- lib/concerns_on_rails/soft_deletable.rb
|
|
@@ -91,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
91
98
|
- !ruby/object:Gem::Version
|
|
92
99
|
version: '0'
|
|
93
100
|
requirements: []
|
|
94
|
-
rubygems_version: 3.
|
|
101
|
+
rubygems_version: 3.4.19
|
|
95
102
|
signing_key:
|
|
96
103
|
specification_version: 4
|
|
97
104
|
summary: Reusable Rails concerns like Sortable, Publishable, and Sluggable
|