【问题标题】:Redirect in destroy action not working properly销毁操作中的重定向无法正常工作
【发布时间】:2022-07-10 06:59:06
【问题描述】:

我正在使用 Ruby on Rails 构建一个简单的博客应用程序,它允许用户根据权限和限制登录/注销、注册并对其文章和个人资料执行操作。 我遇到了销毁用户操作的问题。在 users/index 视图(列出所有现有用户)中,由于 url 路径不包含 {:id},它不会导致错误,但 redirect_to root_path 不起作用。如果在用户/显示页面(包含一些信息和相关文章的个人资料页面)中执行相同的操作,由于 url 是 localhost/users/id,当用户被删除时,我得到“找不到'id'=33" 错误的用户如下所示。如果我手动转到根路由,则会显示成功的帐户删除消息并且操作正确执行。因此,我相信这不是 DESTROY 不起作用的标准,而是重定向的标准。我已经尝试重定向到不同的路径,但它仍然不起作用。以下是相关文件:

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' %>

错误页面

【问题讨论】:

  • 您的routes.rb 是什么样的?你的root_path 是如何定义的?
  • 抱歉文件没有,我现在编辑它
  • 我不知道你为什么在indexshow 页面上都有user_path(current_user) 用于destroy 操作。相反,它应该是user_path(@user),并且应该有上面的条件来根据权限显示或不显示链接。这就是您可能会收到此错误的原因。
  • current_user 和 @user 几乎是一回事,唯一的区别是 current_user 用于与保存当前会话详细信息相关的逻辑。我尝试了您的建议,但不幸的是它没有解决我的问题
  • 最终按照您的回答以更简化的方式解决了这个项目。如果您有兴趣,可以查看它。再次感谢

标签: ruby-on-rails ruby redirect crud destroy


【解决方案1】:

我认为这里的答案实际上是您的路线和控制器的布局非常不同(或者首先不重新启动轮子)。如果您创建一个管理其他用户的系统,通过参数传递用户 ID 会很好 - 但当用户管理他们自己的个人资料时,它会非常不稳定。

例如,这是用户在 vanilla Devise 设置中 CRUD 自己的配置文件的方式:

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  

请注意 URI 模式中缺少 :id 参数。那是因为它暗示有问题的资源是当前登录的用户,并且用户是通过会话(或令牌)识别的。

控制器被命名为Registrations以避免在程序员以后想要添加一个UsersController来管理其他用户时产生歧义。

如果您想做类似的事情,您可以使用resource 宏而不是resources 来生成singular routes

# routes for user registration
resource :registrations, 
    only: [:new, :edit, :update, :create, :destroy]

# routes for viewing other users 
resources :users, only: [:index, :show]

会生成:

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

随意命名。这里的核心要点是不要混淆两个完全不同的问题 - 用户管理用户注册,并且每个职责都有单独的端点和控制器。

然后在您的控制器中,您只需从会话中对用户进行身份验证,如果用户未通过身份验证,则重定向用户:

# 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

由于它们在路径中没有 id,您需要设置删除按钮以发送到正确的路径:

<%= button_to "Delete your account", registrations_path, method: :delete %>

并调整您的表格:

<%= form_with(model: @user, url: registrations_path) do |form| %>
  # ...
<% end %>

这样做的正确方法实际上是按照与 Devise 相同的方式进行操作,并且有一个正常的链接,该链接将 GET 请求发送到“您确定要删除您的帐户吗?”然后页面要求用户输入他们的密码或电子邮件并提交删除请求,以便用户不会意外删除他们的帐户。

但话又说回来,除非您想上一堂冗长乏味的车轮制造课程,否则不要重新发明认证轮子。

【讨论】:

  • 感谢您的详细解答。我目前对 Ruby & Rails 非常陌生,这是我的第一个项目。我正在通过在线视频课程构建此页面,并且非常惊讶地发现它对我不起作用,而对讲师却如此。我的意思是,我可能需要一段时间才能完全理解您的解决方案并将其正确付诸实​​践。因此,如果您确定此解决方案有效(逻辑对我来说似乎是正确的),请告诉我,以便我将此答案标记为我的问题的解决方案。
  • 我不能保证这是一个可行的解决方案。它更多地是作为解释概念的示例。我对这没有成功并不感到惊讶 - 那里有大量的教程涵盖了从头开始进行身份验证,而其中有用的教程相对较少。视频教程 - 尤其是在 youtube 上的教程通常很糟糕。
  • 我明白了,谢谢。如果它在我的应用程序中有效,我会确保将其标记为解决方案。
【解决方案2】:

我不确定您是否真正了解了这一点。在您最初的方法中,我怀疑发生了两件事:

  1. 在 users/index.html.erb 你有link_to 'Delete Profile', user_path(current_user) 但我认为你想要user_path(user)。您目前拥有的每个删除按钮都会尝试删除同一个用户。

  2. 错误表明您正在尝试执行“show”而不是“destroy”,这让我怀疑您没有正确加载 turbo。你没有说你使用的是什么版本的 rails,但是对于早于 7 的版本,你没有开箱即用的 turbo,你应该改用 UJS。 比较这个https://guides.rubyonrails.org/getting_started.html#deleting-an-article 和这个https://guides.rubyonrails.org/v6.1/getting_started.html#deleting-an-article

【讨论】:

  • 我使用的是 Rails 7,我也尝试了 user_path(user) 的方式,它的工作原理是一样的。问题出在控制器的功能中,要求在删除后删除 id。所以我将 destroy_user 移动到另一个控制器,正如我对自己问题的回复中所解释的那样。不过,感谢您的回答。
【解决方案3】:

在 Rails 7 中执行此操作的方法是通过在重定向后添加 status: :see_other 来更新 UsersController 中的 destroy 操作,如下所示:

def destroy
    @user.destroy
    session[:user_id] = nil
    flash[:notice] = "Account and all associated articles deleted!"
    redirect_to root_path, status: :see_other
end

【讨论】:

    【解决方案4】:

    所以我终于自己解决了这个问题,使用了 max 答案的简化版本。

    由于users_controller 中的销毁操作将nil 值分配给此行中会话的用户ID,因此发生错误

    session[:user_id] = nil
    

    这导致应用程序抛出这个丑陋的错误,尽管操作本身正在正确执行,因为在users_controller 的其他操作中需要user_id(在错误的情况下,set_user

    我通过将销毁操作从users_controller 中取出并作为destroy_user 放入sessions_controller 解决了这个问题,这样它就可以独立于users_controller 代码的其余部分,并创建了一个特定的路线对于此操作。

    总结一下:

    1. users_controller删除销毁

    2. sessions_controller中添加destroy_user如下:

      def destroy_user
          @user = User.find_by(id: session[:user_id])
          if current_user != @user
              flash[:alert] = "You can only edit your own profile!"
              redirect_to current_user
          end
          @user.destroy
          session[:user_id] = nil
          flash[:notice] = "Account and all associated articles deleted!"
          redirect_to root_path
      end
      
    3. 将此路径添加到routes.rb

      get 'destroy_user' => :destroy, to: 'sessions#destroy_user'
      
    4. 删除个人资料链接更改为:

      <%= link_to 'Delete Profile', destroy_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" %>
      

    【讨论】:

      猜你喜欢
      • 2018-12-24
      • 2017-09-23
      • 2015-08-09
      • 2014-07-01
      • 2015-12-21
      • 2017-03-04
      • 1970-01-01
      相关资源
      最近更新 更多