I'm coding a simple blog with posts and I can't seem to be able to get the code right for going back to the index page after "destroying" a post. I'm following this tutorial and I'm stuck around minute 14
Tutorial
I'm working with ruby 3.0.3 and Rails 7.0.3, the error I'm getting is the following:
Error
Below are the relevant pieces of code:
Controller
class PostsController < ApplicationController
before_action :find_post, only: [:show, :edit, :update, :destroy]
def index
end
def new
#post = Post.new
end
def create
#post = Post.new post_params
if #post.save
redirect_to #post, notice: "post has been saved"
else
render 'new', notice: "Post could NOT be saved"
end
end
def show
end
def edit
end
def update
if #post.update post_params
redirect_to #post, notice: "Your article has been updated"
else
render 'edit'
end
end
def destroy
#post.destroy
respond_to do posts_path
end
end
private
def post_params
params.require(:post).permit(:title, :content)
end
def find_post
#post = Post.find(params[:id])
end
end
Show (view)
<%= #post.title %><br>
<%= #post.content %>
<br>
<br>
<%= link_to "Edit", edit_post_path(#post) %>
<%= link_to "Delete", post_path(#post), data: { :turbo_method => :delete,
:turbo_confirm => 'Are you sure?'} %>
Routes
Rails.application.routes.draw do
resources :posts
end
Migration
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title
t.text :content
t.timestamps
end
end
end
I would deeply appreciate any help.
# DELETE /posts/1
def destroy
#post.destroy
redirect_to posts_url, notice: "Post was successfully destroyed."
end
This destroy method works with button_to:
<%= button_to "Destroy post", post_path(#post), method: :delete %>
If you want to use link_to:
<%= link_to "Destroy post", post_path(#post), data: { turbo_method: :delete } %>
redirect status has to be changed to status: :see_other:
# DELETE /posts/1
def destroy
#post.destroy
redirect_to posts_url, status: :see_other, notice: "Post was successfully destroyed."
end
Turbo DELETE request work in progress: https://github.com/hotwired/turbo-rails/issues/259
When you didn't find a record, is no way to destroy it. Rewrite post finder to:
def find_post
#post = Post.find_by(id: params[:id])
end
I replaced respond_to with redirect_to, but it is not a principal:
def destroy
#post.destroy
ensure
redirect_to posts_path
end
I am building a simple blog app using Ruby on Rails that allows users to log in/out, sign up and perform actions on their articles and profiles based on permissions and restrictions.
I have encountered a problem with the destroy User action. In the users/index view(where all existing users are listed), it causes no errors due to the url path containing no {:id}, but the redirect_to root_path does not work. If the same action is executed in the users/show page(personal profile page with some info and associated articles), due to the url being localhost/users/id, when the user is deleted I get "Couldn't find User with 'id'=33" error shown below. If I manually go to the root route, the successful account deletion message shows up and the action is performed correctly. So this is not a metter of DESTROY not working, but of redirection I believe. I have tried redirecting to different paths but it still doesn't work. Here are the related files:
routes.rb
Rails.application.routes.draw do
root "pages#home"
get "about", to: "pages#about"
resources :articles
get "signup", to: "users#new"
resources :users, except: [:new]
get 'login', to: 'sessions#new'
post 'login', to: 'sessions#create'
get 'logout' => :destroy, to: 'sessions#destroy'
end
pages_controller
class PagesController < ApplicationController
def home
redirect_to articles_path if logged_in?
end
def about
end
end
users_controller
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
before_action :require_user, only: [:edit, :update]
before_action :require_same_user, only: [:edit, :update, :destroy]
def index
#users = User.all
end
def show
#articles = #user.articles
end
def new
#user = User.new
end
def edit
end
def create
#user = User.new(user_params)
if(#user.save)
session[:user_id] = #user.id #logs user in automatically once they are signed up
flash[:notice] = "Welcome to AlphaBlog, #{#user.username}!"
redirect_to articles_path
else
render 'new'
end
end
def update
if #user.update(user_params)
flash[:notice] = "Account updated!"
redirect_to #user
else
render 'edit'
end
end
def destroy
#user.destroy
session[:user_id] = nil
flash[:notice] = "Account and all associated articles deleted!"
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:username, :email, :password)
end
def set_user
#user = User.find(params[:id])
end
def require_same_user
if current_user != #user
flash[:alert] = "You can only edit your own profile!"
redirect_to current_user
end
end
end
sessions_controller
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
session[:user_id] = user.id
flash[:notice] = "Logged in successfully!"
redirect_to user
else
flash.now[:alert] = "There was something wrong with your login details!"
render 'new'
end
end
def destroy
session[:user_id] = nil
flash[:notice] = "Logged out."
redirect_to root_path
end
end
users/index.html.erb
<div class = "header">
<h1>
AlphaBlog
<% if logged_in? %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<% else %>
<%= link_to 'Home', root_path(), method: :get, class: "index-button-to" %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<% end %>
<%= render 'layouts/log_in_out_navigation'%>
</h1>
</div>
<h2>Alpha Bloggers</h2>
<div class="index-container">
<%# cycle through all articles and show them all in a table %>
<% #users.each do |user| %>
<div class = "index-article-container">
<div class="index-article-user" style = "color:rgb(16, 136, 255);">
<%= user.username %>
</div>
<div class="white">
<div class="index-article-title">
<%= gravatar_for(user, size: 150) %>
</div>
<div class="index-article-description">
<%# gives the plural word for multiple articles %>
<%= pluralize(user.articles.count, "article") %>
</div>
<div class="index-article-actions">
<%# shows selected article page %>
<%= link_to 'View Profile', user, class: "index-link-to show" %>
<% if logged_in? && current_user.username == user.username %>
<%# shows selected article EDIT page. edit_article_path because in routes,
the prefix for edit is edit_article && (article) because we need the id for the path as well%>
<%= link_to 'Edit Profile', edit_user_path(user), data: { turbo_method:
:get}, class: "index-link-to edit" %>
<%= link_to 'Delete Profile', user_path(current_user), data: {
turbo_method: :delete, turbo_confirm: "Are you sure? (This will also delete all of your
articles)" }, class: "index-link-to delete" %>
<% end %>
</div>
</div>
<div class="index-created-updated">
Joined <%= time_ago_in_words(user.created_at) %> ago.
</div>
</div>
<% end %>
users/show.html.erb
<div class = "header">
<h1>
AlphaBlog
<% if logged_in? %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<%= link_to 'Bloggers', users_path, method: :get, class: "index-button-to" %>
<% else %>
<%= link_to 'Home', root_path(), method: :get, class: "index-button-to" %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<%= link_to 'Bloggers', users_path, method: :get, class: "index-button-to" %>
<% end %>
<%= render 'layouts/log_in_out_navigation'%>
</h1>
</div>
<h2> <%= #user.username %>'s profile </h2>
<div class="show-users-image">
<%# gravatar_for method created in helpers/application_helper %>
<%= gravatar_for #user, size: 200 %>
<% if logged_in? && current_user.username == #user.username %>
<div class="index-profile-actions">
<%= link_to "Edit Profile", edit_user_path(#user), class: "index-link-to edit" %>
<%= link_to 'Delete Profile', user_path(current_user), data: { turbo_method: :delete,
turbo_confirm: "Are you sure? (This will also delete all of your articles)" }, class: "index-
link-
to delete", style: "margin-top:0.3vh" %>
</div>
<% end %>
</div>
<h3 style = "text-align:center">Articles</h3>
<%= render 'articles/article' %>
error page
The way to do this in Rails 7 is to update the destroy action in the UsersController by adding status: :see_other after the redirect, as follows:
def destroy
#user.destroy
session[:user_id] = nil
flash[:notice] = "Account and all associated articles deleted!"
redirect_to root_path, status: :see_other
end
I think the answer here is really a very different layout of your routes and controller (or to not reivent the wheel in the first place). Passing the user id through the parameters would be fine if your making a system where you are managing other users - but its pretty wonky when users are managing their own profiles.
For example this is how users CRUD their own profiles in a vanilla Devise setup:
Verb URI Pattern Controller#Action
------------------------------------------------------------------------
GET /users/cancel(.:format) devise/registrations#cancel
GET /users/sign_up(.:format) devise/registrations#new
GET /users/edit(.:format) devise/registrations#edit
PATCH /users(.:format) devise/registrations#update
PUT /users(.:format) devise/registrations#update
DELETE /users(.:format) devise/registrations#destroy
POST /users(.:format) devise/registrations#create
Note the lack of the :id parameter in the URI Pattern. Thats because its implied that the resource in question is the currently signed in user, and that the user is identified through the session (or a token).
The controller is named Registrations to avoid the ambiguity if the programmer later wants to add a UsersController to manage other users.
If you want to do something similiar you can generate singular routes by using the resource macro instead of resources.
# routes for user registration
resource :registrations,
only: [:new, :edit, :update, :create, :destroy]
# routes for viewing other users
resources :users, only: [:index, :show]
Which will generate:
Prefix Verb URI Pattern Controller#Action
-----------------------------------------------------------------------
new_registrations GET /registrations/new(.:format) registrations#new
edit_registrations GET /registrations/edit(.:format) registrations#edit
registrations GET /registrations(.:format) registrations#show
PATCH /registrations(.:format) registrations#update
PUT /registrations(.:format) registrations#update
DELETE /registrations(.:format) registrations#destroy
POST /registrations(.:format) registrations#create
Name it whatever you want. The core takeaway here is to not confuse two completely different problems - user management and user registrations and have separate endpoints and controllers for each responsibilty.
Then in your controller you simply authenticate the user from the session and redirect the user if they are not authenticated:
# Handles user account registration, updates and deleting accounts
class RegistrationsController < ApplicationController
before_action :require_user, except: [:new, :create]
# Displays the form for signing up a user
# GET /registrations
def new
#user = User.new
end
# Register a new user and sign them in
# POST /registrations
def create
#user = User.new(user_params)
if #user.save
reset_session # avoids session fixation attacks
session[:user_id] = #user.id #logs user in automatically once they are signed up
flash[:notice] = "Welcome to AlphaBlog, #{#user.username}!"
redirect_to articles_path
else
render :new
end
end
# Form for editing the users own profile
# GET /registrations/edit
def edit
#user = current_user
end
# Update the currently signed in user
# PATCH /registrations
def update
#user = current_user
if #user.update(user_params)
flash[:notice] = "Account updated!"
redirect_to current_user
else
render :new
end
end
# Cancel the current users registration
# DELETE /registrations
def delete
current_user.delete
reset_session # avoids session fixation attacks
flash[:notice] = "Account and all associated articles deleted!"
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:username, :email, :password)
end
end
# Displays users
# Managing accounts is handled by RegistrationsController
class UsersController < ApplicationController
# GET /users
def index
#users = User.all
end
# GET /users/1
def show
#user = User.find(params[:id])
#articles = #user.articles
end
end
Since their is no id in the path you you need to set the delete button to send to the right path:
<%= button_to "Delete your account", registrations_path, method: :delete %>
And adjust your forms:
<%= form_with(model: #user, url: registrations_path) do |form| %>
# ...
<% end %>
Doing this the correct way would really be to do it the same way that Devise does and have a normal link that sends a GET request to a "Are you sure you want to delete your account?" page when then requires the user to enter their password or email and submits a DELETE request so that users don't accidentially delete their accounts.
But then again don't reinvent the authentication wheel unless you want a long and tedious lesson into wheelmaking.
I'm not sure you have truly got to the bottom of this. In your original approach I suspect two things are going on:
In users/index.html.erb you have link_to 'Delete Profile', user_path(current_user) but I think you want user_path(user). What you currently have will have every delete button try to delete the same user.
The fact that error says your are attempting to execute 'show' rather than 'destroy' makes me suspect that you do not have turbo loaded properly. You don't say what version of rails you are using, but for versions earlier than 7 you don't get turbo out of the box, and you should use UJS instead.
compare this https://guides.rubyonrails.org/getting_started.html#deleting-an-article with this https://guides.rubyonrails.org/v6.1/getting_started.html#deleting-an-article
I am creating a new app with simple authentication, but i get this error:
No route matches {:action=>"show", :controller=>"users", :id=>nil},
missing
required keys: [:id]
I have tried other stackoverflow solutions but none have seemed to work for me.
This is my users_controller:
class UsersController < ApplicationController
def new; end
def create
#user = User.create(password: params[:password], email: params[:email],
firstname: params[:firstname], lastname: params[:lastname])
redirect_to user_path(#user)
end
def show
#user = User.find(params[:id])
end
end
My sessions controller:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by_email(params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_url, notice: "Logged in!"
else
flash.now[:alert] = "Email or password is invalid"
render "new"
end
end
def destroy
session[:user_id] = 1
redirect_to root_url, notice: "Logged out!"
end
end
routes.rb :
Rails.application.routes.draw do
# Fubyonrails.org/routing.html
root :to => 'static#welcome'
resources :users, only: [:new, :create, :show]
resources :sessions, only: [:new, :create, :destroy]
resources :studios
end
<h1> Hey, <%= #user.firstname %>! </h1>
<h3>Home</h3>
<%= link_to "Log Out", session_path, method: :delete %>
<%= stylesheet_link_tag 'users', media: 'all', 'data-turbolinks-track' =>
true %>
The problem is after create a user? If yes, make sure that the user is being created and exists. The problem could be that #user is nil
Check your user_show route it would be like
users/:id/show
You have to pass param id along with your request to show that id object. Right now you're call is missing param id.
You're call should be like this
users/1/show
Where as 1 is the user id in DB
I set up posts in a folder called 'contents' and then it fell over when I tried to delete the records. Why am I getting this error?
No route matches {:action=>"edit", :controller=>"content/stories", :id=>nil} missing required keys: [:id]
Routes.rb
namespace :content do
resources :posts
end
PostsController
def index
#posts = Post.all
end
def new
#post = Post.new
end
def create
#post = current_user.posts.new(post_params)
if #post.save
redirect_to content_posts_path
else
redirect_to root_path, notice: #post.errors.full_messages.first
end
end
def show
end
def delete
#post = Post.find(params[:id])
end
def destroy
post = Post.find(params[:id]).destroy
redirect_to :back
end
private
def post_params
params.require(:post).permit(:content)
end
end
show.html
<ol>
<% for p in #posts %>
<li>
<%= p.title %>
<%= link_to 'Edit', edit_content_post_path(#post) %>
<%= link_to 'Delete', content_post_path(#post), method: :delete %>
</li>
<% end %>
</ol>
#post is not defined, you probably need to use p in place of #post.
Also in your show.html which corresponds to show action, not sure how you are getting #posts.
I'm currently trying to implement likes and unlikes rails app using thumbs_up. I followed the instructions on this page: Clarification on how to use "thumbs_up" voting gem with Rails 3
Users can like and unlike books. I have 2 buttons, like and unlike and I would like to hide one or the other from the user, depending on the users current like status. So I figured an if else would be appropriate like this:
<% if #user.voted_on?(#book) %>
<div class="unlike_button"><%= link_to("Unlike", unvote_book_path(book), method: :post) %></div>
<% else %>
<div class="like_button"><%= link_to("Like", vote_up_book_path(book), method: :post) %></div>
<% end %>
and in my route.rb file:
resources :books do
member do
post :vote_up
post :unvote
end
end
But when I run this I get the error message:
undefined method `voted_on?' for nil:NilClass
Is there anything I might be doing wrong?
Update
As Mischa suggested, I changed it to current_user.voted_on. Now I get this error message:
Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
Below is a snippet of my Books controller
include UsersHelper
include SessionsHelper
before_filter :signed_in_user, only: [:index]
before_filter :admin_user, only: :destroy
def index
array = Book.search(params[:search])
#books = Kaminari.paginate_array(array).page(params[:page]).per(5)
end
def show
#book = Book.find(params[:id])
#respond_to do |format|
#format.js
#end
end
def destroy
Book.find(params[:id]).destroy
flash[:success] = "Book deleted."
redirect_to books_url
end
def vote_up
begin
current_user.vote_for(#book = Book.find(params[:id]))
flash[:success] = "Liked!."
redirect_to books_url
end
end
def unvote
begin
current_user.unvote_for(#book = Book.find(params[:id]))
flash[:success] = "Unliked!."
redirect_to books_url
end
end
You should change your method of link from post to put and
change your routes.rb to
resources :books
You can also update a data using a link by using this code:
<%= link_to "Like", book, :method => :put %>
or
<%= link_to "Unlike", book, :method => :put %>
and this goes with your controller in
assuming that you have a is_liked boolean field in your Post model
def update
#post = Post.find(params[:id])
if #post.is_liked?
#post.attributes = {
:is_liked => "f"
}
redirect_to posts_path, :flash => "Unliked"
else
#post.attributes = {
:is_liked => "t"
}
redirect_to posts_path, :flash => "Liked"
end
end