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.
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
I am developing spree extension. What I want to do, is to create new database record when order finalized. I need some suggestion how to do it.
How I understand one way is to create custom method in order class, and register new hook (where I should register new hook? in initializer?).
Other way is to use Activators, but I don't have any idea how to subscribe events. And where I should put code which subscribe order events.
module Spree
class Credit < ActiveRecord::Base
def create_new_line(order)
#I need call this method when order finalized
end
end
end
I found solution. My order decorator looks like this.
Spree::Order.class_eval do
register_update_hook :add_user_credits
def add_user_credits
if (!self.user.nil? and !self.completed_at.nil?)
# do some stuff, only for registered users and when order complete
end
end
end
In your solution I think that the hook will be called every time you update the oder. So if you change something after the order is completed that method will be called again. If it's like this by design that could be the right solution anyway Spree suggests to directly use state machine callback to do stuff like this. For example:
Spree::Order.class_eval do
state_machine do
after_transition :to => :complete, :do => :add_user_credits
end
def add_user_credits
# do some stuff
end
end
This way the code will be executed immediately after the order goes into the complete state.
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.
i'm on the way of redesigning my activity feed, i already implemented the logic with redis and rails (wich works great by the way) but i'm still unsure how to create/trigger the events.
In my first approach i used observer, which had the downside of not having current_user available. and, using observer is a bad idea anyways :)
My preferred method would be to create/trigger the events in the controller, which should look sth like:
class UserController < LocationController
def invite
...
if user.save
trigger! UserInvitedEvent, {creator: current_user, ...}, :create
....
end
end
end
The trigger method should
create the UserInvitedEvent with some params. (:create can be default option)
could be deactivate (e.g. deactivate for testing)
could be executed with e.g. resque
i looked in some gems (fnordmetrics, ...) but i could not find a slick implementation for that.
I'd build something like the following:
# config/initializers/event_tracking.rb
modlue EventTracking
attr_accessor :enabled
def enable
#enabled = true
end
def disable
#enabled = false
end
module_function
def Track(event, options)
if EventTracking.enabled
event.classify.constantize.new(options)
end
end
end
include EventTracking
EventTracking.enable unless Rails.env.test?
The module_function hack let's us have the Track() function globally, and exports it to the global namespace, you (key thing is that the method is copied to the global scope, so it's effectively global, read more here: http://www.ruby-doc.org/core-1.9.3/Module.html#method-i-module_function)
Then we enable tracking for all modes except production, we call event.classify.constantize in Rails that should turn something like :user_invited_event into UserInvitedEvent, and offers the possibility of namespacing, for example Track(:'users/invited'). The semantics of this are defined by ActiveSupport's inflection module.
I think that should be a decent start to your tracking code I've been using that in a project with a lot of success until now!
With the (new) rails intrumentation and ActiveSupport::Notifications system you can completely decouple the notification and the actual feed construction.
See http://railscasts.com/episodes/249-notifications-in-rails-3?view=asciicast
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.