Project has_many Users through

Ruby on Rails tutorial

Here is a Ruby on Rails example of adding a join table so that you can create a has_many Users through situation with Projects. This assumes that you already have a Project and a User model.

1. Generate your scaffold:

Generating a scaffold here called UserProjects. It has two reference fields for user and project. We will need to add indexes. Jump into your bash and generate the scaffold.

$ rails g scaffold UserProjects user:references project:references

*That added a bunch of files. You now have the model, the views and the controller as well as the migration to create the table.

2. Check the Migration – adjust and add the indexes if needed:

This is what you are going to need there,
Check the Migration – adjust and add the indexes if needed:

class CreateUserProjects < ActiveRecord::Migration
  def change
    create_table :user_projects do |t|
      t.belongs_to :project, index: true, foreign_key: true
      t.belongs_to :user, index: true, foreign_key: true

      t.timestamps null: false
    end
  end
end

#if it looks right, rake:
$ rake db:migrate

*double check, we need this in the schema:
  create_table "user_projects", force: :cascade do |t|
    t.integer  "project_id"
    t.integer  "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["project_id"], name: "index_user_projects_on_project_id"
    t.index ["user_id"], name: "index_user_projects_on_user_id"
  end

3. Bootstrap model

$ rails g bootstrap:themed UserProjects
# answer Y to all conflicts

4. add to User.rb

  has_many :user_projects, :dependent => :destroy
  has_many :projects, through: :user_projects

5. add to Project.rb

  has_many :user_projects
  has_many :users, through: :user_projects, dependent: :destroy

6. make sure routes.rb has what you need:

#this should have generated with the scaffold
resources :user_projects

7. make uniqueness index on user_projects:

#This stops it from being able to happen, but throws an error
#causing bad UI, we address that below
# generate the migration
$ rails g migration AddUniqueIndexToUserProjects

#Check and add to the migration
  class AddUniqueIndexToUserProjects < ActiveRecord::Migration[5.0]
    def change
      add_index :user_projects, [ :user_id, :project_id ], :unique => true, :name => 'by_user_and_project'
    end
  end

  #Check the schema - you should have 3 indexes now:
  #Schema:
  create_table "user_projects", force: :cascade do |t|
    t.integer  "user_id"
    t.integer  "project_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["project_id"], name: "index_user_projects_on_project_id"
    t.index ["user_id", "project_id"], name: "by_user_and_project", unique: true
    t.index ["user_id"], name: "index_user_projects_on_user_id"
  end

8. and add error exception to the model:

***this is the UI fix for the error spoken about above.
class UserProject < ApplicationRecord
  belongs_to :user
  belongs_to :project

  validates_each :user_id, :on => [:create, :update] do |record, attr, value|
    c = value; p = record.project_id
    if c && p && # If no values, then that problem 
                 # will be caught by another validator
      UserProject.find_by_user_id_and_project_id(c, p)
      record.errors.add :base, 'This combination already exists'
    end
  end
end

9. Add routes for /project/:id/users

resources :projects do
  member do
    get 'users'
    put 'add_user'
  end
end

10. add to the ProjectsController

def users
  @project_users = @project.users.all
  @other_users = User.all - (@project_users + [current_user])
  @all_users = User.all
end

def add_user
  @project_user = UserProject.new(user_id: params[:user_id], project_id: @project.id)
  respond_to do |format|
    if @project_user.save
      format.html { redirect_to users_project_url(id: @project.id),
      notice: 'User was successfully added to project' }
    else
      format.html { redirect_to users_project_url(id: @project.id),
      error: 'User was not added to project' }
    end
  end
end

11. Add /projects/users.html.erb

Sorry, there is a lot of code here. Just trying to give the full idea. This would still have some clean-up needed.

<div class="row">
  <div class="col-xs-5 col-xs-offset-1">
    <div class="page-header">
        <h3>Project Users</h3>
        <strong>(Members already added)</strong>
    </div>
    
    <table class="table table-striped">
      <thead>
        <tr>
          <th>Email</th>
          <th><%=t '.actions', :default => t("helpers.actions") %></th>
        </tr>
      </thead>
      <tbody>
          <% @project_users.each do |project_user| %>
          <tr>
            <td><%= project_user.email %></td>
            <td>
            <% if !project_user.is_admin? %>
            <%= link_to 'Remove',
            user_project_path(project_user.user_projects.find_by(project_id: @project.id)),
            :method => :delete,
            :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
            :class => 'btn btn-xs btn-danger' %>
            <% end %>
            </td>
          </tr>
          <% end %>
      </tbody>
    </table>
  </div>
  <div class="col-xs-5">

  <div class="page-header">
    <h3>Users to add to Project</h3>
    <strong>(Super-Admins don't need to be added)</strong>
  </div>
  
    <table class="table table-striped">
        <thead>
          <tr>
            <th>Email</th>
            <th><%=t '.actions', :default => t("helpers.actions") %></th>
          </tr>
        </thead>
        <tbody>
          <%# @candidates.where(:active => false).each do |candidate| %>
          <% @other_users.each do |other_user| %>
          <tr <% if !current_user.is_admin? %><% if other_user.member.invited_by != current_user.id %>style="display:none;"<% end%><% end%>>
              <td><%= other_user.email %></td>
              <td>
              <%= link_to 'Add',
              add_user_project_path(id: @project.id, user_id: other_user.id),
              :method => :put,
              :class => 'btn btn-xs btn-success' %>
              </td>
          </tr>
          <% end %>
        </tbody>
   </table>
  </div><!-- END col-xs-5 -->
</div>

<div class="row">
  <div class="col-xs-10 col-xs-offset-1" style="text-align:center;">
    <hr>
    <%= link_to '<= Back to Project', project_path(@project.id), :class => 'btn btn-primary' %>
    <br><br>
  </div>
</div>

12. change the redirect link after destroy User_Project:

  def destroy
    @user_project.destroy
    respond_to do |format|
      format.html { redirect_to users_project_path(id: @user_project.project_id), notice: 'User project was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

13. add link to Projects#show

<%= link_to '+ Add/Remove Project Users', users_project_path(id: @project.id),
    :class => 'btn btn-default responsive-none print-none', :style => 'float:right;margin-top:0;' %>