paper_trail_history 0.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +188 -0
- data/Rakefile +29 -0
- data/app/assets/stylesheets/paper_trail_history/application.css +93 -0
- data/app/controllers/paper_trail_history/application_controller.rb +6 -0
- data/app/controllers/paper_trail_history/models_controller.rb +57 -0
- data/app/controllers/paper_trail_history/records_controller.rb +58 -0
- data/app/controllers/paper_trail_history/versions_controller.rb +33 -0
- data/app/helpers/paper_trail_history/application_helper.rb +7 -0
- data/app/jobs/paper_trail_history/application_job.rb +6 -0
- data/app/mailers/paper_trail_history/application_mailer.rb +9 -0
- data/app/models/paper_trail_history/application_record.rb +8 -0
- data/app/models/paper_trail_history/trackable_model.rb +112 -0
- data/app/models/paper_trail_history/version_decorator.rb +104 -0
- data/app/models/paper_trail_history/version_service.rb +158 -0
- data/app/views/layouts/paper_trail_history/application.html.erb +64 -0
- data/app/views/paper_trail_history/models/index.html.erb +65 -0
- data/app/views/paper_trail_history/models/show.html.erb +49 -0
- data/app/views/paper_trail_history/models/versions.html.erb +30 -0
- data/app/views/paper_trail_history/records/show.html.erb +57 -0
- data/app/views/paper_trail_history/records/versions.html.erb +36 -0
- data/app/views/paper_trail_history/shared/_version_filters.html.erb +54 -0
- data/app/views/paper_trail_history/shared/_versions_table.html.erb +60 -0
- data/app/views/paper_trail_history/versions/show.html.erb +134 -0
- data/config/locales/de.yml +16 -0
- data/config/locales/en.yml +16 -0
- data/config/routes.rb +23 -0
- data/lib/paper_trail_history/engine.rb +8 -0
- data/lib/paper_trail_history/version.rb +5 -0
- data/lib/paper_trail_history.rb +15 -0
- data/lib/tasks/paper_trail_history_tasks.rake +6 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a439b37d64e8ecaeec57377d5db127935c4b64954f7b2471f935b6c80b8ef777
|
4
|
+
data.tar.gz: 2c246306235393b7ff3dd2c98935535b943b1eba296cc03073d4c3187a19f763
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2f85826967ac33827d1ada925b772a12d5bb5cd7becb799bfc3d2f74fe97abf5c10d6ad45c3087d155d4021f58592649927f8a99eb3dbdb24c3800b87d1500f4
|
7
|
+
data.tar.gz: b407e86259a9e18083f36f9759a34f7b2430c52c101fcf43f6c2dee713c6c47814bd2db7c6d8e9d7b96fbdac7ee244ae0e079c293dcf4e32bce6d8d55e943138
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Benjamin Deutscher
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
# PaperTrailHistory
|
2
|
+
|
3
|
+
A Rails engine providing a comprehensive web interface for managing PaperTrail versions. View, search, filter, and restore audit trail records with an intuitive Bootstrap-styled interface.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **List All Trackable Models**: Discover all models in your application that use `has_paper_trail`
|
8
|
+
- **Browse Version History**: View all versions for specific models or individual records
|
9
|
+
- **Advanced Filtering**: Filter versions by event type, user, date range, and search content
|
10
|
+
- **Detailed Version View**: See exactly what changed in each version with before/after comparisons
|
11
|
+
- **Restore Functionality**: Safely restore records to previous versions with confirmation
|
12
|
+
- **Responsive Design**: Clean, Bootstrap-styled interface that works on all devices
|
13
|
+
- **Breadcrumb Navigation**: Easy navigation between models, records, and versions
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem "paper_trail_history"
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
```bash
|
25
|
+
$ bundle install
|
26
|
+
```
|
27
|
+
|
28
|
+
Mount the engine in your routes file (`config/routes.rb`):
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
Rails.application.routes.draw do
|
32
|
+
mount PaperTrailHistory::Engine, at: '/revisions'
|
33
|
+
# your other routes...
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
## Prerequisites
|
38
|
+
|
39
|
+
This engine requires:
|
40
|
+
- Rails >= 8.0.2
|
41
|
+
- PaperTrail >= 15.0 (configured with `has_paper_trail` in your models)
|
42
|
+
|
43
|
+
Make sure you have PaperTrail properly configured in your Rails application before using this engine.
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
After mounting the engine, navigate to `/revisions` (or whatever path you chose) in your browser to access the interface.
|
48
|
+
|
49
|
+
### Main Features
|
50
|
+
|
51
|
+
1. **Models Overview** (`/revisions/models`)
|
52
|
+
- Lists all models with paper trail enabled
|
53
|
+
- Shows version counts for each model
|
54
|
+
- Quick access to recent versions
|
55
|
+
|
56
|
+
2. **Model Versions** (`/revisions/models/:model_name/versions`)
|
57
|
+
- View all versions for a specific model
|
58
|
+
- Filter by event type, user, date range
|
59
|
+
- Search within version content
|
60
|
+
- Pagination support
|
61
|
+
|
62
|
+
3. **Record Versions** (`/revisions/models/:model_name/:record_id/versions`)
|
63
|
+
- View version history for a specific record
|
64
|
+
- See current record state vs historical versions
|
65
|
+
|
66
|
+
4. **Version Details** (`/revisions/versions/:id`)
|
67
|
+
- Detailed view of what changed in a specific version
|
68
|
+
- Side-by-side comparison of old vs new values
|
69
|
+
- Restore functionality with confirmation
|
70
|
+
|
71
|
+
### Filtering and Search
|
72
|
+
|
73
|
+
The interface provides several ways to filter and search versions:
|
74
|
+
|
75
|
+
- **Event Type**: Filter by create, update, or destroy events
|
76
|
+
- **User**: Filter by who made the changes (whodunnit)
|
77
|
+
- **Date Range**: Show versions within specific date ranges
|
78
|
+
- **Content Search**: Search within the stored object changes
|
79
|
+
|
80
|
+
### Restoring Versions
|
81
|
+
|
82
|
+
You can restore any version (except create events) by:
|
83
|
+
|
84
|
+
1. Navigate to the version details page
|
85
|
+
2. Click "Restore This Version"
|
86
|
+
3. Confirm the restoration
|
87
|
+
|
88
|
+
**Note**: Restoring a version will create a new version entry, so you can always see the restoration in the audit trail.
|
89
|
+
|
90
|
+
## Development
|
91
|
+
|
92
|
+
### Setting up the Test Environment
|
93
|
+
|
94
|
+
```bash
|
95
|
+
# Install dependencies
|
96
|
+
bundle install
|
97
|
+
|
98
|
+
# Set up test database with sample data
|
99
|
+
cd test/dummy
|
100
|
+
bundle exec rails db:create RAILS_ENV=test
|
101
|
+
bundle exec rails db:migrate RAILS_ENV=test
|
102
|
+
bundle exec rails db:seed RAILS_ENV=test
|
103
|
+
cd ../..
|
104
|
+
```
|
105
|
+
|
106
|
+
### Running Tests
|
107
|
+
|
108
|
+
```bash
|
109
|
+
# Run all tests
|
110
|
+
bundle exec rake test
|
111
|
+
|
112
|
+
# Run integration tests only
|
113
|
+
bundle exec rake test:integration
|
114
|
+
|
115
|
+
# Run specific test file
|
116
|
+
bundle exec ruby -I test test/models/paper_trail_history/trackable_model_test.rb
|
117
|
+
|
118
|
+
# Run with verbose output
|
119
|
+
bundle exec rake test TESTOPTS="-v"
|
120
|
+
|
121
|
+
# Run code quality checks
|
122
|
+
bundle exec rubocop
|
123
|
+
```
|
124
|
+
|
125
|
+
### Testing Different Components
|
126
|
+
|
127
|
+
```bash
|
128
|
+
# Test models only
|
129
|
+
bundle exec rake test TEST="test/models/**/*_test.rb"
|
130
|
+
|
131
|
+
# Test controllers only
|
132
|
+
bundle exec rake test TEST="test/controllers/**/*_test.rb"
|
133
|
+
|
134
|
+
# Test integration features
|
135
|
+
bundle exec rake test TEST="test/integration/**/*_test.rb"
|
136
|
+
```
|
137
|
+
|
138
|
+
### Using the Dummy Application
|
139
|
+
|
140
|
+
The `test/dummy` directory contains a full Rails application with sample models and data for testing:
|
141
|
+
|
142
|
+
```bash
|
143
|
+
# Start the development server
|
144
|
+
cd test/dummy && bundle exec rails server
|
145
|
+
|
146
|
+
# Visit the engine interface
|
147
|
+
open http://localhost:3000/paper_trail_history
|
148
|
+
```
|
149
|
+
|
150
|
+
The dummy app includes:
|
151
|
+
- **User, Post, Comment models** - Standard PaperTrail setup
|
152
|
+
- **Product model** - Custom version table (`ProductVersion`)
|
153
|
+
- **Rich seed data** - Various change types, workflows, and scenarios
|
154
|
+
- **I18n translations** - Proper model pluralization
|
155
|
+
|
156
|
+
### Testing Against Multiple Rails Versions
|
157
|
+
|
158
|
+
For comprehensive compatibility testing, use the provided Gemfiles:
|
159
|
+
|
160
|
+
```bash
|
161
|
+
# Test against Rails 7.1
|
162
|
+
BUNDLE_GEMFILE=gemfiles/rails_7.1.gemfile bundle install
|
163
|
+
BUNDLE_GEMFILE=gemfiles/rails_7.1.gemfile bundle exec rake test
|
164
|
+
|
165
|
+
# Test against Rails 8.0
|
166
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.0.gemfile bundle install
|
167
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.0.gemfile bundle exec rake test
|
168
|
+
|
169
|
+
# Test against Rails main branch
|
170
|
+
BUNDLE_GEMFILE=gemfiles/rails_main.gemfile bundle install
|
171
|
+
BUNDLE_GEMFILE=gemfiles/rails_main.gemfile bundle exec rake test
|
172
|
+
```
|
173
|
+
|
174
|
+
### Continuous Integration
|
175
|
+
|
176
|
+
The project uses GitHub Actions to test against multiple Ruby and Rails versions. See `.github/workflows/ci.yml` for the full matrix configuration.
|
177
|
+
|
178
|
+
## Contributing
|
179
|
+
|
180
|
+
1. Fork the project
|
181
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
182
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
183
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
184
|
+
5. Open a Pull Request
|
185
|
+
|
186
|
+
## License
|
187
|
+
|
188
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
6
|
+
load 'rails/tasks/engine.rake'
|
7
|
+
|
8
|
+
load 'rails/tasks/statistics.rake'
|
9
|
+
|
10
|
+
require 'bundler/gem_tasks'
|
11
|
+
|
12
|
+
# Load test tasks
|
13
|
+
require 'rake/testtask'
|
14
|
+
|
15
|
+
Rake::TestTask.new(:test) do |t|
|
16
|
+
t.libs << 'test'
|
17
|
+
t.pattern = 'test/**/*_test.rb'
|
18
|
+
t.verbose = false
|
19
|
+
end
|
20
|
+
|
21
|
+
namespace :test do
|
22
|
+
Rake::TestTask.new(:integration) do |t|
|
23
|
+
t.libs << 'test'
|
24
|
+
t.pattern = 'test/integration/**/*_test.rb'
|
25
|
+
t.verbose = false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
task default: :test
|
@@ -0,0 +1,93 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
16
|
+
|
17
|
+
/* Git-style diff formatting */
|
18
|
+
.diff-container {
|
19
|
+
font-family: 'SFMono-Regular', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
|
20
|
+
font-size: 0.9rem;
|
21
|
+
}
|
22
|
+
|
23
|
+
.diff-section {
|
24
|
+
border: 1px solid #e1e4e8;
|
25
|
+
border-radius: 6px;
|
26
|
+
overflow: hidden;
|
27
|
+
}
|
28
|
+
|
29
|
+
.diff-header {
|
30
|
+
background-color: #f6f8fa;
|
31
|
+
padding: 8px 12px;
|
32
|
+
border-bottom: 1px solid #e1e4e8;
|
33
|
+
font-weight: 600;
|
34
|
+
color: #586069;
|
35
|
+
}
|
36
|
+
|
37
|
+
.diff-content {
|
38
|
+
background-color: #fff;
|
39
|
+
}
|
40
|
+
|
41
|
+
.diff-line {
|
42
|
+
display: flex;
|
43
|
+
align-items: flex-start;
|
44
|
+
padding: 2px 0;
|
45
|
+
line-height: 1.45;
|
46
|
+
position: relative;
|
47
|
+
}
|
48
|
+
|
49
|
+
.diff-line code {
|
50
|
+
background: none;
|
51
|
+
border: none;
|
52
|
+
padding: 4px 8px;
|
53
|
+
margin: 0;
|
54
|
+
flex: 1;
|
55
|
+
font-size: 0.85rem;
|
56
|
+
white-space: pre-wrap;
|
57
|
+
word-break: break-all;
|
58
|
+
}
|
59
|
+
|
60
|
+
.diff-marker {
|
61
|
+
display: inline-block;
|
62
|
+
width: 20px;
|
63
|
+
text-align: center;
|
64
|
+
font-weight: bold;
|
65
|
+
flex-shrink: 0;
|
66
|
+
font-size: 0.9rem;
|
67
|
+
padding: 4px 0;
|
68
|
+
user-select: none;
|
69
|
+
}
|
70
|
+
|
71
|
+
.diff-removed {
|
72
|
+
background-color: #ffeef0;
|
73
|
+
border-left: 3px solid #f85149;
|
74
|
+
}
|
75
|
+
|
76
|
+
.diff-removed .diff-marker {
|
77
|
+
color: #f85149;
|
78
|
+
background-color: #ffdddf;
|
79
|
+
}
|
80
|
+
|
81
|
+
.diff-added {
|
82
|
+
background-color: #e6ffed;
|
83
|
+
border-left: 3px solid #2ea043;
|
84
|
+
}
|
85
|
+
|
86
|
+
.diff-added .diff-marker {
|
87
|
+
color: #2ea043;
|
88
|
+
background-color: #ccf2d4;
|
89
|
+
}
|
90
|
+
|
91
|
+
.diff-line:hover {
|
92
|
+
background-color: rgba(0, 0, 0, 0.05);
|
93
|
+
}
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrailHistory
|
4
|
+
# Controller for managing trackable model operations and displaying version histories
|
5
|
+
class ModelsController < ApplicationController
|
6
|
+
def index
|
7
|
+
@trackable_models = TrackableModel.all_with_counts
|
8
|
+
end
|
9
|
+
|
10
|
+
def show
|
11
|
+
@trackable_model = TrackableModel.find(params[:name])
|
12
|
+
|
13
|
+
unless @trackable_model
|
14
|
+
redirect_to models_path, alert: t('paper_trail_history.errors.model_not_found', model_name: params[:name])
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
@recent_versions = VersionDecorator.decorate_collection(
|
19
|
+
@trackable_model.recent_versions(20)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def versions
|
24
|
+
@trackable_model = find_trackable_model_or_redirect
|
25
|
+
return unless @trackable_model
|
26
|
+
|
27
|
+
load_versions_data
|
28
|
+
load_filter_options
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def find_trackable_model_or_redirect
|
34
|
+
trackable_model = TrackableModel.find(params[:name])
|
35
|
+
unless trackable_model
|
36
|
+
redirect_to models_path, alert: t('paper_trail_history.errors.model_not_found', model_name: params[:name])
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
trackable_model
|
40
|
+
end
|
41
|
+
|
42
|
+
def load_versions_data
|
43
|
+
@versions = VersionService.for_model(params[:name], filter_params)
|
44
|
+
@versions = @versions.includes(:item)
|
45
|
+
@decorated_versions = VersionDecorator.decorate_collection(@versions)
|
46
|
+
end
|
47
|
+
|
48
|
+
def load_filter_options
|
49
|
+
@available_events = VersionService.available_events(params[:name])
|
50
|
+
@available_whodunnits = VersionService.unique_whodunnits(params[:name])
|
51
|
+
end
|
52
|
+
|
53
|
+
def filter_params
|
54
|
+
params.permit(:event, :whodunnit, :from_date, :to_date, :search, :page)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrailHistory
|
4
|
+
# Controller for managing individual record operations and their version histories
|
5
|
+
class RecordsController < ApplicationController
|
6
|
+
def show
|
7
|
+
@trackable_model = find_trackable_model_or_redirect
|
8
|
+
return unless @trackable_model
|
9
|
+
|
10
|
+
load_record_data
|
11
|
+
load_recent_versions
|
12
|
+
end
|
13
|
+
|
14
|
+
def versions
|
15
|
+
@trackable_model = find_trackable_model_or_redirect
|
16
|
+
return unless @trackable_model
|
17
|
+
|
18
|
+
load_record_data
|
19
|
+
load_versions_with_filters
|
20
|
+
load_available_events
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def find_trackable_model_or_redirect
|
26
|
+
trackable_model = TrackableModel.find(params[:model_name])
|
27
|
+
unless trackable_model
|
28
|
+
redirect_to models_path, alert: t('paper_trail_history.errors.model_not_found', model_name: params[:model_name])
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
trackable_model
|
32
|
+
end
|
33
|
+
|
34
|
+
def load_record_data
|
35
|
+
@record = @trackable_model.klass.find_by(id: params[:record_id])
|
36
|
+
@record_id = params[:record_id]
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_recent_versions
|
40
|
+
@recent_versions = VersionDecorator.decorate_collection(
|
41
|
+
VersionService.for_record(params[:model_name], params[:record_id], {}).limit(10)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def load_versions_with_filters
|
46
|
+
@versions = VersionService.for_record(params[:model_name], params[:record_id], filter_params)
|
47
|
+
@decorated_versions = VersionDecorator.decorate_collection(@versions)
|
48
|
+
end
|
49
|
+
|
50
|
+
def load_available_events
|
51
|
+
@available_events = VersionService.available_events(params[:model_name])
|
52
|
+
end
|
53
|
+
|
54
|
+
def filter_params
|
55
|
+
params.permit(:event, :from_date, :to_date, :page)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrailHistory
|
4
|
+
# Controller for managing version operations like viewing and restoring specific versions
|
5
|
+
class VersionsController < ApplicationController
|
6
|
+
before_action :find_version, only: %i[show restore]
|
7
|
+
|
8
|
+
def show
|
9
|
+
@decorated_version = VersionDecorator.decorate(@version)
|
10
|
+
@trackable_model = TrackableModel.find(@version.item_type)
|
11
|
+
end
|
12
|
+
|
13
|
+
def restore
|
14
|
+
result = VersionService.restore_version(@version.id)
|
15
|
+
|
16
|
+
if result[:success]
|
17
|
+
redirect_back fallback_location: version_path(@version),
|
18
|
+
notice: result[:message]
|
19
|
+
else
|
20
|
+
redirect_back fallback_location: version_path(@version),
|
21
|
+
alert: t('paper_trail_history.errors.restore_failed', error: result[:error])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def find_version
|
28
|
+
@version = PaperTrail::Version.find(params[:id])
|
29
|
+
rescue ActiveRecord::RecordNotFound
|
30
|
+
redirect_to root_path, alert: t('paper_trail_history.errors.version_not_found')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrailHistory
|
4
|
+
# Model wrapper for trackable ActiveRecord classes with PaperTrail versioning
|
5
|
+
class TrackableModel
|
6
|
+
attr_reader :name, :klass, :cached_version_count
|
7
|
+
|
8
|
+
def initialize(klass, cached_version_count = nil)
|
9
|
+
@klass = klass
|
10
|
+
@name = klass.name
|
11
|
+
@cached_version_count = cached_version_count
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.all
|
15
|
+
return @all_models if defined?(@all_models) && @all_models
|
16
|
+
|
17
|
+
Rails.application.eager_load!
|
18
|
+
|
19
|
+
# Use a Set to ensure uniqueness by class name
|
20
|
+
trackable_classes = {}
|
21
|
+
ObjectSpace.each_object(Class) do |klass|
|
22
|
+
next unless klass < ActiveRecord::Base
|
23
|
+
next if klass.abstract_class?
|
24
|
+
next unless klass.included_modules.include?(PaperTrail::Model::InstanceMethods)
|
25
|
+
|
26
|
+
# Only keep one instance per class name to avoid duplicates
|
27
|
+
trackable_classes[klass.name] = new(klass)
|
28
|
+
end
|
29
|
+
|
30
|
+
@all_models = trackable_classes.values.sort_by(&:name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.all_with_counts
|
34
|
+
models = all
|
35
|
+
return models if models.empty?
|
36
|
+
|
37
|
+
count_cache = build_count_cache(models)
|
38
|
+
models_with_cached_counts(models, count_cache)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.build_count_cache(models)
|
42
|
+
models_by_version_class = models.group_by(&:version_class)
|
43
|
+
count_cache = {}
|
44
|
+
populate_count_cache(models_by_version_class, count_cache)
|
45
|
+
count_cache
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.populate_count_cache(models_by_version_class, count_cache)
|
49
|
+
models_by_version_class.each do |version_class, models_for_class|
|
50
|
+
counts = fetch_version_counts(version_class, models_for_class)
|
51
|
+
cache_counts_for_models(models_for_class, counts, count_cache)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.fetch_version_counts(version_class, models_for_class)
|
56
|
+
item_types = models_for_class.map(&:item_type_for_versions)
|
57
|
+
version_class.where(item_type: item_types).group(:item_type).count
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.cache_counts_for_models(models_for_class, counts, count_cache)
|
61
|
+
models_for_class.each do |model|
|
62
|
+
count_cache[model.name] = counts[model.item_type_for_versions] || 0
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.models_with_cached_counts(models, count_cache)
|
67
|
+
models.map { |model| new(model.klass, count_cache[model.name]) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.find(model_name)
|
71
|
+
all.find { |model| model.name == model_name }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Clear cached models (useful in development when adding new trackable models)
|
75
|
+
def self.clear_cache!
|
76
|
+
@all_models = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def versions
|
80
|
+
version_class.where(item_type: item_type_for_versions)
|
81
|
+
end
|
82
|
+
|
83
|
+
def item_type_for_versions
|
84
|
+
# PaperTrail stores item_type as the class name, but let's be explicit
|
85
|
+
# and handle potential namespace issues
|
86
|
+
klass.name
|
87
|
+
end
|
88
|
+
|
89
|
+
def total_versions_count
|
90
|
+
# Use cached count if available (from all_with_counts), otherwise query
|
91
|
+
@cached_version_count || versions.count
|
92
|
+
end
|
93
|
+
|
94
|
+
def recent_versions(limit = 10)
|
95
|
+
versions.order(created_at: :desc).limit(limit)
|
96
|
+
end
|
97
|
+
|
98
|
+
def version_class
|
99
|
+
@version_class ||= klass.paper_trail.version_class || PaperTrail::Version
|
100
|
+
end
|
101
|
+
|
102
|
+
def version_table_name
|
103
|
+
version_class.table_name
|
104
|
+
end
|
105
|
+
|
106
|
+
delegate :table_name, to: :klass
|
107
|
+
|
108
|
+
def human_name
|
109
|
+
klass.model_name.human(count: 2)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|