3

I am trying to learn how to use Pundit with my Rails 4 app.

I have the following models:

class User < ActiveRecord::Base
  has_one :profile
  has_many :eois
end

class Profile < ActiveRecord::Base
  belongs_to :user
  has_many :projects, dependent: :destroy
end

class Project < ActiveRecord::Base
  belongs_to :profile
  has_many :eois
end

class Eoi < ActiveRecord::Base
  belongs_to :project
  belongs_to :user
end

I have a scoped EoiPolicy with:

class EoiPolicy < ApplicationPolicy

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user  = user
      @scope = scope
    end

    def resolve
      if user.profile.project.id == @eoi.project_id?
        scope.where(project_id: @user.profile.project.id)
      elsif user.id == eoi.user_id?
        scope.where(user_id: user.id)
      else
        nil
      end
    end
  end

  def index?
    user.profile.project.id == @eoi.project_id? or user.id == eoi.user_id?
  end

  def new?
    true
  end

  def show?
    user.profile.project.id == @eoi.project_id? or user.id == eoi.user_id?
  end

  def edit?
    user.id == eoi.user.id?
  end

  def create?
    true 
  end

  def update?
    user.id == eoi.user.id?
  end

  def destroy?
    user.id == eoi.user.id?
  end    
end

In my EoisController, I have tried to use the scope with:

def index
  # @eois = @project.eois
  @eois = policy_scope(Eoi)
  # @eois = Eois.find_by_project_id(params[:project_id])
end

Then in my view/eois/index, I have tried to display the index with:

<% policy_scope(@user.eois).each do |group| %>

I can't get this to work. The error message highlights this line of my scope method in the policy:

if user.profile.project.id == @eoi.project_id?

To me, this looks correct, although I'm still trying to figure this out. Can anyone see what needs to happen to make this work, so that if the user is the user, who's profile owns the relevant project, all eois relating to that project are visible.

Otherwise, if the user is the user who created the eoi, then all eois they have created are visible?

The error message says:

undefined method `project' for #<Profile:0x007fa03f3faf48>
Did you mean?  projects
               projects=

I'm wondering if that's because an index will have many records, it needs to show something different in the policy to recognise the plurality?

I have also tried replacing that line with:

if  @eoi.project_id == @user.profile.project.id?

although that is also wrong and gives

undefined method `project_id' for nil:NilClass
Did you mean?  object_id

I also tried making the scope:

 def resolve
      # cant figure what is wrong with this
      if  eoi.project_id == user.profile.project.id?
        scope.where(project_id: @user.profile.project.id)
      else
        nil
      end
    end

but that's also wrong and gives this error:

  undefined local variable or method `eoi' for #<EoiPolicy::Scope:0x007ffb505784f8>

I also tried:

    def resolve
      # cant figure what is wrong with this

      if  @eoi.project_id == user.profile.project.id? or Eoi.project_id == user.profile.project.id?
        scope.where(project_id: @user.profile.project.id)
      elsif user.id == eoi.user_id?
        scope.where(user_id: user.id)
      else
        nil
      end
    end
  end



def index?
    user.profile.project.id == Eoi.project_id? or user.id == Eoi.user_id?
  end

but that attempt gives this error message:

undefined method `project_id' for nil:NilClass
Did you mean?  object_id

CURRENT THOUGHT

I think I need to pass more than user and scope to the scope method. If I can also pass project, then I can make the scope referable to the project to which the EoI relates.

If I could have this working, then maybe I could get the scope method to work for the index view on the controller:

class Scope
  attr_reader :user, :scope

  def initialize(user, scope, project)
    @user  = user
    @scope = scope
    @project = project
  end
end

then in the controller:

 def index
   # @eois = @project.eois

   @eois = policy_scope(Eoi, @project)
   # authorize @eois
   # @eois = Eois.find_by_project_id(params[:project_id])
 end

This doesnt work, when I try I get an error saying that the policy

wrong number of arguments (given 2, expected 1)

Please help!

NEXT ATTEMPT

My next attempt is to try taking the suggestions from [this]Pundit issue and implement that idea for how to get the right scope for a particular user.

In my Eoi Policy, I changed the resolve method to:

class Scope
    attr_reader :user, :scope

    def initialize(user, scope) #project
      @user  = user
      @scope = scope
      # @project = project

    end

    def resolve
      # if  Eoi.project_id == user.profile.project.id? or Eoi.project_id == user.profile.project.id?
      if user.id == eoi.projects.profile.user.map(&:id)
        scope.joins(eois: :projects).where(project_id: user.profile.projects.map(&:id)).empty?
      # if scope.eoi.project_id == user.profile.projects.map(&:id)  
        # scope.where(project_id: user.profile.projects.map(&:id)).empty? 
      #   scope.where(project_id: user.profile.project.id)
      # elsif user.id == eoi.user_id?
      #   scope.where(user_id: user.id)
      else
      #   nil
       end
    end
  end

