Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 4.1 enum raises error instead set object as invalid

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
like image 426
Daniel Cukier Avatar asked Jul 28 '14 15:07

Daniel Cukier


2 Answers

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).

like image 148
jonuts Avatar answered Sep 18 '22 15:09

jonuts


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" }
like image 20
DanielBlanco Avatar answered Sep 19 '22 15:09

DanielBlanco