Time Boolean
If you like storing timestamps for your booleans, throw this snippet into your ApplicationRecord
for a quick win.
class ApplicationRecord
class << self
def time_boolean name,
column: "#{name}_at",
negation: "not_#{name}",
predicate: "#{name}?"
scope name, -> { where(column => ..Time.current) }
scope negation,
-> { where(column => nil).or(where.not(column => ..Time.current)) }
define_method(name) { !!self[column] && !self[column].future? }
define_method(:"#{name}=") { |v|
bool = ActiveRecord::Type::Boolean.new.cast(v)
self[column] = Time.current if bool && !self[column]
self[column] = nil unless bool
v
}
alias_method predicate, name
end
end
end
Let’s say your user can be admin?
.
- Make a migration
add_column :users, :admin_at, :datetime
- Declare
time_boolean
in your model
class User < ApplicationRecord
time_boolean :admin
end
Now you can do the following without having to mess with the timestamp:
user.update! admin: true
user.admin? # => true
user.admin_at = Time.current; user.admin # => true
user.admin_at = 1.month.from_now; user.admin # => false
User.admin # => [scope of all admins]
User.not_admin # => [scope of non-admins]
Here’s a quick test you can throw into your Rails Minitest suite to check most things about this feature based on a real attribute on your real model.
test '#admin acts as a time_boolean' do
freeze_time
user = users(:registered)
assert_nil user.admin_at
assert_equal false, user.admin?
# Assignments are synchronized
user.admin_at = Time.current
assert_equal true, user.admin?
user.admin_at = nil
assert_equal false, user.admin?
user.admin = true
assert_equal Time.current, user.admin_at
assert_equal true, user.admin?
user.admin = false
assert_nil user.admin_at
assert_equal false, user.admin?
# Non-boolean input is casted to boolean
user.admin = '0'
assert_equal false, user.admin?
user.admin = 'f'
assert_equal false, user.admin?
user.admin = ''
assert_equal false, user.admin?
user.admin = '1'
assert_equal true, user.admin?
user.admin = 't'
assert_equal true, user.admin?
# Reassigning the boolean doesn't bump the timestamp
user.admin_at = Time.current
travel_to 1.minute.from_now
assert_no_changes('user.admin_at') { user.admin = true }
assert_no_changes('user.admin_at') { user.save! }
user.update! admin: false
user.update! admin: true
assert_no_changes('user.admin_at') {
travel_to 1.minute.from_now
user.update! admin: true
}
# Future time is not true
user.admin_at = 1.minute.from_now
assert_equal false, user.admin?
# It's possible to update the value via either timestamp or boolean
user.update! admin_at: Time.current
assert_equal true, user.admin?
user.admin = false
user.save!
user.reload
assert_equal false, user.admin
assert_nil user.admin_at
# Scopes work as expected
user.update! admin: true
assert_includes User.admin, user
refute_includes User.not_admin, user
user.update! admin: false
refute_includes User.admin, user
assert_includes User.not_admin, user
user.update! admin_at: 1.minute.from_now
refute_includes User.admin, user
assert_includes User.not_admin, user
user.update! admin_at: 1.minute.ago
assert_includes User.admin, user
refute_includes User.not_admin, user
end
Code snippets in this post are covered by 0BSD License.