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;' %>