paper_trail-human 0.3.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.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +435 -0
  5. data/config/locales/en.yml +6 -0
  6. data/config/locales/pt-BR.yml +6 -0
  7. data/lib/generators/paper_trail/human/install_generator.rb +17 -0
  8. data/lib/generators/paper_trail/human/templates/initializer.rb +23 -0
  9. data/lib/paper_trail/human/adapters/formatters/html.rb +44 -0
  10. data/lib/paper_trail/human/adapters/formatters/markdown.rb +32 -0
  11. data/lib/paper_trail/human/adapters/formatters/text.rb +33 -0
  12. data/lib/paper_trail/human/adapters/resolvers/boolean.rb +22 -0
  13. data/lib/paper_trail/human/adapters/resolvers/custom.rb +21 -0
  14. data/lib/paper_trail/human/adapters/resolvers/date.rb +36 -0
  15. data/lib/paper_trail/human/adapters/resolvers/enum.rb +57 -0
  16. data/lib/paper_trail/human/adapters/resolvers/number.rb +62 -0
  17. data/lib/paper_trail/human/adapters/resolvers/relation.rb +41 -0
  18. data/lib/paper_trail/human/adapters/resolvers/text.rb +29 -0
  19. data/lib/paper_trail/human/configuration.rb +88 -0
  20. data/lib/paper_trail/human/core/batch_presenter.rb +133 -0
  21. data/lib/paper_trail/human/core/change_extractor.rb +79 -0
  22. data/lib/paper_trail/human/core/event_translator.rb +25 -0
  23. data/lib/paper_trail/human/core/field_formatter.rb +92 -0
  24. data/lib/paper_trail/human/core/presenter.rb +76 -0
  25. data/lib/paper_trail/human/core/timeline.rb +30 -0
  26. data/lib/paper_trail/human/ports/resolver.rb +13 -0
  27. data/lib/paper_trail/human/railtie.rb +26 -0
  28. data/lib/paper_trail/human/version.rb +7 -0
  29. data/lib/paper_trail/human.rb +74 -0
  30. data/lib/paper_trail-human.rb +5 -0
  31. metadata +100 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a4ca501d7c667357906931d0e4460948d92dcd0b8595f1162b0a25d980650c72
