Rails 4 - how to use enum? - ruby-on-rails

I'm trying to make an app on Rails 4.
I posted this question and got some advice: Rails 4 -Simple Form how to save key and display value
I am trying to figure out how to implement this advice.
At the moment, I have a preference model with:
enum self_governance: {
tier_1: 1,
tier_2: 2,
tier_3: 3,
tier_4: 4,
tier_5: 5
}
enum autonomy: {
tier_11: 1,
tier_21: 2,
tier_31: 3,
tier_41: 4,
tier_51: 5
}
In my preference form, I have:
<%= f.input :self_governance, as: :select, label: "Select your governance approach", collection: Preference.self_governance.to_a.map { |p| [p.humanize, p] } %>
I have a preferences show view:
<%= #organisation.preference.self_governance.try(:humanize) %>
When I save all this and try it, I get this error:
NoMethodError at /preferences/1/edit
undefined method `self_governance' for #<Class:0x007fde5b9fb500>
Did you mean? self_governances
Can anyone see how to make this work?
Do I maybe need to add def/end tags to the enum in the preference model? I don't have any experience with using the code 'enum'

You're so close :) The fix is right in the error.
Your select is calling
Preference.self_governance.to_a.map { |p| [p.humanize, p] }
And your error tells you the pluralization is wrong. Remember that if you call enum on a single object, it will be
#preference.self_governance
But if you call on the model itself, Preference, and request a collection it's plural.
Preference.self_governances
Because enum is special, uour enum's could just be arrays, instead of hashes:
enum self_governance: [ tier_1, tier_2, tier_3, tier_4, tier_55 ]
enum autonomy: [ tier_11, tier_21, tier_31, tier_41, tier_51 ]
Your view would look like:
<%= f.input :self_governance, as: :select, label: "Select your governance approach", collection: Preference.self_governances.map { |key, value| [key.humanize, key] } %>
It will store the index number of the array, like magic :)

Rails pluralizes the enum collection for you, so yours could be self_governances, for instance.
What that means is that Preference.self_governances would return the hash with the definitions and the attribute that actually holds the value is the one in singular like #preference.self_governance
An example:
#preference = Preference.new
#preference.self_governance = Preference.self_governances[:tier_1]
When you use enum what rails will do internally is add a pluralized class method definition with the name that you defined that will return a hash with the values and, will use the name that you defined, as it was written by you, for an attribute accessor that will either get/set the actual value of the enumeration on instances of your object.
Another, common use is for status, given a Test class:
enum status: {
active: 1,
inactive: 2
}
So for the above sample rails would add a Test.statuses methods that simply returns the values of your enum. Then, for an instance of a Test object you would have an accessor #instance.status with the name of your name which you can use to get or set a status from the hash returned by Test.statuses
Hopefully it makes sense.

Related

Rails 4 -Simple Form how to save key and display value

I'm trying to make an app in Rails 4. I use simple form for forms.
I have an attribute called 'self_governance' in my model.
I've written a helper method so that I can define 5 levels which can be stored for this attribute, but which are displayed to the user as options (rather than numbers 1 .. 5).
In my helper, I have:
module PreferencesHelper
def self_gov_selector
[
[ 'tier 1','1'],
[ 'tier 2','2'],
[ 'tier 3','3'],
[ 'tier 4','4'],
[ 'tier 5','5'],
]
end
In my form, I then have:
<%= f.input :self_governance, :label => "Select your tier", collection: self_gov_selector %>
Then in my show, I'm trying to figure out how to display 'tier 1' instead of '1'.
I have tried:
<%= #preference.self_governance %>
<%= #preference.self_gov_selector %>
I can't find anything that works. They all display 1 instead of Tier 1 in the views.
How can I make this work?
The params posted by the form will only include the second value in the array, so you're likely storing your value as an integer in your database table.
A simple solution is to use an enum to map the integers you're storing to the values they represent:
In your Preference model:
enum self_governance: {
tier_1: 1,
tier_2: 2,
tier_3: 3,
tier_4: 4,
tier_5: 5
}
Then update your view accordingly:
<%= #preference.self_governance.try(:humanize) %>
EDIT:
An additional bonus of this approach is that you can replace your helper method with calling the enum directly:
f.input :self_governance, as: :select, label: "your label", collection: Preference.self_governances.map { |key, val| [key.humanize, val] }

Rails 4 - key value pair

I'm trying to make an app in Rails 4.
I've recently asked these 2 questions, and taken the advice in the responses. Rails 4 - how to use enum?
I'm still struggling.
I have a form with an input selector:
<%= f.input :self_governance, as: :select, label: "Select your governance approach", collection: Preference.self_governances.to_a.map { |p| [p.to_s.humanize, p] } %>
When I save this and try it, the select menu shows:
["tier_1", 1]
What I want is to display: Tier 1
At the moment, I have a preference model with:
enum self_governance: {
tier_1: 1,
tier_2: 2,
tier_3: 3,
tier_4: 4,
tier_5: 5
}
enum autonomy: {
tier_11: 1,
tier_21: 2,
tier_31: 3,
tier_41: 4,
tier_51: 5
}
I have a preferences show view:
<%= #organisation.preference.self_governance.try(:humanize) %>
Also, when I accept the form problem (for now) and try to render the show page, I get this error:
'["tier_1", 1]' is not a valid self_governance
Can anyone see what I've done wrong?
I just want to save the number 1 in the database, but display the words 'Tier 1'.
Update your form to properly return a collection of keys and values from your enum. Preference.self_governances is a type of hash object.r Rather than call to_a, just iterate over the keys and values:
<%= f.input :self_governance, as: :select, label: "Select your governance approach", collection: Preference.self_governances.map { |key, val| [key.humanize, key] } } %>
If we look at just the output of:
Preference.self_governances.map { |key, val| [key.humanize, key] }
We get the following:
[
["Tier 1", "tier_1"],
["Tier 2", "tier_2"]
...
]
Note that the first value is what gets shown as the select label and the second value is what gets sent to your controller within the param.
EDIT:
When using an enum, you can assign either the key or value of the enum to the field.
preference.self_governance = 1 # Works
preference.self_governance = :tier_1 # Works
preference.self_governance = "tier_1" # Works
But you can't assign the value as a string:
preference.self_governance = '1'
=> "ArgumentError: '1' is not a valid self_governance" # Doesn't work, tries to look for key '1' in enum, but doesn't exist.
So make sure you pass the key of the selected enum (i.e. "tier_1") aback to your form or else you ma

Error `undefined method `state' for "AK":String` for `collection_select`