Then in my eoi controller index action, I tried this:

def index
    # @eois = @project.eois

    # @eois = policy_scope(Eoi, @project)
    policy_scope(Eoi).where(project_id: params[:project_id])
    # authorize @eois
    # @eois = Eois.find_by_project_id(params[:project_id])
  end

That doesnt work either. The error message for this attempt says:

undefined local variable or method `eoi' for #<EoiPolicy::Scope:0x007f98677c9cf8>

Im out of ideas for things to try. Can anyone see a way to give the scope the right inputs to set this up?

OBSERVATION I have noticed that a lot of the repos on github that use Pundit with scopes also include a method like this:

def scope
  Pundit.policy_scope!(user, record.class)
end

That method is in addition to the Scope class and isn't shown in the Pundit gem docs. If that is necessary to include, what does it do? 1

REWRITE

I've now looked through more than 200 repos on github for insight into how I'm supposed to write a policy to meet my objectives. I'm out of ideas for how to use Pundit as intended.

I've changed my setup completely to try and work around the bits I can't understand. I now have:

Eois Controller

class EoisController < ApplicationController

  def index
    @eois = Eoi.by_user_id(current_user.id)
  end
end

Projects:: Eois controller

module Projects
  class EoisController < ApplicationController
    before_action :get_project
    before_action :set_eoi, only: [:edit, :update, :destroy]
    # after_action :verify_authorized

    def index
      @eois = Project.by_user_id(current_user.id).find_by(id: params[:project_id]).try(:eois) || []
    end

 def show
      @eoi = Eoi.find(params[:id])
      authorize @eoi
    end

def set_eoi
        @eoi = EoiPolicy::Scope.new(current_user, params[:project_id]).resolve.find(params[:id])
      end

      def get_project
        @project = Project.find(params[:project_id])
      end

Eoi Policy (to decide when to show all eois made by a user)

class EoiPolicy < ApplicationPolicy

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user  = user
      @scope = scope
    end

    def resolve
      if scope.present?
          Eoi.by_user_id(user.id)
        # end
      else
        []
      end
    end

  end

  def index?
    user.profile.project.id == Eoi.project_id? or user.id == Eoi.user_id?
  end

  def new?
    true
  end

  def show?
    record.user_id == user.id || user.profile.project_id == record.project_id
    # user.profile.project.id == @eoi.project_id? or user.id == eoi.user_id?
  end

  def edit?
    user.id == eoi.user.id?
  end

  def create?
    true
  end

  def update?
    user.id == eoi.user.id?
  end

  def destroy?
    user.id == eoi.user.id?
  end


end

Routes

resources :eois

resources :projects do
    member do
    resources :eois, controller: 'projects/eois
  end

When I want to show EoIs that are submitted in relation to a project, I use the Projects Eoi Policy and when I want to show the Eois that a user has created, I use the Eoi Policy -- no scopes.

I would love to figure this out so I can use this gem the way it is intended. Advice would be greatly appreciated. I'm sure this attempt isn't what Pundit is meant for - but I can't figure out how to use this gem as shown in the docs.

I can't use policy_scope because I need to pass the project_id param into the index action for the projects eoi controller index action.

PaReeOhNos SUGGESTION

My attempt at trying to implement PareeOhNos suggestion is set out below. I'm not sure I understand it properly because eois will always have a project id and a user id, but maybe I'm not getting the point of what the load_parent method is doing.

In my Eois Controller, I have:

class EoisController < ApplicationController
  before_action :load_parent
  before_action :load_eoi, only: [:show, :edit, :update, :destroy]



  def index
    authorize @parent
    @eois = EoiPolicy::Scope.new(current_user, @parent).resolve
  end



  def show

  end

  # GET /eois/new
  def new
    @project = Project.find(params[:project_id])
    @eoi = @project.eois.build
    @contribute = params[:contribute] || false
    @participate = params[:participate] || false
    @partner = params[:partner] || false
    @grant = params[:grant] || false
    @invest = params[:invest] || false
  end

  # GET /eois/1/edit
  def edit
  end

  # POST /eois
  # POST /eois.json
  def create
    @eoi = Project.find(params[:project_id]).eois.build(eoi_params)
    @eoi.user_id = @current_user.id

    respond_to do |format|
      if @eoi.save
        format.html { redirect_to Project.find(params[:project_id]), notice: 'Eoi was successfully created.' }
        format.json { render :show, status: :created, location: @project }
      else
        format.html { render :new }
        format.json { render json: @eoi.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /eois/1
  # PATCH/PUT /eois/1.json
  def update
    respond_to do |format|
      if @eoi.update(eoi_params)
        format.html { redirect_to @project, notice: 'Eoi was successfully updated.' }
        format.json { render :show, status: :ok, location: @eoi }
      else
        format.html { render :edit }
        format.json { render json: @eoi.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /eois/1
  # DELETE /eois/1.json
  def destroy
    @eoi.destroy
    respond_to do |format|
      format.html { redirect_to @project, notice: 'Eoi was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private

    def load_parent
      # @parent = (params[:project_id] ? Project.find(params[:project_id] : current_user)
      @parent =  params[:project_id] ? Project.find(params[:project_id]) : current_user
    end

    def load_eoi
      @eoi = Eoi.find(params[:id])
      authorize @eoi
    end

In my Eoi Policy, I have:

class EoiPolicy < ApplicationPolicy
class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user  = user
      @scope = scope
    end

    def resolve
      if scope.is_a?(User)
        Eoi.where(user_id: scope.id)
      elsif scope.is_a?(Project)
        Eoi.where(project_id: scope.id)
      else
        []
      end
    end

  end

  def index?
    record.is_a?(User) || user.profile.project.id == record.project_id
  end

  def new?
    true
  end

  def show?
    record.user_id == user.id || user.profile.project_id == record.project_id
  end

  def edit?
    user.id == eoi.user.id?
  end

  def create?
    true
  end

  def update?
    user.id == eoi.user.id?
  end

  def destroy?
    user.id == eoi.user.id?
  end


end

In my routes.rb, I have:

resources :projects do
    member do
  resources :eois, shallow: true

resources :eois, only: [:index]

In my eois/index, I have:

    <% @eois.sort_by(&:created_at).in_groups_of(2) do |group| %>
        <% group.compact.each do |eoi| %>
            <h4><%= link_to eoi.user.full_name %></h4>
            <%= link_to 'VIEW DETAILS', eoi_path(eoi), :class=>"portfolio-item-view" %>
<% end %>  
<% end %>  

In my eois/ show, I have:

"test"

When I try all this, the eois/index page loads. When I try to show a specific eoi page, I get an error that says:

wrong number of arguments (given 2, expected 0)

the error message points to authorise @eoi line of the controller:

def load_eoi
      @eoi = Eoi.find(params[:id])
      authorize @eoi
    end

The same error arises if I put authorize @eoi in the show action instead of the load eoi method.

APPLICATION POLICY HAS

class ApplicationPolicy
  attr_reader :user,  :scope

  class Scope
    def initialize(user, scope)
      #byebug        
      @user = user
      # record = record
      @scope = scope
    end

    def resolve
      scope
    end
  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

NEXT ATTEMPT

Taking PaReeOhNos suggestion (copied above), I've tried to adapt it a bit to better fit my use cases.

Now, I have:

Eoi controller

class EoisController < ApplicationController
  # before_action :get_project
  # before_action :set_eoi, only: [:show, :edit, :update, :destroy]
  before_action :load_parent
  before_action :load_eoi, only: [:show, :edit, :update, :destroy]


  # GET /eois
  # GET /eois.json
  # def index
  #   @eois = @project.eois
  #   # @eois = Eois.find_by_project_id(params[:project_id])
  # end

  def index
    # authorize @parent
    @eois = policy_scope(Eoi.where(project_id: params[:project_id]))
    # @eois = EoiPolicy::Scope.new(current_user, @parent).resolve
  end


  # GET /eois/1
  # GET /eois/1.json
  def show

  end

  # GET /eois/new
  def new
    @project = Project.find(params[:project_id])
    @eoi = @project.eois.build
    @contribute = params[:contribute] || false
    @participate = params[:participate] || false
    @partner = params[:partner] || false
    @grant = params[:grant] || false
    @invest = params[:invest] || false
  end

  # GET /eois/1/edit
  def edit
  end

  # POST /eois
  # POST /eois.json
  def create
    @eoi = Project.find(params[:project_id]).eois.build(eoi_params)
    @eoi.user_id = @current_user.id

    respond_to do |format|
      if @eoi.save
        format.html { redirect_to Project.find(params[:project_id]), notice: 'Eoi was successfully created.' }
        format.json { render :show, status: :created, location: @project }
      else
        format.html { render :new }
        format.json { render json: @eoi.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /eois/1
  # PATCH/PUT /eois/1.json
  def update
    respond_to do |format|
      if @eoi.update(eoi_params)
        format.html { redirect_to @project, notice: 'Eoi was successfully updated.' }
        format.json { render :show, status: :ok, location: @eoi }
      else
        format.html { render :edit }
        format.json { render json: @eoi.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /eois/1
  # DELETE /eois/1.json
  def destroy
    @eoi.destroy
    respond_to do |format|
      format.html { redirect_to @project, notice: 'Eoi was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private

    def load_parent
      # @parent = (params[:project_id] ? Project.find(params[:project_id] : current_user)
      @parent = params[:project_id] ? Project.find(params[:project_id]) : current_user
    end

    def load_eoi
      @eoi = Eoi.find(params[:id])
      # authorize @eoi
    end

Eoi policy

class EoiPolicy < ApplicationPolicy

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user  = user
      @scope = scope
    end

    def resolve
      # since we send the scoped eois from controller, we can pick
      # any eoi and get its project id

      # check if the current user is the owner of the project
    #   if (user.profile.projects.map(&:id).include?(project_id))
    #     # user is the owner of the project, get all the eois 
    #     scope.all 
    #   end
    #   #not the owner , then get only the eois created by the user
    #   scope.where(user_id: user.id)
    # end 
      if scope.is_a?(User)
        Eoi.where(user_id: scope.id)
      elsif scope.is_a?(Project) && (user.profile.projects.map(&:id).include?(project_id))
        project_id = scope.first.project_id 
        Eoi.where(project_id: scope.id)
      else
        Eoi.none
      end
    end

  end

  def index?
    record.is_a?(User) || user.profile.project.id == record.project_id
  end

  def new?
    true
  end

  def show?
    record.user_id == user.id || user.profile.project_id == record.project_id
  end

  def edit?
    user.id == eoi.user.id?
  end

  def create?
    true
  end

  def update?
    user.id == eoi.user.id?
  end

  def destroy?
    user.id == eoi.user.id?
  end


end

Routes

resources :eois#, only: [:index]
  concern :eoiable do
    resources :eois
  end

resources :projects do
    concerns :eoiable
  end

Index

   <% @eois.sort_by(&:created_at).in_groups_of(2) do |group| %>
     <% group.compact.each do |eoi| %>
     <h4><%= link_to eoi.user.full_name %></h4>
     <%= link_to 'VIEW DETAILS', project_eoi_path(eoi.project, eoi), :class=>"portfolio-item-view" %>
                            <% end %>  
                        <% end %>   

View

'test'

This isn't working, because when I navigate to a project and then try to render the index of eois that have a matching project id, I get an empty index page, when I have 4 records in my database that should be rendered.

LEITO'S SUGGESTION

Taking Leito's suggestion, I've also tried this:

Eoi Controller

class EoisController < ApplicationController
  before_action :get_project
  before_action :set_eoi, only: [:show, :edit, :update, :destroy]
  # before_action :load_parent
  # before_action :load_eoi, only: [:show, :edit, :update, :destroy]


  # GET /eois
  # GET /eois.json
  # def index
  #   @eois = @project.eois
  #   # @eois = Eois.find_by_project_id(params[:project_id])
  # end

  def index
    # authorize @eois
    # authorize @parent
    # policy_scope(@project.eois)
    @eois = policy_scope(Eoi.where(project_id: params[:project_id]))
    # @eois = EoiPolicy::Scope.new(current_user, @parent).resolve
  end


  # GET /eois/1
  # GET /eois/1.json
  def show

  end

  # GET /eois/new
  def new
    @project = Project.find(params[:project_id])
    @eoi = @project.eois.build
    @contribute = params[:contribute] || false
    @participate = params[:participate] || false
    @partner = params[:partner] || false
    @grant = params[:grant] || false
    @invest = params[:invest] || false
  end

  # GET /eois/1/edit
  def edit
  end

  # POST /eois
  # POST /eois.json
  def create
    @eoi = Project.find(params[:project_id]).eois.build(eoi_params)
    @eoi.user_id = @current_user.id

    respond_to do |format|
      if @eoi.save
        format.html { redirect_to Project.find(params[:project_id]), notice: 'Eoi was successfully created.' }
        format.json { render :show, status: :created, location: @project }
      else
        format.html { render :new }
        format.json { render json: @eoi.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /eois/1
  # PATCH/PUT /eois/1.json
  def update
    respond_to do |format|
      if @eoi.update(eoi_params)
        format.html { redirect_to @project, notice: 'Eoi was successfully updated.' }
        format.json { render :show, status: :ok, location: @eoi }
      else
        format.html { render :edit }
        format.json { render json: @eoi.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /eois/1
  # DELETE /eois/1.json
  def destroy
    @eoi.destroy
    respond_to do |format|
      format.html { redirect_to @project, notice: 'Eoi was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private

    # def load_parent
    #   # @parent = (params[:project_id] ? Project.find(params[:project_id] : current_user)
    #   @parent = params[:project_id] ? Project.find(params[:project_id]) : current_user
    # end

    # def load_eoi
    #   @eoi = Eoi.find(params[:id])
    #   # authorize @eoi
    # end
    # # Use callbacks to share common setup or constraints between actions.
    def set_eoi
      @eoi = Eoi.find(params[:id])
    end

    def get_project
      @project = Project.find(params[:project_id])
    end

Eoi Policy

def initialize(user, scope)
      @user  = user
      @scope = scope
    end

    def resolve

      if scope.joins(project: :profile).where profiles: { user_id: user }
        Eoi.where(project_id: scope.ids)
      elsif scope.joins(eoi: :user).where eois: { user_id: user }  
        Eoi.where(user_id: scope.ids)
      else
        Eoi.none
      end  
      # since we send the scoped eois from controller, we can pick
      # any eoi and get its project id

      # check if the current user is the owner of the project
    #   if (user.profile.projects.map(&:id).include?(project_id))
    #     # user is the owner of the project, get all the eois 
    #     scope.all 
    #   end
    #   #not the owner , then get only the eois created by the user
    #   scope.where(user_id: user.id)
    # end 
      # if scope.is_a?(User)
      #   Eoi.where(user_id: scope.id)
      # elsif scope.is_a?(Project) && (user.profile.projects.map(&:id).include?(project_id))
      #   project_id = scope.first.project_id 

      #   Eoi.where(project_id: scope.id)
      # else
      #   Eoi.none
      # end
    end

  end

  def index?
    true
    # record.is_a?(User) || user.profile.project.id == record.project_id
  end

  def new?
    true
  end

  def show?
    true
    # record.user_id == user.id || user.profile.project_id == record.project_id
  end

  def edit?
    user.id == eoi.user.id?
  end

  def create?
    true
  end

  def update?
    user.id == eoi.user.id?
  end

  def destroy?
    user.id == eoi.user.id?
  end


end

The routes and views are the same as the attempt above

The problem here is with the get project method in my controller. I need that for the scenario where Im trying to show all the eois on a specific project. I don't need it when I'm trying to show all of a user's eois.

When I save all this and try it, the eois on a project show correctly. However the eois (not nested inside a project) that are supposed to show me all of my (as a user) eois, shows an error that says:

Couldn't find Project with 'id'=

The error message highlights the 'get_project method'.

LEITO'S UPDATED SUGGESTION

Taking Leito's updated suggestion, I have set out the current attempt.

Before doing so, I want to clarify that all Eois will have both a user id and a project id. I use this table for users to express interest in projects. My objective is to have the user whose profile owns the project to see all eois submitted on that project. Then, I also want users to see all of their own eois submitted (across all projects).

Eoi Policy

def resolve
  if scope.joins(project: :profile).where 'profiles.user_id = ? OR eois.user_id = ?', user.id, user.id
   Eoi.all
  else
    Eoi.none
  end  

Eoi controller

def index
    @eois = policy_scope(Eoi)
    @eois = @eois.where(project_id: params[:project_id]) if params[:project_id]
  end

Currently this works fine in finding the eois that are nested under a project (project/26/eois). However, when I try to do eois/index (not nested under project), which I want to return all the user's eois, I get an error that says:

Couldn't find Project with 'id'=

It highlights this line of the eoi controller:

def get_project
  @project = Project.find(params[:project_id])
end

I'm not sure I understand the resolve method or the controller culling idea now. I can't see what's wrong with the scope line to see what to try changing.

Mel
  • 2,481
  • 26
  • 113
  • 273
  • One at a time, please! The first is a simple error: your profile doesn't have a single project, but many projects. that's why the error. Let's get rid of that one and focus on the one you try with `.projects` – Leonel Galán Aug 30 '16 at 15:11
  • Did you change your `application_policy.rb` or is the same that pundit generates when you install it ? – lcguida Sep 01 '16 at 09:20
  • I added the application policy to the end of my post – Mel Sep 02 '16 at 00:32
  • Mel, that new error, is simply because `params[:project_id]` is nil. It doesn't have to do much with Pundit and the policy. Pundit's policy concern is: **What Eoi this user have access to?**. The english response is: those that belong to a project that belong to a profile that belong to the user _AND those that belong to the user_. If Eoi `belongs_to :user`, you are missing a User `has_many: eois`? Could you tell us more about this relation? – Leonel Galán Sep 02 '16 at 16:07
  • Hi Leito, I forgot to copy the has many eois association to my user model above. The association is there in the user model. I have 5 eois in my db, all of which have a project id and a user id. – Mel Sep 02 '16 at 23:02

3 Answers3

3

I'm the previous commenter on that issue.

For your EoiScope, you simply want what Eois the user has access to (because they belong to projects under this profile), independent from the project (this requirement is only for the controller, because is nested), so your controller should look something like this:

Edit: Based on your latest attempt, I've updated the scope to account for Eois belonging directly to the user (not through a project) and you should simply scope it to a project or not based on the presence of params[:project_id], see updated answer.

@eois = policy_scope(Eoi)
@eois = @eios.where(project_id: params[:project_id]) if params[:project_id]

And your scope should do joins until it reaches user or simply look for the user_id property on Eoi.

  class EoiPolicy < ApplicationPolicy
    class Scope < Scope
      def resolve
        scope.joins(project: : profile).where 'profiles.user_id = ? OR eois.user_id = ?', user.id, user.id
      end
    end

    # Other methods that differ from ApplicationPolicy's methods
  end

Please note, Scope isn't calling eoi, but default* scope only knows about scope and user. * By default, I mean when it inherits from ApplicationPolicy::Scope

Leonel Galán
  • 6,993
  • 2
  • 41
  • 60
  • Thanks very much for trying to help Leito. I tried your suggestion, but get this error: undefined local variable or method `eoi' for #. The error message points to this line: if user.id == eoi.projects.profile.user.map(&:id) – Mel Aug 30 '16 at 22:48
  • That's on the `show?` method? The default ApplicationPolicy, which you might be inheriting, has a `record` method which probably holds the eoi, but not an `eoi` method, hence the error. – Leonel Galán Aug 31 '16 at 02:35
  • It's on the index? method. I don't understand the error you're describing. Do you have a suggestion for how I can figure out what this error means or how to solve it? – Mel Aug 31 '16 at 03:14
  • I'm describing your error, If you follow Pundit's README and used the generator it created an ApplicationPolicy. That has a generic `record` instead of `eoi`. Why do you have an `index?`, simply use hte scope. User will get the records they have access to, not more no less, there is no need to authorize the `:index` action, worst case scenario they get no Eois, because of the scope. – Leonel Galán Aug 31 '16 at 13:34
  • I really don't understand Leito. The pundit documents do not dismiss using an index action in conjunction with a scope class. The scope informs what records get delivered through the index. I do have an application policy and it does define a generic record. I'm not sure what to make from your advice. I can't understand it. Thanks anyway. – Mel Aug 31 '16 at 22:15
  • Forget about index?, apparently the error is on `EoiPolicy::Scope`. See your code: `user.id == eoi.projects.profile.user.map(&:id)`, you are calling `eoi` (just after `==`) but it doesn't exist, hence the error: "undefined local variable or method `eoi' for # – Leonel Galán Sep 01 '16 at 17:33
  • Hi @Leito - i tried to take your suggestion and incorporate it in the policy. It works great for the scenario where im looking for all the eois that belong to a specific project. It doesnt work when I'm looking for all the eois that belong to a specific user (where a user has expressed interest in several projects). I copied my attempt to the end of my post above. – Mel Sep 02 '16 at 04:07
  • Hi @Leito, I tried this, and copied the updated content to the end of my post. It isn't working, but I'm not sure I've understood the concept of the scope. – Mel Sep 02 '16 at 23:13
  • Mel, you are close, see new conditional you added to the index method? That replaces `get_project`, remove `before_action :get_project` to get rid of the error you are getting, (at the very least, do not call this when you are asking for all Eois) Also, you don't need the `if ... else` in the scope, simply use my version of the scope's `resolve`. – Leonel Galán Sep 03 '16 at 17:56
  • Leito, thanks for this. It doesnt work. It works fine to show the user whose profile owns the project, all of the Eois in that project. If Im logged in as a different user looking at a project show page in which I have submitted an eoi, I should see my own eoi. I don't. I get no results in that instance (there is one in my database). Also, if I got to localhost:3000/eois I expect to see an index of all of my own eois. Instead, I get an empty page. When I should see the 2 records that are in my db which have a user_id which is equal to the current user's id. – Mel Sep 04 '16 at 06:31
  • Actually - Leito it does work correctly on the project show page, to give an index to the project owner showing all eois on the project, and otherwise to show the current user their own eoi on that project. I can't get it working to create an index of all eois submitted by a particular user. I'm going to leave that as a problem for another day. Thanks very much for helping me get this far. – Mel Sep 04 '16 at 07:09
  • That empty page, it could be simply because the join. If you are on Rails 5, you could try: `left_joins` instead. If you are not, look at the resulting sql query and search how to do a left join on Rails 4. – Leonel Galán Sep 04 '16 at 18:44
  • It's rails 4. The join is SELECT "eois".* FROM "eois" INNER JOIN "projects" ON "projects"."id" = "eois"."project_id" INNER JOIN "profiles" ON "profiles"."id" = "projects"."profile_id" WHERE "eois"."project_id" IS NULL AND (profiles.user_id = 1 OR eois.user_id = 1) AND (profiles.user_id = 1 OR eois.user_id = 1). I'll try to figure this out. Is your idea that I change the resolve method so that I use the one you gave me when the path to eois is nested in projects and then figure out a different one when the path is not nested under projects? – Mel Sep 04 '16 at 22:36
  • Nope, again it's about concerns and policies shouldn't know if you are calling it within the project or not, the idea is to find a resolve that works for both, looks up eois through project or directly through the user. I wish I could help more, but it's hard without knowing the actual data. My guess (and why I mentioned joins) is that some eois don't have projects, so making an inner join filter those. – Leonel Galán Sep 05 '16 at 15:42
  • Thanks Leito. All eois have project ids. I'll do some more reading about concerns next to try and understand what that means. Thanks for your help to this point. – Mel Sep 05 '16 at 20:24
  • 1
    Not Ruby/rails concerns (the pattern), but separation of concerns (the concept). Scope's result should simply limit the results based on the user (not the controller, or params, or anything else), that's up to the controller. – Leonel Galán Sep 05 '16 at 22:44
