best practice for gems like workflow or AASM
i would like to know how you guys use the workflow or the AASM gem in the controller if you want to update all attributes, but also need the workflow/AASM callbacks to fire pr开发者_JAVA技巧operly.
currently, i use it like this:
class ModelController < ApplicationController
def update
@model = model.find(params[:id])
if params[:application]['state'].present?
if params[:application]['state'] == "published"
@model.publish!
end
end
if @model.update_attributes(params[:application]); ... end
end
end
that does not feel right, what would be a better solution ?
I usually define multiple actions that handle the transition from one state to another and have explicit names. In your case I would suggest you add a publish
action:
def publish
# as the comment below states: your action
# will have to do some error catching and possibly
# redirecting; this goes only to illustrate my point
@story = Story.find(params[:id])
if @story.may_publish?
@story.publish!
else
# Throw an error as transition is not legal
end
end
Declare that in your routes.rb
:
resources :stories do
member do
put :publish
end
end
Now your route reflects exactly what happens to a story: /stories/1234/publish
You can override the models aasm_state setter (or status in my example) so it can accept event names. Then we check to see if it's a valid event then check to see if the transition is valid. If they are not we add the correct error message.
A request spec
it "should cancel" do
put "/api/ampaigns/#{@campaign.id}", {campaign: {status: "cancel"}, format: :json}, valid_session
response.code.should == "204"
end
The Model Spec
it "should invoke the cancel method" do
campaign.update_attribute(:status, "cancel")
campaign.canceled?.should be_true
end
it "should add an error for illegal transition" do
campaign.update_attribute(:status, "complete")
campaign.errors.should include :status
campaign.errors[:status].should == ["status cannot transition from pending to complete"]
end
it "should add an error for invalid status type" do
campaign.update_attribute(:status, "foobar")
campaign.errors.should include :status
campaign.errors[:status].should == ["status of foobar is not valid. Legal values are pending, active, canceled, completed"]
end
The model
class Campaign < ActiveRecord::Base
include AASM
aasm column: :status do
state :pending, :initial => true
state :active
state :canceled
state :completed
# Events
event :activate do
transitions from: :pending, to: :active
end
event :complete do
transitions from: :active, to: [:completed]
end
event :cancel do
transitions from: [:pending, :active], to: :canceled
end
end
def status=(value)
if self.class.method_defined?(value)
if self.send("may_#{value}?")
self.send(value)
else
errors.add(:status, "status cannot transition from #{status} to #{value}")
end
else
errors.add(:status, "status of #{value} is not valid. Legal values are #{aasm.states.map(&:name).join(", ")}")
end
end
end
It's a small thing but a hash return nil if the thing is not present, so you could remove the call to present?
I realize that's not what you're asking, of course. One alternative is to put a before filter in the model and do your check for status there. That leaves your controller blind to the underlying storage of your status.
On a side note, we use AASM here and I love it :)
I wanted my model to return the new state after being updated, and this was the easiest way I could think to do this without a lot of "fat" in the controllers, and it makes it easier going forward if your workflow changes:
class Article < ActiveRecord::Base
include Workflow
attr_accessible :workflow_state, :workflow_event # etc
validates_inclusion_of :workflow_event, in: %w(submit approve reject), allow_nil: true
after_validation :send_workflow_event
def workflow_event
@workflow_event
end
def workflow_event=(workflow_event)
@workflow_event = workflow_event
end
# this method should be private, normally, but I wanted to
# group the meaningful code together for this example
def send_workflow_event
if @workflow_event && self.send("can_#{@workflow_event}?")
self.send("#{@worklow_event}!")
end
end
# I pulled this from the workflow website, to use that example instead.
workflow do
state :new do
event :submit, :transitions_to => :awaiting_review
end
state :awaiting_review do
event :review, :transitions_to => :being_reviewed
end
state :being_reviewed do
event :accept, :transitions_to => :accepted
event :reject, :transitions_to => :rejected
end
state :accepted
state :rejected
end
end
精彩评论