approval 0.3.6 → 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d170c89efb6ba5bbe4ce8133d3a147c2be3ed08f01565d6b4108a4f2adce5d3b
4
- data.tar.gz: e9b81099a7b01e3c43ff49c68d81ac5924189234d6c24fe314726abd736538e1
3
+ metadata.gz: 036447a5b614f818fa6a3ace7ee80d56c736d307bf1b8020cca81c2e6458ac07
4
+ data.tar.gz: 95feed0024428c045f96adaa675618751481ac9111f3aeafb79e02d6d9e17856
5
5
  SHA512:
6
- metadata.gz: 5444fc5b21bff736ea8a1459f24b6e79dac72f88748911fcdc94c464502c59b33691867e6bfa8d79f782ef0fcb297ce888d58295c9a37976371130cea8efec99
7
- data.tar.gz: 01fb588b9092d6ed0515ef34e83e0ab545ae8b3ffb529b687909a54e7ed18613f8574d9c85913af5e249a8f2f82e47cd04b398082d83710672d96b7190e0f2c3
6
+ metadata.gz: afdc581773b236d893187061146bbb58e4c3e5760eda31b74a73105c52ee22b3647ad2400f5b31421cf3224b9c5920fcc0f4ae572101721f741e746fd567f19b
7
+ data.tar.gz: 05a14a3c41748d87ecbdc9365b3198a7f750be241385966c5a5b55cc53c7c7874fdcb4092c933195a1f43b01dff5c81c91fbdd4ceb5c5e81c4df50fd67f58f8b
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Yoshiyuki Hirano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,5 @@
1
+ module Approval
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ module Approval
2
+ class Comment < ApplicationRecord
3
+ self.table_name = :approval_comments
4
+
5
+ def self.define_user_association
6
+ belongs_to :user, class_name: Approval.config.user_class_name
7
+ end
8
+
9
+ belongs_to :request, class_name: :"Approval::Request", inverse_of: :comments
10
+ validates :content, presence: true, length: { maximum: Approval.config.comment_maximum }
11
+ end
12
+ end
@@ -0,0 +1,72 @@
1
+ module Approval
2
+ class Item < ApplicationRecord
3
+ class UnexistResource < StandardError; end
4
+
5
+ self.table_name = :approval_items
6
+ EVENTS = %w[create update destroy].freeze
7
+
8
+ belongs_to :request, class_name: :"Approval::Request", inverse_of: :items
9
+ belongs_to :resource, polymorphic: true, optional: true
10
+
11
+ serialize :params, Hash
12
+
13
+ validates :resource_type, presence: true
14
+ validates :event, presence: true, inclusion: { in: EVENTS }
15
+
16
+ with_options unless: :create_event? do
17
+ validates :resource_id, presence: true
18
+ end
19
+
20
+ with_options if: :update_event? do
21
+ validates :params, presence: true
22
+ end
23
+
24
+ validate :ensure_resource_be_valid
25
+
26
+ EVENTS.each do |event_name|
27
+ define_method "#{event_name}_event?" do
28
+ event_name.to_s == event.to_s
29
+ end
30
+ end
31
+
32
+ def apply
33
+ case event
34
+ when "create"
35
+ resource_model.create!(params).tap do |created_resource|
36
+ update!(resource_id: created_resource.id)
37
+ end
38
+ when "update"
39
+ raise UnexistResource unless resource
40
+
41
+ resource.update!(params)
42
+ when "destroy"
43
+ raise UnexistResource unless resource
44
+
45
+ resource.destroy
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def resource_model
52
+ @resource_model ||= resource_type.to_s.safe_constantize
53
+ end
54
+
55
+ def ensure_resource_be_valid
56
+ return if resource_model.nil? || destroy_event?
57
+
58
+ record = if resource_id.present?
59
+ resource_model.find(resource_id).tap {|m| m.assign_attributes(params) }
60
+ else
61
+ resource_model.new(params || {})
62
+ end
63
+
64
+ unless record.valid?
65
+ errors.add(:base, :invalid)
66
+ record.errors.full_messages.each do |message|
67
+ request.errors.add(:base, message)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,39 @@
1
+ module Approval
2
+ class Request < ApplicationRecord
3
+ self.table_name = :approval_requests
4
+
5
+ def self.define_user_association
6
+ belongs_to :request_user, class_name: Approval.config.user_class_name
7
+ belongs_to :respond_user, class_name: Approval.config.user_class_name, optional: true
8
+ end
9
+
10
+ has_many :comments, class_name: :"Approval::Comment", inverse_of: :request, dependent: :destroy
11
+ has_many :items, class_name: :"Approval::Item", inverse_of: :request, dependent: :destroy
12
+
13
+ enum state: { pending: 0, cancelled: 1, approved: 2, rejected: 3 }
14
+
15
+ scope :recently, -> { order(id: :desc) }
16
+
17
+ validates :state, presence: true
18
+ validates :respond_user, presence: true, unless: :pending?
19
+ validates :comments, presence: true
20
+ validates :items, presence: true
21
+
22
+ validates_associated :comments
23
+ validates_associated :items
24
+
25
+ validate :ensure_state_was_pending
26
+
27
+ before_create do
28
+ self.requested_at = Time.current
29
+ end
30
+
31
+ private
32
+
33
+ def ensure_state_was_pending
34
+ return unless persisted?
35
+
36
+ errors.add(:base, :already_performed) if state_was != "pending"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ module Approval
2
+ module RequestForm
3
+ class Base
4
+ include ::ActiveModel::Model
5
+
6
+ attr_accessor :user, :reason, :records
7
+
8
+ def initialize(user:, reason:, records:)
9
+ @user = user
10
+ @reason = reason
11
+ @records = records
12
+ end
13
+
14
+ with_options presence: true do
15
+ validates :user
16
+ validates :reason, length: { maximum: Approval.config.comment_maximum }
17
+ validates :records
18
+ end
19
+
20
+ def save
21
+ return false unless valid?
22
+
23
+ prepare(&:save)
24
+ end
25
+
26
+ def save!
27
+ raise ::ActiveRecord::RecordInvalid unless valid?
28
+
29
+ prepare(&:save!)
30
+ end
31
+
32
+ def request
33
+ @request ||= user.approval_requests.new
34
+ end
35
+
36
+ def error_full_messages
37
+ [errors, request.errors].flat_map(&:full_messages)
38
+ end
39
+
40
+ private
41
+
42
+ def prepare
43
+ raise NotImplementedError, "you must implement #{self.class}##{__method__}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,21 @@
1
+ module Approval
2
+ module RequestForm
3
+ class Create < Base
4
+ private
5
+
6
+ def prepare
7
+ ::Approval::Request.transaction do
8
+ request.comments.new(user_id: user.id, content: reason)
9
+ Array(records).each do |record|
10
+ request.items.new(
11
+ event: "create",
12
+ resource_type: record.class.to_s,
13
+ params: record.create_params_for_approval,
14
+ )
15
+ end
16
+ yield(request)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Approval
2
+ module RequestForm
3
+ class Destroy < Base
4
+ private
5
+
6
+ def prepare
7
+ ::Approval::Request.transaction do
8
+ request.comments.new(user_id: user.id, content: reason)
9
+ Array(records).each do |record|
10
+ request.items.new(
11
+ event: "destroy",
12
+ resource_type: record.class.to_s,
13
+ resource_id: record.id,
14
+ )
15
+ end
16
+ yield(request)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ module Approval
2
+ module RequestForm
3
+ class Update < Base
4
+ private
5
+
6
+ def prepare
7
+ ::Approval::Request.transaction do
8
+ request.comments.new(user_id: user.id, content: reason)
9
+ Array(records).each do |record|
10
+ request.items.new(
11
+ event: "update",
12
+ resource_type: record.class.to_s,
13
+ resource_id: record.id,
14
+ params: record.update_params_for_approval,
15
+ )
16
+ end
17
+ yield(request)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module Approval
2
+ module RespondForm
3
+ class Approve < Base
4
+ validate :ensure_user_cannot_respond_to_my_request
5
+
6
+ private
7
+
8
+ def prepare
9
+ ::Approval::Request.transaction do
10
+ request.lock!
11
+ request.assign_attributes(state: :approved, approved_at: Time.current, respond_user_id: user.id)
12
+ request.comments.new(user_id: user.id, content: reason)
13
+ request.items.each(&:apply)
14
+ yield(request)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ module Approval
2
+ module RespondForm
3
+ class Base
4
+ include ::ActiveModel::Model
5
+
6
+ attr_accessor :user, :reason, :request
7
+
8
+ def initialize(user:, reason:, request:)
9
+ @user = user
10
+ @reason = reason
11
+ @request = request
12
+ end
13
+
14
+ with_options presence: true do
15
+ validates :user
16
+ validates :reason, length: { maximum: Approval.config.comment_maximum }
17
+ validates :request
18
+ end
19
+
20
+ def save
21
+ return false unless valid?
22
+
23
+ prepare(&:save)
24
+ end
25
+
26
+ def save!
27
+ raise ::ActiveRecord::RecordInvalid unless valid?
28
+
29
+ prepare(&:save!)
30
+ end
31
+
32
+ private
33
+
34
+ def prepare
35
+ raise NotImplementedError, "you must implement #{self.class}##{__method__}"
36
+ end
37
+
38
+ def ensure_user_cannot_respond_to_my_request
39
+ return if Approval.config.permit_to_respond_to_own_request?
40
+
41
+ errors.add(:user, :cannot_respond_to_own_request) if user.try(:id) == request.request_user_id
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ module Approval
2
+ module RespondForm
3
+ class Cancel < Base
4
+ private
5
+
6
+ def prepare
7
+ ::Approval::Request.transaction do
8
+ request.lock!
9
+ request.assign_attributes(state: :cancelled, cancelled_at: Time.current, respond_user_id: user.id)
10
+ request.comments.new(user_id: user.id, content: reason)
11
+ yield(request)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ module Approval
2
+ module RespondForm
3
+ class Reject < Base
4
+ validate :ensure_user_cannot_respond_to_my_request
5
+
6
+ private
7
+
8
+ def prepare
9
+ ::Approval::Request.transaction do
10
+ request.lock!
11
+ request.assign_attributes(state: :rejected, rejected_at: Time.current, respond_user_id: user.id)
12
+ request.comments.new(user_id: user.id, content: reason)
13
+ yield(request)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ module Approval
2
+ module ActsAsResource
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :approval_ignore_fields
7
+ self.approval_ignore_fields = %w[id created_at updated_at]
8
+
9
+ has_many :approval_items, class_name: :"Approval::Item", as: :resource
10
+ end
11
+
12
+ class_methods do
13
+ def assign_ignore_fields(ignore_fields = [])
14
+ self.approval_ignore_fields = approval_ignore_fields.concat(ignore_fields).map(&:to_s).uniq
15
+ end
16
+ end
17
+
18
+ def create_params_for_approval
19
+ attributes.except(*approval_ignore_fields).compact
20
+ end
21
+
22
+ def update_params_for_approval
23
+ changes.except(*approval_ignore_fields).each_with_object({}) {|(k, v), h| h[k] = v.last }.compact
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ module Approval
2
+ module ActsAsUser
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :approval_requests, class_name: :"Approval::Request", foreign_key: :request_user_id
7
+ has_many :approval_comments, class_name: :"Approval::Comment", foreign_key: :user_id
8
+ end
9
+
10
+ def request_for_create(records, reason:)
11
+ Approval::RequestForm::Create.new(user: self, reason: reason, records: records)
12
+ end
13
+
14
+ def request_for_update(records, reason:)
15
+ Approval::RequestForm::Update.new(user: self, reason: reason, records: records)
16
+ end
17
+
18
+ def request_for_destroy(records, reason:)
19
+ Approval::RequestForm::Destroy.new(user: self, reason: reason, records: records)
20
+ end
21
+
22
+ def cancel_request(request, reason:)
23
+ Approval::RespondForm::Cancel.new(user: self, reason: reason, request: request)
24
+ end
25
+
26
+ def approve_request(request, reason:)
27
+ Approval::RespondForm::Approve.new(user: self, reason: reason, request: request)
28
+ end
29
+
30
+ def reject_request(request, reason:)
31
+ Approval::RespondForm::Reject.new(user: self, reason: reason, request: request)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ module Approval
2
+ module Mixins
3
+ def acts_as_approval_resource(ignore_fields: [])
4
+ include ::Approval::ActsAsResource
5
+ assign_ignore_fields(ignore_fields)
6
+ end
7
+
8
+ def acts_as_approval_user
9
+ include ::Approval::ActsAsUser
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,43 @@
1
+ class CreateApprovalTables < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :approval_requests do |t|
4
+ t.integer :request_user_id, null: false
5
+ t.integer :respond_user_id
6
+ t.integer :state, null: false, limit: 1, default: 0
7
+ t.datetime :requested_at, null: false
8
+ t.datetime :cancelled_at
9
+ t.datetime :approved_at
10
+ t.datetime :rejected_at
11
+
12
+ t.timestamps
13
+
14
+ t.index :request_user_id
15
+ t.index :respond_user_id
16
+ t.index :state
17
+ end
18
+
19
+ create_table :approval_items do |t|
20
+ t.integer :request_id, null: false
21
+ t.integer :resource_id
22
+ t.string :resource_type, null: false
23
+ t.string :event, null: false
24
+ t.text :params
25
+
26
+ t.timestamps
27
+
28
+ t.index :request_id
29
+ t.index [:resource_id, :resource_type]
30
+ end
31
+
32
+ create_table :approval_comments do |t|
33
+ t.integer :request_id, null: false
34
+ t.integer :user_id, null: false
35
+ t.text :content, null: false
36
+
37
+ t.timestamps
38
+
39
+ t.index :request_id
40
+ t.index :user_id
41
+ end
42
+ end
43
+ end
@@ -1,3 +1,3 @@
1
1
  module Approval
2
- VERSION = "0.3.6".freeze
2
+ VERSION = "0.3.7".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: approval
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.6
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoshiyuki Hirano
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-09 00:00:00.000000000 Z
11
+ date: 2018-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -31,9 +31,26 @@ executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
+ - LICENSE.txt
34
35
  - README.md
36
+ - app/models/approval/application_record.rb
37
+ - app/models/approval/comment.rb
38
+ - app/models/approval/item.rb
39
+ - app/models/approval/request.rb
40
+ - app/models/approval/request_form/base.rb
41
+ - app/models/approval/request_form/create.rb
42
+ - app/models/approval/request_form/destroy.rb
43
+ - app/models/approval/request_form/update.rb
44
+ - app/models/approval/respond_form/approve.rb
45
+ - app/models/approval/respond_form/base.rb
46
+ - app/models/approval/respond_form/cancel.rb
47
+ - app/models/approval/respond_form/reject.rb
48
+ - app/models/concerns/approval/acts_as_resource.rb
49
+ - app/models/concerns/approval/acts_as_user.rb
50
+ - app/models/concerns/approval/mixins.rb
35
51
  - config/locales/en.yml
36
52
  - config/locales/ja.yml
53
+ - db/migrate/20180409000000_create_approval_tables.rb
37
54
  - lib/approval.rb
38
55
  - lib/approval/config.rb
39
56
  - lib/approval/engine.rb