1

Within my Ruby on Rails application I am trying to implement a relationship between Group and Contact, whereby one group can contain many contacts and one contact can be part of many groups. I am using a model called Contactgroup to deal with this relationship, and so the tables are:

Group (id, name)
Contact (id, firstname, surname)
Contactgroup (group_id, contact_id)

With example data being:

Groups:
ID      Name
1       Singers
2       Drummers

Contacts:
ID      Firstname    Surname
1       Freddy       Mercury
2       Roger        Taylor
3       Kurt         Cobain
4       Dave         Grohl

Contact Groups:
Group_ID        Contact_ID
1               1
1               3
1               4
2               2
2               4

What I am trying to do is get it so that when a user creates a group, they can select the contacts that they want to add to that group. This means that there is the group form, whereby the user types the group name, and on this form I want to display checkboxes for each of the user's contacts so that the user can select the contacts they want to add to the group, and when they click submit the new group will be saved in the Group table and the new contact group records will be saved in the Contactgroup table.

This is the app/views/groups/_form.html.erb code:

<%= form_for @group do |f| %>

  <% if @group.errors.any? %>
    <div id="error_explanation">
      <h2>
        <%= pluralize(@group.errors.count, "error") %> prohibited this group from being saved:
      </h2>
      <ul>
        <% @group.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <p>
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </p>

  <h2>Add members:</h2>
    <%= form_for([@group, @group.contactgroups.build]) do |f| %>
      <p>
        <%= f.collection_check_boxes(:contact_id, @contacts, :id, :firstname) %>
      </p>
      <p>
        <%= f.submit %>
      </p>
    <% end %>
  <p>
    <%= f.submit %>
  </p>

<% end %>

On here you can see the code I am trying to use to do this:

<h2>Add members:</h2>
    <%= form_for([@group, @group.contactgroups.build]) do |f| %>
      <p>
        <%= f.collection_check_boxes(:contact_id, @contacts, :id, :firstname) %>
      </p>
      <p>
        <%= f.submit %>
      </p>
    <% end %>
  <p>
    <%= f.submit %>
  </p>

<% end %>

I have got this from rails guides (http://guides.rubyonrails.org/getting_started.html) but I get the error undefined methodcontactgroups' for #` and don't think this will give me what I want.

My routes file is:

Rails.application.routes.draw do
  get 'sessions/new'
  get 'sessions/create'
  get 'sessions/destroy'
  resources :users
  get 'welcome/index'
  root 'welcome#index'
  resources :contacts

  resources :groups do
    resources :contactgroups
  end
  resources :contactgroups


  get 'sessions/new'
  get 'sessions/create'
  get 'sessions/destroy'
  controller :sessions do
    get  'login' => :new
    post 'login' => :create
    get 'logout' => :destroy
  end 
end

My groups_controller:

class GroupsController < ApplicationController
    def index
        @groups = Group.where(user_id: session[:user_id])
    end
    def show
        @group = Group.find(params[:id])
        @members = Contactgroup.where(group_id: @group.id)
    end
    def new
        @group = Group.new
        @contacts = Contact.where(user_id: session[:user_id])
    end
    def edit
        @group = Group.find(params[:id])
    end
    def create
        @group = Group.new(group_params)
        @group.user_id = session[:user_id]
        if @group.save
            redirect_to @group
        else
            render 'new'
        end
    end
    def update
        @group = Group.find(params[:id])
        if @group.update(group_params)
            redirect_to @group
        else
            render 'edit'
        end
    end
    def destroy
        @group = Group.find(params[:id])
        @group.destroy
        redirect_to groups_path
    end
    private
    def group_params
        params.require(:group).permit(:name, :user_id)
    end
end

And contactgroups_controller:

class ContactgroupsController < ApplicationController
    def destroy
        @contactgroup = Contactgroup.find(params[:id])
        @contactgroup.destroy
        redirect_to(:back)
    end
end

My models are as follows:

Contact.rb:

class Contact < ActiveRecord::Base
end

Group.rb:

class Group < ActiveRecord::Base
end

Contactgroup.rb:

class Contactgroup < ActiveRecord::Base
  belongs_to :contact
  belongs_to :group
end

There must be a simple solution to solve this as I assume it is commonly done on other systems, but I am not sure how to do this.

Can someone please help.

Ben Smith
  • 809
  • 5
  • 21
  • 46
  • Have a google for "Join tables in Rails" and "has and belongs to many" - Rails gives you nice helper methods that mean you don't have to manually deal with the join-table yourself (ie what you have called a `Contactgroup` is taken care of for you). Also look into "nested resources" and "nested forms". Likewise, rails has some nice helpers to do the heavy-lifting for you :) – Taryn East Jan 09 '17 at 20:19
  • @TarynEast thank you for your comment. I understand that Rails has the has and belongs to many relationships and that this can be done in Ruby, but for the transparency of it I would like to use the join-table. I will google nested forms as I assume that this is what I want? – Ben Smith Jan 09 '17 at 20:22
  • Just curious: how is it more/less transparent to reuse the code that rails has already written to handle this? :) – Taryn East Jan 09 '17 at 20:23
  • Because I can physically see the data in an SQL browser, just makes error checking easier – Ben Smith Jan 09 '17 at 20:24
  • You can also see the data in the SQL browser with HABTM... the data is still there, rails just gives you better code for handling it. – Taryn East Jan 09 '17 at 20:25
  • Note that my original response is mainly with regards to your comment "I assume it is commonly done on other systems" -> because it isn't. Most other systems just use the standard Rails HABTM to handle the join-table instead of doing it by hand. – Taryn East Jan 09 '17 at 20:25
  • 1
    I probably didn't word that very well, I meant that I assume it is common to save records in one table on a form for a different model – Ben Smith Jan 09 '17 at 20:27
  • Yeah - it's pretty common to use nested-forms for nested-associations (again, usually with HABTM and similar). Can you show us the associations you have in your models? That's where the method that rails is asking for is usually defined. – Taryn East Jan 09 '17 at 20:40
  • @TarynEast I have added the models into my question, thanks for looking at this for me, it is much appreciated – Ben Smith Jan 09 '17 at 20:47

