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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4ca501d7c667357906931d0e4460948d92dcd0b8595f1162b0a25d980650c72
4
- data.tar.gz: 87e69c7b0461fcf423fece4f6f8f6471214861f0fd2300ad484795a3eacc19c6
3
+ metadata.gz: f7c20298a4e2c9c68d4fe4b9a4e403b571fe3590d8970b32fd9cf20a8a902df9
4
+ data.tar.gz: 7f0e47633690bed45da988a5ff96f26cb9c83e61635710c27e15923da25322ce
5
5
  SHA512:
6
- metadata.gz: d16233ba896c6f145177f3852b763aa7f8390cd12b68ae2077e2d9b7287e7cbb93d0944f940fafc77241161a5e5a0db764d160b75fde29338cc6e14143e14165
7
- data.tar.gz: 342d5cdf3e9742e00bc58dab8ccfaca91b3a47e990d4dcfde4bc4b184c54e1a7f42af4830548f74bc444b2fd5ffcc8213759255950bc205049522595d9b4cdce
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
@@ -45,7 +45,7 @@ module PaperTrail
45
45
  end
46
46
 
47
47
  def format_currency(num)
48
- "#{@unit} #{format_number(num)}"
48
+ @unit ? "#{@unit} #{format_number(num)}" : format_number(num)
49
49
  end
50
50
 
51
51
  def format_percentage(num)
@@ -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 = preload_relations(versions_data)
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
- def format_version(version, changes, preloaded, only: nil, except: nil)
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
- formatter = FieldFormatter.new(
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
- result = {
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
- result.delete(:previous_value)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module PaperTrail
4
4
  module Human
5
- VERSION = '0.3.0'
5
+ VERSION = '0.3.1'
6
6
  end
7
7
  end
@@ -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
- as ? results.map { |r| formatter(as).call(r) } : results
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.0
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