Search code examples
ruby-on-railsactiverecordpundit

Pundit raises an error, which I wouldn't have otherwise


I'm encountering a pundit response which I didn't expect and I was wondering if someone could tell me what is happening.

Issue

In my index action I would like to show attractions that below to a park. Currently, this resulted in the following unexpected behavior(for me at least):

  1. Before raising pundit, all attractions of a park are show. This is tested with 1. text to stop app

--> result >> @attractions => #<ActiveRecord::Associations::CollectionProxy [#<Attraction id: 185, park_id: 109, name: "Spider", description: "", status: nil, persons_max: 4, persons_min: 4, persons_included: 4, attraction_count: 4, enabled_attraction_count: nil, thumb: nil, included_services: nil, created_at: "2019-10-10 11:00:07", updated_at: "2019-10-12 07:31:27", photo: nil>, #<Attraction id: 189, park_id: 109, name: "Throwing balls at cans", description: nil, status: nil, persons_max: 1, persons_min: 1, persons_included: 1, attraction_count: 1, enabled_attraction_count: nil, thumb: nil, included_services: nil, created_at: "2019-10-15 14:29:28", updated_at: "2019-10-15 14:29:28", photo: nil>]>

  1. After including pundit it raises an error. This is tested with 2. text to stop app

--> result >> @attractions ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column attractions.user does not exist LINE 1: ...ERE "attractions"."park_id" = $1 AND "attracti... ^ : SELECT "attractions".* FROM "attractions" WHERE "attractions"."park_id" = $1 AND "attractions"."user" = $2 LIMIT $3 from /Users/xx/.rbenv/versions/2.5.3/lib/ruby/gems/2.5.0/gems/activerecord-5.2.3/lib/active_record/connection_adapters/postgresql_adapter.rb:677:in `prepare' from /Users/xx/.rbenv/versions/2.5.3/lib/ruby/gems/2.5.0/gems/activerecord-5.2.3/lib/active_record/connection_adapters/postgresql_adapter.rb:677:in `block in prepare_statement'[]

What I am doing wrong and how can I make sure to only display attractions of a park after pundit?

Code attractions controller

  def index
    @user = current_user
    if params[:park_id]
      @park = @user.parks.find(params[:park_id])
      @attractions = @park.attractions

      1. text to stop app

      @attractions = policy_scope(@attractions)

      2. text to stop app
    end
  end

policies/attraction_policy.rb

class AttractionPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if user.admin?
        scope.where(user: user)
      else
        raise Pundit::NotAuthorizedError
      end
    end
  end

  def index?
    user.admin?
  end
end

models

class Attraction < ApplicationRecord
  belongs_to :park
end

class Park < ApplicationRecord
  has_many :attractions, dependent: :destroy
  has_many :user_parks, dependent: :destroy
  has_many :users, through: :user_parks
end

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  has_many :user_parks, dependent: :destroy
  has_many :parks, through: :user_parks
  enum role: [:owner, :admin, :employee, :accountant]
  after_initialize :set_default_role, :if => :new_record?

  def set_default_role
    self.role ||= :admin
  end

  devise :invitable, :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :invitable
end

class UserPark < ApplicationRecord
  belongs_to :park
  belongs_to :user
end


Solution

  • You try to scope Attraction by user:

    scope.where(user: user)
    

    but you have neither user nor user_id column in your attractions table, so this obviously isn't gonna work. If you want to set up user association on attraction model, you need to do:

    rails g migration add_user_id_to_attractions user:references
    

    and add

    belongs_to :user
    

    to Attraction model.

    If you want to stick to your indirect association, you can also achieve your goal by modifying your scope a little bit, using joins:

    scope.joins(park: :user_parks).where(user_parks: { user_id: user.id })