serious_business 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +179 -0
- data/Rakefile +37 -0
- data/app/assets/config/serious_business_manifest.js +2 -0
- data/app/assets/javascripts/serious_business/application.js +13 -0
- data/app/assets/stylesheets/serious_business/application.css +15 -0
- data/app/controllers/serious_business/application_controller.rb +5 -0
- data/app/helpers/serious_business/application_helper.rb +12 -0
- data/app/jobs/serious_business/application_job.rb +4 -0
- data/app/mailers/serious_business/application_mailer.rb +6 -0
- data/app/models/concerns/serious_business/actor.rb +4 -0
- data/app/models/serious_business/action.rb +249 -0
- data/app/models/serious_business/actor.rb +4 -0
- data/app/models/serious_business/affected.rb +8 -0
- data/app/models/serious_business/affected_by_actions.rb +9 -0
- data/app/models/serious_business/application_record.rb +5 -0
- data/app/models/serious_business/base_form_model.rb +31 -0
- data/app/views/layouts/serious_business/application.html.erb +14 -0
- data/config/routes.rb +2 -0
- data/lib/generators/serious_business/install_generator.rb +53 -0
- data/lib/generators/serious_business/templates/initializer.rb +6 -0
- data/lib/generators/serious_business/templates/migration/actions.rb +24 -0
- data/lib/serious_business/engine.rb +5 -0
- data/lib/serious_business/version.rb +3 -0
- data/lib/serious_business.rb +18 -0
- data/lib/tasks/serious_business_tasks.rake +4 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: '02748be03ce33b0d73d118a6182ad52120cf8be9'
|
4
|
+
data.tar.gz: a0af8e6f7633262856ba169dd2bccc335073945e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f3b317bd1cc78843df8f10b1405af12e311d033d3d0d8cb6805c044141dcf75b6970f83ef270dc35b02e47e81ee1b211b25dd82286a56a97d5f2ec6cbc38107b
|
7
|
+
data.tar.gz: 50f0e42a28a2690eab2c8fdd59b31ece8a82be2e568edb803920c68715caed8bf19e5aa3103acd3fca822a48eacbf9df3fa77ddb6d4ac47254ff94abb556bc38
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 Axel Tetzlaff
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
# SeriousBusiness: Best application pratices in a handsome DSL
|
2
|
+
|
3
|
+
A small library helping app developers to comply with basic usability and security pattern.
|
4
|
+
By encapsulating business actions in separate classes good pratices are enforced and controllers and models are less likely to turn into [God objects](https://en.wikipedia.org/wiki/God_object)
|
5
|
+
|
6
|
+
Using it you will get:
|
7
|
+
|
8
|
+
* a declarative DSL to specify authorization
|
9
|
+
* a simple way to check permissions object based in views
|
10
|
+
* a guided mechanism to a concise user experience
|
11
|
+
* a simpler way to test logic that before went into controllers
|
12
|
+
* transactional business actions per default
|
13
|
+
* automagic tracking on which users and actions affecting you models
|
14
|
+
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'serious_business'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
```bash
|
25
|
+
$ bundle
|
26
|
+
```
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
```bash
|
30
|
+
$ gem install serious_business
|
31
|
+
```
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
The gem uses polymorphic associations to track the actions you specify. For that it creates some tables and a initializer you may need to modify:
|
35
|
+
|
36
|
+
rails g serious_business:install
|
37
|
+
|
38
|
+
The gem expects one model class to act as *actor* for your actions. The default for this is `User`.
|
39
|
+
|
40
|
+
You will need to make the following changes to you model class to be aware of the actions:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
# app/models/user.rb
|
44
|
+
class User < ApplicationRecord
|
45
|
+
include SeriousBusiness::Actor
|
46
|
+
end
|
47
|
+
|
48
|
+
# this is needed for hot code replacement in dev mode to work
|
49
|
+
# assuming you put your actions in app/models/actions
|
50
|
+
Dir[Rails.root.join('app','models', 'actions', '*.rb')].each {|file| require_dependency file }
|
51
|
+
```
|
52
|
+
|
53
|
+
To create a 'business action' you inherit from `SeriousBusiness::Action`
|
54
|
+
```ruby
|
55
|
+
# app/models/actions/update_user.rb
|
56
|
+
module Actions
|
57
|
+
class UpdateUser < SeriousBusiness::Action
|
58
|
+
# this action needs to be initialized with a user that should be updated
|
59
|
+
needs :user
|
60
|
+
|
61
|
+
# this actions reads the email from a hash most likely set from params
|
62
|
+
att :email, presence: true
|
63
|
+
|
64
|
+
|
65
|
+
# this action should be able to change only the user
|
66
|
+
# that is executing the action
|
67
|
+
forbid_if :not_self { |action| action.actor != action.user }
|
68
|
+
|
69
|
+
# unless it's an admin - they are allowed to do everything, right?
|
70
|
+
allow_if {|action| action.actor.admin? }
|
71
|
+
|
72
|
+
|
73
|
+
# implement this method to specify the actual logic of the action.
|
74
|
+
# return an array of all objects that are logically affected by the
|
75
|
+
# change (the actor is persisted automatically)
|
76
|
+
def execute
|
77
|
+
user.update_attributes(email: form_model.email)
|
78
|
+
[user]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
```
|
84
|
+
|
85
|
+
By creating action classes you actor class (the class you included `SeriousBusiness::Actor` in) gets a method for every action.
|
86
|
+
For the above example that means you can now call:
|
87
|
+
|
88
|
+
action = user.update_user
|
89
|
+
|
90
|
+
Since we declared that this action applies to another user we can pass other user like this:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
other_user = User.find(params[:user_id])
|
94
|
+
action = user.update_user.for_user(other_user)
|
95
|
+
```
|
96
|
+
|
97
|
+
Since we now specified everything thats 'needed' for the action, we can ask if the action can be executed. And since we created it from an user as actor it can anwser itself:
|
98
|
+
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
action.can? # will return if user.admin? or user == other_user
|
102
|
+
```
|
103
|
+
And since it's an action that takes further data probably generated from a form submit we have to pass that in to actually execute the action
|
104
|
+
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
action = user.update_user.for_user(other_user).with_params(email: 'foo@example.org')
|
108
|
+
|
109
|
+
# Note the exclamation mark at the end of 'execute!'
|
110
|
+
action.execute! # true if the action was executed successfully
|
111
|
+
# this is transactional - no changes are made to the db
|
112
|
+
# if anything prevents the action from succeeding (ie.
|
113
|
+
# failing guards/permissions or validations)
|
114
|
+
```
|
115
|
+
Actions themselves are fully fleged ActiveRecord Models that are persisted when successfully executed. With them references to the affected models you return from you implementation of the `execute` are stored.
|
116
|
+
|
117
|
+
To access the list of actions that affected an object you have to include the include `SeriousBusiness::AffectedByActions` concern. Since in this model we modify the User:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
# app/models/user.rb
|
121
|
+
class User < ApplicationRecord
|
122
|
+
include SeriousBusiness::Actor
|
123
|
+
include SeriousBusiness::AffectedByActions
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
Afterwards you can retrieve all actions that affected this model.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
actions = user.modifiers # [SeriousBusiness::Action]
|
131
|
+
actions.last.actor # The User that executed the action
|
132
|
+
actions.last.description # A textual description of the action
|
133
|
+
```
|
134
|
+
|
135
|
+
SeriousBusiness encurages to maintain human consumable descriptions for a better
|
136
|
+
user experience
|
137
|
+
|
138
|
+
To start create the following structure in your i18n yaml file:
|
139
|
+
|
140
|
+
```
|
141
|
+
serious_action:
|
142
|
+
description_with_failed_guards: '%{description} not possible because %{reasons}'
|
143
|
+
failed_guard_join_with: ' and '
|
144
|
+
update_user
|
145
|
+
description: Updating user data
|
146
|
+
cta: update
|
147
|
+
guards:
|
148
|
+
not_self: you are not this user
|
149
|
+
```
|
150
|
+
|
151
|
+
This enables you to generate descriptive labels for those actions independent of their location of use:
|
152
|
+
|
153
|
+
```
|
154
|
+
action = user.update_user.for_user(other_user)
|
155
|
+
action.cta_label # 'update' - ie for buttons
|
156
|
+
|
157
|
+
action.description # 'Updating the user' - i.e. for history
|
158
|
+
|
159
|
+
# the description is extended automatically if the action can not be executed
|
160
|
+
action.description # 'Updating the user not possible because you are not this user'
|
161
|
+
```
|
162
|
+
|
163
|
+
The latter is especially useful to give the user meaningful feedback to an unavailble action:
|
164
|
+
|
165
|
+
```
|
166
|
+
<% if update_action.can? %>
|
167
|
+
<%= link_to 'Edit', edit_account_path(account) %>
|
168
|
+
<% else %>
|
169
|
+
<span title= "<%= update_action.description %>" >Edit</span>
|
170
|
+
<% end %>
|
171
|
+
```
|
172
|
+
|
173
|
+
|
174
|
+
## Contributing
|
175
|
+
|
176
|
+
Pull-requests are welcome - with tests loved!
|
177
|
+
|
178
|
+
## License
|
179
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'SeriousBusiness'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
load 'rails/tasks/statistics.rake'
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
require 'bundler/gem_tasks'
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
30
|
+
t.libs << 'lib'
|
31
|
+
t.libs << 'test'
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
33
|
+
t.verbose = false
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
task default: :test
|
@@ -0,0 +1,13 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require_tree .
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module SeriousBusiness
|
2
|
+
module ApplicationHelper
|
3
|
+
def form_for_action action, path, custom_opts = {}, &blk
|
4
|
+
options = {
|
5
|
+
as: action.form_model.to_param,
|
6
|
+
url: path,
|
7
|
+
method: :post
|
8
|
+
}
|
9
|
+
form_for action.form_model, options.merge(custom_opts), &blk
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
module SeriousBusiness
|
2
|
+
class Action < ApplicationRecord
|
3
|
+
self.table_name= 'serious_actions'
|
4
|
+
belongs_to :actor, class_name: SeriousBusiness.actor_class_name
|
5
|
+
has_many :affecteds, foreign_key: :serious_action_id
|
6
|
+
has_many :affectables, through: :affecteds, source: :affected
|
7
|
+
attr_accessor :transient_affected_models
|
8
|
+
|
9
|
+
def self.att(name, options = {})
|
10
|
+
name = name.to_sym
|
11
|
+
self.form_model_class.send(:attr_accessor, name)
|
12
|
+
if options.any?
|
13
|
+
self.form_model_class.validates name, options
|
14
|
+
end
|
15
|
+
self.needed_attributes << name
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.required_attributes
|
19
|
+
@_required_attributes ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
class MissingModelException < StandardError
|
23
|
+
def initialize(model_names)
|
24
|
+
method_names = model_names.map{|n| "for_#{n}"}.join(', ')
|
25
|
+
super("You have to call the following methods before setting params #{method_names}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_params(params = {})
|
30
|
+
if params.respond_to? :require
|
31
|
+
params = params
|
32
|
+
.require(self.class.param_name)
|
33
|
+
.permit!
|
34
|
+
params.to_h.slice!(*self.class.needed_attributes.map(&:to_s))
|
35
|
+
end
|
36
|
+
|
37
|
+
# make sure 'needed' models were set before trying to apply params
|
38
|
+
missing_values = self.class.required_attributes.select do |needed_name|
|
39
|
+
self.send(needed_name).nil?
|
40
|
+
end
|
41
|
+
raise MissingModelException.new(missing_values) if missing_values.any?
|
42
|
+
|
43
|
+
reset_form_model
|
44
|
+
form_model.assign_attributes(params) unless params.empty?
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.needs(name)
|
49
|
+
required_attributes << name.to_sym
|
50
|
+
self.send(:attr_reader, name)
|
51
|
+
self.send(:define_method, "for_#{name}") do |needed|
|
52
|
+
reset_form_model
|
53
|
+
self.instance_variable_set("@#{name}", needed)
|
54
|
+
self
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.guards
|
59
|
+
@_guards ||= []
|
60
|
+
end
|
61
|
+
|
62
|
+
IfGuard = Struct.new(:prc) do
|
63
|
+
def pass?(action)
|
64
|
+
action.actor.instance_exec(action, &prc)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
UnlessGuard = Struct.new(:reason, :prc) do
|
69
|
+
def pass?(action)
|
70
|
+
!action.actor.instance_exec(action, &prc)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.forbid_if reason, &blk
|
75
|
+
guards << UnlessGuard.new(reason, blk)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.allow_if &blk
|
79
|
+
guards << IfGuard.new(blk)
|
80
|
+
end
|
81
|
+
|
82
|
+
def failed_guards
|
83
|
+
if_guards, unless_guards = self.class.guards
|
84
|
+
.partition{ |guard| guard.is_a?(IfGuard) }
|
85
|
+
return [] if Array.wrap(if_guards).any?{|g| g.pass?(self)}
|
86
|
+
|
87
|
+
Array.wrap(unless_guards).select{ |guard| !guard.pass?(self) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def full_guard_messages
|
91
|
+
failed_guards
|
92
|
+
.map(&:reason)
|
93
|
+
.map do |reason|
|
94
|
+
i18n = I18n.t("serious_action.#{self.class.param_name}.guards.#{reason}")
|
95
|
+
if i18n.starts_with? 'translation missing:'
|
96
|
+
global_i18n = I18n.t("serious_action.global_guards.#{reason}")
|
97
|
+
i18n = global_i18n unless global_i18n.starts_with? 'translation missing:'
|
98
|
+
end
|
99
|
+
i18n
|
100
|
+
end.join(I18n.t('serious_action.failed_guard_join_with'))
|
101
|
+
end
|
102
|
+
|
103
|
+
def can?
|
104
|
+
failed_guards.empty?
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.form_model_class
|
108
|
+
@model_class ||= begin
|
109
|
+
clazz = Class.new(BaseFormModel)
|
110
|
+
self.const_set(:FormModel, clazz)
|
111
|
+
clazz
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.needed_attributes
|
116
|
+
@_attribs ||= []
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.param_name
|
120
|
+
name.demodulize.underscore
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.build(actor_id: , for_model: [], params: {} )
|
124
|
+
action = self.new(actor_id: actor_id)
|
125
|
+
if params.respond_to? :require
|
126
|
+
params = params
|
127
|
+
.require(param_name)
|
128
|
+
.permit self.needed_attributes
|
129
|
+
end
|
130
|
+
action
|
131
|
+
end
|
132
|
+
|
133
|
+
def init_from_needed
|
134
|
+
{}
|
135
|
+
end
|
136
|
+
|
137
|
+
def all_attributes_from(other_model)
|
138
|
+
other_model.attributes.slice(*self.class.needed_attributes.map(&:to_s))
|
139
|
+
end
|
140
|
+
|
141
|
+
def form_model(params={})
|
142
|
+
@_form_model ||= begin
|
143
|
+
attributes = init_from_needed.merge(params)
|
144
|
+
model_instance = self.class.form_model_class.new
|
145
|
+
model_instance.instance_variable_set(:@_action, self)
|
146
|
+
model_instance.assign_attributes(attributes)
|
147
|
+
model_instance
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def params
|
152
|
+
form_model
|
153
|
+
end
|
154
|
+
|
155
|
+
def actor_class
|
156
|
+
@_actor_class ||= begin
|
157
|
+
raise "No actor_class specified! #TODO link to config" unless SeriousBusinessConfig.actor_class_name.present?
|
158
|
+
Kernel.const_get(SeriousBusinessConfig.actor_class_name)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.inherited(child_class)
|
163
|
+
super
|
164
|
+
|
165
|
+
method_name = child_class.name.demodulize.underscore
|
166
|
+
actor_class = Kernel.const_get SeriousBusiness.actor_class_name
|
167
|
+
|
168
|
+
if actor_class.respond_to? method_name
|
169
|
+
raise "Action with the same name already registered #{child_class.name}"
|
170
|
+
end
|
171
|
+
|
172
|
+
SeriousBusiness::Actor.send(:define_method, method_name) do |params: {}, for_model: nil|
|
173
|
+
child_class.build(actor_id: self.id, for_model: for_model, params: params)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def cta_label
|
178
|
+
I18n.t("serious_action.#{self.class.param_name}.cta")
|
179
|
+
end
|
180
|
+
|
181
|
+
def description
|
182
|
+
i18n_params = self.class.required_attributes.inject({}) do |sum, attr_name|
|
183
|
+
sum[attr_name] = self.instance_variable_get("@#{attr_name}")
|
184
|
+
sum
|
185
|
+
end
|
186
|
+
content = I18n.t("serious_action.#{self.class.param_name}.description", plan: i18n_params[:plan] )
|
187
|
+
if persisted? || can?
|
188
|
+
content
|
189
|
+
else
|
190
|
+
I18n.t("serious_action.description_with_failed_guards", description: content, reasons: full_guard_messages)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def success_description
|
195
|
+
custom_msg = I18n.t("serious_action.#{self.class.param_name}.success_description")
|
196
|
+
return custom_msg unless custom_msg.starts_with? 'translation missing'
|
197
|
+
content = I18n.t("serious_action.#{self.class.param_name}.description")
|
198
|
+
I18n.t("serious_action.success_description", description: content)
|
199
|
+
end
|
200
|
+
|
201
|
+
def execute!
|
202
|
+
unless can?
|
203
|
+
form_model.errors.add(:base, description)
|
204
|
+
return false
|
205
|
+
end
|
206
|
+
self.class.transaction do
|
207
|
+
begin
|
208
|
+
if self.class.needed_attributes.any? && !form_model.valid?
|
209
|
+
return false
|
210
|
+
end
|
211
|
+
self.transient_affected_models = self.execute
|
212
|
+
self.transient_affected_models.each do |model|
|
213
|
+
model.errors.each do |key, error|
|
214
|
+
error_key = if self.class.needed_attributes.include? key
|
215
|
+
key
|
216
|
+
else
|
217
|
+
:base
|
218
|
+
end
|
219
|
+
form_model.errors.add error_key, error
|
220
|
+
end
|
221
|
+
end
|
222
|
+
if form_model.errors.any?
|
223
|
+
return false
|
224
|
+
end
|
225
|
+
self.save!
|
226
|
+
self.transient_affected_models.each do |model|
|
227
|
+
SeriousBusiness::Affected.create!(action: self, affected: model)
|
228
|
+
end
|
229
|
+
return true
|
230
|
+
rescue Exception => e
|
231
|
+
raise e
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
protected
|
237
|
+
|
238
|
+
def execute
|
239
|
+
raise "execute should be overwritten in subclass"
|
240
|
+
end
|
241
|
+
|
242
|
+
private
|
243
|
+
|
244
|
+
def reset_form_model
|
245
|
+
@_form_model = nil
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# stores the relation between a model and an action affecting it
|
2
|
+
class SeriousBusiness::Affected < ApplicationRecord
|
3
|
+
self.table_name = 'serious_affecteds'
|
4
|
+
belongs_to :action, class_name: 'SeriousBusiness::Action', foreign_key: 'serious_action_id'
|
5
|
+
belongs_to :affected, polymorphic: true
|
6
|
+
end
|
7
|
+
|
8
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module SeriousBusiness::AffectedByActions
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do |clazz|
|
5
|
+
clazz.has_many :serious_affecteds, as: :affected, class_name: 'SeriousBusiness::Affected'
|
6
|
+
clazz.has_many :modifiers, through: :serious_affecteds, source: :action
|
7
|
+
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module SeriousBusiness
|
2
|
+
class BaseFormModel
|
3
|
+
include ActiveModel::AttributeAssignment
|
4
|
+
include ActiveModel::Validations
|
5
|
+
|
6
|
+
def take_attributes_from(model, fields = nil)
|
7
|
+
fields ||= @_action.class.needed_attributes.map(&:to_s)
|
8
|
+
self.assign_attributes(model.attributes.slice(*fields))
|
9
|
+
end
|
10
|
+
|
11
|
+
def persisted?
|
12
|
+
# is set on instantiation
|
13
|
+
@_action.persisted?
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_key
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_param
|
21
|
+
@_action.class.param_name
|
22
|
+
end
|
23
|
+
|
24
|
+
def attributes
|
25
|
+
@_action.class.needed_attributes.inject({}) do |sum, attr_name|
|
26
|
+
sum[attr_name] = self.instance_variable_get("@#{attr_name}")
|
27
|
+
sum
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Serious business</title>
|
5
|
+
<%= stylesheet_link_tag "serious_business/application", media: "all" %>
|
6
|
+
<%= javascript_include_tag "serious_business/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module SeriousBusiness
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
source_root File.expand_path('../templates', __FILE__)
|
10
|
+
|
11
|
+
class_option :use_uuid, :optional => true, :type => :string, :banner => "use_uuid",
|
12
|
+
:desc => "Set to true if you're using uuid as primary key type"
|
13
|
+
|
14
|
+
# Copy the initializer file to config/initializers folder.
|
15
|
+
def copy_initializer_file
|
16
|
+
template "initializer.rb", "config/initializers/serious_business.rb"
|
17
|
+
end
|
18
|
+
|
19
|
+
def use_uuid
|
20
|
+
options.key? :use_uuid
|
21
|
+
end
|
22
|
+
|
23
|
+
# Copy the migrations files to db/migrate folder
|
24
|
+
def copy_migration_files
|
25
|
+
# Copy core migration file in all cases except when you pass --only-submodules.
|
26
|
+
return unless defined?(SeriousBusiness::Generators::InstallGenerator::ActiveRecord)
|
27
|
+
migration_template "migration/actions.rb", "db/migrate/create_serious_business.rb", migration_class_name: migration_class_name
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
# Define the next_migration_number method (necessary for the migration_template method to work)
|
32
|
+
def self.next_migration_number(dirname)
|
33
|
+
if ActiveRecord::Base.timestamped_migrations
|
34
|
+
sleep 1 # make sure each time we get a different timestamp
|
35
|
+
Time.new.utc.strftime("%Y%m%d%H%M%S")
|
36
|
+
else
|
37
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def migration_class_name
|
44
|
+
if Rails::VERSION::MAJOR >= 5
|
45
|
+
"ActiveRecord::Migration[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
46
|
+
else
|
47
|
+
"ActiveRecord::Migration"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class CreateSeriousBusiness < <%= migration_class_name %>
|
2
|
+
def change
|
3
|
+
create_table :serious_actions<%= ', id: :uuid' if use_uuid %> do |t|
|
4
|
+
t.string :type, null: false
|
5
|
+
<% if use_uuid %>
|
6
|
+
t.uuid :actor_id
|
7
|
+
<% else %>
|
8
|
+
t.integer :actor_id
|
9
|
+
<% end %>
|
10
|
+
t.timestamps null: false
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :serious_affecteds do |t|
|
14
|
+
t.references :serious_action, foreign_key: true, null: false
|
15
|
+
<% if use_uuid %>
|
16
|
+
t.uuid :affected_id, null: false
|
17
|
+
<% else %>
|
18
|
+
t.integer :affected_id, null: false
|
19
|
+
<% end %>
|
20
|
+
t.string :affected_type
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "serious_business/engine"
|
2
|
+
require "generators/serious_business/install_generator"
|
3
|
+
#require "i18n-dot_lookup"
|
4
|
+
|
5
|
+
module SeriousBusiness
|
6
|
+
|
7
|
+
SeriousBusinessConfig = Struct.new(:actor_class_name)
|
8
|
+
|
9
|
+
def self.config &blk
|
10
|
+
@config ||= SeriousBusinessConfig.new
|
11
|
+
blk.call(@config)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.actor_class_name
|
15
|
+
@config.actor_class_name
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: serious_business
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Axel Tetzlaff
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-09-17 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: '5.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sqlite3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Secure your app by using our structured data model to specify the data
|
56
|
+
flow between views and semnatic actions
|
57
|
+
email:
|
58
|
+
- axel.tetzlaff@gmx.de
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- MIT-LICENSE
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- app/assets/config/serious_business_manifest.js
|
67
|
+
- app/assets/javascripts/serious_business/application.js
|
68
|
+
- app/assets/stylesheets/serious_business/application.css
|
69
|
+
- app/controllers/serious_business/application_controller.rb
|
70
|
+
- app/helpers/serious_business/application_helper.rb
|
71
|
+
- app/jobs/serious_business/application_job.rb
|
72
|
+
- app/mailers/serious_business/application_mailer.rb
|
73
|
+
- app/models/concerns/serious_business/actor.rb
|
74
|
+
- app/models/serious_business/action.rb
|
75
|
+
- app/models/serious_business/actor.rb
|
76
|
+
- app/models/serious_business/affected.rb
|
77
|
+
- app/models/serious_business/affected_by_actions.rb
|
78
|
+
- app/models/serious_business/application_record.rb
|
79
|
+
- app/models/serious_business/base_form_model.rb
|
80
|
+
- app/views/layouts/serious_business/application.html.erb
|
81
|
+
- config/routes.rb
|
82
|
+
- lib/generators/serious_business/install_generator.rb
|
83
|
+
- lib/generators/serious_business/templates/initializer.rb
|
84
|
+
- lib/generators/serious_business/templates/migration/actions.rb
|
85
|
+
- lib/serious_business.rb
|
86
|
+
- lib/serious_business/engine.rb
|
87
|
+
- lib/serious_business/version.rb
|
88
|
+
- lib/tasks/serious_business_tasks.rake
|
89
|
+
homepage: https://github.com/axelerator
|
90
|
+
licenses:
|
91
|
+
- MIT
|
92
|
+
metadata: {}
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 2.6.8
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: A gem to formalize application flow of views and actions
|
113
|
+
test_files: []
|