After adding the second method, uniq.pluck(:state) to the code below, I'm getting the following error message:
undefined method `state' for "AK":String.
I looked at all the posts on here and couldn't find anything related to this problem. Any insights or help you could offer would be greatly appreciated.
<%= f.collection_select :state, (Boli.order(:state).uniq.pluck(:state)), :id, :state, include_blank: true %>
Thank you #D-side, now having difficulties using grouped_collection. I'd like the user to be able to select a group of banks in a particular state. Getting the error message undefined method `map' for :id:Symbol, using the following code:
<%= f.grouped_collection_select :bank, :id, Boli.order(:bank), :id, :bank, include_blank: true%>
pluck with a single attribute name returns an array of attribute values. Strings, in your case.
collection_select, however, is built with model instances in mind, in that it accepts... well, the docs say it better anyway:
collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
<...>
The :value_method and :text_method parameters are methods to be called on each member of collection. The return values are used as the value attribute and contents of each <option> tag, respectively. They can also be any object that responds to call, such as a proc, that will be called for each member of the collection to retrieve the value/text.
Obviously, since you've already fetched values of the attribute state, calling state on the resulting values once more is nonsensical.
You can fix this in multiple ways.
PostgreSQL's DISTINCT ON (expr)
By replacing .uniq.pluck(:state) with .select("DISTINCT ON (state) id, state") you'll get ActiveRecord model instances, so every element of the resulting collection will have methods id and state, as collection_select expects.
Or use the query you have, with pluck
...by giving procs instead of :id and :state that take a string as an argument and produce appropriate values.
It all boils down to what you need.

Array as Parameter from Rails Select Helper

I'm working on a legacy project that is using acts_as_taggable_on which expects tags to come in arrays. I have a select box allowing users to select a tag on a Course in a field called categories. The only way mass assignment create will work is if params looks like this params = {:course => {:categories => ['Presentation']}}. I've currently a view with this helper:
<%= f.select 'categories', ['Presentation' , 'Round Table' , 'Demo', 'Hands-on'] %>
Which will give me a parameter like params = {:course => {:categories => 'Presentation'}}. This doesn't work since Acts as tag gable apparently can't handle being passed anything other than a collection.
I've tried changing categories to categories[] but then I get this error:
undefined method `categories[]' for #<Course:0x007f9d95c5b810>
Does anyone know the correct way to format my select tag to return an array to the controller? I'm using Rails 3.2.3
I didn't work with acts_as_taggable_on, but maybe this simple hack will be suitable for you? You should put it before mass-assignment.
category = params[:course][:categories]
params[:course][:categories] = [category]
If you are only going to allow the selection of ONE tag, you could do:
<%= f.select 'categories', [['Presentation'] , ['Round Table'] , ['Demo'], ['Hands-on']] %>
Each one item array will have first for the display value, and last for the return value, which in this case will both return the same thing, as the first element of the array is the same as the last element when the array as one element.
Seems like select doesn't give you that option.
If I understand correctly, one option might be to use a select_tag instead and just be explicit about where you want the selection in the params:
<%= select_tag 'course[categories][]', options_for_select(['Presentation' , 'Round Table' , 'Demo', 'Hands-on']) %>
That ought to get your params the way you need them.
Here's what I'm using for one of my projects:
<% options = { include_blank: true } %>
<% html_options = { required: true, name: "#{f.object_name}[#{resource.id}][days][]" } %>
<%= f.select :days, DAYS, options, html_options %>
Without html_options[:name], Rails handles the name of the select tag and spits out something like
service[service_add_ons_attributes][11][days]
but I need
service[service_add_ons_attributes][11][days][]
So I override it.
Hope that helps.

String concatenation not possible?

So over the last 2 hours, I've been trying to fill a combobox with all my users. I managed to get all firstnames in a combobox, but I want their full name in the combobox. No problem you would think, just concatenate the names and you're done. + and << should be the concatenation operator to do this.So this is my code:
<%= collection_select(:user, :user_id, #users, :user_id, :user_firstname + :user_lastname, {:prompt => false}) %>
But it seems RoR doesn't accept this:
undefined method `+' for :user_firstname:Symbol
What am I doing wrong?
What you need to do is define a method on the User model that does this concatenation for you. Symbols can't be concatenated. So to your user model, add this function:
def name
"#{self.first_name} #{self.last_name}"
end
then change the code in the view to this:
<%= collection_select(:user, :user_id, #users, :user_id, :name, {:prompt => false}) %>
Should do the trick.
This isn't really rails giving you an error, it's ruby. You're trying to combine the symbols :user_firstname and :user_lastname
A symbol is a variable type, just like integer, string, or datetime (Well technically they're classes, but in this context we can think of them as variable types). They look similar to strings, and can function similarly to them, but there is no definition for the behavior of symbol concatenation. Essentially you're trying to send the method user_firstnameuser_lastname which is just as non-sensical as trying to concat two Symbols.
What you need to understand is that this parameter is looking for a method on your User object, and it won't understand the combination of two symbols. You need to define a method in your model:
def fullname
[user_firstname, user_lastname].reject{|v| v.blank?}.join(" ")
end
This'll return your first + last name for you, and then in the parameter you should send :fullname (because that's the method it'll call on each user object in the collection):
<%= collection_select(:user, :user_id, #users, :user_id, :fullname, {:prompt => false})%>
Also, it's considered poor practice to prefix every single column with the table name. user.user_firstname just looks redundant. I prefer to drop that prefix, but I guess it's mostly up to personal preference.
The arguments for value and display attribute are method names, not expressions on a user object.
To control the format more precisely, you can use the select tag helper instead:
select("user", "user_id", #users.each {|u| [ "#{u.first_name u.last_name}", u.user_id ] })
The docs are pretty useful.

Resources