2

In your first example, there's a couple of issues. Firstly, @eoi does not exist, and can't exist. The @eoi variable is set in the controller, and this is a different object. It doesn't work in the same way as your views where this is accessible, so this will never be set.

Equally, the eoi variable will not be set, as your initialize method is only assigning the user and resource variables, so they're the only two you have access to (unless you rename)

The scope in the policy works a little differently to how you think it works. The policy itself generally takes the user logged in, and a class, or a record that you are authorising. The scope however, doesn't normally take a record as the second argument. It is a scope, so either an active record sub-class, or a relation. You're not restricted to this however, and you could work around it by supplying a record but do note this is not normal behaviour for Pundit.

In order to achieve what you're after, you should only have to make a few adjustments:

class EoiPolicy < ApplicationPolicy

  class Scope
    attr_reader :user, :eoi

    def initialize(user, eoi)
      @user = user
      @eoi  = eoi
    end

    def resolve
      if user.profile.project.id == eoi.project_id
        Eoi.where(project_id: user.profile.project.id)
      elsif user.id == eoi.user_id
        Eoi.where(user_id: user.id)
      else
        nil
      end
    end
  end

  def index?
    user.profile.project.id == record.project_id or user.id == record.user_id
  end

  def new?
    true
  end

  def show?
    user.profile.project.id == record.project_id? or user.id == record.user_id
  end

  def edit?
    user.id == record.user.id
  end

  def create?
    true 
  end

  def update?
    user.id == record.user.id
  end

  def destroy?
    user.id == record.user.id
  end


