1

For our application I am newly creating records in two ways:

  1. With the default values in the database.
  2. Manually setting attributes.

I need to distinguish between these changes so that I know whether the attribute is manually overridden, or whether it was set because it was the default.

I am using the before_validation callback on: :create with a method that checks changes, but this method can't differentiate between the two scenario's. These two lines are equal when it concerns the changes method (I'm using Minitest and FactoryBot to get my point across):

# Table name: users
# admin       :boolean          default(FALSE), not null

FactoryBot.create(:user, admin: false)
FactoryBot.create(:user)

Is there a way I can make the distinction anyway?

Yan
  • 23
  • 5

3 Answers3

0

One trick uses the fact that that Rails doesn't use the setter method if the value is provided by the default. So you could override the setter and set some other attribute to indicate this was set by the caller:

  def initialize(*)
    @default_admin = true
    super
  end

  def default_admin?; @default_admin; end

  def admin=(*)
    @default_admin = false
    super
  end

Added advantage of also working for:

u = User.new
u.admin = false
smathy
  • 26,283
  • 5
  • 48
  • 68
0

A boolean column has only 3 possible values: true, false and NULL. It's not possible (in any meaningful way, at least) to distinguish false from false, you'll need more data.

The easiest way would be to use NULL as the default value. Rails considers NULL "falsey", so such users won't get admin privileges, but you'll be able to easily tell the difference: NULL - the default one; FALSE - the explicit one.

0
>> User.new(admin: false).admin_changed?
=> false
>> User.new(admin: false).admin_came_from_user?
=> true

They just forgot to tell us about it: https://github.com/rails/rails/blob/v7.0.6/activerecord/lib/active_record/attribute_methods/before_type_cast.rb#L33


You can also force the change whenever you touch an attribute, even if it didn't actually change:

class User < ApplicationRecord
  def admin=(_value)
    admin_will_change!
    super
  end
end

>> User.new(admin: false).admin_changed?
=> true
>> User.new(admin: false).changes
=> {"admin"=>[false, false]}

Call [attr_name]_will_change! before each change to the tracked attribute.

https://api.rubyonrails.org/classes/ActiveModel/Dirty.html

Alex
  • 16,409
  • 6
  • 40
  • 56