ActiveRecord attribute_present? lies. How can I fix it? - ruby-on-rails

The docs for attribute_present? say:
Returns true if the specified attribute has been set by the user or by
a database load ...
BUT... that's not true! We see here that Rails initializes attributes on a new object from database defaults.
So, suppose we have a users table with age not null default 0. Then
User.new.attribute_present?(:age) == true
But it hasn't been set by us OR a database load.
Perhaps I'm arguing semantics, but in any case, I'd like a method that does what it says: tells me if a field has been explicitly set.
e.g.
u = User.new
# u.attribute_set?(:age) == false
u.age = u.age #set explicitly to default, for example
# u.attribute_set?(:age) == true
Does that exist?

As far as I know, there isn't a way (provided you have default arguments in your db) (as is good design)
Edited: edited from original answer based on Z5h's comments below

Related

Is there any way to find which direction Rails toggle method worked?

ActiveRecord has the toggle method which assigns to attribute the boolean opposite of attribute?. I was using this. Now, I want to know which direction that method worked i.e. it set the attribute? to true or false. But since I used that method with symbolized parameter, I cannot call attribute? directly like below.
role = params[:role]
role = role.to_sym
#user.roles.toggle(role)
I know I can use instance.send method, but I consider it as anti-pattern because it depends on the name of the method. Is there any way to know the direction the toggle worked?
Edit: I just realized that you're calling toggle on a relation. The answer below is for toggle on a model. As I can't check it live on the relation ATM, I'll leave this answer until I can update it for relations. Maybe it will point you into the right direction with the linked docs.
There's a whole rails module https://api.rubyonrails.org/classes/ActiveModel/Dirty.html that handles just that. You can use changes method (https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) which will give you an answer of what was before and what is now:
role = params[:role]
role = role.to_sym
#user.toggle(role)
#user.changes[role] # => [false, true]
[false, true] means that it was false and became true
Thanks to #Stefan, I can figure out. The answer was super easy (at least) in my case.
role = params[:role]
role = role.to_sym
#user.toggle(role)
if #user.roles.include?(role)
// toggled to set it
else
// toggled to unset it
end
Really, really, thank you for all answers.

Allowing only certain values though a strong parameter in Rails 4

I have a field otp_set_up, which in the company_user model is allowed to be "true" or "false".
There is a use case where a sys admin user can reset this field to "false".
While the field can be set to "true" through code, NO user can set it to "true" via a form edit etc.
I haven't added to it the validation in the model since it can be "true" or "false".
I have the following code in a params method specific to an update in the controller before the params.require .permit bit:
if curr_company_user.is_sys_admin? && curr_company_user.can_crud_company_users? && params[:id].to_i != curr_company_user.id
params[:company_user] = params[:company_user].except(:otp_set_up) if params[:company_user][:otp_set_up] == true
params.require(:company_user).permit(:otp_setup, etc. etc....
elsif etc. etc...
This works. A Sys admin user can not set otp_set_up to "true".
My question is:
Is this the best and correct way to do this in Rails? It seems a bit hacky to me, going through the params hash and removing a bit.
Is there a better / cleaner way?
delete_if cleans it up. Still a bit hacky, but slightly less so : )
params.require(:company_user).permit(:otp_setup).delete_if do |key, val|
key == 'otp_setup' && val == true
end
This leaves the original params object intact.
There isn't a built in way to do this. It looks like there used to be but no more https://github.com/rails/strong_parameters/issues/167
delete_if is defined on Hash in the core library, so it is probably the best way to do it in Ruby and by extension in Rails in the absence of a built in method.
Update
I thought it was an interesting idea, so I wrote a small gem called allowable for this type of use case. It will add a few methods to Hash and ActionController::Parameters: #allow, #allow!, #forbid and #forbid!
You would use it like this
params.require(:company_user).permit(:otp_setup).forbid(otp_setup: [true])
# or
params.require(:company_user).permit(:otp_setup).allow(otp_setup: [false])
You can specify a single value or an array of values, and it doesn't mutate the original params object
I don't really recommend messing around with the params object in this case. I think it's best to leave that untouched for the most part to preserve what was actually requested. That way you're not left scratching your head if you need that value again somewhere downstream.
Another approach is to build the list of attributes to accept before passing into permit.
# Attributes that everyone can modify.
attrs = [:attrs, :everyone, :can, :modify]
# Then "whitelist" other attributes based on your permission logic.
if curr_company_user.is_sys_admin? && curr_company_user.can_crud_company_users? && params[:id].to_i != curr_company_user.id
attrs << :otp_set_up unless params[:company_user][:otp_set_up] == true
elsif something_else?
# Modify what can be permitted for this case.
# etc...
end
params.require(:company_user).permit(*attrs)
I have a suggestion that you set it in the params only if the user is an admin and not otherwise. I think this is a better way.
In the model, do something like this:
if user.role == 'admin'
attr_accessor #All the params
else
attr_accessor #All the other params except the one you want to
exclude

