ruby-on-railsparameterscontrollernested-resourcescreateinstance

Nested resources / pundit - param is missing or the value is empty:


I've been on a big trouble for 2 days and need you help !! I have 2 models : garden and booking (with nested resources and using pundit). From the "show" of garden, I want to click on a button that "create"s the booking directly from the already created garden. But I don't succeed with the create action which always displays : param is missing or the value is empty: booking. How to make "booking" appearing in the params and be able to call for example @booking.booker ?? Do you have a solution without deleting the require(:booking) ?

I tried both either with or without a "new_booking" page but don't succeed.

Thank you soooo much !!

booking_controller

class BookingsController < ApplicationController
  def index
  end

  def my_bookings
    @bookings = BookingPolicy::Scope.new(current_user, Booking).scope.where(user: current_user)
  end

  def show
    @booking.garden = @garden
  end

  def new
    @garden = Garden.find(params[:garden_id])
    @garden_id = @garden.id
    @booker_id = current_user.id
    @booking = Booking.new
    authorize @booking

  end

  def create
    @booking = Booking.new(booking_params)
    authorize @booking
    @booking.save
    redirect_to root_path
  end



  private

  def booking_params
    params.require(:booking).permit(:garden_id, :booker_id)
  end
end

gardens_controller

class GardensController < ApplicationController
  before_action :set_garden, only: [:show, :destroy, :edit, :update ]
  def new
    @garden = Garden.new
    authorize @garden
  end

  def create
    @garden = Garden.new(garden_params)
    authorize @garden
    @garden.user = current_user

    respond_to do |format|
      if @garden.save
        format.html { redirect_to @garden, notice: 'Garden was successfully created.' }
        format.json { render :show, status: :created, location: @garden }
      else
        format.html { render :new }
        format.json { render json: @garden.errors, status: :unprocessable_entity }
      end
    end
  end

  def index
      @gardens = policy_scope(Garden)
  end

  def my_gardens
    @gardens = GardenPolicy::Scope.new(current_user, Garden).scope.where(user: current_user)

  end

  def show
    authorize @garden
  end

  def destroy
    authorize @garden

    @garden.destroy
    respond_to do |format|
      format.html { redirect_to gardens_url, notice: 'garden was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  def edit
    authorize @garden
  end

 def update
  authorize @garden

  respond_to do |format|
    if @garden.update(garden_params)
      format.html { redirect_to @garden, notice: 'Garden was successfully updated.' }
      format.json { render :show, status: :ok, location: @garden }
    else
      format.html { render :edit }
      format.json { render json: @garden.errors, status: :unprocessable_entity }
    end
  end
end

  private

  def garden_params
    params.require(:garden).permit(:title, :details, :surface, :address, :availabilities)
  end

  def set_garden
    @garden = Garden.find(params[:id])
  end
end

application_controller

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :authenticate_user!
include Pundit

  # Pundit: white-list approach.
  after_action :verify_authorized, except: [:index, :my_gardens], unless: :skip_pundit?
#  after_action :verify_policy_scoped, only: [:index, :my_gardens], unless: :skip_pundit?

  # Uncomment when you *really understand* Pundit!
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
  def user_not_authorized
    flash[:alert] = "You are not authorized to perform this action."
    redirect_to(root_path)
  end

  private

  def skip_pundit?
    devise_controller? || params[:controller] =~ /(^(rails_)?admin)|(^pages$)/
  end
end

pages_controller

class PagesController < ApplicationController
  skip_before_action :authenticate_user!, only: [:home]

  def home
  end
end

application_record.rb

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

booking.rb

class Booking < ApplicationRecord
  belongs_to :gardens
  has_many :users, through: :gardens
end

garden.rb

class Garden < ApplicationRecord
  belongs_to :user
  has_many :bookings
end

user.rb

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :gardens
  has_many :bookings, through: :gardens
end

application_policy.rb

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  def my_gardens?
    false
  end

  def show?
    scope.where(:id => record.id).exists?
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

  def scope
    Pundit.policy_scope!(user, record.class)
  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope
    end
  end
end

booking_policy.rb

class BookingPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      scope.all
    end
  end

  def create?
    true
  end

  def update?
    user_is_the_owner_or_admin?
  end

  def destroy?
    user_is_the_owner_or_admin?
  end

  def index?
    true
  end

  def show
    scope.where(:id => record.id).exists?
  end

  private

  def user_is_the_owner_or_admin?
    user.admin || record.booker == user
  end
end

garden_policy.rb

class GardenPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
        scope.all #>> permet d'afficher tous les restos dans index
    end
  end

  def create?
    true
  end

  def update?
    user_is_the_owner_or_admin?
  end

  def destroy?
    user_is_the_owner_or_admin?
  end

  def index?
    true
  end

  def show
    scope.where(:id => record.id).exists?
  end

  private

  def user_is_the_owner_or_admin?
    user.admin || record.user == user
  end
end

new_booking.html.erb

<h1>My booking</h1>
<%= simple_form_for [@garden, @booking] do |f| %>
<%= f.input :booker_id, disabled:true %>
<%= f.input :garden_id, disabled:true %>
<%= f.button :submit %>
<% end %>

show_garden.html.erb

<h1>My Garden</h1>
<ul>
<li>Title : <%= @garden.title  %></li>
<li>Address : <%= @garden.address %></li>
<li>Details : <%= @garden.details %></li>
<li>Surface : <%= @garden.surface %></li>
<p> Here will appear the availabilities to select</p>
<% if policy(@garden).destroy? %>
  <li><%= link_to "Delete" %> //
<% end %>
<% if policy(@garden).edit? %>
  <%= link_to "Edit" %></li>
<% end %>
<li><%= link_to "home", gardens_path %></li>
<li><%= link_to "My gardens", my_gardens_path %></li>
<li><%= link_to "Book the Garden", new_garden_booking_path(@garden) %></li> 
</ul>

routes.rb

Rails.application.routes.draw do
  devise_for :users
  delete 'gardens/:id', to: "gardens#destroy", as: :delete_garden
  resources :gardens, except: [:delete] do
    resources :bookings, only: [:new, :create, :show]
  end
  get "my_gardens", to: "gardens#my_gardens", as: :my_gardens
  root to: 'pages#home'
end

Solution

  • The problem lies in the way you are creating a booking. I have a couple of suggestions. First, you don't need a form to create a booking since you are only passing values you can use a link_to with aPOST method instead.

    First update your routes:

    # ...
    resources :gardens, except: [:delete] do
       resource :booking, only: :create  # notice resource is singular
    end
    

    Change the book the garden link to:

    <%= link_to "Book the Garden", garden_booking_path(@garden), method: :post %>
    

    To able to call booking.booker you need to update your Booking model association:

    class Booking < ApplicationRecord
      belongs_to :garden  # notice garden is singular
      belongs_to :booker, class_name: "User"
    end
    

    Then update your BookingsController create action:

    def create
      @garden  = Garden.find params[:garden_id]
      @booking = @garden.bookings.build(booker: current_user)
    
      # ... your pundit authorizations
    
      if @booking.save
        redirect_to @garden
      end
    end
    

    Notice that after a booking is created the controller redirects to the garden. This will allow you to show the booking. I think is better this way but you can redirect to the root_path if you prefer.