Search code examples
ruby-on-railsruby-on-rails-4devisepundit

Pundit policy error undefined method `image' for nil:NilClass


I have been stuck with this issue for quite some time now and not sure what I am doing wrong.

I am using Rails 4.2.5.1, Pundit 1.1.0 and Devise.

I have a blog post which displays the following:

  • title
  • author username
  • image
  • sanitized excerpt (on index page)
  • body (on show page)

The index page displays correctly (with exception of the author username which does not show, because it doesn't recognize the username param). However, when I try to view an individual post via the show page, I get the following error:

undefined method `image' for nil:NilClass

If I remove that line of code for displaying the image, I get an error for the title with the same undefined method error.

I have followed the example at SitePoint-source/Authorization_with_Pundit almost exactly for policies and the controllers (only minor modifications)

Everything was working perfectly before adding Pundit for creating authorization between admins, editors, and users.

Here is my current code:

Application Controller

class ApplicationController < ActionController::Base
  include Pundit
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  before_filter :configure_permitted_parameters, if: :devise_controller?

  private

    def user_not_authorized
      flash[:alert] = "Access denied. You are not authorized to view that page."
      redirect_to (request.referrer || root_path)
    end


protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up) { |u| u.permit(:username, :email, :password, :password_confirmation, :remember_me) }
    devise_parameter_sanitizer.permit(:sign_in) { |u| u.permit(:username, :email, :password, :remember_me) }
    devise_parameter_sanitizer.permit(:account_update) {|u| u.permit(:username, :email, :password, :password_confirmation, :current_password)}
  end


end

Post Controller

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  after_action :verify_authorized, only: [:destroy]
  after_action :verify_policy_scoped, only: [:user_posts]

  def index
    @meta_title = "Blog"
    @meta_description = "description here"
    @posts = Post.all.order("created_at DESC").paginate(:page => params[:page], :per_page => 4)
  end

  def show
  end

  def new
    @meta_title = "Add New Blog"
    @meta_description ="Add a new blog to your profile."
    @post = Post.new
  end

  def edit
    @meta_title = "Edit Blog"
    @meta_description ="Edit an existing blog from your profile."
  end

  def create
    @post = Post.new
    @post.update_attributes(permitted_attributes(@post))

    if @post.save
      redirect_to @post, notice: 'Post was successfully created.'
    else
      render :new
    end
  end

  def update

    @post = Post.find(params[:id])
    if @post.update_attributes(permitted_attributes(@post))
      redirect_to @post, notice: 'Post was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    if @post.present?
      authorize @post
      @post.destroy
    else
      skip_authorization
    end

    redirect_to posts_url, notice: 'Post was successfully deleted.'
  end

  def user_posts
    @posts = policy_scope(Post)
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_post
      @post = Post.find_by(id: params[:id])
    end

    # Only allow the white list through.
    def post_params
      params.require(:post).permit(policy(@post).permitted_attributes)
    end
end

Application Policy

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    raise Pundit::NotAuthorizedError, "You must be logged in to perform this action" unless user
    @user = user
    @record = record
  end

  def index?
    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

Post Policy

class PostPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      scope.where(user: user)
    end
  end

  def permitted_attributes
    if user.admin? || user.editor?
      [:title, :body, :image, :permalink, :description, :tag_list, :username]
    else
      [:title, :body, :image, :username]
    end
  end

  def new?
    user.admin? || user.editor?
  end

  def index?
    true
  end

  def create?
    user.admin? || user.editor?
  end

  def update?
    user.admin? || user.editor? || record.user == user
  end

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

Post.rb

class Post < ActiveRecord::Base
    include ActiveModel::ForbiddenAttributesProtection
    belongs_to :user

    # This method associates the attribute ":image" with a file attachment
    has_attached_file :image, styles: { 
        thumb: '100x100>',
        square: '200x200#',
        medium: '300x300>',
    }

    extend FriendlyId
    friendly_id :permalink, use: [:slugged, :history, :finders]
    validates :permalink, presence: true, uniqueness: true
    validates :title, presence: true, length: { minimum: 5}
    validates :description, presence: true, uniqueness: true, length: {maximum: 160}
    validates :body, presence: true
    validates :image, presence: true
    # Validate the attached image is image/jpg, image/png, etc
    validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/

    def should_generate_new_friendly_id?
        permalink_changed?
    end
