Nathan M
Nathan M

Reputation: 35

Rails - Insert on a has_many through table

I have two classes, users and opportunities, that utilize a join table with a has_many :through relationship to allow a user to enrol in one to multiple opportunities (and allow one opportunity to have many users enrol).

class User < ApplicationRecord
  has_many :opportunity_enrolments, :class_name => 'OpportunityEnrolment', foreign_key: "user_id"
  has_many :opportunities, through: :opportunity_enrolments, foreign_key: "opportunity_id"
  has_secure_password
  validates :email, presence: true, uniqueness: true
end
class Opportunity < ApplicationRecord
  has_many :opportunity_enrolments, :class_name => 'OpportunityEnrolment'
  has_many :users, through: :opportunity_enrolments
end
class OpportunityEnrolment < ApplicationRecord
  belongs_to :opportunity
  belongs_to :user
end

Users will only enrol in an opportunity when both the user and the opportunity exist. So the enrol functionality will take place outside the creation of user and opportunity, those happen separately and work fine. I thought that I could use the OpportunityEnrolment.create method when viewing an Opportunity to add a new row to the opportunity_enrolments table with the below form and controller.

opportunity/show.html.erb

<%= form_with(model: OpportunityEnrolment.create, local: true) do |form| %>
<p>
  <strong>Name:</strong>
  <%= @opportunity.voppname %>
</p>
<p>
  <strong>Shortdescr:</strong>
  <%= @opportunity.shortdescr %>
</p>
<p>
  <%= form.submit %>
</p>
<% end %>

opportunity_enrolments_controller.rb

def create      
   @opportunity_enrolment = OpportunityEnrolment.new(user_id: current_user.id, opportunity_id: @opportunity.id)

  error checking and .save...
end

However, the @opportunity.id in my form is not being passed to the OpportunityEnrolment.create method, so I'm getting an "Undefined method `id' for nil:NilClass" error upon submission. I've tried it a different way, with a hidden field in the form (understand its not secure) and the object is still not passed through, but I get past the Undefined method error.

How can I pass the object information (Opportunity) to another class (OpportunityEnrolments) so I can add a row to the opportunity_enrolments table?

Thanks

Upvotes: 2

Views: 795

Answers (2)

3limin4t0r
3limin4t0r

Reputation: 21150

This should be done using a nested route instead of passing the opportunity id in the form.

# config/routes.rb
Rails.application.routes.draw do

  # create the route: GET /opportunities/:id
  # helper: opportunity_path(opportunity || opportunity_id)
  # handled by: OpportunitiesController#show
  resources :opportunities, only: :show do

    # create the route: POST /opportunities/:opportunity_id/opportunity_enrolments
    # helper: opportunity_opportunity_enrolments_path(opportunity || opportunity_id)
    # handled by: OpportunityEnrolmentsController#create
    resources :opportunity_enrolments, only: :create
  end

end

# app/controllers/opportunity_enrolments_controller.rb
class OpportunityEnrolmentsController < ApplicationController
  # opportunity should be set for every nested action, create in this scenario
  before_action :set_opportunity, only: :create

  def create
    # initialize a new opportunity enrolment with opportunity id set to
    # the id of the current opportunity
    @opportunity_enrolment = @opportunity.opportunity_enrolments.build
    # set the user id equal to the current user
    @opportunity_enrolment.user = current_user

    # assign the passed attributes by the form and try to save the record
    # if your form doesn't contain any attributes, call #save instead
    if @opportunity_enrolment.update(opportunity_enrolment_params)
      redirect_to @opportunity
    else
      # display errors using @opportunity_enrolment.errors in the form, see note
      render 'opportunities/show' 
    end
  end

  private

  def opportunity_enrolment_params
    # if you only need to set the attributes opportunity_id and user_id
    # you can leave this call out and call #save instead of #update
    # ...
  end

  def set_opportunity
    # params[:opportunity_id] is retrieved from the current path, it is not
    # a query or request body param
    @opportunity = Opportunity.find(params[:opportunity_id])
  end
end

<% # app/views/opportunities/show.html.erb %>

<% # If rendered from opportunity show: opportunity enrolment is not set thus a new     %>
<% # opportunity enrolment will be initialized. If rendered from the opportunity        %>
<% # enrolment create action: opportunity enrolment will already be present with errors %>
<% # set, no new opportunity will be initialized.                                       %>
<% @opportunity_enrolment ||= @opportunity.opportunity_enrolments.build %>

<% # Passing an array containing an opportunity and an opportunity enrolment will build  %>
<% # the path in 3 steps. 1) Is opportunity a new record? Use /opportunities, if not use %>
<% # /opportunities/:id. 2) Is opportunity enrolment a new record? Use                   %>
<% # /opportunity_enrolments, if not use /opportunity_enrolments/:id. 3) Is the last     %>
<% # element in the array a new record? Use POST, if not use PUT.                        %>
<% # Combining the above together you get the path:                                      %>
<% # POST /opportunities/:opportunity_id/opportunity_enrolments                          %>
<% # Handled by OpportunityEnrolmentsController#create (see routes).                     %>
<%= form_with model: [@opportunity, @opportunity_enrolment], local: true do |form| %>
  <p><strong>Name:</strong><%= @opportunity.voppname %></p>
  <p><strong>Shortdescr:</strong><%= @opportunity.shortdescr %></p>
  <p><%= form.submit %></p>
<% end %>

Note: If you are going to read up on nested resources don't skip the part about shallow nesting. It keeps your routes and application cleaner. The page for error access can be found here.

Upvotes: 2

John H.
John H.

Reputation: 21

If you use a hidden field, you will need to instantiate the opportunity in the create action in the opportunity_enrolments_controller.

def create
  @opportunity = Opportunity.find(params[:opportunity_id])
  @opportunity_enrolment = OpportunityEnrolment.new(user_id: current_user.id, opportunity_id: @opportunity.id)
end

In the form:

<%= form_with(model: OpportunityEnrolment.create, local: true) do |form| %>
<p>
  <strong>Name:</strong>
  <%= @opportunity.voppname %>
</p>
<p>
  <strong>Shortdescr:</strong>
  <%= @opportunity.shortdescr %>
  <%= form.hidden_field :opportunity_id, value: @opportunity.id %>
</p>
<p>
  <%= form.submit %>
</p>
<% end %>

Upvotes: 1

Related Questions