3 Answers3

1

You cannot use form inside form. The correct way to use collection_check_boxes is following.

Replace

<%= form_for([@group, @group.contactgroups.build]) do |f| %>
  <p>
    <%= f.collection_check_boxes(:contact_id, @contacts, :id, :firstname) %>
  </p>
  <p>
    <%= f.submit %>
  </p>
<% end %>

With just

<p>
  <%= f.collection_check_boxes(:contact_ids, @contacts, :id, :firstname) %>
</p>
Priyank Gupta
  • 411
  • 3
  • 8
  • Alternatively use `fields_for` instead of `form_for` (definitely remove the extra submit) :) – Taryn East Jan 09 '17 at 20:26
  • Sorry but can you explain how this would work? I understand that the line `<%= f.collection_check_boxes(:contact_ids, @contacts, :id, :firstname) %>` should display checkboxes for each contact but I don't understand how that will add a new contactgroup record for each one selected? – Ben Smith Jan 09 '17 at 20:30
  • This will send all ids in an array for key `contact_ids`, like `contact_ids = [1,2,3,4,5]`. And assigning this to groups object will do the trick. http://apidock.com/rails/v4.0.2/ActionView/Helpers/FormOptionsHelper/collection_check_boxes – Priyank Gupta Jan 09 '17 at 20:37
  • @PriyankGupta thank you for your comment, but this can't work as there's not a relationship between the contact and group models as this is done through the contactgroup model, and so it can't the `contact_ids` – Ben Smith Jan 09 '17 at 20:49
  • You can create an association from group for `has_many :contacts, through: :contacts_groups` – Priyank Gupta Jan 09 '17 at 20:54
1

This was much simpler than initially thought/suggested.

What I needed to do was change the models to:

Contactgroup
  belongs_to :contact
  belongs_to :group

Contact
  has_many :contactgroups
  has_many :groups, through: :contactgroups, :dependent => :destroy

Group
  has_many :contactgroups
  has_many :contacts, through: :contactgroups, :dependent => :destroy

In the groups_controller I needed to change the new method and params to:

def new
    @group = Group.new
    @group.contactgroups.build
end

private
def group_params
    params.require(:group).permit(:name, :user_id, { contact_ids: [] })
end

And then add the following line of code into app/views/groups/_form.html.erb:

<%= f.collection_check_boxes :contact_ids, Contact.where(user_id: session[:user_id]), :id, :firstname ,{ prompt: "firstname" } %>

This provides me with a checkbox for each contact, and allows contactgroup records to be created from the group form.

Ben Smith
  • 809
  • 5
  • 21
  • 46
0

Ok so the issue is very simple. You are calling @group.contactgroups but you haven't actually set up that association on the group model yet. only have associations set up from the contactgroup side. So you can do contactgroup.group but not group.contactgroups

Your best bet is to actually model this as habtm - as I mentioned earlier. This is how you'd do that:

Contact.rb:

class Contact < ActiveRecord::Base
  has_and_belongs_to_many :groups 
end

Group.rb:

class Group < ActiveRecord::Base
  has_and_belongs_to_many :contacts
end

Note: you still have the concept of the contact-group for HABTM but using Rails standard naming it would be in your database as the contacts_groups table. Then you could build your forms that way.

With a quick google, here's a S/O question on using checkboxes with HABTM (haven't vetted it for usefulness to your situation): Rails 4 - checkboxes for has_and_belongs_to_many association

Using HABTM is Rails standard practice for lots of very good reasons. It really does actually fit your situation (honest!) and it does not actually break the requirement you have of wanting to see it in the SQL (seriously!).

Give it a try first :) I can tell you how to break Rails conventions... but it's generally well-understood that you shouldn't break conventions until you know what the conventions are there for.

Community
  • 1
  • 1
Taryn East
  • 27,486
  • 9
  • 86
  • 108