I would like to know how I can set a models attribute and possible associations to its default value.
user = User.find_by(name: "Martin")
user.phone = 012345
user.save!
# some time later
user.phone = # set to default
user.save!
Few options to set a default value of a column:
Set the default value in migration (preferable)
Set the default value in before_* callback
To revert to default column's value you can use ActiveRecord::ConnectionAdapters::SchemaCache#columns_hash:
user.phone = user.class.columns_hash['phone'].default
You already set default in the migration.
:default => 'your_default'
It's better to use:
User.column_defaults["phone"]
instead of:
User.columns_hash['phone'].default
since columns_hash gets the raw default value defined at database level and skips defaults set in ActiveModel. See the following example:
class Order < ApplicationRecord
enum status: %i[open closed]
attribute :deliver_at, default: -> { Date.tomorrow }
end
Order.columns_hash['status'].default # => "0" ('0' if default value was defined in the database or 'nil' otherwise)
Order.columns_hash['deliver_at'].default # => NoMethodError (undefined method `default' for nil:NilClass) if it's a virtual attribute or 'nil' if the column exists in the database
Order.column_defaults['status'] # => "open"
Order.column_defaults['deliver_at'] # => Wed, 06 May 2020
Related
I'm trying to setup an attribute that isn't saved to the database but I can't work out how to change it and read the new value.
class User < ApplicationRecord
attribute :online, :boolean, default: false
end
in Rails Console:
User.first.online = true
=> true
User.first.online
=> false
I'm running Ruby-on-rails 5.2.4.1 and ruby 2.4.1
https://api.rubyonrails.org/v5.2.4.1/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute
The line:
User.first
Creates an instance for the first user each time you call it.
User.first.equal?(User.first) #=> false
# ^^^^^^
# Equality — At the Object level, returns true only if obj
# and other are the same object.
You're setting the online attribute of a different instance than the one you're reading from (although they represent the same record). Store the user in a variable. That way you're working with the same instance for both the set and get call.
user = User.first
user.online = true
user.online #=> true
I have an encrypted type in my model
attribute :name, :encrypted
Which is
class EncryptedType < ActiveRecord::Type::Text
And implements #serialize, #deserialize, and #changed_in_place?.
How can I get the raw value from the database before deserialization?
I want to create a rake task to encrypt values which are in the DB that existed before the fields were encrypted. So before the encryption, the name field contained Bob. After the code change with encryption, reading that value will produce an error (caught), returning an empty string. I want to read the raw value and set it like a normal attribute so it will encrypt it. After the encryption, the field will look like UD8yDrrXYEJXWrZGUGCCQpIAUCjoXCyKOsplsccnkNc=.
I want something like user.name_raw or user.raw_attributes[:name].
There's ActiveRecord::AttributeMethods::BeforeTypeCast which
provides a way to read the value of the attributes before typecasting and deserialization
and has read_attribute_before_type_cast and attributes_before_type_cast. Additionally,
it declares a method for all attributes with the *_before_type_cast suffix
So for instance:
User.last.created_at_before_type_cast # => "2017-07-29 23:31:10.862924"
User.last.created_at_before_type_cast.class # => String
User.last.created_at # => Sat, 29 Jul 2017 23:31:10 UTC +00:00
User.last.created_at.class # => ActiveSupport::TimeWithZone
User.last.attributes_before_type_cast # => all attributes before type casting and such
I imagine this would work with your custom encrypted type
As SimpleLime suggested...
namespace :encrypt do
desc "Encrypt the unencrypted values in database"
task encrypt_old_values: :environment do
User.all.each do |user|
if user.name.blank? && ! user.name_before_type_cast.blank?
User.class_variable_get(:##encrypted_fields).each do |att|
user.assign_attributes att => user.attributes_before_type_cast[att.to_s]
end
user.save validate: false
end
end
I've a model Cart having has_many relationship with cart_items.
# cart.rb:
accepts_nested_attributes_for :cart_items, allow_destroy: true
has_many :cart_items, dependent: :destroy, inverse_of: :cart
# cart_item.rb:
validates :quantity, presence: true, numericality: { greater_than: 0 }
# Controller code:
def update
current_cart.assign_attributes(params[:cart])
.....
current_cart.valid?
end
While updating cart_items, if integer (to_i) value of quantity is same as old value then validation is not working.
For example,
If old value of quantity is 4, now new value is updated to 4abc then quantity validation is not working and record is considered as valid.
Although, if new value is updated from 4 to 5abc then it shows a validation error, as expected.
Any suggestions why this all is happening?
EDIT 1:
Here's the output of rails console:
[3] pry(#<Shopping::CartsController>)> cart
=> #<Cart id: 12, created_at: "2017-06-22 13:52:59", updated_at: "2017-06-23 08:54:27">
[4] pry(#<Shopping::CartsController>)> cart.cart_items
[#<CartItem id: 34201, cart_id: 12, quantity: 4, created_at: "2017-06-23 05:25:39", updated_at: "2017-06-23 08:54:27">]
[5] pry(#<Shopping::CartsController>)> param_hash
=> {"cart_items_attributes"=>{"0"=>{"id"=>"34201", "quantity"=>"4abc"}}}
[6] pry(#<Shopping::CartsController>)> cart.assign_attributes param_hash
=> nil
[7] pry(#<Shopping::CartsController>)> cart.valid?
=> true
Here, previous quantity of cart_item is 4 and new value is 4abc, but still the cart is validated.
EDIT 2:
I've checked answers of How to validate numericality and inclusion while still allowing attribute to be nil in some cases?, as it is masked as duplicate in comments, but it does not seems to work.
As I mentioned above, validation is working fine if to_i of new quantity is different than previous quantity, but if it is same then validation is not working.
Also, I tried to apply a method for custom validation using validate, but I'm getting to_i value in that method. Something like:
Model code:
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => 4abc
def validate_quantity
puts quantity_changed? # => false
puts quantity # => 4
end
EDIT 3:
It seems like if to_i of new value is same as previous value then model is casting the value to integer and considering the field not even updated.
EDIT 4:
I'm getting answers & comments about the pupose of to_i. I know what to_i does.
I just want to know why I'm not getting the validation error if to_i of new quantity is similar to the quantity stored in the database.
I know quantity column is integer and ActiveRecord will cast it to integer BUT there must be a validation error as I've added that in model.
I'm getting the validation error if to_i of new value is different than quantity stored in db.
BUT
I'm not getting the validation error if to_i of new value is same than quantity stored in db.
First to answer your question why:
Problably in history it worked differently than today. Personally I suspect this commit (after a very brief search): https://github.com/rails/rails/commit/7500daec69499e4f2da2fc06cd816c754cf59504
And how to fix that?
Upgrade your rails gem... I can recommend Rails 5.0.3, which I tested and it works as expected.
Following the source code for numeric validation, it casts Integer type on the string
"4abc".to_i
=> 4
Hence the input is greater then 0
To resolve it, try to use <input type="numeric" /> in your view to force a user to type only integer values
You should use quantity_before_type_cast, as explained here, in section "Accessing attributes before they have been typecasted". For example:
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => 4abc
def validate_quantity
q = quantity_before_type_cast
return if q == q.to_i
errors.add(:quantity, 'invalid quantity') unless (q.to_i.to_s == q)
end
Since the quantity column is defined as integer, activerecord will typecast it with .to_i before assigning it to the attribute, however, quantity_changed? will be true in that case unlike what you have shown above, so that gives you a hint for solution 1, else you can check if param contains only integers or not in your controller as in solution 2.
Solution 1
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => '4abc'
def validate_quantity
if quantity_changed?
if quantity_was == quantity || quantity <= 0
errors.add(:base, 'invalid quantity')
return false
else
return true
end
else
true
end
end
Solution 2
In your controller,
before_action :validate_cart_params, only: [:create, :update]
private
def validate_cart_params
unless cart_params[:quantity].scan(/\D/).empty?
render json: { errors: "Oops! Quantity should be numeric and greater than 0." }, status: 422 and return
end
end
UPDATE
I googled a bit, but late and found a helper _before_type_cast, it is also a good solution and #Federico has put it as a solution. I am also adding it to my list.
Solution 3
Using quantity_before_type_cast
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => '4abc'
def validate_quantity
if quantity_changed? || ((actual_quantity = quantity_before_type_cast) != quantity_was)
if actual_quantity.scan(/\D/).present? || quantity <= 0
errors.add(:base, 'invalid quantity')
return false
else
return true
end
else
true
end
end
This type of issue is already reported in Github.
Reported issues:
Parent model not marked as invalid when associated record is invalid, but not dirty
Validations not triggered when attribute is changed from 0 to a string if it's a nested attribute
Validates_numericality_of skipped when changing 0 to "foo" through accepts_nested_attributes_for
Fixed in:
[v4.0]
https://github.com/rails/rails/commit/b9ec47da00e7f5e159ff130441585c3b42e41849
[v3.2]
https://github.com/rails/rails/commit/40617c7e539b8b70469671619e3c1716edcfbf59
All the issues are reported where previous value of an integer attribute is 0 and new value is some string (whose to_i will be 0). Thus the fix is also only for 0.
You can check the same in #changes_from_zero_to_string? in active_record/attribute_methods/dirty.rb, which is initially called from #_field_changed?
Solution:
Override #_field_changed? specifically for quantity field only (due to lack of time. In future, I'll override method for all integer fields).
Now if to_i value of some alphanumeric quantity is equal to current quantity value in database then below method will return true and will not type cast the value.
def _field_changed?(attr, old, value)
if attr == 'quantity' && old == value.to_i && value != value.to_i.to_s
return true
end
super
end
to_i(p1 = v1) public
Returns the result of interpreting leading characters in str as an integer base (between 2 and 36). Extraneous characters past the end of a valid number are ignored. If there is not a valid number at the start of str, 0 is returned. This method never raises an exception when the base is valid.
"12345".to_i #=> 12345
"99 red balloons".to_i #=> 99
"0a".to_i #=> 0
"0a".to_i(16) #=> 10
"hello".to_i #=> 0
"1100101".to_i(2) #=> 101
"1100101".to_i(8) #=> 294977
"1100101".to_i(10) #=> 1100101
"1100101".to_i(16) #=> 17826049
The problem happens because
"4".to_i => 4
"4abc".to_i => 4
That means your custom validation will pass and will not cause any error on the page.
Hope that helps you...
I have a enum in a model. I want to get its integer value, or it's not set I want to get an integer value of a value which I choose to be a default one:
enum my_enum: [:val1, :val2, :val3]
def method1
int_val = self.read_attribute(:my_enum)
# what if my_enum hasn't been set?
unless int_val
int_val = ??? # how to get integer of :val2 ???
end
end
Default value for an enum is the value you set at the 0th index.
For ex. enum Animals{ Cat, Dog, Lion, Tiger} will have its default as 'Cat' which is Animals(0).
However if you define the enum to be enum Animals{ Cat=1, Dog=2, Lion=3, Tiger=0} then the default will be 'Tiger'
Considering that you can define your enum to fulfil your requirement
It's good practive to set the default value of a field in the database. Try using a database migration to change/add the default value to the required field. Example code:
class ChangeDefaultValueToModel < ActiveRecord::Migration
def change
change_column_default :model_name, :field_name, default_value
end
end
For more detail check the Ruby on Rails API for change_column_default
Currently, my model and validation is this:
class Suya < ActiveRecord::Base
belongs_to :vendor
validates :meat, presence: true
validates_inclusion_of :spicy, :in => [true, false]
end
The problem is that when I run this test:
test "suya is invalid if spiciness is not a boolean" do
suya = Suya.new(meat: "beef", spicy: 1)
suya1 = Suya.new(meat: "beef", spicy: "some string")
assert suya.invalid?
refute suya1.valid?
end
I get a deprecation warning that says:
DEPRECATION WARNING: You attempted to assign a value which is not
explicitly true or false to a boolean column. Currently this value
casts to false. This will change to match Ruby's semantics, and will
cast to true in Rails 5.
So I think my validation is not doing what I think it should be doing. I think my validation checks the presence of the column value and if it IS or is converted to true or false. So I think my test fixtures both convert to false and therefore pass the test which I don't want. What can I do?
You can use custom validation like:
validate :check_boolean_field
def check_boolean_field
false unless self.spicy.is_a?(Boolean)
end
Rails performs type casting any time you assign a value to an attribute. This is a convenience thing. It's not really your text case's fault, it's just how Rails works. If the attribute is a Boolean it'll convert truthy-looking values (true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON') to true and anything else to false. For example:
suya.spicy = "asdf"
suya.spicy # => false
# Likewise for other attribute types:
# Assuming Suya has an `id` attribute that is an Integer
suya.id = "asdf"
suya.id # => 0 # Because "asdf".to_i # => 0
# Assuming Suya has a `name` attribute that is a String
suya.name = 1
suya.name # => "1" # Because 1.to_s # => "1"
So this is just how rails works. In your test case your values are being typecast into their respective attributes' types via mass-assignment.
You can either test out Rails's typecasting by assigning "some value" to your booleans or you can just use more obvious boolean values like true and false in your test cases.