Boolean logic with Rails ENV variables

Since Rails ENV variables should only have string values it could be the problem to decide how to use ENV variable for use cases which need boolean logic. For example since ENV variable have a string value and it would always be truthy it wouldn't be too nice to do something like that:
if ENV['MY_VARIABLE']
# do something
else
# do something else
end
So there are at least 2 ways to accomplish things like above:
Initialize variable with particular value and check it
if ENV['MY_VARIABLE'] == 'some string'
# do something
elsif ENV['MY_VARIABLE'] == 'some other string'
# do something else
end
Or just initialize variable with any value and check if it was initialized (the code could be exactly as we wanted).
if ENV['MY_VARIABLE']
# do something
else
# do something else
end
The question is what option is more preferred and what pros and cons each of them could have?
If you use Rails 5+, you can do ActiveModel::Type::Boolean.new.cast(ENV['MY_VARIABLE']).
In Rails 4.2, use ActiveRecord::Type::Boolean.new.type_cast_from_user(ENV['MY_VARIABLE']).
Documentation Rails 5+: https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
Environment Variables as the name suggests, are environment dependent variables which store different values for same keys based on the environment(production, staging, development) you working on.
e.g. it holds an Access_Key for some api which has sandbox mode and production mode. Thus, to make your code DRY and effective you set an environment variable to get that access_key of sandbox mode for development/staging and live key for production.
What you are trying to do is use them unlike the reason they are defined for, no doubt they can be used that way. Since they are constants what I recommend is doing the following.
create a constants.rb file in your initializers containing
class Constant
BOOL_CONSTANT = ENV['MY_VARIABLE'].present?
# OR
BOOL_CONSTANT = ENV['MY_VARIABLE'] == 'true'
end
then you can use it anywhere you like. This way you can achieve what you want to but under the hood. ;)
You should probably refactor your code and use a custom class, so it's more maintenable and changes may be done easily:
class MyEnv
TRUTHY_VALUES = %w(t true yes y 1).freeze
FALSEY_VALUES = %w(f false n no 0).freeze
attr_reader :value
def initialize(name)
#value = ENV[name].to_s.downcase
end
def to_boolean
return true if TRUTHY_VALUES.include?(value.to_s)
return false if FALSEY_VALUES.include?(value.to_s)
# You can even raise an exception if there's an invalid value
raise "Invalid value '#{value}' for boolean casting"
end
end
# Usage example:
MyEnv.new("MY_VARIABLE").to_boolean
I think that, for boolean environment variables, it's more human-friendly to have values like yes, true, no... instead of present or not present variables.
The only drawback that I can see here is performance, where you jump from nil checking (easy) to a comparisson of strings (a little bit more complex). Give the computer power these days, and if performance is not an issue for you, it would be no problem for you.
So, in conclussion: strings-checks are more human friendly and slower, presence-checks are faster but more obscure.
The answers here are fine, but I wanted the most conciseness without sacrificing performance or complexity. Here is a modified version of #Wikiti's answer. I don't think it's necessary to define falsey values. Anything contained in the set of true values can be effectively considered as 'true', otherwise treat it as false.
Note that this is intentionally different than the ActiveModel Boolean class, which will treat everything as true if it is NOT contained in a falsey value. If you're looking for strict boolean flags, IMO it's a better practice to have false as the default value, and true to only be returned if the value is explicitly defined as such. Programming languages tend to use false as the default in a boolean variable, so this better practice is grounded in intuitiveness.
class EnvTrue
TRUE_VALUES = %w[1 y Y true TRUE True].freeze
def self.[](key)
TRUE_VALUES.include?(v)
end
end
# Usage example:
EnvTrue['MY_VARIABLE'] # returns true or false
ENV['MY_VARIABLE'] # When you want the actual value

