Instantiating multiple instances of polymorphic child manually - ruby-on-rails

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.

Related

duplicate names are being created even when I choose an option from the predefined dropdown list

Hi all I am trying to use Ruby on Rails Devise to create a new Company that a User belongs_to. While I am able to create associated company objects (under fields_for), new company objects with duplicate names are being created even when I choose an option from the predefined dropdown list. By right I should be able to select the option from the dropdown list. How can I amend my codes such that I can select and use an option from the dropdown list instead of creating a new object with duplicate names?
My codes are as shown below:-
#new.html.erb
<% resource.build_company %>
<%= form_for resource, as: resource_name, url: registration_path(resource_name), :html => {class: "ui small form"} do |f| %>
<%= f.fields_for :company do |builder| %>
<%= builder.select :name, Company.all.collect{ |p| [p.name, p.id] }, {prompt: "Company"}, :class => "company_options ui fluid search selection dropdown" %>
<%end%>
<%= f.submit "Sign up" %>
#companies_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
protected
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:lastname, :firstname,company_attributes:[:name]])
end
end
#user.rb
class User < ApplicationRecord
belongs_to :company
accepts_nested_attributes_for :company
end
#company.rb
class Company < ApplicationRecord
has_many :users
end
Instead of building a new company and accepting nested attributes for the new company, just allow to choose a company_id:
# in your view
<%= f.collection_select :company_id, Company.all, :id, :name %>
# in your controller
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:lastname, :firstname, :company_id])
end
And remove the accepts_nested_attributes_for :company line from your User model.

Rails 4 Nested Forms Simple_form_for & simple_field_for

I'm just trying to generate a simple nested form, like so:
<%= simple_form_for #profile do |f| %>
<%= f.input :first_name %>
<%= f.input :last_name %>
<%= f.input :phone_number %>
<%= f.simple_fields_for :addresses do |p| %>
<%= p.input :street %>
<%= p.input :city %>
<%= p.input :state, collection: us_states %>
<%= p.input :zip_code %>
<% end %>
<%= f.button :submit %>
My models:
class Profile < ActiveRecord::Base
belongs_to :customer
has_many :addresses
accepts_nested_attributes_for :addresses
end
class Address < ActiveRecord::Base
belongs_to :profile
end
My controller:
class ProfilesController < ApplicationController
before_action :authenticate_customer!
def new
#profile = Profile.new
end
end
Unfortunately that nested attribute addresses doesn't populate anything on the page, I would expect to see fields like street or city but I get nothing.
However, if I change <%= f.simple_fields_for :addresses do |p| %> to <%= f.simple_fields_for :address do |p| %> the fields display correctly.
Unfortunately doing this causes issues because I can't use the accepts_nested_attributes_for helper as outlined in the docs (as far as I can tell). Any idea why this isn't working?
The reason is because nested forms require created objects to work. It looks like Profile gets instantiated but Address does not.
class ProfilesController < ApplicationController
before_action :authenticate_customer!
def new
#profile = Profile.new
#profile.addresses.create # this will create the address object that the nested form will use
end
end
I think you will need to create Profile as well rather than create an instance of it.
#profile = Profile.create
I've just been working with nested forms myself and this is how it worked for me.
The solution was to build the profile and the addresses in the #new action for it to work. Revised working code:
class ProfilesController < ApplicationController
before_action :authenticate_customer!
def new
#profile = current_customer.build_profile
#profile.addresses.build
end
end
You'll need to look at how your params come through, but since I have a has_many, they came through hashed with a key of a record id.

Creation form for models with polymorphism

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!

Rails: How do I submit multiple objects into strong params?

