recorder 0.1.23 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.rubocop.yml +27 -0
- data/Gemfile +2 -0
- data/README.md +33 -3
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/lib/generators/recorder/install_generator.rb +14 -12
- data/lib/generators/recorder/templates/add_index_by_user_id_to_recorder_revisions.rb +2 -0
- data/lib/generators/recorder/templates/add_number_column_to_recorder_revisions.rb +23 -21
- data/lib/generators/recorder/templates/create_recorder_revisions.rb +4 -2
- data/lib/recorder/changeset.rb +14 -12
- data/lib/recorder/config.rb +13 -5
- data/lib/recorder/manager.rb +18 -0
- data/lib/recorder/observer.rb +11 -18
- data/lib/recorder/rails/controller_concern.rb +10 -7
- data/lib/recorder/rails/railtie.rb +8 -4
- data/lib/recorder/revision.rb +75 -54
- data/lib/recorder/sidekiq/revisions_worker.rb +4 -3
- data/lib/recorder/store.rb +36 -0
- data/lib/recorder/tape/data.rb +83 -0
- data/lib/recorder/tape/record.rb +39 -0
- data/lib/recorder/tape.rb +32 -79
- data/lib/recorder/version.rb +3 -13
- data/lib/recorder.rb +14 -10
- data/recorder.gemspec +24 -23
- metadata +40 -36
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Recorder
|
2
4
|
module Rails
|
3
5
|
# Extensions to rails controllers. Provides convenient ways to pass certain
|
@@ -8,7 +10,7 @@ module Recorder
|
|
8
10
|
base.before_action :set_recorder_meta
|
9
11
|
end
|
10
12
|
|
11
|
-
|
13
|
+
protected
|
12
14
|
|
13
15
|
# Returns the user who is responsible for any changes that occur.
|
14
16
|
# By default this calls `current_user` and returns the result.
|
@@ -17,6 +19,7 @@ module Recorder
|
|
17
19
|
# method, e.g. `current_person`, or anything you like.
|
18
20
|
def recorder_user_id
|
19
21
|
return unless defined?(current_user)
|
22
|
+
|
20
23
|
current_user.try!(:id)
|
21
24
|
rescue NoMethodError
|
22
25
|
current_user
|
@@ -24,10 +27,10 @@ module Recorder
|
|
24
27
|
|
25
28
|
def recorder_info
|
26
29
|
{
|
27
|
-
:
|
28
|
-
:
|
29
|
-
:
|
30
|
-
:
|
30
|
+
user_id: recorder_user_id,
|
31
|
+
ip: request.remote_ip,
|
32
|
+
action_date: Date.current,
|
33
|
+
meta: recorder_meta
|
31
34
|
}
|
32
35
|
end
|
33
36
|
|
@@ -40,11 +43,11 @@ module Recorder
|
|
40
43
|
# Tells Recorder any information from the controller you want to store
|
41
44
|
# alongside any changes that occur.
|
42
45
|
def set_recorder_info
|
43
|
-
::Recorder.info =
|
46
|
+
::Recorder.info = recorder_info
|
44
47
|
end
|
45
48
|
|
46
49
|
def set_recorder_meta
|
47
|
-
::Recorder.meta =
|
50
|
+
::Recorder.meta = recorder_meta
|
48
51
|
end
|
49
52
|
end
|
50
53
|
end
|
@@ -1,7 +1,11 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Recorder
|
4
|
+
module Rails
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
initializer 'recorder.configure' do |_app|
|
7
|
+
require 'recorder/sidekiq/revisions_worker' if defined?(Sidekiq)
|
8
|
+
end
|
5
9
|
end
|
6
10
|
end
|
7
11
|
end
|
data/lib/recorder/revision.rb
CHANGED
@@ -1,72 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record'
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
if ::Recorder.active_record_protected_attributes?
|
7
|
-
attr_accessible(
|
8
|
-
:event,
|
9
|
-
:user_id,
|
10
|
-
:ip,
|
11
|
-
:user_agent,
|
12
|
-
:action_date,
|
13
|
-
:data,
|
14
|
-
:meta
|
15
|
-
)
|
16
|
-
end
|
5
|
+
module Recorder
|
6
|
+
class Revision < ActiveRecord::Base
|
7
|
+
self.table_name = 'recorder_revisions'
|
17
8
|
|
18
|
-
|
19
|
-
|
9
|
+
if ::Recorder.active_record_protected_attributes?
|
10
|
+
attr_accessible(
|
11
|
+
:event,
|
12
|
+
:user_id,
|
13
|
+
:ip,
|
14
|
+
:user_agent,
|
15
|
+
:action_date,
|
16
|
+
:data,
|
17
|
+
:meta
|
18
|
+
)
|
19
|
+
end
|
20
20
|
|
21
|
-
|
22
|
-
validates :event, :presence => true
|
23
|
-
validates :action_date, :presence => true
|
24
|
-
validates :data, :presence => true
|
21
|
+
belongs_to :user
|
25
22
|
|
26
|
-
|
23
|
+
validates :item_type, presence: true
|
24
|
+
validates :event, presence: true
|
25
|
+
validates :action_date, presence: true
|
26
|
+
validates :data, presence: true
|
27
27
|
|
28
|
-
|
29
|
-
# @return [Recorder::Changeset]
|
30
|
-
def item_changeset
|
31
|
-
return @item_changeset if defined?(@item_changeset)
|
32
|
-
return nil if self.data['changes'].nil?
|
28
|
+
scope :ordered_by_created_at, -> { order(created_at: :desc) }
|
33
29
|
|
34
|
-
|
35
|
-
|
30
|
+
def item
|
31
|
+
return @item if defined?(@item)
|
32
|
+
return if item_id.nil?
|
36
33
|
|
37
|
-
|
38
|
-
# @return [Array]
|
39
|
-
def changed_associations
|
40
|
-
self.data['associations'].try(:keys) || []
|
41
|
-
end
|
34
|
+
@item = item_type.classify.constantize.new(data['attributes'])
|
42
35
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
# association = association.source if association.decorated?
|
36
|
+
if data['associations'].present?
|
37
|
+
data['associations'].each do |name, association|
|
38
|
+
@item.send("build_#{name}", association['attributes'])
|
39
|
+
end
|
40
|
+
end
|
49
41
|
|
50
|
-
|
51
|
-
|
42
|
+
@item
|
43
|
+
end
|
52
44
|
|
53
|
-
|
45
|
+
# Get changeset for an item
|
46
|
+
# @return [Recorder::Changeset]
|
47
|
+
def item_changeset
|
48
|
+
return @item_changeset if defined?(@item_changeset)
|
49
|
+
return nil if item.nil?
|
50
|
+
return nil if data['changes'].nil?
|
54
51
|
|
55
|
-
|
56
|
-
|
57
|
-
# If `#recorder_changeset_class` method is not defined, then class name is generated as "#{class}Changeset"
|
58
|
-
# @api private
|
59
|
-
def changeset_class(object)
|
60
|
-
klass = (defined?(Draper) && object.decorated?) ? object.source.class : object.class
|
61
|
-
klass = klass.base_class
|
52
|
+
@item_changeset ||= changeset_class(item).new(item, data['changes'])
|
53
|
+
end
|
62
54
|
|
63
|
-
|
64
|
-
|
55
|
+
# Get names of item associations that has been changed
|
56
|
+
# @return [Array]
|
57
|
+
def changed_associations
|
58
|
+
data['associations'].try(:keys) || []
|
65
59
|
end
|
66
60
|
|
67
|
-
|
61
|
+
# Get changeset for an association
|
62
|
+
# @param name [String] name of association to return changeset
|
63
|
+
# @return [Recorder::Changeset]
|
64
|
+
def association_changeset(name)
|
65
|
+
association = item.send(name)
|
66
|
+
# association = association.source if association.decorated?
|
68
67
|
|
69
|
-
|
70
|
-
|
68
|
+
changeset_class(association).new(association, data['associations'].fetch(name.to_s).try(:fetch, 'changes'))
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
# Returns changeset class for passed object.
|
74
|
+
# Changeset class name can be overriden with `#recorder_changeset_class` method.
|
75
|
+
# If `#recorder_changeset_class` method is not defined, then class name is generated as "#{class}Changeset"
|
76
|
+
# @api private
|
77
|
+
def changeset_class(object)
|
78
|
+
klass = defined?(Draper) && object.decorated? ? object.source.class : object.class
|
79
|
+
klass = klass.base_class
|
80
|
+
|
81
|
+
return klass.send(:recorder_changeset_class) if klass.respond_to?(:recorder_changeset_class)
|
82
|
+
|
83
|
+
klass = "#{klass}Changeset"
|
84
|
+
|
85
|
+
klass = begin
|
86
|
+
klass.constantize
|
87
|
+
rescue
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
klass.present? ? klass : Recorder::Changeset
|
91
|
+
end
|
71
92
|
end
|
72
93
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Recorder
|
2
4
|
module Sidekiq
|
3
5
|
class RevisionsWorker
|
@@ -5,9 +7,8 @@ module Recorder
|
|
5
7
|
|
6
8
|
sidekiq_options Recorder.config.sidekiq_options
|
7
9
|
|
8
|
-
def perform(
|
9
|
-
|
10
|
-
object.revisions.create(params) if object.present?
|
10
|
+
def perform(params)
|
11
|
+
Recorder::Revision.create(params)
|
11
12
|
end
|
12
13
|
end
|
13
14
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'request_store'
|
4
|
+
|
5
|
+
module Recorder
|
6
|
+
class Store
|
7
|
+
def params
|
8
|
+
store[:params]
|
9
|
+
end
|
10
|
+
|
11
|
+
def recorder_enabled!
|
12
|
+
store[:enabled] = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def recorder_disabled!
|
16
|
+
store[:enabled] = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def recorder_enabled?
|
20
|
+
store[:enabled]
|
21
|
+
end
|
22
|
+
|
23
|
+
def recorder_disabled?
|
24
|
+
!store[:enabled]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def store
|
30
|
+
RequestStore.store[:recorder] ||= {
|
31
|
+
enabled: true,
|
32
|
+
params: {}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Recorder
|
4
|
+
class Tape
|
5
|
+
class Data
|
6
|
+
attr_reader :item
|
7
|
+
|
8
|
+
def initialize(item)
|
9
|
+
@item = item
|
10
|
+
end
|
11
|
+
|
12
|
+
def data_for(event, options = {})
|
13
|
+
{
|
14
|
+
**attributes_for(event, options),
|
15
|
+
**changes_for(event, options),
|
16
|
+
**associations_for(event, options)
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def attributes_for(_event, options)
|
21
|
+
{attributes: sanitize_attributes(item.attributes, options)}
|
22
|
+
end
|
23
|
+
|
24
|
+
def changes_for(event, options)
|
25
|
+
changes =
|
26
|
+
case event.to_sym
|
27
|
+
when :update
|
28
|
+
sanitize_attributes(item.saved_changes, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
changes.present? ? {changes: changes} : {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def associations_for(event, options)
|
35
|
+
associations = parse_associations_attributes(event, options)
|
36
|
+
|
37
|
+
associations.present? ? {associations: associations} : {}
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def sanitize_attributes(attributes, options)
|
43
|
+
if options[:only].present?
|
44
|
+
only = wrap_options(options[:only])
|
45
|
+
attributes.symbolize_keys.slice(*only)
|
46
|
+
elsif options[:ignore].present?
|
47
|
+
ignore = wrap_options(options[:ignore])
|
48
|
+
attributes.symbolize_keys.except(*ignore)
|
49
|
+
else
|
50
|
+
attributes.symbolize_keys.except(*Recorder.config.ignore)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def wrap_options(values)
|
55
|
+
Array.wrap(values).map(&:to_sym)
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_associations_attributes(event, options)
|
59
|
+
return unless options[:associations]
|
60
|
+
|
61
|
+
options[:associations].each_with_object({}) do |(association, options), hash|
|
62
|
+
name, data = parse_association(event, association, options)
|
63
|
+
|
64
|
+
hash[name] = data if data.any?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_association(event, association, options)
|
69
|
+
reflection = item.class.reflect_on_association(association)
|
70
|
+
|
71
|
+
if reflection.present?
|
72
|
+
if reflection.collection?
|
73
|
+
|
74
|
+
elsif (object = item.send(association))
|
75
|
+
data = Recorder::Tape::Data.new(object).data_for(event, options || {})
|
76
|
+
|
77
|
+
[reflection.name, data]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Recorder
|
4
|
+
class Tape
|
5
|
+
module Record
|
6
|
+
def record(params, options = {})
|
7
|
+
return if Recorder.store.recorder_disabled?
|
8
|
+
|
9
|
+
params = params_for(params)
|
10
|
+
|
11
|
+
if async?(options)
|
12
|
+
record_async(params, options)
|
13
|
+
else
|
14
|
+
Recorder::Revision.create(params)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def params_for(params)
|
21
|
+
Recorder.store.params.merge({
|
22
|
+
action_date: Date.today,
|
23
|
+
**params
|
24
|
+
})
|
25
|
+
end
|
26
|
+
|
27
|
+
def async?(options)
|
28
|
+
options[:async].nil? ? Recorder.config.async : options[:async]
|
29
|
+
end
|
30
|
+
|
31
|
+
def record_async(params, options)
|
32
|
+
Recorder::Sidekiq::RevisionsWorker.perform_in(
|
33
|
+
options[:delay] || 2.seconds,
|
34
|
+
params
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/recorder/tape.rb
CHANGED
@@ -1,105 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'recorder/tape/data'
|
4
|
+
require 'recorder/tape/record'
|
5
|
+
|
1
6
|
module Recorder
|
2
7
|
class Tape
|
3
|
-
|
8
|
+
extend Record
|
4
9
|
|
5
|
-
|
6
|
-
@item = item;
|
10
|
+
attr_reader :item, :data
|
7
11
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def changes_for(event)
|
12
|
-
changes = case event.to_sym
|
13
|
-
when :create
|
14
|
-
self.sanitize_attributes(self.item.attributes)
|
15
|
-
when :update
|
16
|
-
self.sanitize_attributes(self.item.changes)
|
17
|
-
when :destroy
|
18
|
-
self.sanitize_attributes(self.item.changes)
|
19
|
-
else
|
20
|
-
raise ArgumentError
|
21
|
-
end
|
12
|
+
def initialize(item)
|
13
|
+
@item = item
|
14
|
+
@data = Data.new(item)
|
22
15
|
|
23
|
-
|
16
|
+
item.instance_variable_set(:@recorder_dirty, true)
|
24
17
|
end
|
25
18
|
|
26
19
|
def record_create
|
27
|
-
data =
|
28
|
-
|
29
|
-
associations_attributes = self.parse_associations_attributes(:create)
|
30
|
-
data.merge!(:associations => associations_attributes) if associations_attributes.present?
|
20
|
+
data = data_for(:create, recorder_options)
|
31
21
|
|
32
|
-
if data.any?
|
33
|
-
self.record(
|
34
|
-
Recorder.store.merge({
|
35
|
-
:event => :create,
|
36
|
-
:data => data
|
37
|
-
})
|
38
|
-
)
|
39
|
-
end
|
22
|
+
record(event: :create, data: data) if data.any?
|
40
23
|
end
|
41
24
|
|
42
25
|
def record_update
|
43
|
-
data =
|
26
|
+
data = data_for(:update, recorder_options)
|
44
27
|
|
45
|
-
|
46
|
-
data.merge!(:associations => associations_attributes) if associations_attributes.present?
|
47
|
-
|
48
|
-
if data.any?
|
49
|
-
self.record(
|
50
|
-
Recorder.store.merge({
|
51
|
-
:event => :update,
|
52
|
-
:data => data
|
53
|
-
})
|
54
|
-
)
|
55
|
-
end
|
28
|
+
record(event: :update, data: data) if data.any?
|
56
29
|
end
|
57
30
|
|
58
31
|
def record_destroy
|
59
|
-
|
32
|
+
data = data_for(:destroy, recorder_options)
|
60
33
|
|
61
|
-
|
34
|
+
record(event: :destroy, data: data) if data.any?
|
35
|
+
end
|
62
36
|
|
63
|
-
|
64
|
-
params.merge!({
|
65
|
-
:action_date => Date.today
|
66
|
-
})
|
37
|
+
protected
|
67
38
|
|
68
|
-
|
69
|
-
|
70
|
-
else
|
71
|
-
self.item.revisions.create(params)
|
72
|
-
end
|
39
|
+
def recorder_options
|
40
|
+
item.respond_to?(:recorder_options) ? item.recorder_options : {}
|
73
41
|
end
|
74
42
|
|
75
|
-
def
|
76
|
-
|
77
|
-
ignore = Array.wrap(self.item.recorder_options[:ignore]).map(&:to_sym)
|
78
|
-
attributes.symbolize_keys.except(*ignore)
|
79
|
-
else
|
80
|
-
attributes.symbolize_keys.except(*Recorder.config.ignore)
|
81
|
-
end
|
43
|
+
def data_for(event, options)
|
44
|
+
data.data_for(event, options)
|
82
45
|
end
|
83
46
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
changes = Recorder::Tape.new(object).changes_for(event)
|
94
|
-
hash[reflection.name] = changes if changes.any?
|
95
|
-
object.instance_variable_set(:@recorder_dirty, false)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
hash
|
101
|
-
end
|
102
|
-
end
|
47
|
+
def record(params)
|
48
|
+
Recorder::Tape.record(
|
49
|
+
{
|
50
|
+
item_type: item.class.to_s,
|
51
|
+
item_id: item.id,
|
52
|
+
**params
|
53
|
+
},
|
54
|
+
item.recorder_options
|
55
|
+
)
|
103
56
|
end
|
104
57
|
end
|
105
58
|
end
|