3

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.

Community
  • 1
  • 1
jgthms
  • 864
  • 8
  • 18
  • Why to you have has_many users in the account model? I account, e.g. bbxdesign@github.com belongs to many users? – Zippie Mar 10 '13 at 14:19
  • @Zippie No, it would be 'Account *GitHub* has many Users'. I'm not exactly using that direct relationship between users and accounts, just the join model. – jgthms Mar 10 '13 at 14:32

2 Answers2

2

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.

Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
  • A couple of questions: 1) Will this also populate @account.memberships with the correct :user_id? 2) If he was to store the extra parameter :url, would this work: @user.memberships.build(:account_id => account.id, :url => 'someURL') ? Thanks :-) – nicohvi Mar 14 '13 at 10:41
  • Updated the answer take a look. – Harish Shetty Mar 14 '13 at 17:58
  • Does @user.memberships.empty? return false if even one membership is present in my Memberships table (for that current user)? So, if I were to add new accounts (such as Reddit or Hacker News), they wouldn't be built, would they? – jgthms Mar 15 '13 at 02:51
  • You have to handle the edit action to introduce the new accounts – Harish Shetty Mar 15 '13 at 03:30
  • Added a fix for the `new` scenario. – Harish Shetty Mar 15 '13 at 03:47
2

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

jvnill
  • 29,479
  • 4
  • 83
  • 86