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" }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With