end

Post#show

<% provide(:title, "@post.title") %>
<% provide(:description, "@post.description") %>

<div class="row">
  <div class="col-md-offset-1 col-md-10">
    <div class="panel panel-default">
      <div class="panel-heading center">
        <%= image_tag @post.image.url, :style => "width: 100%; height: auto;" %>
      </div>
      <div class="panel-body">
        <h2 class="title center"><%= @post.title %></h2>
        <p class="posted"><i class="ion-android-time"></i> <%= @post.created_at.strftime("%B %d, %Y") %> </p>
        <p class="posted"><i class="ion-person"></i> Author: <%= link_to @post.username, about_path(:anchor => "coaches") %></p>
        <hr>
        <div class="postBody" id="summernote">
          <%= @post.body.html_safe %>
        </div>
      </div>
      <div class="panel-footer center">
        <%= link_to 'Back', posts_path %> |
        <%= link_to 'Edit', edit_post_path(@post) %> | 
        <%= link_to 'Delete', @post, method: :delete, data: { confirm: 'Are you sure you want to delete this post?' } %>
        <%= render 'disqus' %>
      </div>
      <div class="panel-footer center">
        <%= link_to 'Back', posts_path %>
      </div>
    </div>
  </div>
</div>

Post#index

<div class="container">
  <div class="row">
    <div class="col-md-9">
      <% @posts.each do |post| %>
        <div class="post-wrapper">
          <h3 class="title center"><%= link_to post.title, post %></h3>
          <p class="posted"><i class="ion-android-time"></i> <%= post.created_at.strftime("%B %d, %Y") %></p>
          <p class="posted"><i class="ion-person"></i> Author: <%= link_to post.user(:username), about_path(:anchor => "coaches") %></p><br>
          <div class="post-image center"><%= link_to image_tag(post.image.url, :style => "width: 100%; height: auto;"), post %></div><br>

            <%= sanitize(post.body[0,300]) %>...<br>
            <div class="center">
              <%= link_to 'View Blog', post, class: "btn btn-primary" %>
              <% if policy(post).update? %>
                <%= link_to 'Edit', edit_post_path(post) %> |
              <% end %>
              <% if policy(post).destroy? %>
                <%= link_to 'Delete', post, method: :delete, data: { confirm: 'Are you sure?' } %>
              <% end %>
            </div>
          <br>
        </div>
      <% end %>
      <div class="center">
        <%= will_paginate @posts, renderer: BootstrapPagination::Rails %>
      </div>
    </div>
  </div>
</div>

I also have a few other issues that hopefully will resolve on their own once this issue is fixed:

  • Deleted posts get a flash message saying they delete, but they are still there
  • Edit post gets the same image error message
  • Non-signed in users are denied access to view posts and I want them to be able to view all posts regardless of if signed in or not. This is the same issue, but the solution is not working for me and I don't get any type of rails error message: Pundit policy_scope error. Maybe this has something to do with the initialize in the App Policy?

These other issues can be solved later or if you see an error I'd be grateful for help.

Right now my main issue trying to solve the undefined method "image" for nil:NilClass error


Solution

  • undefined method `image' for nil:NilClass
    

    This means that the object you're trying to call .image on (which is @post) is nil. Trace that back, and find out why it's nil.

    In your Posts#show view, you're relying on the set_post callback to set your post. The set_post callback uses Post.find_by with an id parameter to find the record.

    The way find_by behaves is to return nil if the parameter it's given is nil (i.e. if you call Post.find_by(id: nil), you'll get nil back). This may indicate that params[:id] is itself nil - check that it's set as a query string parameter (example.com/posts/show?id=12) or as part of the URL itself (example.com/posts/12).

    If you can't tell, add a byebug call to your Posts#show action:

    def show
      byebug
    end
    

    This will halt the action as it executes, and gives you a console to work with - at this point, you can type params[:id] to find out what its value is.

    I recommend, instead of using Post.find_by, you use Post.find. The difference is that find defaults to using the ID (so you don't need to specify which parameter you're using), and it raises a 404 Not Found response instead of returning nil if it can't find the record. To do this, your set_post callback should look like this:

    def set_post
      @post = Post.find params[:id]
    end