Synopsis
In Ruby on Rails, does the state machine gem support the use of a model instance that doesn't directly relate to the host model? If they do, how do I do it?
The conclusion I'm leaning toward is that authorization should be left to other parts of the framework, and the state machine should just be an interface defining the transition of states. That being said, I see some support for transition conditions and I was wondering if the data inside those conditions could be something NOT set on the host model, but instead passed in like a parameter.
Background
Say we have a Task that has the states in_progress and completed, and in order to transition from them respectively, the current_user (assigned in the session, access in the controller) needs to pass a check.
I understand through the documentation that in order to add a check to the transition I have to program it like this:
transition :in_progress => :completed, :if => :user_is_owner?
and define the function like:
def user_is_owner()
true
end
but let's try to implement the restriction so that the task can only be edited if the user_id is the same as the id of the user that requested the task USING dynamic data.
def user_is_owner?(user)
user.id == self.requester_id
end
Notice I don't have that user object, how would one pass the user object they need in?
Ruby Version: 1.9.3
Rails Version: 3.2.9
Thanks!
The thought process behind this post was that I wanted to use the framework the way it was meant to be used, MVC. Information specific to the connection doesn't belong on a model that represents something completely independent of the connection, it's just logical.
The solution I chose for my problem was what #SergioTulentsev mentioned, A transient attribute.
My Ruby on Rails solution included setting up a transient attribute on my model, by adding an attr_accessor
attr_accessor :session_user
and a setter
# #doc Setter function for transient variable #session_user
def session_user
#session_user
end
and a function that uses the setter on my Task model
def user_is_owner?
requester == session_user
end
then I utilized that function inside of my state_machine's transition
transition :completed => :archived, :if => :user_is_owner?
The problems I see with this are that anytime you want to use the User to make authorization checks, you can't just pass it in as a parameter; it has to be on the object.
Thanks, I learned a lot. Hopefully this will be somewhat useful over the years...
The original response is a valid approach, but I wound up going with this one. I think it's a much cleaner solution. Override the state machine events and extract the authorization.
state_machine :status, :initial => :new do
event :begin_work do
transition :new => :in_progress
end
end
def begin_work(user)
if can_begin_work?(user)
super # This calls the state transition, but only if we want.
end
end
Sources:
https://github.com/pluginaweek/state_machine/issues/193
https://www.rubydoc.info/github/pluginaweek/state_machine/StateMachine%2FMachine:before_transition
Passing variables to Rails StateMachine gem transitions
Related
I'm currently trying to implement simple audit for users (just for destroy method). This way I know if the user has been deleted by an admin or user deleted itself. I wanted to add deleted_by_id column to my model.
I was thinking to use before_destroy, and to retrieve the user info like described in this post :
http://www.zorched.net/2007/05/29/making-session-data-available-to-models-in-ruby-on-rails/
module UserInfo
def current_user
Thread.current[:user]
end
def self.current_user=(user)
Thread.current[:user] = user
end
end
But this article is from 2007, I'm not sure will this work in multithreaded and is there something more up to date on this topic, has anyone done something like this lately to pass on the experience?
Using that technique would certainly work, but will violate the principle that wants the Model unaware of the controller state.
If you need to know who is responsible for a deletion, the correct approach is to pass such information as parameter.
Instead of using callbacks and threads (both represents unnecessary complexity in this case) simply define a new method in your model
class User
def delete_user(actor)
self.deleted_by_id = actor.id
# do what you need to do with the record
# such as .destroy or whatever
end
end
Then in your controller simply call
#user.delete_user(current_user)
This approach:
respects the MVC pattern
can be easily tested in isolation with minimal dependencies (it's a model method)
expose a custom API instead of coupling your app to ActiveRecord API
You can use paranoia gem to make soft deletes. And then I suggest destroying users through some kind of service. Check, really basic example below:
class UserDestroyService
def initialize(user, destroyer)
#user = user
#destroyer = destroyer
end
def perform
#user.deleted_by_id = #destroyer.id
#user.destroy
end
end
UserDestroyService.new(user, current_user).perform
Is there any way to access that functionality within the state_machine gem? Kinda like levels:
def check_if_editor
redirect_to :root unless current_user.editor? OR ANY NEXT STATE
end
Can't find much in the docs. Thanks!
I don't think there is. I've come across the same requirement and solved it by creating a method that checks each acceptable state. I'm not entirely happy with it because if a new state gets introduced it potentially needs to be added to the list.
def after_state1?
state2? || state3?
end
I saw a closed discussion on the state_machine gem (can't find it again now) where they said they didn't want to implement state ordering because it would make it too complicated.
You can use state machine methods state_paths (which returns an array of transitions from one specified state to another) and to_states (which converts the result to a nice array of states).
redirect_to :root unless editor_or_later?
def editor_or_later?
states_after_editor = current_user.state_paths(:from => :editor, :to => :some_end_state).to_states
states_editor_or_later = [:editor] + states_after_editor
states_editor_or_later.include? current_user.state.to_sym
end
I want to store a state in the model, and one can change from one state to any other state. The list of states are predefined in the model.
A state-machine it too much for me, because I don't need events/transitions between states, and don't want to write N-squared transitions (to allow any state to transfer to any other state).
Is there a good Rails gem for doing this? I want to avoid writing all the constants/accessors/checking validity myself.
A gem would be too much for such functionality.
class Model < ActiveRecord::Base
# validation
validate :state_is_in_list
# All the possible states
STATUS = %w{foo bar zoo loo}
# method to change to a state. !! Not sure if this is the right syntax
STATUS.each do |state|
define_method "#{state}!" do
write_attribute :state, state
end
# Also ? methods are handy for conditions
define_method "#{state}?" do
state == read_attribute(:state)
end
end
# So you can do model.bar! and it will change state to 'bar'
# And model.bar? will return true if it is in 'bar' state
private
def child_and_team_code_exists
errors.add(:state, 'Not a valid state') unless STATUS.include? state
end
end
I found that the correct keyword to search for should be 'Active Record Enumeration'
I choose the second one called enumerize. It provide nice API and good form input generator. It also have a simple scope and accessors.
Background
I'm using ledermann-rails-settings (https://github.com/ledermann/rails-settings) on a Rails 2/3 project to extend virtually the model with certain attributes that don't necessarily need to be placed into the DB in a wide table and it's working out swimmingly for our needs.
An additional reason I chose this Gem is because of the post How to create a form for the rails-settings plugin which ties ledermann-rails-settings more closely to the model for the purpose of clean form_for usage for administrator GUI support. It's a perfect solution for addressing form_for support although...
Something that I'm running into now though is properly validating the dynamic getters/setters before being passed to the ledermann-rails-settings module. At the moment they are saved immediately, regardless if the model validation has actually fired - I can see through script/console that validation errors are being raised.
Example
For instance I would like to validate that the attribute :foo is within the range of 0..100 for decimal usage (or even a regex). I've found that with the previous post that I can use standard Rails validators (surprise, surprise) but I want to halt on actually saving any values until those are addressed - ensure that the user of the GUI has given 61.43 as a numerical value.
The following code has been borrowed from the quoted post.
class User < ActiveRecord::Base
has_settings
validates_inclusion_of :foo, :in => 0..100
def self.settings_attr_accessor(*args)
>>SOME SORT OF UNLESS MODEL.VALID? CHECK HERE
args.each do |method_name|
eval "
def #{method_name}
self.settings.send(:#{method_name})
end
def #{method_name}=(value)
self.settings.send(:#{method_name}=, value)
end
"
end
>>END UNLESS
end
settings_attr_accessor :foo
end
Anyone have any thoughts here on pulling the state of the model at this point outside of having to put this into a before filter? The goal here is to be able to use the standard validations and avoid rolling custom validation checks for each new settings_attr_accessor that is added. Thanks!
Here is a newer version that works in the new 2x syntax. Yes it is ugly and does double eval.
This produces namespaces method names and adds them to the attr_accessible list. The names are in the form of "#{namespace}_#{attribute} and can be use in forms. I am monkeying with a ppatch to the gem to do this automatically but I an not there yet.
has_settings do |s|
eval 'def self.settings_accessors(namespace, defaults)
defaults.keys.each do |method_name|
attr_accessible "#{namespace}_#{method_name}"
eval "def #{namespace}_#{method_name}
self.settings(:#{namespace.to_s}).send(:#{method_name})
end
def #{namespace}_#{method_name}=(value)
self.settings(:#{namespace}).send(:#{method_name}=, value)
end
"
end
end'
namespace = :fileshare
defaults = {:media => false, :sit => false, :quota_size => 1}
s.key namespace, :defaults => defaults
self.settings_accessors(namespace, defaults)
end
I have a "event" model that has many "invitations". Invitations are setup through checkboxes on the event form. When an event is updated, I wanted to compare the invitations before the update, to the invitations after the update. I want to do this as part of the validation for the event.
My problem is that I can't seem to access the old invitations in any model callback or validation. The transaction has already began at this point and since invitations are not an attribute of the event model, I can't use _was to get the old values.
I thought about trying to use a "after_initialize" callback to store this myself. These callbacks don't seem to respect the ":on" option though so I can't do this only :on :update. I don't want to run this every time a object is initialized.
Is there a better approach to this problem?
Here is the code in my update controller:
def update
params[:event][:invited_user_ids] ||= []
if #event.update_attributes(params[:event])
redirect_to #event
else
render action: "edit"
end
end
My primary goal is to make it so you can add users to an event, but you can't not remove users. I want to validate that the posted invited_user_ids contains all the users that currently are invited.
--Update
As a temporary solution I made use for the :before_remove option on the :has_many association. I set it such that it throws an ActiveRecord::RollBack exception which prevents users from being uninvited. Not exactly what I want because I can't display a validation error but it does prevent it.
Thank you,
Corsen
Could you use ActiveModel::Dirty? Something like this:
def Event < ActiveRecord::Base
validates :no_invitees_removed
def no_invitees_removed
if invitees.changed? && (invitees - invitees_was).present?
# ... add an error or re-add the missing invitees
end
end
end
Edit: I didn't notice that the OP already discounted ActiveModel::Dirty since it doesn't work on associations. My bad.
Another possibility is overriding the invited_user_ids= method to append the existing user IDs to the given array:
class Event < ActiveRecord::Base
# ...
def invited_user_ids_with_guard=(ids)
self.invited_user_ids_without_guard = self.invited_user_ids.concat(ids).uniq
end
alias_method_chain :invited_user_ids=, :guard
end
This should still work for you since update_attributes ultimately calls the individual attribute= methods.
Edit: #corsen asked in a comment why I used alias_method_chain instead of super in this example.
Calling super only works when you're overriding a method that's defined further up the inheritance chain. Mixing in a module or inheriting from another class provides a means to do this. That module or class doesn't directly "add" methods to the deriving class. Instead, it inserts itself in that class's inheritance chain. Then you can redefine methods in the deriving class without destroying the original definition of the methods (because they're still in the superclass/module).
In this case, invited_user_ids is not defined on any ancestor of Event. It's defined through metaprogramming directly on the Event class as a part of ActiveRecord. Calling super within invited_user_ids will result in a NoMethodError because it has no superclass definition, and redefining the method loses its original definition. So alias_method_chain is really the simplest way to acheive super-like behavior in this situation.
Sometimes alias_method_chain is overkill and pollutes your namespace and makes it hard to follow a stack trace. But sometimes it's the best way to change the behavior of a method without losing the original behavior. You just need to understand the difference in order to know which is appropriate.