Reputation: 35
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
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
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