I am making a goal tracking app. Right now outcome, purpose, action, priority, resources, and direction are all things which are part of Outcome in the database. However, I want to make purpose and action their own model objects. What I am confused about is how do I submit Outcome, Purpose, and Action, which will be 3 separate model objects, in a single HTTP request?
Should I just use multiple strong params in my controller?
app/view/outcomes/new.html.erb
You need to have model associations of outcomes with purpose and action.
Then you will need to create nested form. So that outform form can wrap purpose and action model attributes.
As you want to have different models for actions and purposes, I'm assuming outcome can has_many purposes and has_many actions. As per this type of association, below is the code you should have.
Your form will become something like:
<%= form_for #outcome do |f| %>
<%= f.label :outcome, "Outcome" %>
<%= f.text_area :outcome %>
<%= f.fields_for :purpose, #outcome.purpose.build do |p| %>
<%= p.text_area :desc, label: "Purpose" %>
<% end %>
<%= f.fields_for :action, #outcome.action.build do |p| %>
<%= p.text_area :desc, label: "Action" %>
<% end %>
<%= f.submit "submit" %>
<% end %>
Models:
# outcome.rb
has_many :purposes, :dependent => :destroy
has_many :actions, :dependent => :destroy
accepts_nested_attributes_of :purposes, :actions
-----------------------------------------
# purpose.rb
belongs_to :outcome
-----------------------------------------
# action.rb
belongs_to :outcome
Controller:
# outcomes_controller.rb
def outcome_params
params.require(:outcome).permit(:outcome, purpose_attributes:[:desc], action_attributes: [:desc])
end
SUGGESTION: You should rename your action model name to avoid unwanted conflicts with rails keyword action.
This may help you
Nestd Attributes
If the objects are associated (as below), you'll be best using the accepts_nested_attributes_for method:
#app/models/outcome.rb
Class Outcome < ActiveRecord::Base
has_many :purposes
has_many :actions
accepts_nested_attributes_for :purposes, :actions
end
#app/models/purpose.rb
Class Purpose < ActiveRecord::Base
belongs_to :outcome
end
#app/models/action.rb
Class Action < ActiveRecord::Base
belongs_to :outcome
end
accepts_nested_attributes_for means you'll be able to send the associated objects through the Outcome model - meaning you can send them all in a single HTTP request
You have to remember the way Rails is set up (MVC pattern), meaning if you send a single request; any further model objects you have will be able to be stored too.
Here's how you can set it up:
#app/controllers/outcomes_controller.rb
Class OutcomesController < ApplicationController
def new
#outcome = Outcome.new
#outcome.purposes.build
#outcoe.actions.build
end
def create
#outcome = Outcome.new(outcome_params)
#outcome.save
end
private
def outcome_params
params.require(:outcome).permit(:outcome, purpose_attributes:[:purpose], action_attributes: [:action])
end
end
Which will give you the ability to use this form:
#app/views/outcomes/new.html.erb
<%= form_for #outcome do |f| %>
<%= f.label :outcome %>
<%= f.text_area :outcome %>
<%= f.fields_for :purposes do |p| %>
<%= p.text_area :purpose %>
<% end %>
<%= f.fields_for :actions do |a| %>
<%= a.text_area :action %>
<% end %>
<%= f.submit %>
<% end %>
--
Recommendation
From the looks of it, I'd recommend you'll be able to keep all of these details in a single model - storing in multiple models seems overkill

How to get Rails build and fields_for to create only a new record and not include existing?

