I am building a digital library, and right now I am currently trying to add soft_delete to an application, but I am having issues with the error showing
ActionController::ParameterMissing in BooksController#update
param is missing or the value is empty: book
The action of the soft_delete method is to update its table in the database from its default false value to true. I have checked through my code but I cannot find where the issue is from.
Books Model
class Book < ApplicationRecord
#add a model scope to fetch only non-deleted records
scope :not_deleted, -> { where(soft_deleted: false) }
scope :deleted, -> { where(soft_deleted: true) }
#create the soft delete method
def soft_delete
update(soft_deleted: true)
soft_deleted
end
# make an undelete method
def undelete
update(soft_deleted: false)
end
end
Books Controller (Truncated)
class BooksController < ApplicationController
before_action :set_book, only: [:show, :edit, :update, :soft_delete, :destroy]
def index
@books = Book.not_deleted
end
...
def destroy
@book.destroy
respond_to do |format|
format.html { redirect_to books_url, notice: 'Book was successfully destroyed.' }
format.json { head :no_content }
end
end
def soft_delete
respond_to do |format|
@book.soft_delete(book_params)
format.html { redirect_to books_url, notice: 'Book was successfully deleted.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_book
@book = Book.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def book_params
params.require(:book).permit(:name, :author, :description, :soft_deleted)
end
end
Route
Rails.application.routes.draw do
resources :books
put '/books/:id' => 'books#soft_delete'
end
Books Index View
<h1>Books</h1>
<tbody>
<% @books.each do |book| %>
<tr>
<td><%= book.name %></td>
<td><%= book.author %></td>
<td><%= book.description %></td>
<td><%= link_to 'Show', book %></td>
<td><%= link_to 'Edit', edit_book_path(book) %></td>
<td><%= link_to 'Destroy', book, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<td><%= link_to 'Soft Delete', book, method: :put, data: { confirm: 'Are you sure?' } %></td>
soft_delete
</tr>
<% end %>
</tbody>
I suggest you not use explicit routes like
put '/books/:id' => 'books#soft_delete'
Rails has 'member routes', so you can rewrite your routes such way:
resources :books, only: %i[index destroy] do
member { put :soft_delete }
end
Here i've used option 'only' which allows you to define only necessary routes for you (not all 7, but 2 - index and destroy)
Firstly: you've defined resources :books
. This definition creates 7 routes for you:
books GET /books(.:format) -> books#index
POST /books(.:format) -> books#create
new_book GET /books/new(.:format) -> books#new
edit_book GET /books/:id/edit(.:format) -> books#edit
book GET /books/:id(.:format) -> books#show
PATCH /books/:id(.:format) -> books#update (this and next route are the same, and considering as 1, so we have 7 routes, not 8)
PUT /books/:id(.:format) -> books#update
DELETE /books/:id(.:format) -> books#destroy
So route with method PUT and path '/books/:id' already present and your put '/books/:id' => 'books#soft_delete'
will never be used. That is what triggered error: 'ActionController::ParameterMissing in BooksController#update ' - your link click has triggered BooksController#update action call, which requires book key in the params.
Secondly:
I suggest you to remove methods :soft_delete and :undelete from model. Rails provide methods :toggle! for instant writing to the DB and :toggle for changing value and not saving changes immediately . So you can write right in the controller such code:
book = Book.find_by(id: params[:id])
book.toggle!(:soft_deleted)
Note that column soft_deleted should be boolean to make that work.
your link shall be such:
link_to 'Soft Delete', soft_delete_book_path(book), data: { method: :put }
Rails is routing <%= link_to 'Soft Delete', book, method: :put...%>
to the update
action because resource :books
is defined before your custom route, and Rails use the first one matching the request.
Run rails routes -g books
in a terminal and you'll see something like:
books GET /books(.:format) books#index
POST /books(.:format) books#create
new_book GET /books/new(.:format) books#new
edit_book GET /books/:id/edit(.:format) books#edit
book GET /books/:id(.:format) books#show
PATCH /books/:id(.:format) books#update
PUT /books/:id(.:format) books#update
DELETE /books/:id(.:format) books#destroy
PUT /books/:id(.:format) books#soft_delete
As you can see, the routes for books#update
and books#soft_delete
are identical.
You can fix it by creating a named route: put '/books/:id' => 'books#soft_delete', as: 'soft_delete'
:
books GET /books(.:format) books#index
POST /books(.:format) books#create
new_book GET /books/new(.:format) books#new
edit_book GET /books/:id/edit(.:format) books#edit
book GET /books/:id(.:format) books#show
PATCH /books/:id(.:format) books#update
PUT /books/:id(.:format) books#update
DELETE /books/:id(.:format) books#destroy
soft_delete PUT /books/:id(.:format) books#soft_delete
Then, modify your template to use the new helper: <%= link_to 'Soft Delete', soft_delete_path(book), method: :put...%>