recorder 0.1.23 → 1.1.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.
- 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
|