aasm_actionable 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +149 -0
- data/app/views/aasm_actionable/_list.html.erb +26 -0
- data/lib/aasm_actionable.rb +5 -0
- data/lib/aasm_actionable/controller_mixin.rb +65 -0
- data/lib/aasm_actionable/engine.rb +5 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ff1e81cf16edd7094a105e73f9aedc2266d8d7ae
|
4
|
+
data.tar.gz: be5ce695a6bb737d076b564e255f3472420c1d24
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b233023e60586033682614fb809ef09d0b3c9550ed58c85e0486aecc25c63f940a3783641c818a50b921d2125fc5e3ec07c88f8130428d675e96b58766be91d3
|
7
|
+
data.tar.gz: 1c523c8679fbfe227026b1093a4f1b9ee4b5a4fa0e200ae63b53a7750c320c9748609135686acddf871e96f0b099a7eb77a6f43b223e040391f863a2812b191f
|
data/README.md
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
aasm_actionable
|
2
|
+
===============
|
3
|
+
|
4
|
+
aasm_actionable is a Rails extension that helps factor out boilerplate state and authorization conditionals from view code. Using [pundit](https://github.com/elabs/pundit) for authorization, it allows developers to write partials for user actions that are automatically displayed to the user if the model is in an appropriate state, and the user has sufficient permissions.
|
5
|
+
|
6
|
+
|
7
|
+
Installation
|
8
|
+
------------
|
9
|
+
|
10
|
+
Add `aasm_actionable` to your Gemfile, and run Bundler to install it. You will also want to install Pundit, as described in the [Pundit documentation](https://github.com/elabs/pundit#installation).
|
11
|
+
|
12
|
+
|
13
|
+
Using aasm_actionable
|
14
|
+
---------------------
|
15
|
+
|
16
|
+
Each action that aasm_actionable may display requires the developer to provide three things:
|
17
|
+
|
18
|
+
1. An event with the same name as the desired action, ex. `do_action`;
|
19
|
+
2. A method on the pundit policy for the model on which the action is to be performed, ex. `do_action?`; and
|
20
|
+
3. A partial for the model with the same name as the action, ex. `mymodel/_do_action.html.erb`.
|
21
|
+
|
22
|
+
Additionally, you may wish to create a controller method and route to handle the action being performed. By convention, the controller method should have the same name as the action, ie. `do_action` for the example above.
|
23
|
+
|
24
|
+
Once you have provided one or more actions for a model, you can render a model instance's available actions by including `AasmActionable::ControllerMixin` in your controller, and adding `<%= render_state_actions my_instance %>` in your view.
|
25
|
+
|
26
|
+
|
27
|
+
Usage Example
|
28
|
+
-------------
|
29
|
+
|
30
|
+
Consider the following contrived Order model (in `app/models/order.rb`) for a (trivial) online store:
|
31
|
+
|
32
|
+
```rb
|
33
|
+
class Order < ActiveRecord::Base
|
34
|
+
include AASM
|
35
|
+
aasm do
|
36
|
+
state :new, initial: true
|
37
|
+
state :processing
|
38
|
+
state :shipping
|
39
|
+
|
40
|
+
event :confirm do
|
41
|
+
transitions from: :new, to: :processing
|
42
|
+
end
|
43
|
+
event :dispatch do
|
44
|
+
transitions from: :processing, to: :shipping
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
Suppose that users in an inventory tracking role manually confirm an order by double-checking if the item is in stock, while users in shipping are responsible for marking an order as dispatched. We would like to render appropriate actions on a view of the order, depending on the order's state and the role that the user occupies. Confirmation does not add any new information to the order, but dispatching the order requires a shipping number.
|
51
|
+
|
52
|
+
First, we need to define an policy to describe which users can perform which actions. In `app/policies/order.rb`, we create a new `OrderPolicy` class, and define appropriate `confirm?` and `dispatch?` methods:
|
53
|
+
|
54
|
+
```rb
|
55
|
+
class OrderPolicy < ApplicationPolicy
|
56
|
+
# ... other default policy methods (ex. show?) omitted for conciseness.
|
57
|
+
|
58
|
+
def confirm?
|
59
|
+
user.in_role? :inventory
|
60
|
+
end
|
61
|
+
|
62
|
+
def dispatch?
|
63
|
+
user.in_role? :shipping
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
Next, we add methods to the order controller to handle these actions, along with the mixin:
|
69
|
+
|
70
|
+
```rb
|
71
|
+
class OrderController < ApplicationController
|
72
|
+
include AasmActionable::ControllerMixin
|
73
|
+
|
74
|
+
# ... other controller methods omitted ...
|
75
|
+
|
76
|
+
def show
|
77
|
+
@order = find_order
|
78
|
+
authorize @order
|
79
|
+
end
|
80
|
+
|
81
|
+
def confirm
|
82
|
+
@order = find_order
|
83
|
+
authorize @order
|
84
|
+
|
85
|
+
if order.confirm!
|
86
|
+
redirect_to order
|
87
|
+
else
|
88
|
+
# ... handle the error and re-render the order page ...
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def dispatch
|
93
|
+
@order = find_order
|
94
|
+
authorize @order
|
95
|
+
|
96
|
+
dispatch_params = params.require(:order).permit(:shipping_number)
|
97
|
+
order.assign_attributes(dispatch_params)
|
98
|
+
|
99
|
+
if order.dispatch!
|
100
|
+
redirect_to order
|
101
|
+
else
|
102
|
+
# ... handle the error and re-render the order page ...
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def find_order
|
109
|
+
Order.find(params[:id])
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
(You may want to consider using [responders](https://github.com/plataformatec/responders) or a similar library to cut down on boilerplate in your custom controller methods.)
|
115
|
+
|
116
|
+
We must also add the new actions to `config/routes.rb`:
|
117
|
+
|
118
|
+
```rb
|
119
|
+
# ... other routes omitted ...
|
120
|
+
|
121
|
+
resources :orders do
|
122
|
+
member do
|
123
|
+
post 'confirm'
|
124
|
+
post 'dispatch'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# ...
|
129
|
+
```
|
130
|
+
|
131
|
+
Next, we create a partial for each action. For example, for the dispatch action we might write the following `app/views/order/_dispatch.html.erb`:
|
132
|
+
|
133
|
+
```erb
|
134
|
+
<% form_for @order, url: dispatch_order_path, method: :post do %>
|
135
|
+
<div>
|
136
|
+
<%= f.label :shipping_number %>:
|
137
|
+
<%= f.text_field :shipping_number %>
|
138
|
+
</div>
|
139
|
+
<%= f.submit "Dispatch" %>
|
140
|
+
<% end %>
|
141
|
+
```
|
142
|
+
|
143
|
+
Finally, we render the actions in the order view by adding `<%= render_state_actions @order %>` to `app/views/order/show.html.erb`.
|
144
|
+
|
145
|
+
|
146
|
+
Customizing Action Rendering
|
147
|
+
----------------------------
|
148
|
+
|
149
|
+
_TODO (mention that the default template uses the Bootstrap 3 tab component)_
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<div class="actions">
|
2
|
+
<% if actions.empty? %>
|
3
|
+
<p>
|
4
|
+
You cannot perform any actions at this time.
|
5
|
+
</p>
|
6
|
+
<% else %>
|
7
|
+
<ul class="nav nav-tabs">
|
8
|
+
<% actions.each_with_index do |action, index| %>
|
9
|
+
<li class="<%= if index == 0 then 'active' end %>">
|
10
|
+
<a href="#<%= action.id %>" data-toggle="tab">
|
11
|
+
<%= action.title %>
|
12
|
+
</a>
|
13
|
+
</li>
|
14
|
+
<% end %>
|
15
|
+
</ul>
|
16
|
+
|
17
|
+
<div class="tab-content">
|
18
|
+
<% actions.each_with_index do |action, index| %>
|
19
|
+
<div class="action-tab tab-pane <%= if index == 0 then 'active' end %>"
|
20
|
+
id="<%= action.id %>">
|
21
|
+
<%= render partial: action.partial, locals: {taskable: taskable} %>
|
22
|
+
</div>
|
23
|
+
<% end %>
|
24
|
+
</div>
|
25
|
+
<% end %>
|
26
|
+
</div>
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module AasmActionable
|
2
|
+
# A StateAction groups together the various attributes for an action
|
3
|
+
# being rendered, such as it's title, HTML ID, and the name of the
|
4
|
+
# partial to render.
|
5
|
+
#
|
6
|
+
# Note that the HTML ID is not unique, so it is not currently possible
|
7
|
+
# to call render_state_actions multiple times for the same taskable type
|
8
|
+
# on one page.
|
9
|
+
#
|
10
|
+
# @author Brendan MacDonell
|
11
|
+
class StateAction
|
12
|
+
attr_reader :title, :id, :partial
|
13
|
+
|
14
|
+
def initialize(event, action_view_prefix)
|
15
|
+
@title = event.to_s.titleize
|
16
|
+
@id = "action-#{event}"
|
17
|
+
@partial = "#{action_view_prefix}/#{event}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# The ControllerMixin concern implements a controller-based helper
|
22
|
+
# that can automatically render those actions which can be performed by a
|
23
|
+
# user on a taskable. In order to use it, you must do the following:
|
24
|
+
#
|
25
|
+
# 1. Create a Pundit policy for the taskable, with one method named
|
26
|
+
# <event>? for every event in the taskable's state machine. For
|
27
|
+
# example, define submit? if there is a submit event.
|
28
|
+
#
|
29
|
+
# 2. Create a partial with the same name as every event defined in the
|
30
|
+
# taskable's state machine. For example, create _submit.html.erb if
|
31
|
+
# there is a submit event.
|
32
|
+
#
|
33
|
+
# Partials are looked up in the view directory with the same name as the
|
34
|
+
# controller by default. You can change this by providing the method
|
35
|
+
# action_view_prefix(event) in your controller, and returning the view
|
36
|
+
# prefix you wish to look up partials in.
|
37
|
+
#
|
38
|
+
# @author Brendan MacDonell
|
39
|
+
module ControllerMixin
|
40
|
+
extend ActiveSupport::Concern
|
41
|
+
|
42
|
+
included do
|
43
|
+
helper_method :render_state_actions
|
44
|
+
end
|
45
|
+
|
46
|
+
def action_view_prefix(event)
|
47
|
+
controller_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def render_state_actions(taskable)
|
51
|
+
a_policy = policy(taskable)
|
52
|
+
events = taskable.aasm.events
|
53
|
+
.select { |event| a_policy.send("#{event}?") }
|
54
|
+
|
55
|
+
actions = events.map do |event|
|
56
|
+
StateAction.new(event, action_view_prefix(event))
|
57
|
+
end
|
58
|
+
actions.sort_by! {|action| action.title}
|
59
|
+
|
60
|
+
rendered = render_to_string partial: 'aasm_actionable/list',
|
61
|
+
locals: {actions: actions, taskable: taskable}
|
62
|
+
rendered.html_safe
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aasm_actionable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brendan MacDonell
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-01-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: aasm
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pundit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.2.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.2.1
|
55
|
+
description: Rails extension to render appropriate workflow actions
|
56
|
+
email: brendan@macdonell.net
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- README.md
|
62
|
+
- app/views/aasm_actionable/_list.html.erb
|
63
|
+
- lib/aasm_actionable.rb
|
64
|
+
- lib/aasm_actionable/controller_mixin.rb
|
65
|
+
- lib/aasm_actionable/engine.rb
|
66
|
+
homepage: http://rubygems.org/gems/aasm_actionable
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 2.2.2
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: AASM Actionable
|
90
|
+
test_files: []
|
91
|
+
has_rdoc:
|