Rails cancan and State Machine - Authorizing states
This wasn't actually as terrible as I thought, and I've done it in a way that keeps the code rather short and current_user out of my models (so big plus).
The trick was to call authorize again in the controller.
basically
def update
authorize! params[:object][:state_event].to_sym, @object unless params[:object][:state_event].empty?
.... etc
end
This way I can just aliases in Ability.rb. So users that can do the action will be authorized, and users that cannot will get the exception. This is also awesome as it's the same ability I'll be using on the button based actions.
The only caveat, is that you can't use @object.state_transistions to get a list of the available states the user can transition to, but it should be possible to do this via some sort of helper method.
UPDATE: though getting those states in a view like layer is easy enough
i'm using simple form so I just a collection input such that
..... collection: @object.status_transistions.select{|t| can? t.event, @object}
Which leaves the select with all the transistions the object can go through, and that the user is also authorized to do :).
Transitions class (state machine) get a list of possible transitions
I can't see any obvious way to enumerate possible-next-states, but you can query the available events like this:
YourClass.state_machines[:default].events_for(:active)
=> [:disable]
(If you have more than one state machine there will be additional members in the YourClass.state_machines
Hash)
Simple states in rails - state_machine
It'll cost you $9, but Ryan Bates did a screencast about this very topic not too long ago: http://railscasts.com/episodes/392-a-tour-of-state-machines.
Well worth watching.
Given a record retrieve users authorized to :read it via cancan
I am averagely familiar with Cancan. Having said that, this is what I could think of:
@myrecord = MyRecord.find(1)
authorized_user_ids = User.find_each do |user|
user.id if Ability.new(user).can? :read, @myrecord
end.compact
puts authorized_user_ids
# => [1, 2, 5, 10, 78]
Note however, this is extremely inefficient because it will loop through all users. You might want to perform caching this with a separate table/model that you would implement.
Design pattern: aggregate state for parent from child states in Rails
Probably the best solution would be to trigger a meeting status update when the Participant model is updated:
class Participant < ActiveRecord::Base
belongs_to :meeting
after_update :update_meeting_after_change
def update_meeting_after_change
self.meeting.update_status if self.status_changed?
end
end
class Meeting < ActiveRecord::Base
has_many :participants
def update_status
#logic here
end
end
self.status_changed?
will return true
if the Participant
's status
attribute changed during the update
Is it possible to use cancan in a model?
You can define abilities in cancan against any methods provided by the model. State machine transitions are themselves methods provided by the model, so just set up your abilities as you would for any other methods.
For example, given a simple model:
class Order < ActiveRecord::Base
state_machine :initial => :new do
event :start_processing do
transition :new => :processing
end
event :complete_order do
transition :processing => :complete
end
event :escalate_order do
transition :processing => :escalated
end
event :complete_escalated_order
transition :escalated => :complete
end
state :new
state :processing
state :escalated
state :complete
end
end
You might define abilities like this:
class Ability
if user.role? :orderer
can [:start_processing, :escalate_order, :complete_order], :orders
end
if user.role? :manager
can :complete_escalated_order, :orders
end
end
EDIT - I should have added, that you would then use these abilities in your controllers handling the user requests:
class OrdersController < ApplicationController
def complete
@order = Order.find_by_ref(params[:id])
if @order.can_complete_order?
authorize! :complete_order, @order
@order.complete_order
elsif @order.can_complete_escalated_order?
authorize! :complete_escalated_order, @order
@order.complete_escalated_order
else
redirect_to root_url, :notice => "Order cannot be completed"
end
redirect_to my_queue_path, :notice => "Order #{@order.ref} has been marked as complete."
end
State Machine, Model Validations and RSpec
It looks like you're running into a caveat noted in the documentation:
One important caveat here is that, due to a constraint in ActiveModel's validation
framework, custom validators will not work as expected when defined to run
in multiple states. For example:class Vehicle
include ActiveModel::Validations
state_machine do
...
state :first_gear, :second_gear do
validate :speed_is_legal
end
end
end
In this case, the :speed_is_legal validation will only get run
for the :second_gear state. To avoid this, you can define your
custom validation like so:class Vehicle
include ActiveModel::Validations
state_machine do
...
state :first_gear, :second_gear do
validate {|vehicle| vehicle.speed_is_legal}
end
end
end
Related Topics
State MAChine, Model Validations and Rspec
How to Create a Custom Method for the Rails Console
Ruby or Rails Sort on Two/Multiple Date Fields
Advice on How to Validate Names and Surnames Using Regex
Using $1, $2, etc. Global Variables Inside Method Definition
Conditional Page Caching [Solution: Conditional Fragment Caching]
Callback for Active Storage File Upload
How to Specify a Struct as the Return Value of a Function in Rubyffi
How Are Respond_To and Respond_To_Missing Different
Pass Command Line Argument to Vagrant Shell Script Provisioner
Unable to Delete File from Amazon S3 Using Ruby Script
Cancan Abilities in Separate File
How to Simplify or Clean Up This Anagram Method