Single Ruby/Rails method to check an object both exists and has an ID

Before I start littering my code with an unwieldy method of my own creation, I'm wondering if there's an existing Ruby/Rails method that will:
Returns true if #foo exists and #foo has an ID (e.g. you're in the foo#show)
Returns false if #foo exists but does not yet have an ID (e.g. you're in foo#new)
Returns false if #foo does not exist (e.g. you're in bar#index)
I've tried to search the API docs but the terms I'm using are by necessity so general (ID, save, exists, etc) that I've not found anything so far.
You can use .try
#foo.try(:id)
will return id or nil.
If you strictly want true or false,
!!#foo.try(:id)
For first 2, use !#foo.new_record? new_record? method returns true/false if model was saved to database and have id or not.
For third, read this: defined?

Why is object_a != object_b, if they have the exact same id, type, and attributes?

I have two User objects that should be the same, but ruby is saying they are not. Can anyone please explain to me what is going on here?
a = current_user
b = votable.user
a == b
false
a.id == b.id
true
a.name == b.name
true
a.attributes == b.attributes
true
Updates / additional information since the original posting:
Turns out the problem I'm having is intermittent. It occurs about 30% of the time I refresh the page in question. 70% of the time, A == B and everything works as expected. But 30% of the time, A does not equal B.
When the problem occurs, Ruby is actually telling me that the class (type) of my objects is not the same, even though they look identical (User and User). Here's the output for that 30% of the time when the problem is happening:
.
a.class == b.class AKA a.type == b.type
false (note: this is a correction to the original post which said true)
a.class.to_s
"User"
b.class.to_s
"User"
a.class.name == b.class.name
true
a.class.methods == b.class.methods
true
a.class.object_id == b.class.object_id
false
So the class (type) of the objects looks exactly the same. It is said to be User. But when you evaluate a.class == b.class, the answer is false the 30% of the time when I render the page, causing the problem.
Can anyone tell me what's going on? Many thanks.
➜ ~ ruby --version
ruby 1.8.7 (2010-01-10 patchlevel 249) [universal-darwin11.0]
➜ ~ rails --version
Rails 3.0.10
If a == b is false then I'm guessing it's doing something like a.object_id == b.object_id which would only be true if the references are pointing to the same object.
a.id == b.id is probably just checking the User.id and it makes sense that it would be true.
Easiest thing to do is keep checking on User.id. If you really want to use == you'll need to override it in the User class with something like:
class User
def ==(other)
self.id == other.id
end
end
I don't know what kind of objects these are. Do they just decent from Object?
Object#== really just does Object#=== unless you override #== and define a more appropriate behavior. For Object#=== to be true they have to actually be references to the SAME OBJECT, not equivalent objects.
Just define a #== method and use some sane logic to determine if two objects are the same... then you can use a == b and get true.
If these are ActiveRecord::Base descendants then something else is going on, as ActiveRecord::Base objects do have a sane #== method (though you can overwrite that behavior too).
The two objets are not the same object. They are, however, referencing the same user in the database.
I suggest that you compare them on the id as you did.
If you are using Rails 3 and assuming each of your methods above returns an ActiveRelation (or at a minimum, one of them does). Then it would be safe to say that the first comparison is right, as each ActiveRelation probably is a different object.
My answer makes a few assumptions about your code, but might help offer some insight.
According to the Rails documentation at http://api.rubyonrails.org/classes/ActiveRecord/Base.html#method-i-3D-3D , the equality operator should be checking type and id.
# File activerecord/lib/active_record/base.rb, line 1811
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
id.present? &&
comparison_object.id == id
end
What must be happening is that they are not the same class. Could you please check and output the results?
votable.user.class
current_user.class
Logicl explanation:
They have simmilar attributes, but they are different essentially. Note that the Earth and the Marth are different planets, even if we only look at the facts that both of them are in the Milky Way galaxy and both of them are planets.
If an object is like another object it is not necessarily the same.
Technical explanation:
When you create an object a reference to the object will be created. If you create another object another reference will be created.
If you have object A and object.
a = current_user
b = votable.user
a == b
false
a = current_user
b = a
a== b
true
Two objects are equal if their reference is the same (and their attributes are the same too, of course). You can always write methods to define new equivalence relations if the standard relations are not enough ;)

Resources