end

The main changes here are that the attr_reader :user, :scope is now attr_reader :user, :eoi which will give you access to eoi within that scope.

Access to this is no longer prefixed with @ as this is in-line with how pundit works.

Throughout the rest of the policy, @eoi again cannot work, but this has been changed to record (assuming this is what it is in ApplicationPolicy). Please bear in mind the the Scope, and the rest of the policy are two different classes.

With this setup, you should now be able to simply call policy_scope(@eoi) from within your controller. Note the usage of the @eoi variable here and NOT the Eoi class as before. This is crucial, as without this, you won't have access to things like user_id or project_id as those methods don't exist in the Eoi class, but only a record.

I've also removed the ? symbols from the end of your if conditions. These are generally used to signify that the method being called returns a boolean, whereas you had them on the end of something that simply returns an integer. I'd imagine you'd actually get an error saying the method doesn't exist but if you've renamed things then you may want to put them back, but as I say that does go against ruby coding styles.

And on a side-note, using or or and in statements instead of || or && can on the odd occasion behave differently to how you expect. In most scenarios it's fine, but it doesn't technically mean the same thing.

Hope this all helps, let me know if you have any further issues with it.

PaReeOhNos
  • 4,338
  • 3
  • 30
  • 41
  • Hi, thanks so much for this explanation. I'm keen to try and get this working. At the moment, when I try this, I get an error message that says: unable to find policy scope of nil and points to this line in my index action in the eois controller: policy_scope(@eoi) – Mel Aug 30 '16 at 22:41
  • That would suggest that `@eoi` is nil. Has this definitely been set to something? – PaReeOhNos Aug 31 '16 at 09:20
  • There is definitely an eoi and its foreign key is definitely on the right project – Mel Aug 31 '16 at 22:13
  • I've tried a work around approach which I've copied above. i'd love your thoughts on how close this might be to what pundit intends – Mel Aug 31 '16 at 22:40
  • @Mel try this https://gist.github.com/pareeohnos/b45d021047553cee8e77ca4fdd18aaa2 There's no real need to have multiple controllers, you can do it all from one. And again I think you're confusing the way Pundit works. That gist uses one controller for everything, and the scope in your policy changes which EOIs are loaded based on whether it is a Project, or the User that is the parent – PaReeOhNos Sep 01 '16 at 08:39
  • Thanks so much for the gist. I see what you are thinking. I'm still not sure I understand properly, but I'm trying to set this up so that I can play with it. When I load this code, the show page gives an error that says: wrong number of arguments (given 2, expected 0) - the error points to the authorize @eoi line in the eois controller show action. I can't see where I'm giving any arguments. – Mel Sep 02 '16 at 00:01
  • Also, when I try to view eois from a project show page (where I am the project profile user), I get an error that says permission denied. The show policy in eoi policy is now: record.user_id == user.id || user.profile.project_id == record.project_id. I should be permitted to see them. – Mel Sep 02 '16 at 00:07
  • i copied the code above, so you can see what's happening with the views – Mel Sep 02 '16 at 00:22
  • I think the issue with the load_parent method is that every eoi has a project id. My objective is to show the project creator, all the eois for their project; and then to show each user, all the eois they have created. Im not sure whether load_parent does that because every eoi has a project id. – Mel Sep 02 '16 at 00:36
  • load_parent will only load a project if the URL contains a project ID. So if you go to `/eois/1` then the parent will be set to the User, but if you go to `/projects/1/eois` then the parent will be the Project. `params` does not refer to your data, but only to the request parameters – PaReeOhNos Sep 02 '16 at 08:52
  • thanks @PaReeOhNos, I'll try to figure out why its not working for me. Thanks so much for trying to explain all of this - there are so many aspects of this that I can't understand. – Mel Sep 02 '16 at 23:25
