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.
Related
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
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
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
This is probably one of the things that all new users find out about Rails sooner or later. I just realized that rails is updating all fields with the serialize keyword, without checking if anything really changed inside. In a way that is the sensible thing to do for the generic framework.
But is there a way to override this behavior? If I can keep track of whether the values in a serialized fields have changed or not, is there a way to prevent it from being pushed in the update statement? I tried using "update_attributes" and limiting the hash to the fields of interest, but rails still updates all the serialized fields.
Suggestions?
Here is a similar solution for Rails 3.1.3.
From: https://sites.google.com/site/wangsnotes/ruby/ror/z00---topics/fail-to-partial-update-with-serialized-data
Put the following code in config/initializers/
ActiveRecord::Base.class_eval do
class_attribute :no_serialize_update
self.no_serialize_update = false
end
ActiveRecord::AttributeMethods::Dirty.class_eval do
def update(*)
if partial_updates?
if self.no_serialize_update
super(changed)
else
super(changed | (attributes.keys & self.class.serialized_attributes.keys))
end
else
super
end
end
end
Yes, that was bugging me too. This is what I did for Rails 2.3.14 (or lower):
# config/initializers/nopupdateserialize.rb
module ActiveRecord
class Base
class_attribute :no_serialize_update
self.no_serialize_update = false
end
end
module ActiveRecord2
module Dirty
def self.included(receiver)
receiver.alias_method_chain :update, :dirty2
end
private
def update_with_dirty2
if partial_updates?
if self.no_serialize_update
update_without_dirty(changed)
else
update_without_dirty(changed | (attributes.keys & self.class.serialized_attributes.keys))
end
else
update_without_dirty
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecord2::Dirty
Then in your controller use:
model_item.no_serialize_update = true
model_item.update_attributes(params[:model_item])
model_item.increment!(:hits)
model_item.update_attribute(:nonserializedfield => "update me")
etc.
Or define it in your model if you do not expect any changes to the serialized field once created (but update_attribute(:serialized_field => "update me" still works!)
class Model < ActiveRecord::Base
serialize :serialized_field
def no_serialize_update
true
end
end
I ran into this problem today and ended up hacking my own serializer together with a getter and setter. First I renamed the field to #{column}_raw and then used the following code in the model (for the media attribute in my case).
require 'json'
...
def media=(media)
self.media_raw = JSON.dump(media)
end
def media
JSON.parse(media_raw) if media_raw.present?
end
Now partial updates work great for me, and the field is only updated when the data is actually changed.
The problem with Joris' answer is that it hooks into the alias_method_chain chain, disabling all the chains done after (like update_with_callbacks which accounts for the problems of triggers not being called). I'll try to make a diagram to make it easier to understand.
You may start with a chain like this
update -> update_with_foo -> update_with_bar -> update_with_baz
Notice that update_without_foo points to update_with_bar and update_without_bar to update_with_baz
Since you can't directly modify update_with_bar per the inner workings of alias_method_chain you might try to hook into the chain by adding a new link (bar2) and calling update_without_bar, so:
alias_method_chain :update, :bar2
Unfortunately, this will get you the following chain:
update -> update_with_bar2 -> update_with_baz
So update_with_foo is gone!
So, knowing that alias_method_chain won't let you redefine _with methods my solution so far has been to redefine update_without_dirty and do the attribute selection there.
Not quite a solution but a good workaround in many cases for me was simply to move the serialized column(s) to an associated model - often this actually was a good fit semantically anyway.
There is also discussions in https://github.com/rails/rails/issues/8328.
I recently had to extend the aasm states for the latest version of restful_authentication (github) in one of my apps. I removed the "include Authorization::AasmRoles", copied the existing states and events from the plugin and made the changes necessary to support an additional "published" state on my account model.
Does anyone have a cleaner way to handle this? I.e. just override the state events? I was able to add new events using the plugin as is, however I wasn't able to just override the state events already in restful_auth so I had to remove the include and write it out myself using it as a starting point.
Adding of a state in AASM consists of creating a new State object, which is then added to AASM::StateMachine[User].states array, which looks like this:
def create_state(name, options)
#states << AASM::SupportingClasses::State.new(name, options) unless #states.include?(name)
end
The thing to notice here is that it won't allow to override a state once it is set. If the state with the same name is set again, create_state method just ignores it. To slove this problem, you can use something like this in your User model:
# this will remove the state with name :name from the states array
states = AASM::StateMachine[self].states
states.delete(states.find{ |s| s == :name })
# ... so we can define the state here again
aasm_state :name ...
If you are just redefining the state you should be fine now. But if you want to remove the state entirely, you should undefine the method defined in the body of aasm_state method, too. It should be possible with calling something like:
undef_method :name
The situation should be the same with events (just use "events" instead of "states" in the code). Ideally, make it User model's class method that overrides methods defined in AASM module. In the case of states it would look like this:
def aasm_state(name, options={})
states = AASM::StateMachine[self].states
states.delete(states.find{ |s| s == name.to_sym })
super(name, options)
end
Warning: I might not be right. This code is untested, I just figured it out by looking into the source code of AASM.