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:
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:
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
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