rails_log_book 1.0.0 → 2.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 +1 -1
- data/.reek.yml +25 -0
- data/.rubocop.yml +35 -0
- data/README.md +104 -27
- data/bin/console +2 -1
- data/lib/log_book/configuration.rb +7 -25
- data/lib/log_book/controller_record.rb +8 -8
- data/lib/log_book/record.rb +0 -24
- data/lib/log_book/recorder.rb +77 -36
- data/lib/log_book/save_records.rb +56 -0
- data/lib/log_book/store.rb +12 -0
- data/lib/log_book/tree.rb +74 -0
- data/lib/log_book/version.rb +1 -1
- data/lib/rails_log_book.rb +17 -37
- data/log_book.gemspec +7 -5
- metadata +51 -19
- data/lib/log_book/squash_records.rb +0 -50
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d058f0c867c6fce846e033d8aafa907e5dd4eff98faa8e26d7c7d92781af1ba
|
4
|
+
data.tar.gz: a7e97c3ca16861c470646e534b11055c8a849d27838de24f39d4ee93e6ac0412
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4181481765b293567c425908b00f9c0518bf69993ccf728016ad997c2f4dbc3434acd9ea852184bc3eadf98d90ba64996c21c20bb7da0755c517e722ce607d40
|
7
|
+
data.tar.gz: ab44200dda93ed92505d3b218bd656b6877ea8e289d56608ffc99cfcd9d7338945c59bc43c552c1e71a2e32664666ec6d02aa7062176fcb38a67624d811d93ac
|
data/.gitignore
CHANGED
data/.reek.yml
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
detectors:
|
2
|
+
IrresponsibleModule:
|
3
|
+
enabled: false
|
4
|
+
directories:
|
5
|
+
"app/controllers":
|
6
|
+
IrresponsibleModule:
|
7
|
+
enabled: false
|
8
|
+
NestedIterators:
|
9
|
+
max_allowed_nesting: 2
|
10
|
+
UnusedPrivateMethod:
|
11
|
+
enabled: false
|
12
|
+
InstanceVariableAssumption:
|
13
|
+
enabled: false
|
14
|
+
"app/helpers":
|
15
|
+
IrresponsibleModule:
|
16
|
+
enabled: false
|
17
|
+
UtilityFunction:
|
18
|
+
enabled: false
|
19
|
+
"app/mailers":
|
20
|
+
InstanceVariableAssumption:
|
21
|
+
enabled: false
|
22
|
+
"app/models":
|
23
|
+
InstanceVariableAssumption:
|
24
|
+
enabled: false
|
25
|
+
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require: rubocop-rspec
|
2
|
+
|
3
|
+
LineLength:
|
4
|
+
Max: 100
|
5
|
+
|
6
|
+
Documentation:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
FrozenStringLiteralComment:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
WordArray:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
SymbolArray:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Rails:
|
19
|
+
Enabled: true
|
20
|
+
|
21
|
+
RSpec/ExampleLength:
|
22
|
+
Max: 10
|
23
|
+
|
24
|
+
Style/MutableConstant:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Metrics/BlockLength:
|
28
|
+
Exclude:
|
29
|
+
- 'spec/**/*_spec.rb'
|
30
|
+
|
31
|
+
AllCops:
|
32
|
+
Exclude:
|
33
|
+
- 'db/schema.rb'
|
34
|
+
- 'db/migrate/*.rb'
|
35
|
+
- 'config/**/*.rb'
|
data/README.md
CHANGED
@@ -21,6 +21,15 @@ Then run:
|
|
21
21
|
rails generate log_book:install
|
22
22
|
rake db:migrate
|
23
23
|
```
|
24
|
+
## Reasoning
|
25
|
+
|
26
|
+
We built this gem because others did not offer us what we needed.
|
27
|
+
|
28
|
+
Benefits:
|
29
|
+
- new features ([Squashing](#squashing))
|
30
|
+
- Explicit (needs to be told when to record, as opposed to paper_trail or audited which always record; ie. do not record stuff done in console)
|
31
|
+
- Has an inbuilt caching mechanism with `meta` field
|
32
|
+
- ``meta`` field can also be used to add aditional keys on which search queries can be run
|
24
33
|
|
25
34
|
## Usage
|
26
35
|
|
@@ -44,6 +53,73 @@ Add to controlers and actions you want the tracker to be active:
|
|
44
53
|
|
45
54
|
By default, whenever a user record is created, updated or deleted in any actions of users\_controller a new log\_book record will be created.
|
46
55
|
|
56
|
+
## Squashing
|
57
|
+
|
58
|
+
The idea of squashing came when we needed to show an activity page where you have a has_many relation setup but you need to show changes only on the "main" object.
|
59
|
+
|
60
|
+
Example:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class Hotel < ApplicationRecord
|
64
|
+
include LogBook::Recorder
|
65
|
+
has_log_book_records
|
66
|
+
|
67
|
+
has_many :amenities
|
68
|
+
accepts_nested_attributes_for :amenities
|
69
|
+
end
|
70
|
+
|
71
|
+
class HotelAmenity < ApplicationRecord
|
72
|
+
include LogBook::Recorder
|
73
|
+
has_log_book_records, parent: :hotel
|
74
|
+
|
75
|
+
belongs_to :hotel
|
76
|
+
belongs_to :amenity
|
77
|
+
end
|
78
|
+
|
79
|
+
class Amenity < ApplicationRecord
|
80
|
+
has_many :hotels
|
81
|
+
end
|
82
|
+
|
83
|
+
class HotelsController < ApplicationController
|
84
|
+
def create
|
85
|
+
@hotel = Hotel.create(hotel_params)
|
86
|
+
end
|
87
|
+
|
88
|
+
def hotel_params
|
89
|
+
params.require(:hotel).permit(amenities_attributes: [:id, :status])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# having this params passed to hotels_controller:
|
94
|
+
{ name: 'Hotel', amenitites_attributes: { 0: {id: 1, status: :avaliable}, 1: { id: 4, status: :unavaliable } } }
|
95
|
+
|
96
|
+
# Without squashing you would have these records in the DB (abbreviated for clairity)
|
97
|
+
[
|
98
|
+
{subject_type: 'Hotel', subject_id: 1, parent_type: nil, parent_id: nil, record_changes: { name: [nil, 'Hotel']}},
|
99
|
+
{subject_type: 'HotelAmenity', subject_id: 1, parent_type: 'Hotel', parent_id: 1, record_changes: { name: [nil, 'avaliable']}},
|
100
|
+
{subject_type: 'HotelAmenity', subject_id: 4, parent_type: 'Hotel', parent_id: 1, record_changes: { name: [nil, 'unavaliable']}},
|
101
|
+
]
|
102
|
+
|
103
|
+
# The above is rather difficult to paginate if you want to show changes done on HotelAmenities to show under Hotel.
|
104
|
+
# With squashing enabled:
|
105
|
+
[
|
106
|
+
{
|
107
|
+
subject_type: 'Hotel',
|
108
|
+
subject_id: 1,
|
109
|
+
parent_type: nil,
|
110
|
+
parent_id: nil,
|
111
|
+
record_changes: {
|
112
|
+
name: [nil, 'Hotel'],
|
113
|
+
hotel_amenitites: {
|
114
|
+
1: {status: [nil, 'avaliable']},
|
115
|
+
2: {status: [nil, 'unavaliable']}
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
]
|
120
|
+
|
121
|
+
```
|
122
|
+
|
47
123
|
## ActiveRecord Options
|
48
124
|
|
49
125
|
### fields
|
@@ -82,7 +158,7 @@ By default, whenever a user record is created, updated or deleted in any actions
|
|
82
158
|
|
83
159
|
### parent
|
84
160
|
|
85
|
-
Define who is a parent of this object.
|
161
|
+
Define who is a parent of this object.
|
86
162
|
|
87
163
|
``` ruby
|
88
164
|
class User < ActiveRecord::Base
|
@@ -94,6 +170,31 @@ Define who is a parent of this object. Will be recorded in a `parent` polymorphi
|
|
94
170
|
end
|
95
171
|
```
|
96
172
|
|
173
|
+
### parent_of
|
174
|
+
|
175
|
+
Define who this object is a parent of.
|
176
|
+
|
177
|
+
``` ruby
|
178
|
+
class Account < ActiveRecord::Base
|
179
|
+
include LogBook::Recorder
|
180
|
+
has_many :user_memberships
|
181
|
+
has_many :users, through: :user_memberships
|
182
|
+
end
|
183
|
+
|
184
|
+
def UserMembership < ActiveRecord::Base
|
185
|
+
belongs_to :account
|
186
|
+
belongs_to :user
|
187
|
+
end
|
188
|
+
|
189
|
+
class User < ActiveRecord::Base
|
190
|
+
has_one :user_membership
|
191
|
+
has_one :account, through: :user_membership
|
192
|
+
|
193
|
+
# Parent is Company and will be recorded with each user change
|
194
|
+
# has_log_book_records parent_of: :account
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
97
198
|
### meta
|
98
199
|
|
99
200
|
Arbitrary column. This is a jsonb field which can have all kinds of information. Useful when you want to cache fields at the exact point of record creation
|
@@ -113,18 +214,6 @@ Arbitrary column. This is a jsonb field which can have all kinds of information.
|
|
113
214
|
end
|
114
215
|
```
|
115
216
|
|
116
|
-
### squash
|
117
|
-
|
118
|
-
Enables/disables suashing on this model
|
119
|
-
|
120
|
-
``` ruby
|
121
|
-
class User < ActiveRecord::Base
|
122
|
-
include LogBook::Recorder
|
123
|
-
|
124
|
-
# Enables squashing (defaults to false)
|
125
|
-
# has_log_book_records squash: true
|
126
|
-
```
|
127
|
-
|
128
217
|
## ActionController options
|
129
218
|
|
130
219
|
### current\_author
|
@@ -159,27 +248,15 @@ end
|
|
159
248
|
|
160
249
|
``` ruby
|
161
250
|
LogBook.with_recording {} #=> Enables recording within block
|
162
|
-
LogBook.
|
163
|
-
LogBook.
|
251
|
+
LogBook.author=(author ) #=> Records as a different author within block
|
252
|
+
LogBook.action=(value) #=> Change default action for this request
|
164
253
|
LogBook.with_record_squashing {} #=> Squashes records within block
|
165
254
|
LogBook.enable_recording #=> Enables recording from this point
|
166
255
|
LogBook.disable_recording #=> Disables recording from this point
|
167
256
|
LogBook.record_squashing_enabled #=> Enables record squashing from this point
|
168
257
|
LogBook.recording_enabled #=> Returns true if recording is enabled
|
169
|
-
LogBook.recording_enabled=(val) #=> Enables/Disables recording
|
170
|
-
LogBook.squash_records #=> Squash records with current :request_uuid
|
171
258
|
```
|
172
259
|
|
173
|
-
## Squashing
|
174
|
-
|
175
|
-
The idea of squashing came when we needed to show an activity page where all changes made in the single request as one change.
|
176
|
-
|
177
|
-
Each request has its own unique `request_uuid` and it is recorded with each record. If squashing is enabled, `after_action` method with squashing is called.
|
178
|
-
All records with the same `request_uuid` are "squashed" into one record.
|
179
|
-
|
180
|
-
``` sql
|
181
|
-
INSERT INTO "users" ("name", "email") VALUES ("test", "test@test.com")
|
182
|
-
```
|
183
260
|
## Development
|
184
261
|
|
185
262
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require "bundler/setup"
|
4
|
-
require "
|
4
|
+
require "rails"
|
5
|
+
require "rails_log_book"
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -1,28 +1,10 @@
|
|
1
1
|
module LogBook
|
2
|
-
|
3
|
-
@configuration ||= Configuration.new
|
4
|
-
yield(@configuration)
|
5
|
-
end
|
2
|
+
extend Dry::Configurable
|
6
3
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
attr_accessor :ignored_attributes
|
14
|
-
attr_accessor :recording_enabled
|
15
|
-
attr_accessor :author_method
|
16
|
-
attr_accessor :record_squashing
|
17
|
-
attr_accessor :skip_if_empty_actions
|
18
|
-
|
19
|
-
def initialize
|
20
|
-
@records_table_name = 'records'
|
21
|
-
@ignored_attributes = [:updated_at, :created_at]
|
22
|
-
@author_method = :current_user
|
23
|
-
@record_squashing = false
|
24
|
-
@recording_enabled = false
|
25
|
-
@skip_if_empty_actions = [:update]
|
26
|
-
end
|
27
|
-
end
|
4
|
+
setting :records_table_name, 'records'
|
5
|
+
setting :ignored_attributes, [:updated_at, :created_at]
|
6
|
+
setting :recording_enabled, false
|
7
|
+
setting :author_method, :current_user
|
8
|
+
setting :record_squashing, false
|
9
|
+
setting :skip_if_empty_actions, [:update]
|
28
10
|
end
|
@@ -4,24 +4,24 @@ module LogBook
|
|
4
4
|
|
5
5
|
included do
|
6
6
|
before_action :enable_recording
|
7
|
-
|
7
|
+
after_action :save_records
|
8
8
|
end
|
9
9
|
|
10
10
|
def enable_recording
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
action = "#{controller_name}##{action_name}"
|
12
|
+
author = current_author
|
13
|
+
request_uuid = try(:request).try(:uuid) || SecureRandom.hex
|
14
|
+
LogBook::Store.tree = LogBook::Tree.new(action: action, author: author, request_uuid: request_uuid)
|
14
15
|
LogBook.enable_recording
|
15
16
|
end
|
16
17
|
|
17
|
-
def
|
18
|
-
LogBook.
|
19
|
-
yield
|
20
|
-
end
|
18
|
+
def save_records
|
19
|
+
LogBook::SaveRecords.call
|
21
20
|
end
|
22
21
|
|
23
22
|
def current_author
|
24
23
|
raise NotImplementedError unless respond_to?(LogBook.config.author_method, true)
|
24
|
+
|
25
25
|
send(LogBook.config.author_method)
|
26
26
|
end
|
27
27
|
end
|
data/lib/log_book/record.rb
CHANGED
@@ -5,29 +5,5 @@ module LogBook
|
|
5
5
|
belongs_to :subject, polymorphic: true
|
6
6
|
belongs_to :author, polymorphic: true
|
7
7
|
belongs_to :parent, polymorphic: true
|
8
|
-
|
9
|
-
before_create :set_request_uuid
|
10
|
-
|
11
|
-
def self.collection_cache_key(collection = all, timestamp_column = :created_at)
|
12
|
-
super(collection, timestamp_column)
|
13
|
-
end
|
14
|
-
|
15
|
-
def subject_key
|
16
|
-
subject.class.table_name
|
17
|
-
end
|
18
|
-
|
19
|
-
def changes_to_record?
|
20
|
-
!(record_changes == {} && skip_if_empty_actions.include?(action))
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def set_request_uuid
|
26
|
-
self.request_uuid ||= LogBook.store[:request_uuid] || SecureRandom.uuid
|
27
|
-
end
|
28
|
-
|
29
|
-
def skip_if_empty_actions
|
30
|
-
(subject.recording_options[:skip_if_empty_actions] || LogBook.config.skip_if_empty_actions).map(&:to_s)
|
31
|
-
end
|
32
8
|
end
|
33
9
|
end
|
data/lib/log_book/recorder.rb
CHANGED
@@ -12,9 +12,9 @@ module LogBook
|
|
12
12
|
scope :with_records, -> { joins(:records) }
|
13
13
|
|
14
14
|
on = Array.wrap(options[:on])
|
15
|
-
after_create :
|
16
|
-
after_update :
|
17
|
-
after_destroy :
|
15
|
+
after_create :store_changes if on.empty? || on.include?(:create)
|
16
|
+
after_update :store_changes if on.empty? || on.include?(:update)
|
17
|
+
after_destroy :store_changes if on.empty? || on.include?(:destroy)
|
18
18
|
|
19
19
|
extend LogBook::Recorder::RecordingClassMethods
|
20
20
|
include LogBook::Recorder::RecordingInstanceMethods
|
@@ -22,8 +22,12 @@ module LogBook
|
|
22
22
|
end
|
23
23
|
|
24
24
|
module RecordingInstanceMethods
|
25
|
-
def
|
26
|
-
|
25
|
+
def recording_changes
|
26
|
+
@recording_changes ||= RecordingChanges.new(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def recording_options
|
30
|
+
self.class.recording_options
|
27
31
|
end
|
28
32
|
|
29
33
|
def save_with_recording
|
@@ -42,6 +46,10 @@ module LogBook
|
|
42
46
|
self.class.non_recording_columns
|
43
47
|
end
|
44
48
|
|
49
|
+
def recording_key
|
50
|
+
"#{self.class.table_name}_#{id}"
|
51
|
+
end
|
52
|
+
|
45
53
|
private
|
46
54
|
|
47
55
|
def record_changes
|
@@ -53,43 +61,23 @@ module LogBook
|
|
53
61
|
end
|
54
62
|
end
|
55
63
|
|
56
|
-
def
|
57
|
-
write_record(:create)
|
58
|
-
end
|
59
|
-
|
60
|
-
def update_record
|
61
|
-
write_record(:update)
|
62
|
-
end
|
63
|
-
|
64
|
-
def destroy_record
|
65
|
-
write_record(:destroy)
|
66
|
-
end
|
67
|
-
|
68
|
-
def write_record(action)
|
64
|
+
def store_changes
|
69
65
|
return unless LogBook.recording_enabled
|
70
|
-
record = new_record(LogBook.store[:action] || action)
|
71
|
-
return unless record.changes_to_record?
|
72
|
-
record.save
|
73
|
-
end
|
74
66
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
meta: {}
|
82
|
-
)
|
83
|
-
record.meta = log_book_meta_info(record) if recording_options[:meta].present?
|
84
|
-
record.parent = send(recording_options[:parent]) if recording_options[:parent].present?
|
85
|
-
record
|
67
|
+
recording_changes.tap do |record|
|
68
|
+
record.record_changes = record_changes
|
69
|
+
record.meta = log_book_meta_info(record) if recording_options[:meta].present?
|
70
|
+
end
|
71
|
+
|
72
|
+
LogBook::Store.tree.add(recording_changes)
|
86
73
|
end
|
87
74
|
|
88
75
|
def log_book_meta_info(record)
|
89
|
-
|
76
|
+
meta_options = recording_options[:meta]
|
77
|
+
case meta_options
|
90
78
|
when NilClass then nil
|
91
|
-
when Symbol then send(
|
92
|
-
when Proc then
|
79
|
+
when Symbol then send(meta_options, record)
|
80
|
+
when Proc then meta_options.call(self, record)
|
93
81
|
when TrueClass then log_book_meta(record)
|
94
82
|
end
|
95
83
|
end
|
@@ -121,5 +109,58 @@ module LogBook
|
|
121
109
|
[primary_key, inheritance_column, *Array.wrap(LogBook.config.ignored_attributes)]
|
122
110
|
end
|
123
111
|
end
|
112
|
+
|
113
|
+
class RecordingChanges
|
114
|
+
attr_reader :subject
|
115
|
+
attr_reader :action
|
116
|
+
attr_reader :author
|
117
|
+
attr_reader :record_changes
|
118
|
+
attr_reader :meta
|
119
|
+
attr_reader :request_uuid
|
120
|
+
attr_accessor :recording_key
|
121
|
+
|
122
|
+
def initialize(recorder)
|
123
|
+
@subject = recorder
|
124
|
+
@recording_key = subject.recording_key
|
125
|
+
@record_changes = {}
|
126
|
+
@meta = {}
|
127
|
+
end
|
128
|
+
|
129
|
+
def record_changes=(value)
|
130
|
+
@record_changes.merge!(value)
|
131
|
+
end
|
132
|
+
|
133
|
+
def meta=(value)
|
134
|
+
@meta.merge!(value)
|
135
|
+
end
|
136
|
+
|
137
|
+
def changes?
|
138
|
+
meta.present? || record_changes.present?
|
139
|
+
end
|
140
|
+
|
141
|
+
def subject_key
|
142
|
+
subject.class.table_name
|
143
|
+
end
|
144
|
+
|
145
|
+
def subject_id
|
146
|
+
subject.id
|
147
|
+
end
|
148
|
+
|
149
|
+
def parent
|
150
|
+
self.class.new(subject.send(subject.recording_options[:parent])) if subject.recording_options[:parent]
|
151
|
+
end
|
152
|
+
|
153
|
+
def children
|
154
|
+
self.class.new(subject.send(subject.recording_options[:parent_of])) if subject.recording_options[:parent_of]
|
155
|
+
end
|
156
|
+
|
157
|
+
def to_h
|
158
|
+
{
|
159
|
+
subject: subject,
|
160
|
+
record_changes: record_changes,
|
161
|
+
meta: meta
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
124
165
|
end
|
125
166
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module LogBook
|
2
|
+
class SaveRecords
|
3
|
+
def initialize
|
4
|
+
@tree = LogBook::Store.tree
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.call
|
8
|
+
new.call
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
return unless LogBook.recording_enabled
|
13
|
+
|
14
|
+
squash_tree(tree) if LogBook.record_squashing_enabled
|
15
|
+
|
16
|
+
tree.records(only_roots: LogBook.record_squashing_enabled).each do |_key, record|
|
17
|
+
create_record(record.value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :tree
|
24
|
+
|
25
|
+
def squash_tree(tree)
|
26
|
+
tree.depth.downto(1).each do |depth|
|
27
|
+
nodes = tree.at_depth(depth)
|
28
|
+
nodes.each do |_, node|
|
29
|
+
next unless node.value.changes?
|
30
|
+
parent = node.parent.value
|
31
|
+
|
32
|
+
parent.record_changes = squashed_changes(node.value, parent.record_changes, :record_changes)
|
33
|
+
parent.meta = squashed_changes(node.value, parent.meta, :meta)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def squashed_changes(record, object, key)
|
39
|
+
object[record.subject_key] ||= {}
|
40
|
+
object[record.subject_key][record.subject_id] = record.send(key)
|
41
|
+
object
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_record(record)
|
45
|
+
return unless record.changes?
|
46
|
+
|
47
|
+
attributes = record.to_h
|
48
|
+
attributes.merge!(
|
49
|
+
author: tree.author,
|
50
|
+
action: tree.action,
|
51
|
+
request_uuid: tree.request_uuid
|
52
|
+
)
|
53
|
+
LogBook::Record.create(attributes)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module LogBook
|
2
|
+
class Store < ActiveSupport::CurrentAttributes
|
3
|
+
# attribute :author
|
4
|
+
# attribute :action
|
5
|
+
# attribute :controller
|
6
|
+
# attribute :request_uuid
|
7
|
+
attribute :recording_enabled
|
8
|
+
attribute :record_squashing
|
9
|
+
|
10
|
+
attribute :tree
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module LogBook
|
2
|
+
class Tree
|
3
|
+
attr_accessor :author, :action, :request_uuid, :nodes, :depth
|
4
|
+
|
5
|
+
def initialize(author: nil, action: nil, request_uuid: nil)
|
6
|
+
@nodes = {}
|
7
|
+
@depth = 0
|
8
|
+
@author = author
|
9
|
+
@action = action
|
10
|
+
@request_uuid = request_uuid
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(record)
|
14
|
+
node = nodes[record.recording_key] ||= Node.new(record)
|
15
|
+
add_parent(node, record.parent)
|
16
|
+
add_children(node, record.children)
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_parent(node, parent)
|
20
|
+
return unless parent
|
21
|
+
|
22
|
+
parent_node = nodes[parent.recording_key] ||= Node.new(parent)
|
23
|
+
node.parent = parent_node
|
24
|
+
parent_node.children << node
|
25
|
+
update_depth(parent_node, parent_node.depth)
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_children(node, children)
|
29
|
+
return unless children
|
30
|
+
|
31
|
+
Array.wrap(children).each do |child|
|
32
|
+
add_child(node, child)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_child(node, child)
|
37
|
+
return unless child
|
38
|
+
|
39
|
+
child_node = nodes[child.recording_key] ||= Node.new(child)
|
40
|
+
child_node.parent = node
|
41
|
+
node.children << child_node
|
42
|
+
update_depth(node, node.depth)
|
43
|
+
end
|
44
|
+
|
45
|
+
def update_depth(node, depth)
|
46
|
+
node.depth = depth
|
47
|
+
@depth = [@depth, depth].max
|
48
|
+
node.children.each do |child|
|
49
|
+
update_depth(child, depth + 1)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def records(only_roots: false)
|
54
|
+
only_roots ? at_depth(0) : nodes
|
55
|
+
end
|
56
|
+
|
57
|
+
def at_depth(depth)
|
58
|
+
nodes.select { |_, node| node.depth == depth}
|
59
|
+
end
|
60
|
+
|
61
|
+
class Node
|
62
|
+
attr_reader :value
|
63
|
+
attr_accessor :parent
|
64
|
+
attr_accessor :children
|
65
|
+
attr_accessor :depth
|
66
|
+
|
67
|
+
def initialize(value)
|
68
|
+
@value = value
|
69
|
+
@depth = 0
|
70
|
+
@children = []
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/log_book/version.rb
CHANGED
data/lib/rails_log_book.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
require 'active_record'
|
2
|
-
require '
|
2
|
+
require 'active_support/current_attributes'
|
3
|
+
require 'dry-configurable'
|
4
|
+
|
3
5
|
require 'log_book/configuration'
|
4
|
-
require 'log_book/
|
6
|
+
require 'log_book/store'
|
7
|
+
require 'log_book/tree'
|
8
|
+
require 'log_book/save_records'
|
5
9
|
require 'log_book/record'
|
6
10
|
require 'log_book/recorder'
|
7
11
|
require 'log_book/controller_record'
|
@@ -10,64 +14,40 @@ require 'log_book/railtie'
|
|
10
14
|
|
11
15
|
module LogBook
|
12
16
|
class << self
|
13
|
-
def store
|
14
|
-
RequestStore.store[:log_book] ||= {}
|
15
|
-
end
|
16
|
-
|
17
17
|
def with_recording
|
18
18
|
recording_was_disabled = recording_enabled
|
19
19
|
enable_recording
|
20
|
-
|
21
|
-
ensure
|
22
|
-
disable_recording unless recording_was_disabled
|
23
|
-
end
|
20
|
+
LogBook::Store.tree = LogBook::Tree.new
|
24
21
|
|
25
|
-
def without_recording
|
26
|
-
recording_was_enabled = recording_enabled
|
27
|
-
disable_recording
|
28
22
|
yield
|
29
|
-
ensure
|
30
|
-
enable_recording unless recording_was_enabled
|
31
|
-
end
|
32
23
|
|
33
|
-
|
34
|
-
prev_author = LogBook.store[:author]
|
35
|
-
LogBook.store[:author] = author
|
36
|
-
yield
|
24
|
+
LogBook::SaveRecords.call
|
37
25
|
ensure
|
38
|
-
|
39
|
-
end
|
40
|
-
|
41
|
-
def with_record_squashing
|
42
|
-
yield
|
43
|
-
squash_records if record_squashing_enabled
|
26
|
+
disable_recording unless recording_was_disabled
|
44
27
|
end
|
45
28
|
|
46
29
|
def recording_enabled
|
47
|
-
LogBook.
|
30
|
+
LogBook::Store.recording_enabled || LogBook.config.recording_enabled
|
48
31
|
end
|
49
32
|
|
50
33
|
def record_squashing_enabled
|
51
|
-
LogBook.
|
34
|
+
LogBook::Store.record_squashing || LogBook.config.record_squashing
|
52
35
|
end
|
53
36
|
|
54
37
|
def disable_recording
|
55
|
-
LogBook.recording_enabled = false
|
38
|
+
LogBook::Store.recording_enabled = false
|
56
39
|
end
|
57
40
|
|
58
41
|
def enable_recording
|
59
|
-
LogBook.recording_enabled = true
|
42
|
+
LogBook::Store.recording_enabled = true
|
60
43
|
end
|
61
44
|
|
62
|
-
def
|
63
|
-
LogBook.
|
45
|
+
def action=(val)
|
46
|
+
LogBook::Store.tree.action = val
|
64
47
|
end
|
65
48
|
|
66
|
-
def
|
67
|
-
|
68
|
-
records = LogBook::Record.where(request_uuid: LogBook.store[:request_uuid])
|
69
|
-
return unless records.exists?
|
70
|
-
LogBook::SquashRecords.new(records).call
|
49
|
+
def author=(val)
|
50
|
+
LogBook::Store.tree.author = val
|
71
51
|
end
|
72
52
|
end
|
73
53
|
end
|
data/log_book.gemspec
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
lib = File.expand_path('
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
3
|
require 'log_book/version'
|
4
4
|
|
@@ -25,12 +25,14 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.require_paths = ['lib']
|
26
26
|
|
27
27
|
spec.add_development_dependency 'bundler', '~> 1.12'
|
28
|
-
spec.add_development_dependency '
|
29
|
-
spec.add_development_dependency 'rspec-rails'
|
30
|
-
spec.add_development_dependency 'rails'
|
28
|
+
spec.add_development_dependency 'factory_bot'
|
31
29
|
spec.add_development_dependency 'pg'
|
32
30
|
spec.add_development_dependency 'pry-byebug'
|
31
|
+
spec.add_development_dependency 'rails'
|
32
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
33
|
+
spec.add_development_dependency 'rspec-rails'
|
33
34
|
|
34
|
-
spec.add_dependency 'request_store'
|
35
35
|
spec.add_dependency 'activerecord', '> 5.2'
|
36
|
+
spec.add_dependency 'activesupport', '> 5.2'
|
37
|
+
spec.add_dependency 'dry-configurable'
|
36
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_log_book
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stjepan Hadjic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-06-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -25,21 +25,21 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.12'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: factory_bot
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: pg
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
@@ -53,7 +53,7 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: pry-byebug
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
@@ -67,7 +67,7 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: rails
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
@@ -81,27 +81,27 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: rake
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
89
|
+
version: '10.0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
96
|
+
version: '10.0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: rspec-rails
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
103
|
version: '0'
|
104
|
-
type: :
|
104
|
+
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
@@ -122,6 +122,34 @@ dependencies:
|
|
122
122
|
- - ">"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '5.2'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: activesupport
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '5.2'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '5.2'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: dry-configurable
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
125
153
|
description: Write a longer description or delete this line.
|
126
154
|
email:
|
127
155
|
- stjepan.hadjic@infinum.co
|
@@ -130,7 +158,9 @@ extensions: []
|
|
130
158
|
extra_rdoc_files: []
|
131
159
|
files:
|
132
160
|
- ".gitignore"
|
161
|
+
- ".reek.yml"
|
133
162
|
- ".rspec"
|
163
|
+
- ".rubocop.yml"
|
134
164
|
- ".travis.yml"
|
135
165
|
- CODE_OF_CONDUCT.md
|
136
166
|
- Gemfile
|
@@ -148,7 +178,9 @@ files:
|
|
148
178
|
- lib/log_book/railtie.rb
|
149
179
|
- lib/log_book/record.rb
|
150
180
|
- lib/log_book/recorder.rb
|
151
|
-
- lib/log_book/
|
181
|
+
- lib/log_book/save_records.rb
|
182
|
+
- lib/log_book/store.rb
|
183
|
+
- lib/log_book/tree.rb
|
152
184
|
- lib/log_book/version.rb
|
153
185
|
- lib/rails_log_book.rb
|
154
186
|
- lib/tasks/log_book.rake
|
@@ -174,7 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
174
206
|
version: '0'
|
175
207
|
requirements: []
|
176
208
|
rubyforge_project:
|
177
|
-
rubygems_version: 2.7.
|
209
|
+
rubygems_version: 2.7.6
|
178
210
|
signing_key:
|
179
211
|
specification_version: 4
|
180
212
|
summary: Write a short summary, because Rubygems requires one.
|
@@ -1,50 +0,0 @@
|
|
1
|
-
module LogBook
|
2
|
-
class SquashRecords
|
3
|
-
def initialize(records)
|
4
|
-
@records = records
|
5
|
-
end
|
6
|
-
|
7
|
-
def call
|
8
|
-
records.group_by(&:parent).each do |parent, children|
|
9
|
-
children_to_squash = children.select { |child| child.subject.try(:to_squash?) }
|
10
|
-
next if children_to_squash.empty?
|
11
|
-
|
12
|
-
if parent.present?
|
13
|
-
parent_in_records = parent_in_records(parent)
|
14
|
-
parent_in_records.record_changes.merge!(squashed_changes(children_to_squash, :record_changes))
|
15
|
-
parent_in_records.meta.merge!(squashed_changes(children_to_squash, :meta))
|
16
|
-
parent_in_records.created_at ||= children_to_squash.first.created_at
|
17
|
-
parent_in_records.save
|
18
|
-
|
19
|
-
children_to_squash.each(&:delete)
|
20
|
-
else
|
21
|
-
next if children_to_squash.one?
|
22
|
-
|
23
|
-
records.reduce do |main_record, record|
|
24
|
-
main_record.record_changes.merge!(record.record_changes)
|
25
|
-
main_record.meta.merge!(record.meta)
|
26
|
-
|
27
|
-
record.delete
|
28
|
-
main_record
|
29
|
-
end.save
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
attr_reader :records
|
37
|
-
|
38
|
-
def squashed_changes(children, key)
|
39
|
-
children.each_with_object({}) do |record, object|
|
40
|
-
object[record.subject_key] ||= {}
|
41
|
-
object[record.subject_key][record.subject_id] = record.send(key)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def parent_in_records(parent)
|
46
|
-
records.find { |record| record.subject == parent } ||
|
47
|
-
parent.new_record
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|