I have a model 'item', two models 'bar' and 'restaurant' and a model 'user'.
The relations between those models are:
User has_many :bars and has_many :restaurants
Item belongs_to :activity, polymorphic: true
Bar has_many :items, as: :activity
Restaurant has_many :items, as: :activity
How my _form view to create a new item should be like?A user can create an item and assign it to a model that can be bar or restaurant, so i would like that user can choose in which activity the item should belongs to.
In my form i have something like <%= f.select :activity, #my_activities.collect { |a| [a.name, a.id] } %> but doesn't work.
For polymorphic associations you use a fields_for block:
<%= form_for(#bar) do |f| %>
Bar<br />
<%= select :id, options_for_select(#bars.map {|i| [i.name, i.id]}, include_blank: true) %><br />
New Item<br />
<%= f.fields_for :items do |a| %>
Kind: <%= a.select :kind, options_for_select(Item.kinds.keys.map.with_index {|k,i| [k, i]}) %><br /> <!-- # If you have an enum of kind for item -->
Vendor: <%= a.select :vendor_id, options_for_select(current_user.vendors.map {|i| [i.name, i.id]}) %><br /> <!-- # If you have specific vendors per user -->
Name: <%= a.text_field :name %><br />
<% end %>
<%= f.submit %>
<% end %>
This will go inside your form_for tag. Use fields_for block to nest any polymorphic relation.
You would only use select for an attribute that exists when creating something new. The example you've partly written out looks like you're simply selecting from existing items. That would be a different answer if you're looking for that. You've specifically asked about creating an item. So you won't be using select for creating something by name, you will need a text_field to enter the new name.
You can ignore the two select fields for your solution. They are there to demonstrate select. I don't believe your answer needs a select field.
https://gorails.com/episodes/forum-nested-attributes-and-fields-for
On another note your naming scheme for Item may be confusing. The standard naming would be more like.
class Item < ActiveRecord::Base
belongs_to :itemable, polymorphic: true
end
class Bar < ActiveRecord::Base
has_many :items, as: :itemable, dependent: :destroy
accepts_nested_attributes_for :items, reject_if: proc { |att| att['name'].blank? }
end
class Restaurant < ActiveRecord::Base
has_many :items, as: :itemable, dependent: :destroy
accepts_nested_attributes_for :items, reject_if: proc { |att| att['name'].blank? }
end
In this case you would use a fields_for on :items. The polymorphic relationship name activity or itemable is not referred to in the fields_for field. Rather it is the plural for Item so :items.
To answer:
i have a page to add an item, where the user fill informations like
title, description, etc, and then chooses in which 'bar' or
'restaurant' he wants to publish it.
<%= form_for(#item) do |f| %>
<%= f.select :itemable_type, options_for_select([Bar.name, Restaurant.name]) %>
<%= f.select :itemable_id, [1,2,3] %># COMPLICATION, NEED AJAX/JS TO GET AVAILABLE IDs
<%= f.text_field :name %>
<% end %>
Well this is basically what you want to do. But you would need to have an Ajax/JavaScript call to change the available options for :itemable_id to the list of either Bars or Restaurants mapped with [:name, :id]. You could just use a text field to input the number of the ID of the Bar/Restaurant but this is not a user friendly experience.
If I was proficient in JavaScript I could give you a way to do this. One way would be to have duplicate :itemable_id fields and have javascript disable/remove whichever the select field it isn't using.
Alternative solution 1:
You could make a page for each type new_bar_item.html.erb and new_restaurant_item.html.erb and each of those you would simply put a hidden_field for itemable_type to be either Bar.name or Restaurant.name respectively. And then you would already know which collection to give to your select field. Map the collections for the select field to your :name, :id. This removes all the complication for doing this.
A workable solution 2:
A good way I can recommend to do it without JavaScript is to have both Bars and Restaurants listed.
<%= form_tag(#item) do |f| %>
Choose either Bar or Restaurant.<br />
Bar: <%= select_tag 'item[bar_id]', options_for_select(#bars.map {|i| [i.name, i.id]}, include_blank: true) %><br />
Restaurant: <%= select_tag 'item[restaurant_id]', options_for_select(#restaurants.map {|i| [i.name, i.id]}, include_blank: true) %><br />
Item name: <%= text_field_tag 'item[name]' %>
<% end %>
Then in your ItemController you will need to write a method to check which field isn't blank and set the polymorphic type with that.
before_action :set_poly_by_params, only: [:create, :update]
private
def set_poly_by_params
if !params["item"]["bar_id"].empty? ^ !params["item"]["restaurant_id"].empty?
if !params["item"]["bar_id"].empty?
params["item"]["itemable_id"] = params["item"].delete("bar_id")
params["item"]["itemable_type"] = Bar.name
else
params["item"]["itemable_id"] = params["item"].delete("restaurant_id")
params["item"]["itemable_type"] = Restaurant.name
end
else
raise "some error about incorrect selection"
end
end
# NOTE: The above code will fail if the form doesn't submit both a bar_id field and a restaurant_id. It expects both, empty or not.
Solution 3 (revised #2)
<%= form_tag(#item) do |f| %>
Location: <%= select_tag 'item[venue]', options_for_select(
#bars.map {|i| [i.name, "b"+i.id.to_s]} +
#restaurants.map {|i| i.name, "r"+i.id.to_s]}
) %><br />
Item name: <%= text_field_tag 'item[name]' %>
<% end %>
We've added a b before the ID for bar or r before the ID for restaurant. Then we simply need to parse the params for it.
before_action :set_poly_by_params, only: [:create, :update]
private
def set_poly_by_params
if params["item"]["venue"]["b"]
params["item"]["itemable_type"] = Bar.name
else
params["item"]["itemable_type"] = Restaurant.name
end
params["item"]["itemable_id"] = params["item"].delete("venue")[1..-1]
end
This meets your requirement of one select field with both Bars and Restaurants in it.
I used part of the solutions posted by 6ft Dan.
in items_controller i created a
before_action :set_activity, only: [:create, :update]
def set_activity
#itemable = params["item"]["itemable"]
if params["item"]["itemable"]["bar"]
#activity = Bar.find(#itemable[3..-1])
elsif params["item"]["itemable"]["res"]
#activity = Restaurant.find(#itemable[3..-1])
end
end
and then in create action i added
#item.update_attribute(:itemable, #activity)
after the
#item = Item.new(item_params)
and in my form i have
<%= f.select :itemable, options_for_select(#bars.map {|i| [i.name, "bar"+i.id.to_s]} + #restaurants.map {|i| [i.name, "res"+i.id.to_s]}) %>
Now item creates and has an itemable attribute linking to the activity which it belongs!
Related
An article called Triple nested Forms in Rails presents a good description of creating a form for saving three nested objects. The example given is of creating a Show that has_many Seasons, and each Season has_many Episodes. Also, Episode --> belongs_to --> Season --> belongs_to --> Show.
Shows are created like this:
def new
#show = Show.new
#show.seasons.build.episodes.build
end
The form looks like this:
<%= form.fields_for :seasons do |s| %>
<%= s.label :number %>
<%= s.number_field :number %> <%= s.fields_for :episodes do |e| %>
<%= e.label :title %>
<%= e.text_field :title %>
<% end %>
<% end %>
<% end %>
This seems straightforward because all the associations run in one direction. I'm trying to do something that's similar, but more complicated. I have a Parent model where each Parent has multiple Children and each Child is enrolled in a School. After specifying that Children is the plural of Child, the association would have to be like this:
Parent has_many Children, accepts_nested_attributes_for :children
Child belongs_to Parent, belongs_to School, accepts_nested_attributes_for :school
School has_many Children, accepts_nested_attributes_for :children
Graphically, it would look like this:
Parent <-- belongs_to <-- Child --> belongs_to --> School
Each Parent is also associated with a User, like this:
User has_many :parents
The data on Parents, Children, and Schools is entered in the following form (generated using the Simple Form gem), where the schools are entered as a dropdown selector populated from the schools table:
#schools = School.all
<%= simple_form_for (#parent) do |f| %>
<%= f.input :name, label: 'name' %>
<%= f.simple_fields_for :children, #children do |child_form| %>
<%= child_form.input :name, label: "Child Name" %>
<%= child_form.simple_fields_for :school, #school do |school %>
<%= school.collection_select :id, #schools, :id, :name, {}, {} %>
<% end %>
<% end %>
<% end %>
I set up the new controller method to create a Parent having three Children enrolled in an existing School. Then I tried to associate the Children with a School that already exists in the schools table with id = 1.
def new
#parent = Parent.new
# creating 3 children
#children = Array.new(3) {#parent.children.build}
#school = School.find(1)
#school.children.build
end
This throws an error
Couldn't find School with ID=1 for Child with ID=
The error is located in the first line of the create method, which looks like this:
def create
#parent = Parent.new(parent_params.merge(:user => current_user))
if #parent.save
redirect_to root_path
else
render :new, :status => :unprocessable_entity
end
end
def parent_params
params.require(:parent).permit(:name, :child_attributes => [:id, :name, age, :school_attributes => [:id, :name]])
end
Since the error text states "Child with ID= ", the error must be thrown before ids for new Children are assigned. Why can a School with ID=1 not be found when it exists in the schools table? Or, does this mean that a School record has not been properly associated with an instance of Child before an attempt is made to save that instance? If so, how can I fix the association?
One of the most common missconceptions/misstakes with nested attributes is to think that you need it to do simple association assignment. You don't. You just need to pass an id to the assocation_name_id= setter.
If fact using nested attributes won't even do what you want at all. It won't create an assocation from an existing record when you do child.school_attributes = [{ id: 1 }] rather it will attempt to create a new record or update an existing school record.
You would only need to accept nested attributes for school if the user is creating a school at the same time. And in that case its probally a better idea to use Ajax rather than stuffing everything into one mega action.
<%= simple_form_for (#parent) do |f| %>
<%= f.input :name, label: 'name' %>
<%= f.simple_fields_for :children, #children do |child_form| %>
<%= child_form.input :name, label: "Child Name" %>
<%= child_form.associaton :school,
collection: #schools, label_method: :name %>
<% end %>
<% end %>
def parent_params
params.require(:parent).permit( :name,
child_attributes: [:id, :name, :age, :school_id]]
)
end
Is it possible to generate multiple rows in the joined-model table using the new_form from exercise?
The code only works when creating a single exercise, that links to a body_section then selecting an existing muscle.
I tried to change the code to use check_box but failed
Original Code
exercise.model
has_many :body_sections
has_many :muscles, through: :body_sections
accepts_nested_attributes_for :body_sections
end
muscle.model
has_many :body_sections
has_many :exercises, through: :body_sections
body_section.model
belongs_to :muscle
belongs_to :exercise
accepts_nested_attributes_for :exercise
end
exercise controller
def new
#exercise = Exercise.new
#exercise.body_sections.build
#muscles = Muscle.all
end
# private method for strong parameter
params.require(:exercise).permit(:name, :note, :body_sections_attributes => [:name, :muscle_id])
Modified for check_box
exercise _form.view
<div>
<%= exercise_form.label :name, "Exercise Name" %>
<%= exercise_form.text_field :name %>
</div>
<div>
<%= exercise_form.fields_for :body_sections do |body_form| %>
<%= body_form.label :name, "Body Section Common Name" %>
<%= body_form.text_field :name %>
<br>
<%= body_form.collection_check_boxes(:muscle_ids, #muscles, :id, :name) do |c| %>
<%= c.label { c.check_box } %>
<% end %>
<% end %>
</div>
exercise controller
# private method for strong parameter
params.require(:exercise).permit(:name, :note, :body_sections_attributes => [:name, :muscle_ids => []])
I get an undefined method `muscle_ids' error
apparently the body_section does not have muscle_ids methods belongs to it. How should I modify the code to be able to use checkbox to select and create multiple rows in body_sections at the same time??
One body section can only have one muscle associated to it.
I would use cocoon to have dynamic nested fields for adding/removing body sections.
And then instead of collection_check_boxes :muscles_ids, I would use body_form.select options_from_collection(#muscles).
Would seem more logical to me that:
Exercise has many ExerciseMuscles
ExerciseMuscles belongs to Exercise
ExerciseMuscles belongs to Muscle
Muscle belongs to BodySection
BodySection has many muscles
This way, you create your muscles/body sections, and then people associate which muscles are used by the exercise in the form (that way you also have access to Exercise has many body sections (through muscles)).
I'm creating an admin interface where the admin (of a company) can add custom fields to their employees.
Example:
Models:
Employee: Basic info like name, contact info, etc (has_many employee_field_values)
EmployeeFields: These are the dynamic ones the admin can add (every company has different needs, it could be anything), lets say favorite_food
EmployeeFieldValues: The actual values based on the fields above, say pizza (belongs_to both models above)
What's a smart way of adding the EmployeeFieldValues fields while editing an employee?
I'm trying something simple like this, but not sure if I like it
# Controller
#custom_fields = EmployeeFields.all
# View
<%= form_for(#employee) do |f| %>
<%= f.text_field :first_name %>
<% #custom_fields.each do |custom_field| %>
<%= custom_field.name %>
<%= text_field_tag "employee_field_values[#{custom_field.name}]" %>
<% end %>
<%= f.submit :save %>
<% end %>
And then when updating, params[:employee_field_values] gives this:
<ActionController::Parameters {"favorite_food"=>"pizza"}>
So, not sure if this is a good direction, also I'm not sure how to handle future edits to an employee's custom_fields if they change.
I think it will be better to use EmployeeField as nested model and EmployeeFieldValue for select field.
For example:
Models
class Employee < ActiveRecord::Base
validates :name, presence: true
has_many :employee_field_values
accepts_nested_attributes_for :employee_field_values, reject_if: ->(x) { x[:value].blank? }
end
class EmployeeFieldValue < ActiveRecord::Base
belongs_to :employee
belongs_to :employee_field
end
class EmployeeField < ActiveRecord::Base
has_many :employee_field_values, inverse_of: :employee_field, dependent: :destroy
validates :title, presence: true, uniqueness: true
end
Controller
class EmployeesController < ApplicationController
def new
#employee = Employee.new
#employee.employee_field_values.build
end
end
View
= simple_form_for #employee, url: '/' do |f|
= f.input :name
= f.simple_fields_for :employee_field_values do |ff|
= ff.input :value
= ff.input :employee_field_id, collection: EmployeeField.all.map{|x| [x.title, x.id]}
Also you need to make buttons for adding/removing :employee_field_value, and you can do it with gem cocoon for example
OR you can build all objects in controller(for each EmployeeField) and do without select box
I am trying to set up some dynamic Dropdown Select Menus in a Search Form using form_tag. What I would like is similar functionality to the example found at Railcasts #88
Models:
class Count < ActiveRecord::Base
belongs_to :host
end
class Host < ActiveRecord::Base
belongs_to :site
has_many :counts
end
class Site < ActiveRecord::Base
belongs_to :state
has_many :hosts
end
class State < ActiveRecord::Base
has_many :sites
end
View:
<%= form_tag(counts_path, :method => "get", id: "search-form") do %>
<%= select_tag "state_id", options_from_collection_for_select(State.all.order(:name), :id, :name) %>
<%= select_tag "site_id", options_from_collection_for_select(Site.all.order(:name), :id, :name) %>
<% end %>
A State has_many Sites which has_many Hosts which has many Counts. Or conversely, Counts belong_to Host whichs belongs_to Site which belongs to State
So I would like to select a state from the States dropdown that would then "group" the Sites based on the state they associate through the Host.
I have struggled with this nested association and can't seem to figure out how build the grouped_collection_select.
I know I'm overlooking something obvious! Could sure use some pointers...
You can fire jquery-ajax request. Change event in first select box will call action on controller and called method will change the value of second dropdown through ajax call. Simple example:
In your view file:
<%= select_tag 'state_id', options_for_select(State.all.order(:name), :id, :name) %>
<%= select_tag "site_id", options_for_select(Site.all.order(:name), :id, :name) %>
In JS file of that controller:
$(document).on('ready page:load', function () {
$('#state_id').change(function(event){
$("#site_id").attr('disabled', 'disabled')
$.ajax({
type:'post',
url:'/NameOfController/NameOfMethod',
data:{ state_id: $(this).val() },
dataType:"script"
});
event.stopImmediatePropagation();
});
});
In NameOfController.rb
def NameOfMethod
##no need to write anything
end
In NameOfMethod.js.erb
<% if params[:state_id].present? %>
$("#site_id").html("<%= escape_javascript(render(partial: 'site_dropdown'))%>")
<% end %>
in _site_dropdown.html.erb file:
<% if params[:state_id].present? %>
<%= select_tag 'site_id', options_for_select(Site.where("state_id = ?", params[:state_id])) %>
<% else %>
<%= select_tag "site_id", options_for_select(Site.all.order(:name), :id, :name) %>
So it will change site dropdown based on selected state dropdown. You can go upto n number of level for searching. Good luck.
I have multiple social networks available in my model:
class Social < ActiveRecord::Base
enum kind: [ :twitter, :google_plus, :facebook, :linked_in, :skype, :yahoo ]
belongs_to :sociable, polymorphic: true
validates_presence_of :kind
validates_presence_of :username
end
I want to declare manually the kinds used. Maybe I need to have an alternative to fields_for?
<%= f.fields_for :socials do |a| %>
<%= a.hidden_field :kind, {value: :facebook} %> Facebook ID: <%= a.text_field :username, placeholder: "kind" %>
<%= a.hidden_field :kind, {value: :twitter} %> Twitter ID: <%= a.text_field :username, placeholder: "kind" %>
<%= a.hidden_field :kind, {value: :google_plus} %> Google ID: <%= a.text_field :username, placeholder: "kind" %>
<%= a.hidden_field :kind, {value: :linked_in} %> Linked In ID: <%= a.text_field :username, placeholder: "kind" %>
<% end %>
But I get just one value saved and displayed for all four IDs.
When doing a fields_for on each individual item I get repeated kinds and repeated values
NOTE: There should be only one of each kind associated with this profile form.
I believe that I need to use something like find_or_create_by to ensure only one of each kind is made and loaded in the editor as the fields_for simply loads everything in the order they were saved. Maybe showing how this Rails find_or_create by more than one attribute? could be used with just kind.
I need to ensure that product will only save one of each kind and when you edit it; it will load correctly by kind and not just any belonging to.
Since in my example all four will display what was saved in the first field on the edit page it's clear it's not ensuring the kind at the moment.
I'd like to use something like this in my application_controller.rb
def one_by_kind(obj, kind)
obj.where(:kind => kind).first_or_create
end
How would I substitute the fields_for method with this?
There are some problems here:
1 - how will you define a Social having many types or kinds when you can only pick one enum state for it ?
2 - Definitely you cannot use :username as the name for all the fields in your model. Rails will understand only the last one as the valid one. All others will be overriden.
But you can solve this problem simplifying your tactics:
Forget about setting kind in your form as a hidden field, that really won't work the way you want.
1 - Instead of a product having many socials, product has_one social, which keeps all data related to the social networks for that model.
class Product < ActiveRecord::Base
has_one :social
accepts_nested_attributes_for :social
#...
end
2 - Your form will be much simpler and you decide the order of appearance. Also you can reuse it on the edit view as a partial:
#your form header with whatever you need here...
<%= f.text_field(:name) %>
<%= f.fields_for :social do |a| %>
Facebook ID: <%= a.text_field :facebook_username %>
Yahoo ID: <%= a.text_field :yahoo_username %>
Linkedin ID: <%= a.text_field :linkedin_username %>
Twitter ID: <%= a.text_field :twitter_username %>
<% end %>
3 - In your new method, you'll need to initialize the has_one relationship:
def new
#product = Product.new
#product.build_social
end
4 - If you're using Rails 4, don't forget to whitelist the allowed attributes:
def product_params
params.require(:product).permit([:name, socials_attributes: [:twitter_username,
:facebook_username, :linkedin_username, :google_username, :skype_username, :yahoo_username] ])
end
5 - Then in your controller, you can assign many kinds to your model based on the fields that were filled. Use a before_save callback in your model for that. Something checking your fields like
def assign_social_kinds
if !self.twitter_username.blank? #or some more refined validation of it
self.twitter = true
end
if !self.skype_username.blank?
self.skype = true
end
end
Manual Polymorphic Creation in Rails
Alright I've discovered the solution. Here's what I've got.
models/profile.rb
class Profile < ActiveRecord::Base
has_many :socials, as: :sociable, dependent: :destroy
accepts_nested_attributes_for :socials, allow_destroy: true
end
models/social.rb
class Social < ActiveRecord::Base
enum kind: [ :twitter, :google_plus, :facebook, :linked_in, :skype, :yahoo ]
belongs_to :sociable, polymorphic: true
validates_presence_of :kind
validates_presence_of :username
end
controllers/profiles_controller.rb
class ProfilesController < ApplicationController
before_action :set_profile, only: [:show, :edit, :update, :destroy]
before_action :set_social_list, only: [:new, :edit]
def new
#profile = Profile.new
end
def edit
end
private
def set_profile
#profile = Profile.find(params[:id])
end
def set_social_list
#social_list = [
["linkedin.com/pub/", :linked_in],
["facebook.com/", :facebook],
["twitter.com/", :twitter],
["google.com/", :google_plus]
]
end
def profile_params
params.require(:profile).permit(
:socials_attributes => [:id,:kind,:username,:_destroy]
)
end
end
I've shortened the actual file for just what's relevant here. You will need any other parameters permitted for your use case. The rest can remain untouched.
controllers/application_controller.rb
class ApplicationController < ActionController::Base
def one_by_kind(obj, kind)
obj.where(:kind => kind).first || obj.where(:kind => kind).build
end
helper_method :one_by_kind
end
This is where the magic will happen. It's designed after .where(...).first_or_create but uses build instead so we don't have to declare build for the socials object in the profile_controller.
And lastly the all important view:
(polymorphics most undocumented aspect.)
views/profiles/_form.html
<% #social_list.each do |label, entry| %>
<%= f.fields_for :socials, one_by_kind(#profile.socials, #profile.socials.kinds[entry]) do |a| %>
<%= a.hidden_field :kind, {value: entry} %><%= label %>: <%= a.text_field :username %>
<% end %>
<% end %>
The #social_list is defined in the profile_controller and is an array of label & kind pairs. So as each one gets passed through, the one_by_kind method we defined in the application_controller seeks for the first polymorphic child that has the right kind which we've named entry. If the database record isn't found, it is then built. one_by_kind then hands back the object for us to write/update.
This maintains one view for both creation and updating polymorphic children. So it allows for a one of each kind within your profile and social relation.