1

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.

enter image description here

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?

Community
  • 1
  • 1
6ft Dan
  • 2,365
  • 1
  • 33
  • 46

2 Answers2

-1

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
Tiago Farias
  • 3,397
  • 1
  • 27
  • 30
  • Does this ensure that product will only have 1 twitter kind? And when you go to edit it will it load that kind in the correct form field? – 6ft Dan Jul 31 '14 at 22:37
  • When submitted it will generate a params hash with a Social hash for every fields_for. So in each param hash you will have the tuple (kind, username). In your controller, of course you'll need to filter the Social objects with username. If a social has no username, you can ignore and not save the parent model (in my case Product) with it. Also, yes. It will load the correct form on edit. – Tiago Farias Aug 01 '14 at 01:47
  • Unfortunately the last field entered becomes the field value for all of them. So in your code example the Linked In field is the one that saves. – 6ft Dan Aug 01 '14 at 03:03
  • Here's what your code output shows on the edit page. https://dl.dropboxusercontent.com/u/27731634/polymorphic-child.png you can ignore the word `kind`... that's just a `placeholder` I have – 6ft Dan Aug 01 '14 at 03:11
  • If you look in your console, the object Product is being saved correctly. Not only the last field. Although, edit is weird. I'll take a look. – Tiago Farias Aug 01 '14 at 03:26
  • There is another bigger problem here: you can only pick one enum state for the model. Then you cannot have a model with twitter and skype for ex. One or another. I suggest deciding the type of the model based on the username. But of course, for every text_field, use a different name. See my edited answer. – Tiago Farias Aug 01 '14 at 04:29
-1

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.

Community
  • 1
  • 1
6ft Dan
  • 2,365
  • 1
  • 33
  • 46