Search code examples
ruby-on-railsdevisecancan

How to allow users to only see edit and delete link if the article is their own?


My users are set up through Devise. I also can use CanCanCan.

I set up an articles model and any user can create articles. They can only delete and edit their own article creations. On the index, they can view all articles that have been created by all users. There is currently an option to View, Edit, and Delete. I only want that option visible on the articles that are owned by the user. I want all other article lines to be blank. (Except for admin of course.) Users can view posts on views/articles/index.html.erb

<table>
  <tr>
    <th>Title</th>
    <th>Description</th>
  </tr>

  <% @articles.each do |article| %>
    <tr>
      <td><%= article.title %></td>
      <td><%= article.description %></td>
      <td><%= link_to 'View', article_path(article) %></td>
      <td><%= link_to 'Edit', edit_article_path(article) %></td>
      <td><%= link_to 'Delete', article_path(article),
              method: :delete,
              data: { confirm: 'Are you sure?' } %></td>
    </tr>
  <% end %>
</table>

How can I allow users to only see the Edit and Delete button on the post that they own?

I tried this but it does not work:

<table>
  <tr>
    <th>Title</th>
    <th>Description</th>
  </tr>

  <% @articles.each do |article| %>
    <tr>
      <td><%= article.title %></td>
      <td><%= article.description %></td>
      <td><%= link_to 'View', article_path(article) %></td>
      <% if user_signed_in? && current_user.articles.exists?(@article.id) %>
      <td><%= link_to 'Edit', edit_article_path(article) %></td>
      <td><%= link_to 'Delete', article_path(article),
              method: :delete,
              data: { confirm: 'Are you sure?' } %></td>
      <% end %>
    </tr>
  <% end %>
</table>

I've also tried:

      <% if current_user && current_user.articles.exists?(@article.id) %>

Here is what my articles controller looks like: (I know I need to make it look better.)

def create
  @article = current_user.articles.build(article_params)

  if @article.save
    redirect_to @article
  else
    render 'new'
  end
end


def update
  @article = Article.find(params[:id])
  if user_signed_in? && current_user.articles.exists?(@article.id)
    if @article.update(article_params)
      redirect_to @article
    else
     render 'edit'
    end
  elsif current_user && current_user.admin_role?
    if @article.update(article_params)
      redirect_to @article
    else
     render 'edit'
    end
  else
    redirect_to @article
  end
end

def destroy
  @article = Article.find(params[:id])
  if user_signed_in? && current_user.articles.exists?(@article.id)
    @article.destroy
    redirect_to articles_path
  elsif current_user && current_user.admin_role?
    @article.destroy
    redirect_to articles_path
  else
    redirect_to articles_path
  end
end

Solution

  • As you have access to the current_user helper provided by Devise, then you can compare it with the article's owner. This can be in the view, to render the proper links to perform the actions:

    <% @articles.each do |article| %>
      <tr>
        <td><%= article.title %></td>
        <td><%= article.description %></td>
        <td><%= link_to 'View', article_path(article) %></td>
        <% if current_user == article.user %>
          <td><%= link_to 'Edit', edit_article_path(article) %></td>
          <td><%= link_to 'Delete', article_path(article),
                  method: :delete,
                  data: { confirm: 'Are you sure?' } %></td>
        <% end %>
      </tr>
    <% end %>
    

    You can move this verification to a helper, in the ApplicationHelper to be available in most of your views, like:

    module ApplicationHelper
      def owner?(object)
        current_user == object.user 
      end
    end
    

    You pass the object, and this returns true or false depending if the current user is equal to the object user. The view only changes to:

    <% if owner?(article) %>
    

    If you want to move this verification to the controller, or also if you want to use it in both cases, then you can do the same verification in the controller. As the owner? helper method isn't available in the controller, you can just redirect back in case the current_user isn't the article's owner, like:

    def edit
      unless current_user == @article.user
        redirect_back fallback_location: root_path, notice: 'User is not owner'
      end
    end
    

    If you want to move this part to a before callback, to be able to use it in the edit and destroy method, then you can add it as a private method, and having access to the @article you can do such comparison, it'd be:

    private
    
    def owner?
      unless current_user == @article.user
        redirect_back fallback_location: root_path, notice: 'User is not owner'
      end
    end
    

    This way you just need to add it as a before_action callback:

    before_action :owner?, only: %i[edit destroy]
    

    Most probably before the one that defines the @article variable.

    Note the use of redirect_back in Rails 5, previous versions might use redirect_to :back.