jgthms
jgthms

Reputation: 864

Update join model in has_many through association, using the two other models

I've read through many other topics here (1, 2, 3...) but none really solved my problem.

Here are my 3 models.

User
  has_many :memberships
  has_many :accounts, :through => :memberships
  accepts_nested_attributes_for :memberships
end

Account
  has_many :memberships
  has_many :users, :through => :memberships
  accepts_nested_attributes_for :memberships
end

Membership
  attr_accessible :account_id, :url, :user_id
  belongs_to :account
  belongs_to :user
end

As you can see, my join model Membership has an additional attribute: :url.

In my Accounts table, I store names of online services, such as GitHub, Stack Overflow, Twitter, Facebook, LinkedIn.. I have 9 in total. It's a fixed amount of accounts that I don't tend to update very often.

In my User form, I'd like to create this:

User nested form

The value entered in any of these field should be submitted in the Memberships table only, using 3 values:

I have tried 3 options. They all work but only partially.

Option #1

<% for account in @accounts %>
  <%= f.fields_for :memberships do |m| %>
    <div class="field">
      <%= m.label account.name %><br>
      <%= m.text_field :url %>
    </div>
  <% end %>
<% end %>

I want to have 9 text field, one for each account. So I loop through my accounts, and create a url field related to my memberships model.

It shows my fields correctly on the first time, but the next time it'll display 81 fields:

Loop issue

Option #2

<% @accounts.each do |account| %>
  <p>
    <%= label_tag(account.name) %><br>
    <%= text_field_tag("user[memberships_attributes][][url]") %>
    <%= hidden_field_tag("user[memberships_attributes][][account_id]", account.id) %>
    <%= hidden_field_tag("user[memberships_attributes][][user_id]", @user.id) %>
  </p>
<% end %>

I'm trying to manually enter the 3 values in each column of my Memberships tables.

It works but :

Option #3 (best one yet)

<%= f.fields_for :memberships do |m| %>
  <div class="field">
    <%= m.label m.object.account.name %><br>
    <%= m.text_field :url %>
  </div>
<% end %>

I'm creating a nested form in my User form, for my Membership model.

It works almost perfectly:

But, it only works if my Memberships table is already populated! (Using Option #2 for example).

So I tried building some instances using the UsersController:

if (@user.memberships.empty?)
  @user.memberships.build
end

But I still get this error for my m.label m.object.account.name line.

undefined method `name' for nil:NilClass

Anyway, I'm probably missing something here about has_many through models. I've managed to create has_and_belongs_to_many associations but here, I want to work on that join model (Membership), through the first model (User), using information about the third model (Account).

I'd appreciate your help. Thank you.

Upvotes: 3

Views: 4892

Answers (2)

Harish Shetty
Harish Shetty

Reputation: 64363

I would use the following data-design approach. All users in your system should have the memebership entries for all possible accounts. The active configurations will have a value for the url field.

User
  has_many :memberships
  has_many :accounts, :through => :memberships
  has_many :active_accounts, :through => :memberships, 
            :source => :account, :conditions => "memberships.url IS NOT NULL"  

  accepts_nested_attributes_for :memberships
end

Now

curent_user.active_accounts # will return the accounts with configuration
curent_user.accounts # will return all possible accounts

Add a before_filter to initialize all the memberships that a user can have.

class UsersController

  before_filter :initialize_memberships, :only => [:new, :edit]

private

  def initialize_memberships
    accounts =  if @user.accounts.present? 
      Account.where("id NOT IN (?)", @user.account_ids) 
    else
      Account.scoped
    end        
    accounts.each do |account|
      @user.memberships.build(:account_id => account.id)
    end
  end
end    

In this scenario you need to initialize the memeberships before the new action and all the memberships should be saved in the create action ( even the ones without url).

Your edit action doesn't need to perform any additional data massaging.

Note:

I am suggesting this approach as it makes the management of the form/data straight forward. It should only be used if the number of Account's being associated is handful.

Upvotes: 2

jvnill
jvnill

Reputation: 29599

in the controller, fetch the list of memberships for a particular user

# controller
# no need to make this an instance variable since you're using fields_for in the view
# and we're building additional memberships later
memberships = @user.memberships

then loop through each account and build a membership if the user has no membership for an account yet.

# still in the controller
Account.find_each do |account|
  unless memberships.detect { |m| m.account_id == account.id }
    @user.memberships.build account_id: account.id
  end
end

then in your view, you change nothing :)

Upvotes: 2

Related Questions