aasm_actionable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,5 @@
1
+ require "aasm_actionable/engine"
2
+ require "aasm_actionable/controller_mixin"
3
+
4
+ module AasmActionable
5
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ module AasmActionable
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace AasmActionable
4
+ end
5
+ 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: