After many years of trying to learn how to use pundit scopes in my Rails app, I have just received an insight into why I can't get it working. Apparently, Pundit can't run an SQL query where one of the query parameters is a statesman state.
The suggestion I have received is to use a different state machine. Before I do that, I'm throwing this out there to see if anyone has managed to find a way to use Pundit (with scopes) and Statesman state machine.
My setup is:
ApplicationPolicy
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
@user = user
@record = record
end
def index?
true
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
ProposalPolicy
class ProposalPolicy < ApplicationPolicy
class Scope < Scope
def resolve
accum_scope = scope.where(user_id: @user.id)
# # Depends on moving off statesman
# # accum_scope = accum_scope.or(accum_scope.reviewable) if @user.has_role?(:research_management, Organisation.first)
accum_scope
end
end
# I think index isnt necessary when I have a Scope
# def index?
# true
# end
def new?
true
end
def create?
new?
end
def show?
#if its in the index results for that user
true
end
def edit?
update?
end
def update?
true if record.try(:user) == user
end
def destroy?
true if record.try(:user) == user
end
def draft?
create?
end
def under_review?
true if record.try(:user) == user
end
def approved?
@user.has_role?(:research_management, Organisation.matching)
end
def not_approved?
approved?
end
def publish_openly?
true if record.try(:user) == user && record.can_transition_to?(:publish_openly)
end
def publish_to_invitees?
true if record.try(:user) == user && record.can_transition_to?(:publish_to_invitees)
end
def publish_counterparties_only?
true if record.try(:user) == user && record.can_transition_to?(:publish_counterparties_only)
end
def remove?
true if record.try(:user) == user #|| @user.has_role? :admin
end
private
def matching
@user.organisation_id == record.user.organisation_id
end
end
Proposal.rb
class Proposal < ApplicationRecord
include Statesman::Adapters::ActiveRecordQueries
has_many :proposal_transitions, class_name: "ProposalTransition", autosave: false
scope :reviewable, -> { in_state(:under_review) }
def state_machine
@state_machine ||= ProposalStateMachine.new(self, transition_class: ProposalTransition,
association_name: :proposal_transitions)
end
delegate :can_transition_to?, :transition_to!, :transition_to, :current_state,
to: :state_machine
private
def self.transition_class
ProposalTransition
end
def self.initial_state
:draft
end
The current problem:
ProposalPolicy::Scope.new(User.first, Proposal).resolve.all
User Load (1.0ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
Organisation Load (0.7ms) SELECT "organisations".* FROM "organisations" ORDER BY "organisations"."id" ASC LIMIT $1 [["LIMIT", 1]]
(0.9ms) SELECT COUNT(*) FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" = $1 AND "roles"."name" = $2 AND "roles"."resource_type" = $3 AND "roles"."resource_id" = $4 [["user_id", 43], ["name", :research_management], ["resource_type", "Organisation"], ["resource_id", 5]]
Proposal Load (0.3ms) SELECT "proposals".* FROM "proposals" WHERE "proposals"."user_id" = $1 [["user_id", 43]]
=> #<ActiveRecord::Relation [#<Proposal id: 17, user_id: 43, title: "asdf", description: "adsf", byline: "asdf", nda_required: true, created_at: "2016-11-16 00:28:31", updated_at: "2016-11-28 01:16:47", trl_id: 1, invitee_id: nil>]>
2.3.1p112 :012 > Proposal.where(user_id: User.first).or(Proposal.reviewable)
User Load (2.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
ArgumentError: Relation passed to #or must be structurally compatible. Incompatible values: [:joins]
The insight I've just received is that the pundit scope cannot query the proposal state because statesman has a series of tables behind the scenes that manage the transitions.
Does anyone see a work around that would let me continue using statesman as well as using Pundit scopes?
Not a complete answer yet - I'm still trying to figure out how to do this - but I found help here: https://github.com/elabs/pundit/issues/440. The help explains the following:
Pundit scopes doesn't do anything special. When reading Pundit scopes, just mentally replace the scope keyword with the class name and try to type exactly the same thing into your Rails console.
The error isn't specific to Pundit. It's an error related to the or operator. I took a peek at how statesman implements the in_state call, and it does a join with your ProposalTransition class.
ActiveRecord's or operator requires the left side and the right side to be compatible. In your case, your left-side query (Proposal.where(user: User.first)) is incompatible with your right-side query:
Proposal.reviewable which translates to Proposal.in_state(:under_review) which translates to Proposal.joins(most_recent_transition_join).where(states_where(most_recent_transition_alias, states), :under_review) which does a LEFT OUTER JOIN with the transition table (proposal_transitions, I believe) [1]
To make them compatible, you'd have to apply the same join to both sides of the or.
Since you're already making an expensive call anyway, you could do a poor man's or by doing the following:
def resolve proposal_ids = current_user.proposal_ids + Proposal.reviewable.pluck(:id) Proposal.where(id: proposal_ids) end [1] I looked at statesman's source code: https://github.com/gocardless/statesman/blob/77958f1c2ae613ac29c6db5329c67538a5655ddf/lib/statesman/adapters/active_record_queries.rb