protected_record 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +11 -0
- data/Gemfile +3 -0
- data/README.md +31 -0
- data/lib/protected_record.rb +5 -0
- data/lib/protected_record/change_log.rb +30 -0
- data/lib/protected_record/change_request.rb +31 -0
- data/lib/protected_record/use_case/base.rb +21 -0
- data/lib/protected_record/use_case/change/change_log/create.rb +43 -0
- data/lib/protected_record/use_case/change/change_request/create.rb +51 -0
- data/lib/protected_record/use_case/change/update.rb +80 -0
- data/lib/protected_record/use_case/result.rb +40 -0
- data/lib/protected_record/version.rb +3 -0
- data/protected_record.gemspec +26 -0
- data/test/minitest_helper.rb +12 -0
- metadata +123 -0
data/.gitignore
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
2
|
+
#
|
3
|
+
# If you find yourself ignoring temporary files generated by your text editor
|
4
|
+
# or operating system, you probably want to add a global ignore instead:
|
5
|
+
# git config --global core.excludesfile ~/.gitignore_global
|
6
|
+
|
7
|
+
# Ignore bundler config
|
8
|
+
/.bundle
|
9
|
+
|
10
|
+
*.sw?
|
11
|
+
.sw?
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
Just include in your tracked models:
|
2
|
+
|
3
|
+
include ProtectedRecord::ChangeRequest::Changeling
|
4
|
+
include ProtectedRecord::ChangeLog::Changeling
|
5
|
+
|
6
|
+
and include in your user model:
|
7
|
+
|
8
|
+
include ProtectedRecord::ChangeRequest::Changer
|
9
|
+
include ProtectedRecord::ChangeLog::Changer
|
10
|
+
|
11
|
+
then in your controller
|
12
|
+
|
13
|
+
# UseCase module will filter changes to protected_keys,
|
14
|
+
# creating a "change request" rather than applying changes
|
15
|
+
|
16
|
+
# UseCase module will allow other changes to be applied,
|
17
|
+
# creating a "change log" entry for the observed changes
|
18
|
+
|
19
|
+
update_result = ProtectedRecord::UseCase::Update.new({
|
20
|
+
params: visit_params,
|
21
|
+
protected_record: @patient_visit,
|
22
|
+
user: current_user,
|
23
|
+
protected_keys: %w{ visit_date do_not_resuscitate }
|
24
|
+
}).execute!
|
25
|
+
|
26
|
+
update_result.successful?
|
27
|
+
|
28
|
+
and call methods like
|
29
|
+
|
30
|
+
@user.change_log_records
|
31
|
+
@user.change_request_records
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ProtectedRecord
|
2
|
+
module ChangeLog
|
3
|
+
class Record < ActiveRecord::Base
|
4
|
+
self.table_name = "change_log_records"
|
5
|
+
attr_accessible :recordable, :user
|
6
|
+
belongs_to :recordable, polymorphic: true
|
7
|
+
belongs_to :user, class_name: "User", foreign_key: :user_id
|
8
|
+
end
|
9
|
+
|
10
|
+
# Include this module in models inheriting from AR::Base
|
11
|
+
module Changer
|
12
|
+
def self.included(base)
|
13
|
+
# Include this in AR models only
|
14
|
+
return unless base.ancestors.include?(ActiveRecord::Base)
|
15
|
+
|
16
|
+
base.has_many :change_log_records, class_name: "ProtectedRecord::ChangeLog::Record"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Include this module in models inheriting from AR::Base
|
21
|
+
module Changeling
|
22
|
+
def self.included(base)
|
23
|
+
# Include this in AR models only
|
24
|
+
return unless base.ancestors.include?(ActiveRecord::Base)
|
25
|
+
|
26
|
+
base.has_many :change_log_records, as: :recordable, class_name: "ProtectedRecord::ChangeLog::Record"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ProtectedRecord
|
2
|
+
module ChangeRequest
|
3
|
+
class Record < ActiveRecord::Base
|
4
|
+
self.table_name = 'change_request_records'
|
5
|
+
attr_accessible :recordable, :user
|
6
|
+
|
7
|
+
belongs_to :recordable, polymorphic: true
|
8
|
+
belongs_to :user, class_name: "User",
|
9
|
+
foreign_key: :user_id
|
10
|
+
end
|
11
|
+
|
12
|
+
module Changer
|
13
|
+
def self.included(base)
|
14
|
+
# Include this in models only
|
15
|
+
return unless base.ancestors.include?(ActiveRecord::Base)
|
16
|
+
|
17
|
+
base.has_many :change_request_records, class_name: "ProtectedRecord::ChangeRequest::Record"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Changeling
|
22
|
+
def self.included(base)
|
23
|
+
# Include this in models only
|
24
|
+
return unless base.ancestors.include?(ActiveRecord::Base)
|
25
|
+
|
26
|
+
base.has_many :change_request_records, as: :recordable,
|
27
|
+
class_name: "ProtectedRecord::ChangeRequest::Record"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ProtectedRecord
|
2
|
+
module UseCase
|
3
|
+
class Base
|
4
|
+
def load_option(option, options)
|
5
|
+
instance_variable_set("@#{option}", options.fetch(option.to_sym) { raise "Missing required option: #{option}" } )
|
6
|
+
end
|
7
|
+
|
8
|
+
def load_options(*option_names, options)
|
9
|
+
option_names.each{|o| load_option(o, options) }
|
10
|
+
@errors ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def errors
|
14
|
+
@errors
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ProtectedRecord
|
2
|
+
module UseCase
|
3
|
+
module Change
|
4
|
+
module ChangeLog
|
5
|
+
class Create < ::ProtectedRecord::UseCase::Base
|
6
|
+
def initialize(options)
|
7
|
+
options = {
|
8
|
+
record_class: ::ProtectedRecord::ChangeLog::Record
|
9
|
+
}.merge!(options) if !options.has_key?(:record_class)
|
10
|
+
|
11
|
+
load_options(:record_class, :user, :changed_object, options) and validate_state
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute!
|
15
|
+
create_change_log_record if @errors.empty?
|
16
|
+
|
17
|
+
if @errors.empty? && @record.save
|
18
|
+
return UseCase::Result.new(data: { change_log_record: @record })
|
19
|
+
else
|
20
|
+
return UseCase::Result.new(data: { change_log_record: @record }, errors: @errors)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def create_change_log_record
|
26
|
+
@record = @record_class.new
|
27
|
+
@record.user = @user
|
28
|
+
@record.recordable = @changed_object
|
29
|
+
@record.observed_changes = ActiveSupport::JSON.encode(@changed_object.previous_changes)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
def validate_state
|
34
|
+
# What are we doing here, if not logging a change that has already happened?
|
35
|
+
if !@changed_object.previous_changes.present?
|
36
|
+
@errors << ActiveRecord::ActiveRecordError.new(':changed_object has no previous_changes')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ProtectedRecord
|
2
|
+
module UseCase
|
3
|
+
module Change
|
4
|
+
module ChangeRequest
|
5
|
+
class Create < ::ProtectedRecord::UseCase::Base
|
6
|
+
def initialize(options)
|
7
|
+
options = {
|
8
|
+
record_class: ::ProtectedRecord::ChangeRequest::Record
|
9
|
+
}.merge!(options) if !options.has_key?(:record_class)
|
10
|
+
|
11
|
+
load_options(:protected_keys, :record_class, :user, :dirty_object, options) and validate_state
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute!
|
15
|
+
return UseCase::Result.new(data: nil, success: true) if !requested_changes.present?
|
16
|
+
|
17
|
+
create_change_request_record if @errors.empty?
|
18
|
+
|
19
|
+
if @errors.empty? && @record.save
|
20
|
+
return UseCase::Result.new(data: { change_request_record: @record })
|
21
|
+
else
|
22
|
+
return UseCase::Result.new(data: { change_request_record: @record }, errors: @errors)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def create_change_request_record
|
28
|
+
@record = @record_class.new
|
29
|
+
@record.user = @user
|
30
|
+
@record.recordable = @dirty_object
|
31
|
+
@record.requested_changes = ActiveSupport::JSON.encode(requested_changes)
|
32
|
+
end
|
33
|
+
|
34
|
+
def requested_changes
|
35
|
+
@dirty_object.changes.select do |key|
|
36
|
+
@protected_keys.map(&:to_s).include? key.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
def validate_state
|
42
|
+
# If there are no changes, there's no need to make a request
|
43
|
+
if !@dirty_object.changes.present?
|
44
|
+
@errors << ActiveRecord::ActiveRecordError.new(':dirty_object not dirty')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module ProtectedRecord
|
2
|
+
module UseCase
|
3
|
+
module Change
|
4
|
+
class Update < ::ProtectedRecord::UseCase::Base
|
5
|
+
def initialize(options)
|
6
|
+
# Defaults
|
7
|
+
options = {
|
8
|
+
change_request_use_case: UseCase::Change::ChangeRequest::Create,
|
9
|
+
change_log_use_case: UseCase::Change::ChangeLog::Create
|
10
|
+
}.merge!(options) if !options.has_key?(:record_class)
|
11
|
+
|
12
|
+
load_options(:params, :protected_record, :change_request_use_case, :change_log_use_case, :protected_keys, :user, options)
|
13
|
+
validate_state
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute!
|
17
|
+
form_change_request
|
18
|
+
|
19
|
+
if @errors.empty? && !@protected_record.changes.present?
|
20
|
+
return UseCase::Result.new(data: { params: @protected_record })
|
21
|
+
else
|
22
|
+
return UseCase::Result.new(data: { params: @protected_record }, errors: @errors)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def form_change_request
|
28
|
+
@protected_record.attributes = @params
|
29
|
+
|
30
|
+
unless @protected_record.id_was.nil?
|
31
|
+
request_result = @change_request_use_case.new(protected_keys: @protected_keys, dirty_object: @protected_record, user: @user).execute!
|
32
|
+
if request_result.successful?
|
33
|
+
revert_protected_attrs
|
34
|
+
save_protected_record
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def revert_protected_attrs
|
40
|
+
@protected_keys.each do |key|
|
41
|
+
if @protected_record.send("#{key.to_s}_changed?")
|
42
|
+
@protected_record.send("#{key.to_s}=", @protected_record.send("#{key.to_s}_was"))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
raise if @protected_keys.any? { |key| @protected_record.send("#{key}_changed?") }
|
47
|
+
end
|
48
|
+
|
49
|
+
def save_protected_record
|
50
|
+
@protected_record.save
|
51
|
+
log_changes
|
52
|
+
end
|
53
|
+
|
54
|
+
def log_changes
|
55
|
+
log_result = @change_log_use_case.new(user: @user, changed_object: @protected_record).execute!
|
56
|
+
return true if log_result.successful?
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
def validate_state
|
61
|
+
# We expect some keys
|
62
|
+
if !@protected_keys.kind_of?(Array)
|
63
|
+
@errors << TypeError.new(':protected_keys not kind of Array')
|
64
|
+
end
|
65
|
+
|
66
|
+
# The keys should respond to to_s
|
67
|
+
if @protected_keys.any? {|el| !el.respond_to?("to_s") }
|
68
|
+
@errors << TypeError.new('All :protected_keys should respond to #to_s')
|
69
|
+
end
|
70
|
+
|
71
|
+
# The dirty_object should respond to all keys (as methods)
|
72
|
+
@protected_keys.each do |key|
|
73
|
+
error = ActiveRecord::ActiveRecordError.new(":dirty_object must respond to #{key.to_s}")
|
74
|
+
@errors << error unless @protected_record.respond_to?(key.to_s)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ProtectedRecord
|
2
|
+
module UseCase
|
3
|
+
class Result
|
4
|
+
# The response from a use case execution
|
5
|
+
#
|
6
|
+
# Every use case should return a Result after it runs.
|
7
|
+
#
|
8
|
+
# @param [options] options_hash
|
9
|
+
# A hash specifying the appropriate options
|
10
|
+
#
|
11
|
+
# @return [UseCase::Result]
|
12
|
+
# the Result instance
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# UseCase::Result.new(success: true, data: {})
|
16
|
+
# # => <UseCase::Result>
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
def initialize(options)
|
20
|
+
@success = options.fetch(:success) { !options[:errors].present? }
|
21
|
+
@errors = options[:errors]
|
22
|
+
@data = options[:data]
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api public
|
26
|
+
def successful?
|
27
|
+
!!@success
|
28
|
+
end
|
29
|
+
|
30
|
+
def errors
|
31
|
+
@errors
|
32
|
+
end
|
33
|
+
|
34
|
+
# @api public
|
35
|
+
def data
|
36
|
+
@data
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "protected_record/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "protected_record"
|
7
|
+
s.version = ProtectedRecord::VERSION
|
8
|
+
s.authors = ["Tad Hosford"]
|
9
|
+
s.email = ["tad.hosford@gmail.com"]
|
10
|
+
s.homepage = "http://github.com/rthbound/protected_record"
|
11
|
+
s.description = %q{
|
12
|
+
Filters changes & logs changes to protected records.
|
13
|
+
Creates change requests when changes are attempted on protected attrs
|
14
|
+
}
|
15
|
+
s.summary = %q{ }
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_development_dependency "minitest"
|
23
|
+
|
24
|
+
s.add_development_dependency "rake"
|
25
|
+
s.add_development_dependency "pry"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: protected_record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tad Hosford
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: minitest
|
16
|
+
version_requirements: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ! '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: !binary |-
|
21
|
+
MA==
|
22
|
+
none: false
|
23
|
+
requirement: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ! '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: !binary |-
|
28
|
+
MA==
|
29
|
+
none: false
|
30
|
+
prerelease: false
|
31
|
+
type: :development
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: rake
|
34
|
+
version_requirements: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: !binary |-
|
39
|
+
MA==
|
40
|
+
none: false
|
41
|
+
requirement: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: !binary |-
|
46
|
+
MA==
|
47
|
+
none: false
|
48
|
+
prerelease: false
|
49
|
+
type: :development
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: pry
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: !binary |-
|
57
|
+
MA==
|
58
|
+
none: false
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: !binary |-
|
64
|
+
MA==
|
65
|
+
none: false
|
66
|
+
prerelease: false
|
67
|
+
type: :development
|
68
|
+
description: ! "\n Filters changes & logs changes to protected\
|
69
|
+
\ records.\n Creates change requests when changes are attempted\
|
70
|
+
\ on protected attrs\n "
|
71
|
+
email:
|
72
|
+
- tad.hosford@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- .gitignore
|
78
|
+
- Gemfile
|
79
|
+
- README.md
|
80
|
+
- lib/protected_record.rb
|
81
|
+
- lib/protected_record/change_log.rb
|
82
|
+
- lib/protected_record/change_request.rb
|
83
|
+
- lib/protected_record/use_case/base.rb
|
84
|
+
- lib/protected_record/use_case/change/change_log/create.rb
|
85
|
+
- lib/protected_record/use_case/change/change_request/create.rb
|
86
|
+
- lib/protected_record/use_case/change/update.rb
|
87
|
+
- lib/protected_record/use_case/result.rb
|
88
|
+
- lib/protected_record/version.rb
|
89
|
+
- protected_record.gemspec
|
90
|
+
- test/minitest_helper.rb
|
91
|
+
homepage: http://github.com/rthbound/protected_record
|
92
|
+
licenses: []
|
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
|
+
segments:
|
102
|
+
- 0
|
103
|
+
hash: 2
|
104
|
+
version: !binary |-
|
105
|
+
MA==
|
106
|
+
none: false
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ! '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
hash: 2
|
114
|
+
version: !binary |-
|
115
|
+
MA==
|
116
|
+
none: false
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 1.8.24
|
120
|
+
signing_key:
|
121
|
+
specification_version: 3
|
122
|
+
summary: ''
|
123
|
+
test_files: []
|