paper_trail-human 0.3.0 → 0.3.1
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/CHANGELOG.md +10 -0
- data/lib/paper_trail/human/adapters/resolvers/number.rb +1 -1
- data/lib/paper_trail/human/core/batch_presenter.rb +25 -67
- data/lib/paper_trail/human/core/item_name_loader.rb +53 -0
- data/lib/paper_trail/human/core/relation_loader.rb +61 -0
- data/lib/paper_trail/human/version.rb +1 -1
- data/lib/paper_trail/human.rb +11 -5
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7c20298a4e2c9c68d4fe4b9a4e403b571fe3590d8970b32fd9cf20a8a902df9
|
|
4
|
+
data.tar.gz: 7f0e47633690bed45da988a5ff96f26cb9c83e61635710c27e15923da25322ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: faa5dd4c0f7d9b3598656b603225bab94fbcabe1e3fdc2a997a70e4357bfcf040508aa1c332dd0b13ca64b3ee8f6869f30df4a2e2a6edfc49e6607d602aa32e2
|
|
7
|
+
data.tar.gz: 319ccffdef16b6a533e7ec47ba422c2cc14d5a35dd75e8127064b0b5d523df15f682b269df89b8319cfe376722dbd0b3dec5b65b9bbb95f48e12cabbf15ddad8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2026-05-31
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- N+1 queries on `item_name` in `format_collection` and `timeline` (batch preloads records)
|
|
7
|
+
- `Number` resolver with `format: :currency` and `unit: nil` no longer produces leading space
|
|
8
|
+
- `format_collection` with `as:` option now joins entries with separators (double newline for text/markdown, newline for HTML)
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Extracted `ItemNameLoader` and `RelationLoader` from `BatchPresenter` for better cohesion
|
|
12
|
+
|
|
3
13
|
## [0.3.0] - 2026-05-30
|
|
4
14
|
|
|
5
15
|
### Added
|
|
@@ -7,29 +7,45 @@ module PaperTrail
|
|
|
7
7
|
def initialize(configuration)
|
|
8
8
|
@configuration = configuration
|
|
9
9
|
@change_extractor = ChangeExtractor.new
|
|
10
|
+
@item_name_loader = ItemNameLoader.new(configuration)
|
|
11
|
+
@relation_loader = RelationLoader.new(configuration)
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def call(versions, only: nil, except: nil)
|
|
13
15
|
versions_data = versions.map { |v| [v, @change_extractor.call(v)] }
|
|
14
|
-
preloaded =
|
|
16
|
+
preloaded = @relation_loader.preload(versions_data)
|
|
17
|
+
item_names = @item_name_loader.preload(versions)
|
|
15
18
|
|
|
16
19
|
versions_data.map do |version, changes|
|
|
17
|
-
format_version(version, changes, preloaded, only: only, except: except)
|
|
20
|
+
format_version(version, changes, preloaded, item_names, only: only, except: except)
|
|
18
21
|
end
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
private
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
# rubocop:disable Metrics/ParameterLists
|
|
27
|
+
def format_version(version, changes, preloaded, item_names, only: nil, except: nil)
|
|
28
|
+
# rubocop:enable Metrics/ParameterLists
|
|
29
|
+
formatter = build_formatter(version, preloaded)
|
|
30
|
+
result = base_result(version, changes, formatter, only: only, except: except)
|
|
31
|
+
|
|
32
|
+
item_name = @item_name_loader.resolve(version, item_names)
|
|
33
|
+
result[:item_name] = item_name if item_name
|
|
34
|
+
|
|
35
|
+
apply_after_format(result, version)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_formatter(version, preloaded)
|
|
24
39
|
model_config = @configuration.config_for(version.item_type)
|
|
25
|
-
|
|
26
|
-
model_config,
|
|
27
|
-
version.item_type,
|
|
40
|
+
FieldFormatter.new(
|
|
41
|
+
model_config, version.item_type,
|
|
28
42
|
field_name_resolver: @configuration.field_name_resolver,
|
|
29
43
|
preloaded: preloaded
|
|
30
44
|
)
|
|
45
|
+
end
|
|
31
46
|
|
|
32
|
-
|
|
47
|
+
def base_result(version, changes, formatter, only: nil, except: nil)
|
|
48
|
+
{
|
|
33
49
|
user: @configuration.resolve_whodunnit(version.whodunnit),
|
|
34
50
|
event: EventTranslator.call(version.event, translate: @configuration.translate_events),
|
|
35
51
|
model: version.item_type,
|
|
@@ -37,11 +53,6 @@ module PaperTrail
|
|
|
37
53
|
created_at: version.created_at,
|
|
38
54
|
fields: build_fields(changes, formatter, version.event, only: only, except: except)
|
|
39
55
|
}
|
|
40
|
-
|
|
41
|
-
item_name = @configuration.resolve_item_name(version)
|
|
42
|
-
result[:item_name] = item_name if item_name
|
|
43
|
-
|
|
44
|
-
apply_after_format(result, version)
|
|
45
56
|
end
|
|
46
57
|
|
|
47
58
|
def build_fields(changes, formatter, event, only: nil, except: nil)
|
|
@@ -54,7 +65,6 @@ module PaperTrail
|
|
|
54
65
|
def filter_field?(field, only, except)
|
|
55
66
|
field_s = field.to_s
|
|
56
67
|
return only.map(&:to_s).include?(field_s) if only
|
|
57
|
-
|
|
58
68
|
return !except.map(&:to_s).include?(field_s) if except
|
|
59
69
|
|
|
60
70
|
true
|
|
@@ -63,65 +73,13 @@ module PaperTrail
|
|
|
63
73
|
def format_field(formatter, field, values, event)
|
|
64
74
|
previous_value, new_value = Array(values)
|
|
65
75
|
result = formatter.call(field, previous_value, new_value)
|
|
66
|
-
|
|
67
76
|
case event
|
|
68
|
-
when 'create'
|
|
69
|
-
|
|
70
|
-
when 'destroy'
|
|
71
|
-
result.delete(:value)
|
|
77
|
+
when 'create' then result.delete(:previous_value)
|
|
78
|
+
when 'destroy' then result.delete(:value)
|
|
72
79
|
end
|
|
73
|
-
|
|
74
80
|
result
|
|
75
81
|
end
|
|
76
82
|
|
|
77
|
-
def preload_relations(versions_data)
|
|
78
|
-
relation_fields = collect_relation_fields(versions_data)
|
|
79
|
-
return {} if relation_fields.empty?
|
|
80
|
-
|
|
81
|
-
relation_fields.each_with_object({}) do |(key, ids), cache|
|
|
82
|
-
class_name, attribute = key
|
|
83
|
-
cache[key] = load_records(class_name, attribute, ids)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def collect_relation_fields(versions_data)
|
|
88
|
-
result = Hash.new { |h, k| h[k] = Set.new }
|
|
89
|
-
|
|
90
|
-
versions_data.each do |version, changes|
|
|
91
|
-
collect_from_version(result, version, changes)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
result
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def collect_from_version(result, version, changes)
|
|
98
|
-
model_config = @configuration.config_for(version.item_type)
|
|
99
|
-
return unless model_config
|
|
100
|
-
|
|
101
|
-
changes.each do |field, values|
|
|
102
|
-
field_cfg = model_config.fields[field.to_s]
|
|
103
|
-
next unless field_cfg && field_cfg[:type] == :relation
|
|
104
|
-
|
|
105
|
-
key = relation_key(field_cfg)
|
|
106
|
-
Array(values).compact.each { |v| result[key].add(v) }
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def relation_key(field_cfg)
|
|
111
|
-
class_name = field_cfg[:options][:class_name] || field_cfg[:options][:class].to_s
|
|
112
|
-
attribute = field_cfg[:options][:attribute] || :name
|
|
113
|
-
[class_name, attribute]
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def load_records(class_name, attribute, ids)
|
|
117
|
-
klass = Object.const_get(class_name)
|
|
118
|
-
klass.where(id: ids.to_a).to_h do |record|
|
|
119
|
-
[record.id, record.public_send(attribute)]
|
|
120
|
-
end
|
|
121
|
-
rescue NameError
|
|
122
|
-
{}
|
|
123
|
-
end
|
|
124
|
-
|
|
125
83
|
def apply_after_format(result, version)
|
|
126
84
|
return result unless @configuration.after_format
|
|
127
85
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaperTrail
|
|
4
|
+
module Human
|
|
5
|
+
module Core
|
|
6
|
+
class ItemNameLoader
|
|
7
|
+
def initialize(configuration)
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def preload(versions)
|
|
12
|
+
grouped = group_batchable(versions)
|
|
13
|
+
|
|
14
|
+
grouped.each_with_object({}) do |(item_type, ids), cache|
|
|
15
|
+
load_names(item_type, ids, cache)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resolve(version, cache)
|
|
20
|
+
model_config = @configuration.config_for(version.item_type)
|
|
21
|
+
return nil unless model_config&.item_name_attribute
|
|
22
|
+
|
|
23
|
+
attr = model_config.item_name_attribute
|
|
24
|
+
return attr.call(version) if attr.respond_to?(:call)
|
|
25
|
+
|
|
26
|
+
cache[[version.item_type, version.item_id]]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def group_batchable(versions)
|
|
32
|
+
versions.each_with_object({}) do |version, grouped|
|
|
33
|
+
model_config = @configuration.config_for(version.item_type)
|
|
34
|
+
next unless model_config&.item_name_attribute
|
|
35
|
+
next if model_config.item_name_attribute.respond_to?(:call)
|
|
36
|
+
|
|
37
|
+
(grouped[version.item_type] ||= Set.new).add(version.item_id)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def load_names(item_type, ids, cache)
|
|
42
|
+
attr = @configuration.config_for(item_type).item_name_attribute
|
|
43
|
+
klass = Object.const_get(item_type)
|
|
44
|
+
klass.where(id: ids.to_a).each do |record|
|
|
45
|
+
cache[[item_type, record.id]] = record.public_send(attr)
|
|
46
|
+
end
|
|
47
|
+
rescue NameError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaperTrail
|
|
4
|
+
module Human
|
|
5
|
+
module Core
|
|
6
|
+
class RelationLoader
|
|
7
|
+
def initialize(configuration)
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def preload(versions_data)
|
|
12
|
+
relation_fields = collect_relation_fields(versions_data)
|
|
13
|
+
return {} if relation_fields.empty?
|
|
14
|
+
|
|
15
|
+
relation_fields.each_with_object({}) do |(key, ids), cache|
|
|
16
|
+
class_name, attribute = key
|
|
17
|
+
cache[key] = load_records(class_name, attribute, ids)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def collect_relation_fields(versions_data)
|
|
24
|
+
result = Hash.new { |h, k| h[k] = Set.new }
|
|
25
|
+
|
|
26
|
+
versions_data.each do |version, changes|
|
|
27
|
+
collect_from_version(result, version, changes)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def collect_from_version(result, version, changes)
|
|
34
|
+
model_config = @configuration.config_for(version.item_type)
|
|
35
|
+
return unless model_config
|
|
36
|
+
|
|
37
|
+
changes.each do |field, values|
|
|
38
|
+
field_cfg = model_config.fields[field.to_s]
|
|
39
|
+
next unless field_cfg && field_cfg[:type] == :relation
|
|
40
|
+
|
|
41
|
+
key = relation_key(field_cfg)
|
|
42
|
+
Array(values).compact.each { |v| result[key].add(v) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def relation_key(field_cfg)
|
|
47
|
+
class_name = field_cfg[:options][:class_name] || field_cfg[:options][:class].to_s
|
|
48
|
+
attribute = field_cfg[:options][:attribute] || :name
|
|
49
|
+
[class_name, attribute]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def load_records(class_name, attribute, ids)
|
|
53
|
+
klass = Object.const_get(class_name)
|
|
54
|
+
klass.where(id: ids.to_a).to_h { |record| [record.id, record.public_send(attribute)] }
|
|
55
|
+
rescue NameError
|
|
56
|
+
{}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/paper_trail/human.rb
CHANGED
|
@@ -7,6 +7,8 @@ require_relative 'human/core/field_formatter'
|
|
|
7
7
|
require_relative 'human/core/event_translator'
|
|
8
8
|
require_relative 'human/core/presenter'
|
|
9
9
|
require_relative 'human/core/batch_presenter'
|
|
10
|
+
require_relative 'human/core/item_name_loader'
|
|
11
|
+
require_relative 'human/core/relation_loader'
|
|
10
12
|
require_relative 'human/core/timeline'
|
|
11
13
|
require_relative 'human/ports/resolver'
|
|
12
14
|
require_relative 'human/adapters/resolvers/relation'
|
|
@@ -54,7 +56,15 @@ module PaperTrail
|
|
|
54
56
|
|
|
55
57
|
def format_collection(versions, only: nil, except: nil, as: nil)
|
|
56
58
|
results = Core::BatchPresenter.new(configuration).call(versions, only: only, except: except)
|
|
57
|
-
|
|
59
|
+
return results unless as
|
|
60
|
+
|
|
61
|
+
formatted = results.map { |r| formatter(as).call(r) }
|
|
62
|
+
separator = as.to_sym == :html ? "\n" : "\n\n"
|
|
63
|
+
formatted.join(separator)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def timeline(versions, group_by: :day, only: nil, except: nil)
|
|
67
|
+
Core::Timeline.new(configuration).call(versions, group_by: group_by, only: only, except: except)
|
|
58
68
|
end
|
|
59
69
|
|
|
60
70
|
private
|
|
@@ -65,10 +75,6 @@ module PaperTrail
|
|
|
65
75
|
|
|
66
76
|
klass.new
|
|
67
77
|
end
|
|
68
|
-
|
|
69
|
-
def timeline(versions, group_by: :day, only: nil, except: nil)
|
|
70
|
-
Core::Timeline.new(configuration).call(versions, group_by: group_by, only: only, except: except)
|
|
71
|
-
end
|
|
72
78
|
end
|
|
73
79
|
end
|
|
74
80
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: paper_trail-human
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriel
|
|
@@ -67,7 +67,9 @@ files:
|
|
|
67
67
|
- lib/paper_trail/human/core/change_extractor.rb
|
|
68
68
|
- lib/paper_trail/human/core/event_translator.rb
|
|
69
69
|
- lib/paper_trail/human/core/field_formatter.rb
|
|
70
|
+
- lib/paper_trail/human/core/item_name_loader.rb
|
|
70
71
|
- lib/paper_trail/human/core/presenter.rb
|
|
72
|
+
- lib/paper_trail/human/core/relation_loader.rb
|
|
71
73
|
- lib/paper_trail/human/core/timeline.rb
|
|
72
74
|
- lib/paper_trail/human/ports/resolver.rb
|
|
73
75
|
- lib/paper_trail/human/railtie.rb
|