concerns_on_rails 1.3.0 → 1.4.2
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 +11 -0
- data/README.md +62 -0
- data/lib/concerns_on_rails/schedulable.rb +128 -0
- data/lib/concerns_on_rails/version.rb +1 -1
- data/lib/concerns_on_rails.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e784268a8fb00ef3e77b613eca3ddc44da0ca95e0f9f1784ce019356f2f5905
|
|
4
|
+
data.tar.gz: fdc824e479b2607b097762debf8940c622044f47a108a5482729e9b74c1233c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae9b3876ae0eea6a02f51710d33074aba898bc1ab22609a33cfb27a8eb1747c5d4c12ef01fc1de180026658adf0f4d1d788842abc5f83322616d80dac35ac238
|
|
7
|
+
data.tar.gz: '09cb150b92adbcd6ab21b670b122d42ad6f71bcbb4616e5bfa89ac2cdc385fc8e42cbac98f8b11d3c2d9bba462c32a0fa2d8e9bd46c5b6e3e0f8407e41242e97'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
<!-- CHANGELOG.md -->
|
|
2
2
|
|
|
3
|
+
## 1.4.2 (2026-05-16)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Schedulable: Manage time-windowed records via `starts_at` / `ends_at` columns. Adds `schedulable_by` macro, scopes (`.current`, `.upcoming`, `.expired`, `.active_at(time)`), predicates (`current?`, `upcoming?`, `expired?`, `active_at?`), and mutators (`start!`, `finish!`, `reschedule!`). Supports custom column names and open-ended schedules (`starts_at: nil`).
|
|
7
|
+
|
|
8
|
+
### Internal
|
|
9
|
+
- Refactored `active_at?` into two private predicate helpers (`schedulable_started_by?` / `schedulable_not_ended_at?`) to satisfy `Metrics/CyclomaticComplexity`.
|
|
10
|
+
|
|
11
|
+
### Notes
|
|
12
|
+
- The `v1.4.0` and `v1.4.1` tags were created but never released to RubyGems (CI failed on `Gemfile.lock` regeneration and a RuboCop complexity check respectively). `1.4.2` is the first usable release of the Schedulable concern.
|
|
13
|
+
|
|
3
14
|
## 1.3.0 (2026-05-16)
|
|
4
15
|
|
|
5
16
|
### Added
|
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A simple collection of reusable Rails concerns to keep your models clean and DRY
|
|
|
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
13
|
- 🔐 `Hashable`: Auto-generate a random hex/UUID/integer/custom-alphabet value on create, with a `regenerate_<field>!` helper
|
|
14
|
+
- 🗓️ `Schedulable`: Manage time-windowed records via `starts_at` / `ends_at` with `.current`, `.upcoming`, `.expired`, and `.active_at(time)` scopes
|
|
14
15
|
|
|
15
16
|
---
|
|
16
17
|
|
|
@@ -238,6 +239,67 @@ hashable_by :code, type: :custom, length: 8,
|
|
|
238
239
|
|
|
239
240
|
---
|
|
240
241
|
|
|
242
|
+
### 6. 🗓️ Schedulable
|
|
243
|
+
|
|
244
|
+
Manage records with a time window using `starts_at` / `ends_at` columns.
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
class Promotion < ApplicationRecord
|
|
248
|
+
include ConcernsOnRails::Schedulable
|
|
249
|
+
|
|
250
|
+
# Defaults: starts_at: :starts_at, ends_at: :ends_at
|
|
251
|
+
schedulable_by
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
promo = Promotion.create!(starts_at: 1.hour.ago, ends_at: 1.day.from_now)
|
|
255
|
+
promo.current? # => true
|
|
256
|
+
promo.upcoming? # => false
|
|
257
|
+
promo.expired? # => false
|
|
258
|
+
|
|
259
|
+
Promotion.current # currently active
|
|
260
|
+
Promotion.upcoming # starts_at in the future
|
|
261
|
+
Promotion.expired # ends_at in the past
|
|
262
|
+
Promotion.active_at(Time.zone.now) # active at any specific time
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### Custom column names
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
class Event < ApplicationRecord
|
|
269
|
+
include ConcernsOnRails::Schedulable
|
|
270
|
+
|
|
271
|
+
schedulable_by starts_at: :starts_on, ends_at: :ends_on
|
|
272
|
+
end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### Open-ended start (only an expiry)
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
class Coupon < ApplicationRecord
|
|
279
|
+
include ConcernsOnRails::Schedulable
|
|
280
|
+
|
|
281
|
+
schedulable_by starts_at: nil, ends_at: :expires_at
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### Mutators
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
promo.start! # sets starts_at to now
|
|
289
|
+
promo.finish! # sets ends_at to now
|
|
290
|
+
promo.reschedule!(starts_at: 1.day.from_now,
|
|
291
|
+
ends_at: 2.days.from_now) # sets both
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### Notes
|
|
295
|
+
- Boundary semantics are **inclusive start, exclusive end**: a record is active at exactly `starts_at`, but not at exactly `ends_at`.
|
|
296
|
+
- A `nil` `ends_at` means "no end" — the record stays active forever once started.
|
|
297
|
+
- A `nil` `starts_at` means "not yet started" — the record is not active (unless `starts_at` is unconfigured).
|
|
298
|
+
- No `default_scope` is added; chain `.current` (or any other scope) explicitly to filter.
|
|
299
|
+
- `schedulable_by` validates that the configured columns exist and raises `ArgumentError` otherwise.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
241
303
|
## 🛠️ Development
|
|
242
304
|
|
|
243
305
|
To build the gem:
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
require "active_support/concern"
|
|
2
|
+
|
|
3
|
+
module ConcernsOnRails
|
|
4
|
+
module Schedulable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
DEFAULT_STARTS_AT_FIELD = :starts_at
|
|
8
|
+
DEFAULT_ENDS_AT_FIELD = :ends_at
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
class_attribute :schedulable_starts_at_field, instance_accessor: false, default: DEFAULT_STARTS_AT_FIELD
|
|
12
|
+
class_attribute :schedulable_ends_at_field, instance_accessor: false, default: DEFAULT_ENDS_AT_FIELD
|
|
13
|
+
|
|
14
|
+
scope :active_at, lambda { |time|
|
|
15
|
+
starts_field = schedulable_starts_at_field
|
|
16
|
+
ends_field = schedulable_ends_at_field
|
|
17
|
+
relation = all
|
|
18
|
+
relation = relation.where(arel_table[starts_field].lteq(time)) if starts_field
|
|
19
|
+
relation = relation.where(arel_table[ends_field].eq(nil).or(arel_table[ends_field].gt(time))) if ends_field
|
|
20
|
+
relation
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
scope :current, -> { active_at(Time.zone.now) }
|
|
24
|
+
|
|
25
|
+
scope :upcoming, lambda {
|
|
26
|
+
field = schedulable_starts_at_field
|
|
27
|
+
next none unless field
|
|
28
|
+
|
|
29
|
+
where(arel_table[field].gt(Time.zone.now))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
scope :expired, lambda {
|
|
33
|
+
field = schedulable_ends_at_field
|
|
34
|
+
next none unless field
|
|
35
|
+
|
|
36
|
+
where(arel_table[field].lteq(Time.zone.now))
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class_methods do
|
|
41
|
+
# Configure the start/end timestamp columns.
|
|
42
|
+
# Example:
|
|
43
|
+
# schedulable_by # uses :starts_at and :ends_at
|
|
44
|
+
# schedulable_by starts_at: :starts_on, ends_at: :ends_on
|
|
45
|
+
# schedulable_by starts_at: nil, ends_at: :expires_at # open-ended start
|
|
46
|
+
def schedulable_by(starts_at: DEFAULT_STARTS_AT_FIELD, ends_at: DEFAULT_ENDS_AT_FIELD)
|
|
47
|
+
self.schedulable_starts_at_field = starts_at&.to_sym
|
|
48
|
+
self.schedulable_ends_at_field = ends_at&.to_sym
|
|
49
|
+
|
|
50
|
+
if schedulable_starts_at_field.nil? && schedulable_ends_at_field.nil?
|
|
51
|
+
raise ArgumentError, "ConcernsOnRails::Schedulable: at least one of starts_at: or ends_at: must be configured"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
[schedulable_starts_at_field, schedulable_ends_at_field].compact.each do |field|
|
|
55
|
+
next if column_names.include?(field.to_s)
|
|
56
|
+
|
|
57
|
+
raise ArgumentError, "ConcernsOnRails::Schedulable: field '#{field}' does not exist in the database"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Is the record active at the given time? Inclusive start, exclusive end.
|
|
63
|
+
def active_at?(time)
|
|
64
|
+
schedulable_started_by?(time) && schedulable_not_ended_at?(time)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def current?
|
|
68
|
+
active_at?(Time.zone.now)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def upcoming?
|
|
72
|
+
field = self.class.schedulable_starts_at_field
|
|
73
|
+
value = field && self[field]
|
|
74
|
+
return false unless value
|
|
75
|
+
|
|
76
|
+
value > Time.zone.now
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def expired?
|
|
80
|
+
field = self.class.schedulable_ends_at_field
|
|
81
|
+
value = field && self[field]
|
|
82
|
+
return false unless value
|
|
83
|
+
|
|
84
|
+
value <= Time.zone.now
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def start!(time = Time.zone.now)
|
|
88
|
+
field = self.class.schedulable_starts_at_field
|
|
89
|
+
raise "ConcernsOnRails::Schedulable: starts_at field not configured" unless field
|
|
90
|
+
|
|
91
|
+
update(field => time)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def finish!(time = Time.zone.now)
|
|
95
|
+
field = self.class.schedulable_ends_at_field
|
|
96
|
+
raise "ConcernsOnRails::Schedulable: ends_at field not configured" unless field
|
|
97
|
+
|
|
98
|
+
update(field => time)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def reschedule!(starts_at:, ends_at:)
|
|
102
|
+
attrs = {}
|
|
103
|
+
starts_field = self.class.schedulable_starts_at_field
|
|
104
|
+
ends_field = self.class.schedulable_ends_at_field
|
|
105
|
+
attrs[starts_field] = starts_at if starts_field
|
|
106
|
+
attrs[ends_field] = ends_at if ends_field
|
|
107
|
+
update(attrs)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def schedulable_started_by?(time)
|
|
113
|
+
field = self.class.schedulable_starts_at_field
|
|
114
|
+
return true unless field
|
|
115
|
+
|
|
116
|
+
value = self[field]
|
|
117
|
+
!value.nil? && value <= time
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def schedulable_not_ended_at?(time)
|
|
121
|
+
field = self.class.schedulable_ends_at_field
|
|
122
|
+
return true unless field
|
|
123
|
+
|
|
124
|
+
value = self[field]
|
|
125
|
+
value.nil? || value > time
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/concerns_on_rails.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ethan Nguyen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -72,6 +72,7 @@ files:
|
|
|
72
72
|
- lib/concerns_on_rails.rb
|
|
73
73
|
- lib/concerns_on_rails/hashable.rb
|
|
74
74
|
- lib/concerns_on_rails/publishable.rb
|
|
75
|
+
- lib/concerns_on_rails/schedulable.rb
|
|
75
76
|
- lib/concerns_on_rails/sluggable.rb
|
|
76
77
|
- lib/concerns_on_rails/soft_deletable.rb
|
|
77
78
|
- lib/concerns_on_rails/sortable.rb
|
|
@@ -91,7 +92,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
91
92
|
requirements:
|
|
92
93
|
- - ">="
|
|
93
94
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: 2.
|
|
95
|
+
version: 3.2.0
|
|
95
96
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
97
|
requirements:
|
|
97
98
|
- - ">="
|