I am using build, fields_for, and accepts_nested_attributes_for to create a new registration note on the same form as a new registration (has many registration notes). Great.
Problem: On the edit form for the existing registration, I want another new registration note to be created, but I don't want to see a field for each of the existing registration notes.
I have this
class Registration < ActiveRecord::Base
attr_accessible :foo, :bar, :registration_notes_attributes
has_many :registration_notes
accepts_nested_attributes_for :registration_notes
end
and this
class RegistrationsController < ApplicationController
def edit
#registration = Registration.find(params[:id])
#registration.registration_notes.build
end
end
and in the view I am doing this:
<%= form_for #registration do |r| %>
<%= r.text_field :foo %>
<%= r.text_field :bar %>
<%= r.fields_for :registration_notes do |n| %>
<%= n.text_area :content %>
<% end %>
<% end %>
and it is creating a blank text area for a new registration note (good) and each existing registration note for that registration (no thank you).
Is there a way to only create a new note for that registration and leave the existing ones alone?
EDIT: My previous answer (see below) was bugging me because it's not very nice (it still loops through all the other registration_notes needlessly). After reading the API a bit more, the best way to get the behaviour the OP wanted is to replace:
<%= r.fields_for :registration_notes do |n| %>
with:
<%= r.fields_for :registration_notes, #registration.registration_notes.build do |n| %>
fields_for optionally takes a second parameter which is the specific object to pass to the builder (see the API), which is built inline. It's probably actually better to create and pass the new note in the controller instead of in the form though (just to move the logic out of the view).
Original answer (I was so close):
Just to clarify, you want your edit form to include a new nested registration note (and ignore any other existing ones)? I haven't tested this, but you should be able to do so by replacing:
<%= r.fields_for :registration_notes do |n| %>
with:
<%= r.fields_for #registration.registration_notes.build do |n| %>
EDIT: Okay, from a quick test of my own that doesn't work, but instead you can do:
<%= r.fields_for :registration_notes do |n| %>
<%= n.text_area :content if n.object.id.nil? %>
<% end %>
This will only add the text area if the id of the registration note is nil (ie. it hasn't been saved yet).
Also, I actually tested this first and it does work ;)
If you want to create a new registration form on your edit action, you can just instantiate a new registration_note object. Right now, your form is for the existing registration object.
I believe this is what you want:
class RegistrationsController < ApplicationController
def edit
#new_registration_note = RegistrationNote.new
#registration = Registration.find(params[:id])
#registration.registration_notes.build
end
end
In your view, you should pass a hidden param that references the registration record id:
<%= form_for #new_registration_note do |r| %>
<%= r.hidden_field :registration_id, :value => #registration.id %>
<%= r.text_area :content %>
<% end %>
Now, you can create your new registration note that belongs to #registration. Make sure you have a column in your registration_notes table to point to the registration. You can read more about associations here: http://guides.rubyonrails.org/association_basics.html
Thank you so much for your help as I said in my post the only problem with the approach from "Zaid Crouch"(I don't know how to make a reference to a user hehe) is that if the form has error fields the form will be clear and boom after the page reloading you'll have nothing filled in your form and can you imagine if you form is like 20 or 30 fields that would be a terrible user experience of course
Here is my solution that works with validation models:
class Registration < ActiveRecord::Base
attr_accessible :foo, :bar, :registration_notes_attributes
has_many :registration_notes
has_one :new_registration, class_name: 'RegistrationNote'
accepts_nested_attributes_for :new_registration
end
class RegistrationsController < ApplicationController
def edit
#registration = Registration.find(params[:id])
#registration.build_new_registration
end
end
<%= form_for #registration do |r| %>
<%= r.text_field :foo %>
<%= r.text_field :bar %>
<%= r.fields_for :new_registration do |n| %>
<%= n.text_area :content %>
<% end %>
<% end %>
I'm using simple_form in my example if you want to see the same working with validations and transaction take a look at the complete post here:
http://elh.mx/ruby/using-simple_form-for-nested-attributes-models-in-a-has_many-relation-for-only-new-records/
As Heriberto Perez correctly pointed out the solution in the most upvoted answer will simply discard everything if there's a validation error on one of the fields.
My approach is similar to Heriberto's but nevertheless a bit different:
Model:
class Registration < ActiveRecord::Base
has_many :registration_notes
accepts_nested_attributes_for :registration_notes
# Because 0 is never 1 this association will never return any records.
# Above all this association don't return any existing persisted records.
has_many :new_registration_notes, -> { where('0 = 1') }
, class_name: 'RegistrationNote'
accepts_nested_attributes_for :new_registration_notes
end
Controller:
class RegistrationsController < ApplicationController
before_action :set_registration
def edit
#registration.new_registration_notes.build
end
private
def set_registration
#registration = Registration.find(params[:id])
end
def new_registration_params
params.require(:registration).permit(new_registrations_attributes: [:content])
end
end
View:
<%= form_for #registration do |r| %>
<%= r.text_field :foo %>
<%= r.text_field :bar %>
<%= r.fields_for :new_registration_notes do |n| %>
<%= n.text_area :content %>
<% end %>
<% end %>

Resources