I am a Sr. Software Developer at Oracle Cloud. The opinions expressed here are my own and not necessarily those of my employer.
Using Rails Admin to implement state machine workflow UI
Happy New Year. As we transition from old to new I was thinking about state machines. Well, not really but I thought it was a good opening for this post.
There are various processes at my job where we would benefit from actually enforcing the business rules. Before implementing solution in production I wanted to prototype it in a standalone app. There are several gems for this but one I like is aasm. Let’s say you are building a publishing system. You have authors and editors. Authors write article drafts and submit them. Editors have to approve articles before they can be published. Or editor can reject an article and it goes back to draft state.
Here is the basic model:
class Article
include Mongoid::Document
field :title, type: String
field :body, type: String
...
field :aasm_state
include AASM
aasm do
state :draft, :initial => true
state :submitted
state :published
event :submit do
transitions :from => :draft, :to => :submitted
end
event :approve do
transitions :from => :submitted, :to => :published
end
event :reject do
transitions :from => :submitted, :to => :draft
end
end
...
end
Rails_admin sees aasm_state field as a string and allows you to edit it anyway you want. A simple way to restrict it is to add this to the Article model using enumerize gem:
extend Enumerize
enumerize :aasm_state, in: aasm.states
Now rails_admin creates a dropdown with the list of possible aasm states but it still allows you to set the field to any option w/o enforcing workflow.
For slightly better solution add this to Article model:
rails_admin do
include_all_fields
field :aasm_state, :enum do
enum do
bindings[:object].aasm.states(:permitted => true).map(&:name)
end
end
end
This will restrict the options in the dropdown to the ones allowed for specific state of the Article. Here is appropriate documentation for aasm and rails_admin enum
But this still does article.update(aasm_state: ‘submitted’) and what we really want to do is article.submit. This way we can really put model through state transition and do things like fire callbacks. One way to achive that is via rails_admin custom actions.
Change aasm_state field to read only by replacing enum section in Article model with this:
rails_admin do
configure :aasm_state do
read_only true
end
end
Add this at the top of config/initializers/rails_admin.rb. Alternatively you can put it in separate file and load it from rails_admin.rb
module RailsAdmin
module Config
module Actions
# common config for custom actions
class Cmsaction < RailsAdmin::Config::Actions::Base
register_instance_option :member do
true
end
register_instance_option :only do
Article
end
register_instance_option :visible? do
authorized? # combine with Devise/CanCanCan or alternative auth tools
end
register_instance_option :controller do
object = bindings[:object]
end
end
class Submit < Cmsaction
RailsAdmin::Config::Actions.register(self)
register_instance_option :visible? do
bindings[:object].may_submit? # this will show/hide link depending on state
end
register_instance_option :link_icon do
'fa fa-location-arrow'
end
register_instance_option :controller do
Proc.new do
object.submit!
flash[:notice] = "Submitted #{object.title}"
redirect_to show_path
end
end
end
class Approve < Cmsaction
RailsAdmin::Config::Actions.register(self)
register_instance_option :visible? do
bindings[:object].may_approve?
end
register_instance_option :link_icon do
'fa fa-thumbs-up'
end
register_instance_option :controller do
Proc.new do
object.approve!
flash[:notice] = "Approved #{@object.title}"
redirect_to show_path
end
end
end
class Reject < Cmsaction
RailsAdmin::Config::Actions.register(self)
register_instance_option :visible? do
bindings[:object].may_reject?
end
register_instance_option :link_icon do
'fa fa-thumbs-down'
end
register_instance_option :controller do
Proc.new do
object.reject!
flash[:notice] = "Rejected #{@object.title}"
redirect_to show_path
end
end
end
end
end
end
Enable these actions in rails_admin.rb config section:
RailsAdmin.config do |config|
config.actions do
dashboard # mandatory
...
submit
approve
reject
And add this to config/locales/en.yml
en:
admin:
actions:
submit:
menu: 'Submit'
...
Bonus feature - use model scopes to filter articles by different states.
class Article
...
scope :draft, ->{ where(aasm_state: 'draft') }
scope :submitted, ->{ where(aasm_state: 'submitted') }
scope :published, ->{ where(aasm_state: 'published') }
...
rails_admin do
list do
scopes [nil, 'draft', 'submitted', 'published']
end
end
...
Now appropriate links/icons with show up depending on user’s permissions and article state. Let’s say you have an author who wants to approve article draft (w/o submitting it) by hacking URL http://website.com/admin/article/article_id/approve. You will get AASM::InvalidTransition error.
We now have a very functional UI and were able to build it very quickly by editing only 3 files - article.rb , rails_amdin.rb and en.yml.
There is also a rails_admin_aasm but it does not seem to be actively maintained and I wanted to have more control over certain aspects.