Mel Mel - 3 months ago 38
Ruby Question

Rails 4 - Pundit - how to write a scope

Im trying to learn how to use Pundit with Rails 4. I have been trying to learn this for the last 2 years and am slowly making a tiny bit of progress.

I am also trying to learn how to write scopes. I'm still trying to figure out how to translate advice into plain english so that I can begin to understand.

I'm getting stuck at the intersection of the scopes pundit policies use and the general scope class that I can write in a model.

I have models for Uer, Profile and Project.

The associations are:

User

has_one :profile


Profile

belongs_to :user
has_many :projects


Project

belongs_to :profile


I am trying to write a pundit policy that allows different users to see different projects. I am writing a scoped policy, in pundit to manage this.

In my project model, I'm trying to write scope that finds all of a user's projects. In plain english, I want to search all projects for those that have a profile id that belongs to a user id that is equal to the current user.

In my pundit policy, I am trying to write this resolve method:

class Scope
attr_reader :user, :scope

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

def resolve
if user.has_role?(:admin)
scope.all
elsif user.id == @project.profile.user_id
scope.projects_for_user
elsif user.present?
scope.in_state(:publish)
else
Project.none
end
end
end


I have tried about 100 different ways of trying to write a scope on my project model that finds projects that belong to a current user. I know I can't use devise's current_user in the model, so I can't use it in a scope. These two are my best attempts- both are wrong.

scope :projects_for_user, -> { joins(:user_id).where('project.profile.user_id = ?', user.id) }

scope :projects_for_user, -> { where(project.profile.user_id: User.id) }


My main problem with learning this is I can't see how to deconstruct this line into different pieces.

From what I can understand the bit before the ":" is the thing you are looking for. The bit after the ":" is the instance you're using when you're running the scope. If that's right, then I'm confused about why my 2nd attempt didn't work (and also very confused about the joins statement in the 1st attempt).

If someone could help with a plain english explanation of how to write scopes, I am confident that I know what I want to look for, just desperately lost on how to write the query to find it.

Currently, when I try to use my project policy (I have tried incorporating Taryn's suggestion below - although I don't understand each component of the scope, so I'm not sure what's going on with it).

class ProjectPolicy < ApplicationPolicy

attr_reader :user, :record

class Scope
attr_reader :user, :scope

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

def resolve
if user.has_role?(:admin)
scope.all
elsif user.id == @project.profile.user_id
scope.projects_for_user(user)
elsif user.present?
scope.in_state(:publish)
else
Project.none
end
end
end


def index?
true
end

def show?
true
end

private
def project
record
end


Project model:

scope :projects_for_user, -> (user){ joins(:user_id).where('project.profile.user_id = ?', user.id) }


In my project controller, I have:

class ProjectsController < ApplicationController

before_action :set_project, only: [:show, :edit, :update, :destroy ]
before_action :authenticate_user!

def index
@projects = Project.all
authorize @projects
end

def show
@project = Project.find(params[:id])
# authorize @project

end

private
def set_project
@project = Project.find(params[:id])
authorize @project
end


When I save this and try it, it gives an error which says:

wrong number of arguments (given 2, expected 0)


