Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • url (the value entered in the text field)
  • user_id (the id of the current user form)
  • account_id (the id of the related account, e.g. LinkedIn is '5')

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 :

  • displaying both account and user id's doesn't seem very secure (no?)
  • it will reset the fields everytime I edit my user
  • it will duplicate the values on each submit

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:

  • exactly 9 fields, one for each account
  • no duplicates

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.

like image 533
jgthms Avatar asked Jan 22 '26 16:01

jgthms


2 Answers

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 :)

like image 61
jvnill Avatar answered Jan 25 '26 12:01

jvnill


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.

like image 20
Harish Shetty Avatar answered Jan 25 '26 10:01

Harish Shetty