0

For others, I'm not sure if this is a solution that makes use of Pundit in the way it was intended, however it does generate the flows that I want, within the limits of my ability.

Thank you to everyone who helped on this. I'm sure I've still got lots to learn about how to improve this, but for now, this is a solution that works.

In summary - I now have two policies for 1 controller.

Eoi Policy

class EoiPolicy < ApplicationPolicy

  class Scope

    def initialize(user, scope)
      @user  = user
      @scope = scope
    end

    def resolve
      # selects all the EOI's for a given user
      @scope.where(user_id: @user.id)
    end

  end

  def index?
    true
  end

Project Eoi Policy

class ProjectEoiPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve(project_id)
      project = Project.find(project_id)
      if project.owner?(@user)
        # if the user is the owner of the project, then get
        # all the eois
        project.eois
      else
        # select all the eois for the project
        # created by this user
        Eoi.for_user(@user.id).for_project(project_id)
      end
    end
  end

end

Eoi Controller index action

class EoisController < ApplicationController
  before_action :get_project, except: [:index, :show]
  before_action :set_eoi, only: [:show, :edit, :update, :destroy]


  def index
    if params[:project_id]
      @eois = ProjectEoiPolicy::Scope.new(current_user, Eoi).resolve(params[:project_id])
    else
      @eois = policy_scope(Eoi)
    end
  end
Mel
  • 2,481
  • 26
  • 113
  • 273
  • POST SCRIPT: Scratch this. It doesnt work. When I add authorise @eoi to my controller action (just testing on show action), I get an error that says: wrong number of arguments (given 2, expected 0). Back to the drawing board. I'm still stuck. – Mel Sep 08 '16 at 01:41