This error is returned when I try to see an index of projects or a specific project (so I don't think it is to do with the scope). I don't understand which two arguments are being given to know how to go about trying to solve this problem.

ADD STACK TRACE

ArgumentError - wrong number of arguments (given 2, expected 0):
pundit (1.1.0) lib/pundit.rb:112:in `policy!'
pundit (1.1.0) lib/pundit.rb:235:in `policy'
pundit (1.1.0) lib/pundit.rb:194:in `authorize'
app/controllers/eois_controller.rb:20:in `show'
actionpack (4.2.4) lib/action_controller/metal/implicit_render.rb:4:in `send_action'
actionpack (4.2.4) lib/abstract_controller/base.rb:198:in `process_action'
actionpack (4.2.4) lib/action_controller/metal/rendering.rb:10:in `process_action'
actionpack (4.2.4) lib/abstract_controller/callbacks.rb:20:in `block in process_action'
activesupport (4.2.4) lib/active_support/callbacks.rb:117:in `call'
activesupport (4.2.4) lib/active_support/callbacks.rb:555:in `block (2 levels) in compile'
activesupport (4.2.4) lib/active_support/callbacks.rb:505:in `call'
activesupport (4.2.4) lib/active_support/callbacks.rb:92:in `__run_callbacks__'
activesupport (4.2.4) lib/active_support/callbacks.rb:778:in `_run_process_action_callbacks'
activesupport (4.2.4) lib/active_support/callbacks.rb:81:in `run_callbacks'
actionpack (4.2.4) lib/abstract_controller/callbacks.rb:19:in `process_action'
actionpack (4.2.4) lib/action_controller/metal/rescue.rb:29:in `process_action'
actionpack (4.2.4) lib/action_controller/metal/instrumentation.rb:32:in `block in process_action'
activesupport (4.2.4) lib/active_support/notifications.rb:164:in `block in instrument'
activesupport (4.2.4) lib/active_support/notifications/instrumenter.rb:20:in `instrument'
activesupport (4.2.4) lib/active_support/notifications.rb:164:in `instrument'
actionpack (4.2.4) lib/action_controller/metal/instrumentation.rb:30:in `process_action'
actionpack (4.2.4) lib/action_controller/metal/params_wrapper.rb:250:in `process_action'
searchkick (1.3.0) lib/searchkick/logging.rb:153:in `process_action'
activerecord (4.2.4) lib/active_record/railties/controller_runtime.rb:18:in `process_action'
actionpack (4.2.4) lib/abstract_controller/base.rb:137:in `process'
actionview (4.2.4) lib/action_view/rendering.rb:30:in `process'
actionpack (4.2.4) lib/action_controller/metal.rb:196:in `dispatch'
actionpack (4.2.4) lib/action_controller/metal/rack_delegation.rb:13:in `dispatch'
actionpack (4.2.4) lib/action_controller/metal.rb:237:in `block in action'
actionpack (4.2.4) lib/action_dispatch/routing/route_set.rb:76:in `dispatch'
actionpack (4.2.4) lib/action_dispatch/routing/route_set.rb:45:in `serve'
actionpack (4.2.4) lib/action_dispatch/journey/router.rb:43:in `block in serve'
actionpack (4.2.4) lib/action_dispatch/journey/router.rb:30:in `serve'
actionpack (4.2.4) lib/action_dispatch/routing/route_set.rb:821:in `call'
omniauth (1.3.1) lib/omniauth/strategy.rb:186:in `call!'
omniauth (1.3.1) lib/omniauth/strategy.rb:164:in `call'
omniauth (1.3.1) lib/omniauth/strategy.rb:186:in `call!'
omniauth (1.3.1) lib/omniauth/strategy.rb:164:in `call'
omniauth (1.3.1) lib/omniauth/strategy.rb:186:in `call!'
omniauth (1.3.1) lib/omniauth/strategy.rb:164:in `call'
omniauth (1.3.1) lib/omniauth/strategy.rb:186:in `call!'
omniauth (1.3.1) lib/omniauth/strategy.rb:164:in `call'
meta_request (0.4.0) lib/meta_request/middlewares/app_request_handler.rb:13:in `call'
meta_request (0.4.0) lib/meta_request/middlewares/meta_request_handler.rb:13:in `call'
warden (1.2.6) lib/warden/manager.rb:35:in `block in call'
warden (1.2.6) lib/warden/manager.rb:34:in `call'
rack (1.6.4) lib/rack/etag.rb:24:in `call'
rack (1.6.4) lib/rack/conditionalget.rb:25:in `call'
rack (1.6.4) lib/rack/head.rb:13:in `call'
actionpack (4.2.4) lib/action_dispatch/middleware/params_parser.rb:27:in `call'
actionpack (4.2.4) lib/action_dispatch/middleware/flash.rb:260:in `call'
rack (1.6.4) lib/rack/session/abstract/id.rb:225:in `context'
rack (1.6.4) lib/rack/session/abstract/id.rb:220:in `call'
actionpack (4.2.4) lib/action_dispatch/middleware/cookies.rb:560:in `call'
activerecord (4.2.4) lib/active_record/query_cache.rb:36:in `call'
activerecord (4.2.4) lib/active_record/connection_adapters/abstract/connection_pool.rb:653:in `call'
activerecord (4.2.4) lib/active_record/migration.rb:377:in `call'
actionpack (4.2.4) lib/action_dispatch/middleware/callbacks.rb:29:in `block in call'
activesupport (4.2.4) lib/active_support/callbacks.rb:88:in `__run_callbacks__'
activesupport (4.2.4) lib/active_support/callbacks.rb:778:in `_run_call_callbacks'
activesupport (4.2.4) lib/active_support/callbacks.rb:81:in `run_callbacks'
actionpack (4.2.4) lib/action_dispatch/middleware/callbacks.rb:27:in `call'
actionpack (4.2.4) lib/action_dispatch/middleware/reloader.rb:73:in `call'
actionpack (4.2.4) lib/action_dispatch/middleware/remote_ip.rb:78:in `call'
better_errors (2.1.1) lib/better_errors/middleware.rb:84:in `protected_app_call'
better_errors (2.1.1) lib/better_errors/middleware.rb:79:in `better_errors_call'
better_errors (2.1.1) lib/better_errors/middleware.rb:57:in `call'
actionpack (4.2.4) lib/action_dispatch/middleware/debug_exceptions.rb:17:in `call'
rack-contrib (1.4.0) lib/rack/contrib/response_headers.rb:17:in `call'
meta_request (0.4.0) lib/meta_request/middlewares/headers.rb:16:in `call'
web-console (2.3.0) lib/web_console/middleware.rb:28:in `block in call'
web-console (2.3.0) lib/web_console/middleware.rb:18:in `call'
actionpack (4.2.4) lib/action_dispatch/middleware/show_exceptions.rb:30:in `call'
railties (4.2.4) lib/rails/rack/logger.rb:38:in `call_app'
railties (4.2.4) lib/rails/rack/logger.rb:20:in `block in call'
activesupport (4.2.4) lib/active_support/tagged_logging.rb:68:in `block in tagged'
activesupport (4.2.4) lib/active_support/tagged_logging.rb:26:in `tagged'
activesupport (4.2.4) lib/active_support/tagged_logging.rb:68:in `tagged'
railties (4.2.4) lib/rails/rack/logger.rb:20:in `call'
request_store (1.3.1) lib/request_store/middleware.rb:9:in `call'
actionpack (4.2.4) lib/action_dispatch/middleware/request_id.rb:21:in `call'
rack (1.6.4) lib/rack/methodoverride.rb:22:in `call'
rack (1.6.4) lib/rack/runtime.rb:18:in `call'
activesupport (4.2.4) lib/active_support/cache/strategy/local_cache_middleware.rb:28:in `call'
rack (1.6.4) lib/rack/lock.rb:17:in `call'
actionpack (4.2.4) lib/action_dispatch/middleware/static.rb:116:in `call'
rack (1.6.4) lib/rack/sendfile.rb:113:in `call'
skylight (0.10.6) lib/skylight/middleware.rb:61:in `call'
railties (4.2.4) lib/rails/engine.rb:518:in `call'
railties (4.2.4) lib/rails/application.rb:165:in `call'
rack (1.6.4) lib/rack/content_length.rb:15:in `call'
puma (3.4.0) lib/puma/configuration.rb:224:in `call'
puma (3.4.0) lib/puma/server.rb:569:in `handle_request'
puma (3.4.0) lib/puma/server.rb:406:in `process_client'
puma (3.4.0) lib/puma/server.rb:271:in `block in run'
puma (3.4.0) lib/puma/thread_pool.rb:114:in `block in spawn_thread'


Started POST "/__better_errors/123578515c1e4e10/variables" for ::1 at 2016-09-08 13:23:01 +1000

ANALYSIS OF STACK TRACE

The only line of this that I have written myself is the authorize @eoi line in the eois controller (inside the show action). That is a key part of using pundit. The rest of the stack trace is from things I didn't write and don't know how to change.

app/controllers/eois_controller.rb:20:in `show'

Answer

The problem is that you aren't actually giving the scope the user id. In this one: User.id this will never work... the User class represents all users... it doesn't make any sense to ask for the id of it (you'll just get back the id of the ruby-object that stores the class methods).

in the other one... you use user.id but don't actually set the value of the user variable (so it will always fail).

Maybe try actually passing the relevant user id into the method as a parameter eg:

# define the `user` parameter as an argument to this scope-method
scope :projects_for_user, -> (user){ joins(:user_id).where('project.profile.user_id = ?', user.id) }

def resolve
  if user.has_role?(:admin)
    scope.all
  elsif user.id == @project.profile.user_id
    scope.projects_for_user(user) # pass the user into the method
  elsif user.present?
    scope.in_state(:publish)
  else 
    Project.none  
  end
end

Note: I have not (and will not) test this code, it may have typos or bugs... give it a go and fix the bugs.