Rails 4.1 enum raises error instead set object as invalid - ruby-on-rails

I am using the Rails 4.1 enum field
class User
enum category: [ :client, :seller, :provider ]
end
When the user signs up, he chooses from a select box his category. The default is empty, because I want to force the user to choose one option.
If the user does not select any option, I would like to return to the form with a validation message. Here is the select box code in sign up form
<%= f.select :category, [], {}, class: "form-control" do %>
<option value="99">Choose an option</option>
<% User.categories.each do |cat,code| %>
<option value="<%= code %>" <% if params["user"] && code.to_s == params["user"]["category"] %>selected='selected'<%end%> ><%= t(cat) %></option>
<% end %>
<% end %>
When the controller creates the user, instead of adding a validation error to the record, it raises an exception. How to avoid this?
ArgumentError - '99' is not a valid category:
(gem) activerecord-4.1.1/lib/active_record/enum.rb:103:in `block (3 levels) in enum'
(gem) activerecord-4.1.1/lib/active_record/attribute_assignment.rb:45:in `_assign_attribute'
(gem) activerecord-4.1.1/lib/active_record/attribute_assignment.rb:32:in `block in assign_attributes

I really dislike the reasoning as stated in the issue listed above. Since the value is coming over the wire, it should be treated the same as a freetext input where the expectation is to validate in the model and not the controller. This is especially true in APIs where the developers have even less of a say as far as expected input coming from form data (for example).
In case anyone wants a hack, here is what I came up with. Passes basic testing, but would love feedback if anyone finds issues with it:
class User
enum category: [ :client, :seller, :provider ]
def category=(val)
super val
rescue
#__bad_cat_val = val
super nil
end
end
This will reset category to nil and we can then validate the field:
class User
...
validates :category, presence: true
end
The problem is that we really want to be validating inclusion, not just presence. To get around that we have to capture the input (e.g. as above in def category=). Then we can output our message using that value:
class User
...
validates :category, inclusion: {
in: categories.keys,
message: ->(record,error) {
val = record.instance_variable_get(:#__bad_cat_val)
return "Category is required" if val.nil?
"#{val.inspect} is not a valid category"
}
}
end
That will give us messages for presence and inclusion. If you need to fine tune it more you would have to use a custom validator (I believe).

Read: https://github.com/rails/rails/issues/13971
Specifically read Senny's comment:
The current focus of AR enums is to map a set of states (labels) to an integer for performance reasons. Currently assigning a wrong state is considered an application level error and not a user input error. That's why you get an ArgumentError.
That being said you can always set nil or an empty string to the enum attribute without raising an error:
<option value="">Choose an option</option>
and add a simple presence validation like:
validates :category, presence: { message: "is required" }

The enum in Rails always raise a error if you try to set an invalid value. There is no such validation and add an error message to base or adding a validation error to the record. You should create own validation by rescue errors.
=> u = User.last
=> User.genders
=> {"male"=>0, "female"=>1}
=> u.gender = 'boy'
#> ArgumentError: 'boy' is not a valid gender
=> u.gender = 'male'
#> "male"

In case someone still struggles with this. You can add a standard inclusion validation like this (in the User model):
validates :category, inclusion: {
in: categories.keys,
message: "%{value} is not a valid category"
}

Related

Why does Rails pass "0" from a select statement to ActiveRecord?

I have been working on this for 2 hours and still have found no solution or any reasoning to why this is happening.
in my model I have
models/course.rb
class Course < ActiveRecord::Base
validates! :province, inclusion: { :in => ['Alberta','British Columbia','Manitoba','New Brunswick','Newfoundland and Labrador','Nova Scotia','Ontario','Prince Edward Island','Quebec','Saskatchewan', 'Province'], message: "%{value} is not included in the list" }
end
in my view I have this select statement
= form_for #course, url: teach_path do |f|
= f.select :province, options_for_select(['Alberta','British Columbia','Manitoba','New Brunswick','Newfoundland and Labrador','Nova Scotia','Ontario','Prince Edward Island','Quebec','Saskatchewan'], "#{#course.province.to_s}"), {include_blank: "Choose your Province"}, {required: :required}
= f.submit
However when I submit the form I get this error:
ActiveModel::StrictValidationFailed in CoursesController#create
Province 0 is not included in the list
I have tried even defining my own method in the active record model
def includes_province
unless ['Alberta','British Columbia','Manitoba','New Brunswick','Newfoundland and Labrador','Nova Scotia','Ontario','Prince Edward Island','Quebec','Saskatchewan', 'Province'].include?(province.to_s)
errors.add(:base, "There is no #{province} in the list")
end
end
It just returns
There is no 0 in the list
Why is province always 0 no matter what? I have checked the logs and it shows:
Processing by CoursesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"***", "course"=>{"province"=>"Manitoba"}, "commit"=>"submit"}
Indeed as #spickermann says ensure your province field is not Integer in DB. Check migration that create Course table (something like CreateCourses) in rails_app/db/migrate folder, or simply check schema.rb file under rails_app/db/ folder. Check whether it says
t.integer :province
If so, your column is interger and by assigning it a string (any string) it would actually set 0 to integer value.
To replicate same thing, try calling "rails console" (or "rails c") in your OS command line and then typing
c = Course.take # takes random course from db and loads it to c var
c.province = "Manitoba" # assign string value to the field - like it happens in form
c.province # will actually print 0 !
Once you change column type to t.string instead of i.integer it should work.

Rails 4 enum validation

This is the first time I'm using enums with rails 4 and I ran into some issues, have couple of dirty solutions in mind and wanted to check are there any more elegant solutions in place :
This is my table migration relevant part:
create_table :shippings do |t|
t.column :status, :integer, default: 0
end
My model:
class Shipping < ActiveRecord::Base
enum status: { initial_status: 0, frozen: 1, processed: 2 }
end
And I have this bit in my view (using simple form for) :
= f.input :status, :as => :select, :collection => Shipping.statuses, :required => true, :prompt => 'Please select', label: false
So in my controller:
def create
#shipping = Shipping.create!(shipping_params)
if #shipping.new_record?
return render 'new'
end
flash[:success] = 'Shipping saved successfully'
redirect_to home_path
end
private
def shipping_params
params.require(:shipping).permit(... :status)
end
So when I submit create form and the create action fire I get this validation error :
'1' is not a valid status
So I thought I knew that the issue was data type so I added this bit in the model :
before_validation :set_status_type
def set_status_type
self.status = status.to_i
end
But this didn't seem to do anything, how do I resolve this ? Has anyone had the similar experience?
You can find the solution here.
Basically, you need to pass the string ('initial_status', 'frozen' or 'processed'), not the integer. In other words, your form needs to look like this:
<select ...><option value="frozen">frozen</option>...</select>
You can achieve this by doing statuses.keys in your form. Also (I believe) you don't need the before_validation.
Optionally, you could add a validation like this:
validates_inclusion_of :status, in: Shipping.statuses.keys
However, I'm not sure that this validation makes sense, since trying to assign an invalid value to status raises an ArgumentError (see this).

Rails: Virtual Attribute Reader Causing Error During Validations

Rails 3.0.3 application. . .
I'm using a virtual attribute in a model to convert a value stored in the database for display based on a user's preference (U.S. or metric units). I'm doing the conversion in the reader method, but when I test my presence validation I get a NoMethodError because the real attribute is nil. Here's the code:
class Weight < ActiveRecord::Base
belongs_to :user
validates :converted_weight, :numericality => {:greater_than_or_equal_to => 0.1}
before_save :convert_weight
attr_accessor :converted_weight
def converted_weight(attr)
self.weight_entry = attr
end
def converted_weight
unless self.user.nil?
if self.user.miles?
return (self.weight_entry * 2.2).round(1)
else
return self.weight_entry
end
else
return nil
end
end
...
This is the line that's causing the problem:
return (self.weight_entry * 2.2).round(1)
I understand why self.weight_entry is nil, but what's the best way to handle this? Should I just throw in an unless self.weight_entry.nil? check in the reader? Or should I perform this conversion somewhere else? (if yes, where?)
Thanks!
Here's what I've done:
Model
validates :weight_entry, :numericality => {:greater_than_or_equal_to => 0.1}
before_save :convert_weight
attr_reader :converted_weight
def converted_weight
unless self.user.nil?
unless self.weight_entry.nil?
if self.user.miles?
return (self.weight_entry * 2.2).round(1)
else
return self.weight_entry
end
end
else
return nil
end
end
Form
<%= f.label :weight_entry, 'Weight' %><br />
<%= f.text_field :weight_entry, :size => 8, :value => #weight.converted_weight %> <strong><%= weight_units %></strong> (<em>Is this not right? Go to your <%= link_to 'profile', edit_user_registration_path %> to change it</em>)
The unless.self.weight_entry.nil? check allows the validation to do it's job. If anyone knows of a better way to do this I'm open to suggestion.
Thanks!
P.S. The before_save convert_weight method converts U.S. units to metric. I want to store values in the same units consistently so if a user changes her preference later previously stored values don't become invalid.

Rails 3 custom formatted validation errors?

With this model:
validates_presence_of :email, :message => "We need your email address"
as a rather contrived example. The error comes out as:
Email We need your email address
How can I provide the format myself?
I looked at the source code of ActiveModel::Errors#full_messages and it does this:
def full_messages
full_messages = []
each do |attribute, messages|
messages = Array.wrap(messages)
next if messages.empty?
if attribute == :base
messages.each {|m| full_messages << m }
else
attr_name = attribute.to_s.gsub('.', '_').humanize
attr_name = #base.class.human_attribute_name(attribute, :default => attr_name)
options = { :default => "%{attribute} %{message}", :attribute => attr_name }
messages.each do |m|
full_messages << I18n.t(:"errors.format", options.merge(:message => m))
end
end
end
full_messages
end
Notice the :default format string in the options? So I tried:
validates_presence_of :email, :message => "We need your email address", :default => "something"
But then the error message actually appears as:
Email something
So then I tried including the interpolation string %{message}, thus overriding the %{attribute} %{message} version Rails uses by default. This causes an Exception:
I18n::MissingInterpolationArgument in SubscriptionsController#create
missing interpolation argument in "%{message}" ({:model=>"Subscription", :attribute=>"Email", :value=>""} given
Yet if I use the interpolation string %{attribute}, it doesn't error, it just spits out the humanized attribute name twice.
Anyone got any experience with this? I could always have the attribute name first, but quite often we need some other string (marketing guys always make things more complicated!).
Errors on :base are not specific to any attribute, so the humanized attribute name is not appended to the message. This allows us to add error messages about email, but not attach them to the email attribute, and get the intended result:
class User < ActiveRecord::Base
validate :email_with_custom_message
...
private
def email_with_custom_message
errors.add(:base, "We need your email address") if
email.blank?
end
end
Using internationalization for this is probably your best bet. Take a look at
http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models
Particularly this section:
5.1.2 Error Message Interpolation
The translated model name, translated
attribute name, and value are always
available for interpolation.
So, for example, instead of the
default error message "can not be
blank" you could use the attribute
name like this : "Please fill in your
%{attribute}"

How do you validate the presence of one field from many

I'm answering my own questions - just putting this up here for google-fu in case it helps someone else. This code allows you to validate the presence of one field in a list. See comments in code for usage. Just paste this into lib/custom_validations.rb and add require 'custom_validations' to your environment.rb
#good post on how to do stuff like this http://www.marklunds.com/articles/one/312
module ActiveRecord
module Validations
module ClassMethods
# Use to check for this, that or those was entered... example:
# :validates_presence_of_at_least_one_field :last_name, :company_name - would require either last_name or company_name to be filled in
# also works with arrays
# :validates_presence_of_at_least_one_field :email, [:name, :address, :city, :state] - would require email or a mailing type address
def validates_presence_of_at_least_one_field(*attr_names)
msg = attr_names.collect {|a| a.is_a?(Array) ? " ( #{a.join(", ")} ) " : a.to_s}.join(", ") +
"can't all be blank. At least one field (set) must be filled in."
configuration = {
:on => :save,
:message => msg }
configuration.update(attr_names.extract_options!)
send(validation_method(configuration[:on]), configuration) do |record|
found = false
attr_names.each do |a|
a = [a] unless a.is_a?(Array)
found = true
a.each do |attr|
value = record.respond_to?(attr.to_s) ? record.send(attr.to_s) : record[attr.to_s]
found = !value.blank?
end
break if found
end
record.errors.add_to_base(configuration[:message]) unless found
end
end
end
end
end
This works for me in Rails 3, although I'm only validating whether one or the other field is present:
validates :last_name, :presence => {unless => Proc.new { |a| a.company_name.present? }, :message => "You must enter a last name, company name, or both"}
That will only validate presence of last_name if company name is blank. You only need the one because both will be blank in the error condition, so to have a validator on company_name as well is redundant. The only annoying thing is that it spits out the column name before the message, and I used the answer from this question regarding Humanized Attributes to get around it (just setting the last_name humanized attribute to ""

Resources