4
+ data.tar.gz: 87e69c7b0461fcf423fece4f6f8f6471214861f0fd2300ad484795a3eacc19c6
5
+ SHA512:
6
+ metadata.gz: d16233ba896c6f145177f3852b763aa7f8390cd12b68ae2077e2d9b7287e7cbb93d0944f940fafc77241161a5e5a0db764d160b75fde29338cc6e14143e14165
7
+ data.tar.gz: 342d5cdf3e9742e00bc58dab8ccfaca91b3a47e990d4dcfde4bc4b184c54e1a7f42af4830548f74bc444b2fd5ffcc8213759255950bc205049522595d9b4cdce
data/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ ## [0.3.0] - 2026-05-30
4
+
5
+ ### Added
6
+ - `:date` resolver with configurable `strftime` format
7
+ - `:number` resolver with currency, percentage, and custom formatting
8
+ - `item_name` resolver for human-readable record identifiers
9
+ - Output formats: `as: :text`, `as: :markdown`, `as: :html` (XSS-safe)
10
+ - `PaperTrail::Human.timeline` for grouping versions by day/week/month/year
11
+ - `after_format` hook for post-processing results
12
+ - CONTRIBUTING.md with architecture guide
13
+
14
+ ### Changed
15
+ - Minimum Ruby version raised to 3.1 (dropped 2.7, 3.0)
16
+ - Minimum Rails version raised to 6.1 (dropped 5.2, 6.0)
17
+ - Minimum PaperTrail version raised to 12.0
18
+ - CI matrix: Ruby 3.1–3.4 × Rails 6.1–8.0 × PaperTrail 12–15
19
+ - README rewritten as full English documentation
20
+
21
+ ### Removed
22
+ - Support for Ruby < 3.1, Rails < 6.1, PaperTrail < 12
23
+
24
+ ## [0.2.0] - 2026-05-30
25
+
26
+ ### Added
27
+ - I18n integration for field names via `activerecord.attributes`
28
+ - I18n event label translation with locale files (en, pt-BR)
29
+ - Rails native enum support (`from_model:` option)
30
+ - `:text` resolver for long text truncation with diff stats
31
+ - `only:` and `except:` field filters
32
+ - Batch loading of relations (N+1 prevention)
33
+ - Event-specific fields (create omits previous_value, destroy omits value)
34
+ - Warning when `object_changes` column is missing
35
+
36
+ ### Fixed
37
+ - `Psych::DisallowedClass` with YAML serializer (added ActiveSupport::TimeWithZone)
38
+ - Field names now remove `_id` suffix automatically
39
+
40
+ ## [0.1.0] - 2026-05-29
41
+
42
+ ### Added
43
+ - Initial release
44
+ - Core presenter with hexagonal architecture
45
+ - Resolvers: relation, enum, boolean, custom
46
+ - Configuration DSL with per-model field registration
47
+ - Thread-safe configuration
48
+ - Support for JSON, YAML, and jsonb object_changes
49
+ - Whodunnit resolver callback
50
+ - Configurable ignored fields
51
+ - RuboCop configuration (rubocop-rspec, rubocop-performance)
52
+ - CI with GitHub Actions matrix (Ruby 3.0-3.3 × Rails 6.1-8.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Gabriel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,435 @@
1
+ # paper_trail-human
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/paper_trail-human.svg)](https://rubygems.org/gems/paper_trail-human)
4
+ [![CI](https://github.com/gabrielrumiranda/paper_trail-human/actions/workflows/ci.yml/badge.svg)](https://github.com/gabrielrumiranda/paper_trail-human/actions)
5
+
6
+ Transforms `PaperTrail::Version` records into structured, human-readable hashes ready for UI display — audit logs, timelines, activity feeds.
7
+
8
+ Resolves foreign keys to names, translates enums and constants, formats dates and numbers, and accepts custom transformations via lambda.
9
+
10
+ ## Table of Contents
11
+
12
+ - [1. Introduction](#1-introduction)
13
+ - [1a. Compatibility](#1a-compatibility)
14
+ - [1b. Installation](#1b-installation)
15
+ - [1c. Quick Start](#1c-quick-start)
16
+ - [2. Configuration](#2-configuration)
17
+ - [2a. Global Options](#2a-global-options)
18
+ - [2b. Per-Model Fields](#2b-per-model-fields)
19
+ - [2c. Item Name](#2c-item-name)
20
+ - [2d. After Format Hook](#2d-after-format-hook)
21
+ - [3. Resolvers](#3-resolvers)
22
+ - [3a. Relation](#3a-relation)
23
+ - [3b. Enum](#3b-enum)
24
+ - [3c. Boolean](#3c-boolean)
25
+ - [3d. Custom](#3d-custom)
26
+ - [3e. Text](#3e-text)
27
+ - [3f. Date](#3f-date)
28
+ - [3g. Number](#3g-number)
29
+ - [4. Formatting](#4-formatting)
30
+ - [4a. Single Version](#4a-single-version)
31
+ - [4b. Collection](#4b-collection)
32
+ - [4c. Filtering Fields](#4c-filtering-fields)
33
+ - [4d. Output Formats](#4d-output-formats)
34
+ - [5. Timeline](#5-timeline)
35
+ - [6. I18n](#6-i18n)
36
+ - [6a. Field Names](#6a-field-names)
37
+ - [6b. Event Labels](#6b-event-labels)
38
+ - [7. Architecture](#7-architecture)
39
+ - [8. Requirements](#8-requirements)
40
+ - [9. Contributing](#9-contributing)
41
+ - [10. License](#10-license)
42
+
43
+ ## 1. Introduction
44
+
45
+ ### 1a. Compatibility
46
+
47
+ | paper_trail-human | ruby | activerecord | paper_trail |
48
+ |-------------------|---------|--------------|-------------|
49
+ | 0.3.x | >= 3.1 | >= 6.1 | >= 12.0 |
50
+ | 0.2.x | >= 3.0 | >= 6.1 | >= 12.0 |
51
+ | 0.1.x | >= 2.7 | >= 5.2 | >= 9.0 |
52
+
53
+ **CI matrix (0.3.x):**
54
+
55
+ | Rails | PaperTrail | Ruby |
56
+ |-------|-----------|-------------------|
57
+ | 6.1 | ~> 12.0 | 3.1, 3.2, 3.3, 3.4 |
58
+ | 7.0 | ~> 13.0 | 3.1, 3.2, 3.3, 3.4 |
59
+ | 7.1 | ~> 14.0 | 3.1, 3.2, 3.3, 3.4 |
60
+ | 7.2 | ~> 15.0 | 3.1, 3.2, 3.3, 3.4 |
61
+ | 8.0 | ~> 15.0 | 3.2, 3.3, 3.4 |
62
+
63
+ ### 1b. Installation
64
+
65
+ Add to your Gemfile:
66
+
67
+ ```ruby
68
+ gem "paper_trail-human"
69
+ ```
70
+
71
+ Then run:
72
+
73
+ ```bash
74
+ bundle install
75
+ rails generate paper_trail:human:install
76
+ ```
77
+
78
+ The generator creates an initializer at `config/initializers/paper_trail_human.rb`.
79
+
80
+ **Important:** This gem reads from the `object_changes` column. If your versions table doesn't have it, add it:
81
+
82
+ ```bash
83
+ rails generate paper_trail:install --with-changes
84
+ rails db:migrate
85
+ ```
86
+
87
+ ### 1c. Quick Start
88
+
89
+ ```ruby
90
+ # config/initializers/paper_trail_human.rb
91
+ PaperTrail::Human.configure do |config|
92
+ config.whodunnit_resolver = ->(id) { User.find_by(id: id)&.name }
93
+ end
94
+
95
+ # Anywhere in your app
96
+ PaperTrail::Human.format(version)
97
+ # => {
98
+ # user: "John",
99
+ # event: "update",
100
+ # model: "User",
101
+ # item_id: 1,
102
+ # created_at: 2026-05-29 12:00:00,
103
+ # fields: [
104
+ # { field: "Name", previous_value: "John", value: "John Smith" },
105
+ # { field: "Company", previous_value: "Acme", value: "Globex" }
106
+ # ]
107
+ # }
108
+ ```
109
+
110
+ ## 2. Configuration
111
+
112
+ ### 2a. Global Options
113
+
114
+ ```ruby
115
+ PaperTrail::Human.configure do |config|
116
+ # Resolve whodunnit IDs to names (default: nil, returns raw ID)
117
+ config.whodunnit_resolver = ->(id) { User.find_by(id: id)&.name }
118
+
119
+ # Fields to exclude from output (default: %w[id created_at updated_at])
120
+ config.ignored_fields = %w[id created_at updated_at]
121
+
122
+ # Custom field name resolver (default: nil, uses I18n then humanize)
123
+ config.field_name_resolver = ->(field, model) { ... }
124
+
125
+ # Translate event names via I18n (default: false)
126
+ config.translate_events = true
127
+
128
+ # Post-processing hook (default: nil)
129
+ config.after_format = ->(result, version) { result }
130
+ end
131
+ ```
132
+
133
+ ### 2b. Per-Model Fields
134
+
135
+ ```ruby
136
+ PaperTrail::Human.configure do |config|
137
+ config.register "User" do |m|
138
+ m.field :role, :enum, class_name: "UserRole", method: :label
139
+ m.field :company_id, :relation, class_name: "Company", attribute: :name
140
+ m.field :active, :boolean, true_label: "Active", false_label: "Inactive"
141
+ m.field :bio, :text, max_length: 100, show_diff_stats: true
142
+ m.field :due_date, :date, format: "%d/%m/%Y"
143
+ m.field :salary, :number, format: :currency, unit: "R$"
144
+ m.field :score, :custom, resolve: ->(v) { "#{v} points" }
145
+ end
146
+ end
147
+ ```
148
+
149
+ ### 2c. Item Name
150
+
151
+ Adds a human-readable identifier for the record to the output:
152
+
153
+ ```ruby
154
+ config.register "User" do |m|
155
+ m.item_name :name
156
+ # or with a lambda:
157
+ m.item_name ->(version) { "User ##{version.item_id}" }
158
+ end
159
+
160
+ PaperTrail::Human.format(version)[:item_name]
161
+ # => "João Silva"
162
+ ```
163
+
164
+ The `item_name` key is only present when the record exists and the attribute is configured.
165
+
166
+ ### 2d. After Format Hook
167
+
168
+ Post-process every formatted result:
169
+
170
+ ```ruby
171
+ config.after_format = ->(result, version) {
172
+ result[:record_url] = "/#{result[:model].tableize}/#{result[:item_id]}"
173
+ result
174
+ }
175
+ ```
176
+
177
+ The lambda receives the formatted hash and the original `PaperTrail::Version`, and must return the hash.
178
+
179
+ ## 3. Resolvers
180
+
181
+ ### 3a. Relation
182
+
183
+ Resolves a foreign key to an attribute of the associated model.
184
+
185
+ ```ruby
186
+ m.field :company_id, :relation, class_name: "Company", attribute: :name
187
+ ```
188
+
189
+ | Option | Description | Default |
190
+ |--------|-------------|---------|
191
+ | `class_name:` | The associated model class | required |
192
+ | `attribute:` | Attribute to display | `:name` |
193
+
194
+ In batch mode (`format_collection`), relations are preloaded to prevent N+1 queries.
195
+
196
+ ### 3b. Enum
197
+
198
+ Resolves enum values to human labels.
199
+
200
+ ```ruby
201
+ # With a class that responds to a method
202
+ m.field :role, :enum, class_name: "UserRole", method: :label
203
+
204
+ # With a static mapping
205
+ m.field :status, :enum, mapping: { "active" => "Active", "inactive" => "Inactive" }
206
+
207
+ # With Rails native enum
208
+ m.field :role, :enum, from_model: "User"
209
+ m.field :role, :enum, from_model: "User", labels: { admin: "Administrator" }
210
+ ```
211
+
212
+ | Option | Description |
213
+ |--------|-------------|
214
+ | `class_name:` + `method:` | Calls `ClassName.method(value)` |
215
+ | `mapping:` | Static hash lookup |
216
+ | `from_model:` | Reads from `Model.defined_enums` |
217
+ | `labels:` | Custom labels for `from_model` |
218
+
219
+ ### 3c. Boolean
220
+
221
+ Custom labels for boolean fields:
222
+
223
+ ```ruby
224
+ m.field :active, :boolean, true_label: "Active", false_label: "Inactive"
225
+ ```
226
+
227
+ ### 3d. Custom
228
+
229
+ Arbitrary transformation via lambda:
230
+
231
+ ```ruby
232
+ m.field :score, :custom, resolve: ->(value) { "#{value} points" }
233
+ ```
234
+
235
+ ### 3e. Text
236
+
237
+ Truncates long text fields:
238
+
239
+ ```ruby
240
+ m.field :body, :text, max_length: 100, show_diff_stats: true
241
+ # => "Lorem ipsum dolor sit amet..." (250 chars)
242
+ ```
243
+
244
+ | Option | Description | Default |
245
+ |--------|-------------|---------|
246
+ | `max_length:` | Maximum characters before truncation | `80` |
247
+ | `show_diff_stats:` | Append total char count | `false` |
248
+
249
+ ### 3f. Date
250
+
251
+ Formats date/time values:
252
+
253
+ ```ruby
254
+ m.field :due_date, :date, format: "%d/%m/%Y"
255
+ # => "30/05/2026"
256
+ ```
257
+
258
+ | Option | Description | Default |
259
+ |--------|-------------|---------|
260
+ | `format:` | `strftime` format string | `"%Y-%m-%d"` |
261
+
262
+ Accepts `Date`, `Time`, `DateTime`, and parseable strings.
263
+
264
+ ### 3g. Number
265
+
266
+ Formats numeric values:
267
+
268
+ ```ruby
269
+ m.field :amount, :number, format: :currency, unit: "R$"
270
+ # => "R$ 1,500.99"
271
+
272
+ m.field :rate, :number, format: :percentage
273
+ # => "85.50%"
274
+ ```
275
+
276
+ | Option | Description | Default |
277
+ |--------|-------------|---------|
278
+ | `format:` | `:default`, `:currency`, `:percentage` | `:default` |
279
+ | `unit:` | Currency symbol (for `:currency`) | `nil` |
280
+ | `precision:` | Decimal places | `2` |
281
+ | `delimiter:` | Thousands separator | `","` |
282
+ | `separator:` | Decimal separator | `"."` |
283
+
284
+ ## 4. Formatting
285
+
286
+ ### 4a. Single Version
287
+
288
+ ```ruby
289
+ PaperTrail::Human.format(version)
290
+ ```
291
+
292
+ Returns a hash with keys: `user`, `event`, `model`, `item_id`, `created_at`, `fields`, and optionally `item_name`.
293
+
294
+ Event-specific behavior:
295
+ - **create**: fields omit `previous_value`
296
+ - **update**: fields include both `previous_value` and `value`
297
+ - **destroy**: fields omit `value`
298
+
299
+ ### 4b. Collection
300
+
301
+ ```ruby
302
+ PaperTrail::Human.format_collection(user.versions)
303
+ ```
304
+
305
+ Same as `format` but for multiple versions. Relations are batch-loaded to prevent N+1 queries.
306
+
307
+ ### 4c. Filtering Fields
308
+
309
+ ```ruby
310
+ PaperTrail::Human.format(version, only: [:name, :email])
311
+ PaperTrail::Human.format(version, except: [:password_digest])
312
+ ```
313
+
314
+ ### 4d. Output Formats
315
+
316
+ By default, methods return hashes. Use `as:` for string output:
317
+
318
+ ```ruby
319
+ PaperTrail::Human.format(version, as: :text)
320
+ # => "Updated User#1 by John at 2026-05-30\n • Name: Old → New"
321
+
322
+ PaperTrail::Human.format(version, as: :markdown)
323
+ # => Markdown with header and table
324
+
325
+ PaperTrail::Human.format(version, as: :html)
326
+ # => HTML div with table (XSS-safe, escapes entities)
327
+ ```
328
+
329
+ Available formats: `:text`, `:markdown`, `:html`.
330
+
331
+ Works with both `format` and `format_collection`.
332
+
333
+ ## 5. Timeline
334
+
335
+ Group versions by time period:
336
+
337
+ ```ruby
338
+ PaperTrail::Human.timeline(user.versions, group_by: :day)
339
+ # => {
340
+ # "2026-05-28" => [{ user: ..., fields: [...] }, ...],
341
+ # "2026-05-30" => [{ user: ..., fields: [...] }]
342
+ # }
343
+ ```
344
+
345
+ | `group_by` | Format | Example |
346
+ |-----------|--------|---------|
347
+ | `:day` | `%Y-%m-%d` | `"2026-05-30"` |
348
+ | `:week` | `%G-W%V` | `"2026-W22"` |
349
+ | `:month` | `%Y-%m` | `"2026-05"` |
350
+ | `:year` | `%Y` | `"2026"` |
351
+
352
+ Supports `only:` and `except:` filters.
353
+
354
+ ## 6. I18n
355
+
356
+ ### 6a. Field Names
357
+
358
+ Field names are resolved in this order:
359
+
360
+ 1. Custom `field_name_resolver` lambda (if configured)
361
+ 2. `I18n.t("activerecord.attributes.model_name.field_name")` (if I18n available)
362
+ 3. Automatic humanization (removes `_id` suffix, titleizes)
363
+
364
+ Example: `company_id` → looks up `activerecord.attributes.user.company_id` → falls back to `"Company"`.
365
+
366
+ ### 6b. Event Labels
367
+
368
+ Enable translated event labels:
369
+
370
+ ```ruby
371
+ config.translate_events = true
372
+ ```
373
+
374
+ The gem includes locale files for `en` and `pt-BR`. Add your own:
375
+
376
+ ```yaml
377
+ # config/locales/paper_trail_human.en.yml
378
+ en:
379
+ paper_trail_human:
380
+ events:
381
+ create: "Created"
382
+ update: "Updated"
383
+ destroy: "Destroyed"
384
+ ```
385
+
386
+ ```yaml
387
+ # config/locales/paper_trail_human.pt-BR.yml
388
+ pt-BR:
389
+ paper_trail_human:
390
+ events:
391
+ create: "Criação"
392
+ update: "Atualização"
393
+ destroy: "Exclusão"
394
+ ```
395
+
396
+ ## 7. Architecture
397
+
398
+ Hexagonal (Ports & Adapters):
399
+
400
+ ```
401
+ ┌─────────────────────────────────────────────┐
402
+ │ Core │
403
+ │ ChangeExtractor · FieldFormatter │
404
+ │ EventTranslator · Presenter │
405
+ │ BatchPresenter · Timeline │
406
+ ├─────────────────────────────────────────────┤
407
+ │ Ports │
408
+ │ Resolver (interface) │
409
+ ├─────────────────────────────────────────────┤
410
+ │ Adapters │
411
+ │ Resolvers: Relation, Enum, Boolean, │
412
+ │ Custom, Text, Date, Number │
413
+ │ Formatters: Text, Markdown, Html │
414
+ └─────────────────────────────────────────────┘
415
+ ```
416
+
417
+ - **Core** — pure formatting logic, no external dependencies
418
+ - **Ports** — `Resolver` interface that every adapter implements
419
+ - **Adapters** — concrete implementations for resolving values and formatting output
420
+
421
+ The gem has zero dependencies beyond `activerecord` and `paper_trail`. The Railtie is optional — it works in non-Rails apps (Sinatra, Hanami, etc).
422
+
423
+ ## 8. Requirements
424
+
425
+ - Ruby >= 3.1
426
+ - Rails >= 6.1 (or standalone ActiveRecord)
427
+ - PaperTrail >= 12.0
428
+
429
+ ## 9. Contributing
430
+
431
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on setting up the development environment, running tests, and submitting pull requests.
432
+
433
+ ## 10. License
434
+
435
+ MIT. See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,6 @@
1
+ en:
2
+ paper_trail_human:
3
+ events:
4
+ create: "Created"
5
+ update: "Updated"
6
+ destroy: "Destroyed"
@@ -0,0 +1,6 @@
1
+ pt-BR:
2
+ paper_trail_human:
3
+ events:
4
+ create: "Criação"
5
+ update: "Atualização"
6
+ destroy: "Exclusão"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module PaperTrail
6
+ module Human
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ desc 'Creates a PaperTrail::Human initializer'
11
+
12
+ def copy_initializer
13
+ template 'initializer.rb', 'config/initializers/paper_trail_human.rb'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ PaperTrail::Human.configure do |config|
4
+ # Resolver quem fez a alteração (recebe whodunnit ID, retorna nome)
5
+ # config.whodunnit_resolver = ->(id) { User.find_by(id: id)&.name }
6
+
7
+ # Campos ignorados globalmente (default: id, created_at, updated_at)
8
+ # config.ignored_fields = %w[id created_at updated_at]
9
+
10
+ # Resolver customizado para nomes de campos (usa I18n/human_attribute_name)
11
+ # config.field_name_resolver = ->(field_name, item_type) {
12
+ # item_type.constantize.human_attribute_name(field_name)
13
+ # }
14
+
15
+ # Configuração por model:
16
+ #
17
+ # config.register 'User' do |m|
18
+ # m.field :role, :enum, class_name: 'UserRole', method: :label
19
+ # m.field :company_id, :relation, class_name: 'Company', attribute: :name
20
+ # m.field :active, :boolean, true_label: 'Ativo', false_label: 'Inativo'
21
+ # m.field :score, :custom, resolve: ->(v) { "#{v} pontos" }
22
+ # end
23
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Human
5
+ module Adapters
6
+ module Formatters
7
+ class Html
8
+ def call(result)
9
+ [
10
+ %(<div class="paper-trail-version">),
11
+ " <p>#{header(result)}</p>",
12
+ ' <table>',
13
+ ' <thead><tr><th>Field</th><th>Previous</th><th>Current</th></tr></thead>',
14
+ ' <tbody>',
15
+ *result[:fields].map { |f| table_row(f) },
16
+ ' </tbody>',
17
+ ' </table>',
18
+ '</div>'
19
+ ].join("\n")
20
+ end
21
+
22
+ private
23
+
24
+ def header(result)
25
+ event = escape(result[:event])
26
+ model = escape(result[:model])
27
+ user = escape(result[:user].to_s)
28
+ "<strong>#{event}</strong> #{model}##{result[:item_id]} by #{user}"
29
+ end
30
+
31
+ def table_row(field)
32
+ prev = escape(field.fetch(:previous_value, '—').to_s)
33
+ curr = escape(field.fetch(:value, '—').to_s)
34
+ " <tr><td>#{escape(field[:field])}</td><td>#{prev}</td><td>#{curr}</td></tr>"
35
+ end
36
+
37
+ def escape(str)
38
+ str.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;').gsub("'", '&#39;')
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Human
5
+ module Adapters
6
+ module Formatters
7
+ class Markdown
8
+ def call(result)
9
+ lines = [header(result), '']
10
+ lines << '| Field | Previous | Current |'
11
+ lines << '|-------|----------|---------|'
12
+ result[:fields].each { |f| lines << table_row(f) }
13
+ lines.join("\n")
14
+ end
15
+
16
+ private
17
+
18
+ def header(result)
19
+ "**#{result[:event]}** `#{result[:model]}##{result[:item_id]}` " \
20
+ "by #{result[:user]} at #{result[:created_at]}"
21
+ end
22
+
23
+ def table_row(field)
24
+ prev = field.fetch(:previous_value, '—')
25
+ curr = field.fetch(:value, '—')
26
+ "| #{field[:field]} | #{prev} | #{curr} |"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Human
5
+ module Adapters
6
+ module Formatters
7
+ class Text
8
+ def call(result)
9
+ lines = [header(result)]
10
+ result[:fields].each { |f| lines << field_line(f) }
11
+ lines.join("\n")
12
+ end
13
+
14
+ private
15
+
16
+ def header(result)
17
+ "#{result[:event]} #{result[:model]}##{result[:item_id]} by #{result[:user]} at #{result[:created_at]}"
18
+ end
19
+
20
+ def field_line(field)
21
+ if field.key?(:previous_value) && field.key?(:value)
22
+ " • #{field[:field]}: #{field[:previous_value]} → #{field[:value]}"
23
+ elsif field.key?(:value)
24
+ " • #{field[:field]}: #{field[:value]}"
25
+ else
26
+ " • #{field[:field]}: #{field[:previous_value]} (removed)"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end