Translating activerecord collection for a dropdown - ruby-on-rails

In a model I have some states
STATES = ["in_progress", "active", "archived"]
In my form I want a drop down/select with
In Progress, Active, Archived for english
and
ชำระ , ขัน , ยื่น in Thai
What is the best way to handle this ?
One option I have thought of is the following
def self.states
#states = {}
STATES.each do |s|
#states[s] = I18n.t(s)
end
#states
end
Is there a better way?

This is supported by simple_form:
In your view, you should just use
<%= f.input :state, collection: ["in_progress", "active", "archived"] %>
In your yaml file, you should have
simple_form:
options:
defaults:
state:
in_progress: In progress
active: Active
archived: Archived

Related

Rails ActiveRecord::AssociationTypeMismatch - ActsAsTaggableOn::Tag(#755220) expected, got "" which is an instance of String(#7280):

I'm using the acts-as-taggable-on gem to add tags on my business + service models in my Rails 6 app in order to enable users to find whatever service/business they're seeking more easily. A service will have food-specific tags available if the business is a restaurant, and more general ones if it's another type of business. Whenever I try to create either a new business or a new service, I'm getting the same error:
ActiveRecord::AssociationTypeMismatch - ActsAsTaggableOn::Tag(#755220) expected, got "" which is an instance of String(#7280):
app/controllers/services_controller.rb:12:in `create'
Here is the relevant part of my Service model code:
class Service < ApplicationRecord
belongs_to :business
acts_as_taggable_on :tags
acts_as_taggable_on :food_taggings, :service_taggings
Here's the relevant part of my new service form:
<%= simple_form_for [#business, #service] do |f|%>
<%= f.input :food_taggings, collection: Service.foodlist, input_html: {multiple: true, id: "food_tagging_new", class: "select2"}, label: "Please add some descriptive tags to the dish that you're offering so that local users could find it more easily" %>
<%= f.input :service_taggings, collection: Business.offerings, input_html: {multiple: true, id: "service_tagging_new", class: "select2"}, label: "Please add some descriptive tags to the service that you're offering so that local users could find it more easily" %>
<%= f.button :submit, 'Submit', class: 'btn btn-primary'%>
</div>
<% end %>
Here's the relevant part of my services controller code:
def create
#service = Service.new(service_params)
if #service.save
flash[:notice] = "This service was successfully added!"
redirect_to #service
else
render "new"
end
end
private
def service_params
params.require(:service).permit( :tag_list, tag_list: [], food_taggings: [], service_taggings: [] )
end
And here are the params that go along with the create new service request:
{"authenticity_token"=>"kld9sOSfro/nrINxQdKpXCZnxt6Cjb6TIw+jcjW5XmpUhvfm767dPXStOGB2vEBbckZvb87uKXlZo2KGjAo8vA==", "service"=>{"name"=>"", "description"=>"", "price_cents"=>"", "food"=>"0", "food_taggings"=>[""], "service_taggings"=>[""]}, "commit"=>"Submit", "controller"=>"services", "action"=>"create", "business_id"=>"5"}
How would I go about fixing this issue so that both of the models can get created successfully? I was already able to create some seeds for both without including any tags successfully, and I'm not sure exactly what Rails is expecting now?
To recap what acts_as_taggable_on is doing, say you declare a single taggable attribute:
class Service < ApplicationRecord
acts_as_taggable_on :tags
end
The gem does two things:
It dynamically creates an association (i.e., has_many/belongs_to) between your class and ActsAsTaggableOn::Tag, which is how the gem models tags you define. When you access tags on a instance of Service, you get an array of these Tag objects like you would with any has_many association.
It also creates a friendly convenience wrapper called tag_list (note: singluarized), which is the main way the gem expects you to interact with tags. Calling this will do the work of querying the associated Tag objects and return you a nice array of strings. Or you can assign it an array of strings, which get parsed into Tag objects.
In your form and controller, you are using the raw association references (food_taggings and service_taggings). Thus when your form POSTs, Rails properly raises an error because it is expecting those parameters to be arrays of Tag objects not arrays of strings.
If you change your form to use the convenience wrapper names for the form fields, the gem will properly parse the array of strings in your params and create the associated objects:
<%= simple_form_for [#business, #service] do |f|%>
<%= f.input :food_tagging_list, ... %>
<%= f.input :service_tagging_list, ... %>
...
<% end %>
Don't forget to alter your permitted parameters on the controller as well:
def service_params
params.require(:service).permit(tag_list: [],
food_tagging_list: [],
service_tagging_list: [])
end
Try adding include_blank: false in your form input options. I think Rails gives you this error because it will always send "" (empty string) from the taggings form.
<%= f.input :food_taggings,
collection: Service.foodlist,
input_html: {
multiple: true,
id: "food_tagging_new",
class: "select2"
},
include_blank: false # Add this
label: "Please add some descriptive tags to the dish that you're offering so that local users could find it more easily"
%>

Rails maintaining selected value for states using a helper in a form

I'm going through and cleaning up some forms and realized that when a user clicks to edit a training the state reverts back to Alaska instead of whatever state it was currently on. Right now the code looks like this in the helper:
module Admin::StatesHelper
STATES = [
['Alaska', 'AK'],
['Alabama', 'AL'],
['Arkansas', 'AR'],
['American Samoa', 'AS'],
['Arizona', 'AZ'],
['California', 'CA'],
['Colorado', 'CO'],
['Connecticut', 'CT'],
['District of Columbia', 'DC']
].freeze
def options_for_us_state(selected = nil, abbrev_only: true)
values = abbrev_only ? STATES.map { |s| s[1] } : STATES
options_for_select(values, selected)
end
end
Then in the form we have:
<%= f.tb_select :state, options_for_us_state(params[:state]), prompt: 'Select State'%>
I could create states as it's separate model then use options_from_collection_for_select but shouldn't my method be working or did I put in something that's not catching the selected?
It's the stupid form. Used the following:
<%= f.tb_select :state, options_for_us_state(#location.state), prompt: 'Select State'%>

Rails select values

I have a select option in a form:
<%= f.select :role, options_for_select(User.roles.keys.to_a, params[:role]), {}, class: 'form-control form-control-lg roleSelect' %>
These roles are defined in my model:
enum role: {user: 0, profile_user: 1}
Now in my dropdown when the user choses it shows user and profile_user as drop down option.
Is there any way to show another value to represent these in a drop down?
For example:
In the drop down I would rather show "I am a teacher" which maps to user.
In the drop down I would rather show "I am here to study" which maps to profile_user.
In your model add your narrative descriptions.
NARRATIVE_ROLES = {user: 'I am a teacher', profile_user: 'I am here to learn'}
Add a method to create the select_array
def self.roles_select
User.roles.keys.map {|role| [NARRATIVE_ROLES[role], role]}
end
Then in the select you use
options_for_select(User.roles_select, params[:role])
Or (slightly simpler)
NARRATIVE_ROLES = {'I am a teacher' => :user, 'I am here to learn' => :profile_user}
options_for_select(User::NARRATIVE_ROLES, params[:role])

Multiple identical collection_select tags in a form in Rails

I have multiple identical collection selects inside a single form. I prefer this over a multiple select list for aesthetic and UX reasons. I have to use a terrible kludge to make everything work right, and I'm wondering if there is a more elegant way to do this:
From the view:
<% 3.times do |i| %>
<%= collection_select("selected_item_" + i.to_s.to_s, :name, #items, :name, :name, { :include_blank => true }, { id: "selected_item_" + i.to_s }) %>
<% end %>
From the controller:
ItemContainer = Struct.new(:name)
3.times do |i|
param = ('selected_item_' + i.to_s).to_sym
instance_variable = '#' + param_name
if params[param] && !params[param].empty?
#selected_items << params[param][:name]
instance_variable_set(instance_variable, ItemContainer.new(params[param][:name]))
end
end
#selected_channels.each.... # do what I need to with these selections
Most of these gymnastics are needed to ensure that the item is still selected if the page is refreshed. If there were some way to force collection select to use arrays, that would be the answer, but I couldn't make that work.
If I understang right you're looking for selegt_tag method (docs: http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-select_tag)
You can write something like this
select_tag "people[]", options_from_collection_for_select(#people, "id", "name")
select_tag "people[]", options_from_collection_for_select(#people, "id", "name")
and it youd output two selects for people, which would be sent as an array on submit.
if you use the [] naming in your collection_select calls then the params will send over the data as an array
I am a bit confused as to the usage of collection_select here as it doesn't seem like you are using a model object? this example using select_tag - might be able to come up with something more appropriate to your issue if the model structures were known
# run this in the loop
# set selected_value to appropriate value if needed to pre-populate the form
<%= select_tag('name[]',
options_from_collection_for_select(#items, 'name', 'name', selected_value),
{ include_blank: true }
)
%>
in controller update/create action
# this works because the select tag 'name' is named with [] suffix
# but you have to ensure it is set to empty array if none are passed, usually only issue with checkboxes
names = params[:name] || []
names.each do |name|
puts name
end
side note: you can use string interpolation with ruby double quotes in places of + for string concatenation
<%= collection_select("selected_item_#{i}",
:name,
#items,
:name,
:name,
{ include_blank: true },
{ id: "selected_item_#{i}" }
)
%>
see also: http://apidock.com/rails/v3.2.13/ActionView/Helpers/FormOptionsHelper/options_from_collection_for_select

Can collection_select be used against yaml files?

My Rails application currently uses collection_select to select lookup values for drop downs etc. This has two advantages:
The values are consistent
The id of the selected value is stored in the database, not the text value
For example:
edit.html.erb
<div class="field">
<%= f.label :course_type %><br />
<%= f.collection_select :course_type, Lookup.find(:all,:conditions => ["model_name = 'course' and field_name = 'course_type'"]), :id, :lookup_text, include_blank: false,:prompt => "Course Type" %>
</div>
course_controller.rb
private
def get_lookups
#course = Course.find(params[:id])
#course_type = Lookup.find(#course.course_type).lookup_text
show.html.erb
<b>Course type:</b>
<%= #course_type %>
My application will be multi-lingual, and Rails handles this by using locale files.
The question is: Is it possible (and sensible) to populate lookup values from yml files, rather than model/tables, and can this be easily extended to handle multiple languages? How could the above code be replaced with yml-based code?
One solution would be to keep translations in the DB, perhaps with our Traco lib. I suspect it would work with collection_select.
If you want to pull options from your translation YML files, I suggest options_for_select. All in all something like:
en.yml
en:
my_options:
one: "Option 1"
two: "Option 2"
View:
select_tag :foo, options_for_select(t("my_options").invert)
Rails i18n gives you a hash if you translate a non-leaf key, like "my_options". You need the invert because options_for_select expects the text before the value, and a translation hash is the other way around.
To translate your collection_select, you simply create a new model method (let's say, "name_translated") which returns your translation from the YAML file:
View:
<%= f.collection_select :product_id, Product.all, :id, :name_translated %>
Model:
class Product < ActiveRecord::Base
def name_translated
I18n.t(name)
end
end
YAML file:
en:
name1: "Hammer"
name2: "Plastic sheets"
name3: "Duct tape"
I use select:
<%= f.select :role, MAIN_CONFIG['manager_roles'].map { |s| [s.last, s.first] }, selected: #manager.role %>
And my yaml file main_config.yml:
manager_roles:
admin: 'Суперадмин'
partner_admin: 'Администратор'
manager: 'Менеджер'

Resources