rails_audit_log 0.7.0 → 0.9.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/README.md +88 -0
- data/app/assets/stylesheets/rails_audit_log/_01_base.css +32 -0
- data/app/assets/stylesheets/rails_audit_log/_02_layout.css +34 -0
- data/app/assets/stylesheets/rails_audit_log/_03_table.css +39 -0
- data/app/assets/stylesheets/rails_audit_log/_04_badges.css +13 -0
- data/app/assets/stylesheets/rails_audit_log/_05_diff.css +94 -0
- data/app/assets/stylesheets/rails_audit_log/_06_timeline.css +21 -0
- data/app/assets/stylesheets/rails_audit_log/_07_detail.css +15 -0
- data/app/assets/stylesheets/rails_audit_log/_08_pagination.css +56 -0
- data/app/assets/stylesheets/rails_audit_log/_09_filters.css +54 -0
- data/app/assets/stylesheets/rails_audit_log/application.css +1 -15
- data/app/concerns/rails_audit_log/auditable.rb +14 -0
- data/app/controllers/rails_audit_log/application_controller.rb +12 -0
- data/app/controllers/rails_audit_log/audit_log_entries_controller.rb +31 -0
- data/app/controllers/rails_audit_log/resources_controller.rb +13 -0
- data/app/helpers/rails_audit_log/application_helper.rb +13 -0
- data/app/javascript/rails_audit_log/application.js +8 -0
- data/app/javascript/rails_audit_log/diff_controller.js +20 -0
- data/app/javascript/rails_audit_log/search_controller.js +12 -0
- data/app/models/rails_audit_log/audit_log_entry.rb +5 -3
- data/app/views/layouts/rails_audit_log/application.html.erb +16 -10
- data/app/views/rails_audit_log/audit_log_entries/_diff.html.erb +55 -0
- data/app/views/rails_audit_log/audit_log_entries/index.html.erb +76 -0
- data/app/views/rails_audit_log/audit_log_entries/show.html.erb +50 -0
- data/app/views/rails_audit_log/resources/show.html.erb +35 -0
- data/config/importmap.rb +5 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rails_audit_log/initializer/initializer_generator.rb +15 -0
- data/lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb +41 -0
- data/lib/rails_audit_log/engine.rb +28 -0
- data/lib/rails_audit_log/matchers.rb +103 -0
- data/lib/rails_audit_log/minitest_assertions.rb +31 -0
- data/lib/rails_audit_log/test_helpers.rb +7 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +10 -0
- metadata +67 -1
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
<
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Audit Log</title>
|
|
7
|
+
<link rel="icon" href="data:,">
|
|
5
8
|
<%= csrf_meta_tags %>
|
|
6
9
|
<%= csp_meta_tag %>
|
|
7
|
-
|
|
8
|
-
<%=
|
|
9
|
-
|
|
10
|
-
<%= stylesheet_link_tag "rails_audit_log/application", media: "all" %>
|
|
10
|
+
<%= dashboard_stylesheets %>
|
|
11
|
+
<%= javascript_importmap_tags "rails_audit_log" %>
|
|
11
12
|
</head>
|
|
12
13
|
<body>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
<header class="ral-header">
|
|
15
|
+
<div class="ral-header__inner">
|
|
16
|
+
<%= link_to "Audit Log", root_path, class: "ral-header__logo" %>
|
|
17
|
+
</div>
|
|
18
|
+
</header>
|
|
19
|
+
<main class="ral-main">
|
|
20
|
+
<%= yield %>
|
|
21
|
+
</main>
|
|
16
22
|
</body>
|
|
17
|
-
</html>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<% changes = entry.object_changes.presence %>
|
|
2
|
+
<% if changes %>
|
|
3
|
+
<div class="ral-diff-wrap" data-controller="diff">
|
|
4
|
+
<div class="ral-diff-toggle">
|
|
5
|
+
<button class="ral-diff-btn ral-diff-btn--active"
|
|
6
|
+
data-diff-target="inlineBtn"
|
|
7
|
+
data-action="click->diff#setInline">Inline</button>
|
|
8
|
+
<button class="ral-diff-btn"
|
|
9
|
+
data-diff-target="sideBtn"
|
|
10
|
+
data-action="click->diff#setSide">Side by side</button>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<table class="ral-diff" data-diff-target="inline">
|
|
14
|
+
<thead>
|
|
15
|
+
<tr>
|
|
16
|
+
<th>Attribute</th>
|
|
17
|
+
<th>Before</th>
|
|
18
|
+
<th>After</th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
<% changes.each do |attr, (before, after)| %>
|
|
23
|
+
<tr>
|
|
24
|
+
<td class="ral-diff__attr"><%= attr %></td>
|
|
25
|
+
<td class="ral-diff__before"><%= format_diff_value(before) %></td>
|
|
26
|
+
<td class="ral-diff__after"><%= format_diff_value(after) %></td>
|
|
27
|
+
</tr>
|
|
28
|
+
<% end %>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
|
|
32
|
+
<div class="ral-diff-side" data-diff-target="side" hidden>
|
|
33
|
+
<div class="ral-diff-side__panel ral-diff-side__panel--before">
|
|
34
|
+
<div class="ral-diff-side__header">Before</div>
|
|
35
|
+
<% changes.each do |attr, (before, _after)| %>
|
|
36
|
+
<div class="ral-diff-side__row">
|
|
37
|
+
<span class="ral-diff-side__attr"><%= attr %></span>
|
|
38
|
+
<span class="ral-diff-side__value"><%= format_diff_value(before) %></span>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="ral-diff-side__panel ral-diff-side__panel--after">
|
|
43
|
+
<div class="ral-diff-side__header">After</div>
|
|
44
|
+
<% changes.each do |attr, (_before, after)| %>
|
|
45
|
+
<div class="ral-diff-side__row">
|
|
46
|
+
<span class="ral-diff-side__attr"><%= attr %></span>
|
|
47
|
+
<span class="ral-diff-side__value"><%= format_diff_value(after) %></span>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<% else %>
|
|
54
|
+
<p class="ral-muted">No changes recorded.</p>
|
|
55
|
+
<% end %>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<div class="ral-page-header">
|
|
2
|
+
<h1 class="ral-page-header__title">Audit Entries</h1>
|
|
3
|
+
<span class="ral-page-header__meta"><%= @pagy.count %> <%= "entry".pluralize(@pagy.count) %></span>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%= turbo_frame_tag "ral-entries", data: { turbo_action: "advance" } do %>
|
|
7
|
+
<form class="ral-filters" action="<%= audit_log_entries_path %>" method="get"
|
|
8
|
+
data-controller="search">
|
|
9
|
+
<%= hidden_field_tag :period, @period if @period %>
|
|
10
|
+
|
|
11
|
+
<input type="search" name="q" value="<%= @q %>"
|
|
12
|
+
class="ral-filter-input" placeholder="Filter by actor…"
|
|
13
|
+
autocomplete="off" aria-label="Filter by actor"
|
|
14
|
+
data-action="input->search#filter">
|
|
15
|
+
|
|
16
|
+
<select name="event" class="ral-filter-select" aria-label="Filter by event"
|
|
17
|
+
data-action="change->search#select">
|
|
18
|
+
<option value="">All events</option>
|
|
19
|
+
<% RailsAuditLog::AuditLogEntry::EVENTS.each do |event| %>
|
|
20
|
+
<option value="<%= event %>" <%= "selected" if @event == event %>><%= event.capitalize %></option>
|
|
21
|
+
<% end %>
|
|
22
|
+
</select>
|
|
23
|
+
|
|
24
|
+
<% if @item_types.size > 1 %>
|
|
25
|
+
<select name="item_type" class="ral-filter-select" aria-label="Filter by resource"
|
|
26
|
+
data-action="change->search#select">
|
|
27
|
+
<option value="">All resources</option>
|
|
28
|
+
<% @item_types.each do |type| %>
|
|
29
|
+
<option value="<%= type %>" <%= "selected" if @item_type == type %>><%= type %></option>
|
|
30
|
+
<% end %>
|
|
31
|
+
</select>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<div class="ral-period-filter" role="group" aria-label="Time period">
|
|
35
|
+
<%= link_to "All", audit_log_entries_path(event: @event, item_type: @item_type, q: @q),
|
|
36
|
+
class: "ral-period-btn#{" ral-period-btn--active" if @period.blank?}" %>
|
|
37
|
+
<% RailsAuditLog::AuditLogEntry::PERIODS.each_key do |p| %>
|
|
38
|
+
<%= link_to p, audit_log_entries_path(event: @event, item_type: @item_type, q: @q, period: p),
|
|
39
|
+
class: "ral-period-btn#{" ral-period-btn--active" if @period == p}" %>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<% if @event || @item_type || @period || @q %>
|
|
44
|
+
<%= link_to "Clear", audit_log_entries_path, class: "ral-link ral-filters__clear" %>
|
|
45
|
+
<% end %>
|
|
46
|
+
</form>
|
|
47
|
+
|
|
48
|
+
<% if @entries.any? %>
|
|
49
|
+
<div class="ral-table-wrap">
|
|
50
|
+
<table class="ral-table">
|
|
51
|
+
<thead>
|
|
52
|
+
<tr>
|
|
53
|
+
<th>Event</th>
|
|
54
|
+
<th>Resource</th>
|
|
55
|
+
<th>Actor</th>
|
|
56
|
+
<th>When</th>
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody>
|
|
60
|
+
<% @entries.each do |entry| %>
|
|
61
|
+
<tr>
|
|
62
|
+
<td><span class="ral-badge ral-badge--<%= entry.event %>"><%= entry.event %></span></td>
|
|
63
|
+
<td><%= link_to "#{entry.item_type} ##{entry.item_id}", resource_path(item_type: entry.item_type, item_id: entry.item_id), class: "ral-link", data: { turbo_frame: "_top" } %></td>
|
|
64
|
+
<td><%= entry.whodunnit_snapshot.presence || (entry.actor_type ? "#{entry.actor_type} \##{entry.actor_id}" : "—") %></td>
|
|
65
|
+
<td class="ral-timestamp"><%= entry.created_at.utc.strftime("%Y-%m-%d %H:%M UTC") %></td>
|
|
66
|
+
</tr>
|
|
67
|
+
<% end %>
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
|
|
73
|
+
<% else %>
|
|
74
|
+
<p class="ral-empty">No audit entries found.</p>
|
|
75
|
+
<% end %>
|
|
76
|
+
<% end %>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<div class="ral-page-header">
|
|
2
|
+
<div>
|
|
3
|
+
<p class="ral-breadcrumb">
|
|
4
|
+
<%= link_to "Audit Entries", audit_log_entries_path, class: "ral-link" %> /
|
|
5
|
+
<%= link_to "#{@entry.item_type} ##{@entry.item_id}", resource_path(item_type: @entry.item_type, item_id: @entry.item_id), class: "ral-link" %>
|
|
6
|
+
</p>
|
|
7
|
+
<h1 class="ral-page-header__title">Entry #<%= @entry.id %></h1>
|
|
8
|
+
</div>
|
|
9
|
+
<nav class="ral-entry-nav" aria-label="Entry navigation">
|
|
10
|
+
<% if (prev_entry = @entry.previous) %>
|
|
11
|
+
<%= link_to "← Previous", audit_log_entry_path(prev_entry), class: "ral-pagination__link" %>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% if (next_entry = @entry.next) %>
|
|
14
|
+
<%= link_to "Next →", audit_log_entry_path(next_entry), class: "ral-pagination__link" %>
|
|
15
|
+
<% end %>
|
|
16
|
+
</nav>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="ral-card">
|
|
20
|
+
<dl class="ral-meta">
|
|
21
|
+
<div class="ral-meta__row">
|
|
22
|
+
<dt>Event</dt>
|
|
23
|
+
<dd><span class="ral-badge ral-badge--<%= @entry.event %>"><%= @entry.event %></span></dd>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="ral-meta__row">
|
|
26
|
+
<dt>Resource</dt>
|
|
27
|
+
<dd><%= link_to "#{@entry.item_type} ##{@entry.item_id}", resource_path(item_type: @entry.item_type, item_id: @entry.item_id), class: "ral-link" %></dd>
|
|
28
|
+
</div>
|
|
29
|
+
<% actor = @entry.whodunnit_snapshot.presence || (@entry.actor_type ? "#{@entry.actor_type} \##{@entry.actor_id}" : "—") %>
|
|
30
|
+
<div class="ral-meta__row">
|
|
31
|
+
<dt>Actor</dt>
|
|
32
|
+
<dd><%= actor %></dd>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="ral-meta__row">
|
|
35
|
+
<dt>When</dt>
|
|
36
|
+
<dd class="ral-timestamp"><%= @entry.created_at.utc.strftime("%Y-%m-%d %H:%M:%S UTC") %></dd>
|
|
37
|
+
</div>
|
|
38
|
+
<% if @entry.reason.present? %>
|
|
39
|
+
<div class="ral-meta__row">
|
|
40
|
+
<dt>Reason</dt>
|
|
41
|
+
<dd><%= @entry.reason %></dd>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</dl>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<h2 class="ral-section-title">Changes</h2>
|
|
48
|
+
<div class="ral-card">
|
|
49
|
+
<%= render "diff", entry: @entry %>
|
|
50
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<div class="ral-page-header">
|
|
2
|
+
<div>
|
|
3
|
+
<p class="ral-breadcrumb"><%= link_to "Audit Entries", audit_log_entries_path, class: "ral-link" %></p>
|
|
4
|
+
<h1 class="ral-page-header__title"><%= @item_type %> #<%= @item_id %></h1>
|
|
5
|
+
</div>
|
|
6
|
+
<span class="ral-page-header__meta"><%= @pagy.count %> <%= "entry".pluralize(@pagy.count) %></span>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<%= turbo_frame_tag "ral-resource-entries", data: { turbo_action: "advance" } do %>
|
|
10
|
+
<% if @entries.any? %>
|
|
11
|
+
<div class="ral-timeline">
|
|
12
|
+
<% @entries.each do |entry| %>
|
|
13
|
+
<div class="ral-timeline__entry">
|
|
14
|
+
<div class="ral-timeline__header">
|
|
15
|
+
<span class="ral-badge ral-badge--<%= entry.event %>"><%= entry.event %></span>
|
|
16
|
+
<span class="ral-timestamp"><%= entry.created_at.utc.strftime("%Y-%m-%d %H:%M UTC") %></span>
|
|
17
|
+
<% actor = entry.whodunnit_snapshot.presence || (entry.actor_type ? "#{entry.actor_type} \##{entry.actor_id}" : nil) %>
|
|
18
|
+
<% if actor %>
|
|
19
|
+
<span class="ral-muted">by <%= actor %></span>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% if entry.reason.present? %>
|
|
22
|
+
<span class="ral-reason">"<%= entry.reason %>"</span>
|
|
23
|
+
<% end %>
|
|
24
|
+
<%= link_to "Details →", audit_log_entry_path(entry), class: "ral-link ral-timeline__detail-link", data: { turbo_frame: "_top" } %>
|
|
25
|
+
</div>
|
|
26
|
+
<%= render "rails_audit_log/audit_log_entries/diff", entry: entry %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
|
|
32
|
+
<% else %>
|
|
33
|
+
<p class="ral-empty">No audit entries for this record.</p>
|
|
34
|
+
<% end %>
|
|
35
|
+
<% end %>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"
|
|
2
|
+
pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
|
|
3
|
+
pin "rails_audit_log", to: "rails_audit_log/application.js"
|
|
4
|
+
pin "rails_audit_log/search_controller", to: "rails_audit_log/search_controller.js"
|
|
5
|
+
pin "rails_audit_log/diff_controller", to: "rails_audit_log/diff_controller.js"
|
data/config/routes.rb
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module RailsAuditLog
|
|
4
|
+
module Generators
|
|
5
|
+
class InitializerGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Creates a commented RailsAuditLog initializer in config/initializers."
|
|
9
|
+
|
|
10
|
+
def create_initializer_file
|
|
11
|
+
template "rails_audit_log.rb", "config/initializers/rails_audit_log.rb"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# config/initializers/rails_audit_log.rb
|
|
2
|
+
# Generated by `bin/rails generate rails_audit_log:initializer`
|
|
3
|
+
#
|
|
4
|
+
# All settings are optional. Remove the comments around any line you want to activate.
|
|
5
|
+
|
|
6
|
+
RailsAuditLog.configure do |config|
|
|
7
|
+
# Global columns excluded from all audited models. Default: ["updated_at"]
|
|
8
|
+
# config.ignored_attributes = %w[updated_at cached_at]
|
|
9
|
+
|
|
10
|
+
# Store a full attribute snapshot alongside object_changes. Default: true
|
|
11
|
+
# Disable to save storage; reify and version_at fall back to diff-only reconstruction.
|
|
12
|
+
# config.store_snapshot = false
|
|
13
|
+
|
|
14
|
+
# Capture remote_ip and user_agent into each entry's metadata column. Default: false
|
|
15
|
+
# Requires including RailsAuditLog::Controller in ApplicationController.
|
|
16
|
+
# config.capture_request_metadata = true
|
|
17
|
+
|
|
18
|
+
# Customise how the actor's display name is stored at write time. Default: actor.name
|
|
19
|
+
# config.whodunnit_display = ->(actor) { actor.email }
|
|
20
|
+
|
|
21
|
+
# Global cap on entries retained per tracked record. Default: nil (no limit)
|
|
22
|
+
# Per-model `audit_log version_limit: N` takes precedence.
|
|
23
|
+
# config.version_limit = 100
|
|
24
|
+
|
|
25
|
+
# Write all audit entries asynchronously via WriteAuditLogJob. Default: false
|
|
26
|
+
# Per-model `audit_log async: true` also works.
|
|
27
|
+
# config.async = true
|
|
28
|
+
|
|
29
|
+
# Route AuditLogEntry to a dedicated database (Rails multi-DB). Default: nil
|
|
30
|
+
# config.connects_to = { database: { writing: :audit_log, reading: :audit_log } }
|
|
31
|
+
|
|
32
|
+
# Number of entries per page in the web dashboard. Default: 25
|
|
33
|
+
# config.page_size = 50
|
|
34
|
+
|
|
35
|
+
# Gate web dashboard access. Block runs in controller context — controller
|
|
36
|
+
# methods like current_user are available directly, or accept the controller
|
|
37
|
+
# as an argument. Falls back to HTTP Basic auth if the block returns falsy.
|
|
38
|
+
# Leave unset to allow unauthenticated access (development default).
|
|
39
|
+
# config.authenticate { current_user&.admin? }
|
|
40
|
+
# config.authenticate { |c| c.current_user&.admin? }
|
|
41
|
+
end
|
|
@@ -1,7 +1,35 @@
|
|
|
1
|
+
require "turbo-rails"
|
|
2
|
+
require "importmap-rails"
|
|
3
|
+
require "pagy"
|
|
4
|
+
require "pagy/toolbox/paginators/method"
|
|
5
|
+
|
|
1
6
|
module RailsAuditLog
|
|
2
7
|
class Engine < ::Rails::Engine
|
|
3
8
|
isolate_namespace RailsAuditLog
|
|
4
9
|
|
|
10
|
+
config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")
|
|
11
|
+
|
|
12
|
+
initializer "rails_audit_log.pagy" do |app|
|
|
13
|
+
app.config.after_initialize do
|
|
14
|
+
Pagy::OPTIONS[:limit] = RailsAuditLog.page_size
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
initializer "rails_audit_log.assets" do |app|
|
|
19
|
+
if app.config.respond_to?(:assets)
|
|
20
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
|
21
|
+
app.config.assets.paths << root.join("app/assets/images")
|
|
22
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
initializer "rails_audit_log.importmap", before: "importmap" do |app|
|
|
27
|
+
if app.config.respond_to?(:importmap)
|
|
28
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
29
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
5
33
|
initializer "rails_audit_log.connect_audit_db" do
|
|
6
34
|
ActiveSupport.on_load(:active_record) do
|
|
7
35
|
RailsAuditLog::AuditLogEntry.configure_connection!
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
module Matchers
|
|
3
|
+
def have_audit_log_entry(event = nil)
|
|
4
|
+
HaveAuditLogEntry.new(event)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def create_audit_log_entry(event: nil, touching: nil)
|
|
8
|
+
CreateAuditLogEntry.new(event: event, touching: touching)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class HaveAuditLogEntry
|
|
12
|
+
def initialize(event)
|
|
13
|
+
@event = event
|
|
14
|
+
@touching = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def touching(attribute)
|
|
18
|
+
@touching = attribute
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def matches?(record)
|
|
23
|
+
@record = record
|
|
24
|
+
scope = record.audit_log_entries
|
|
25
|
+
scope = scope.where(event: @event.to_s) if @event
|
|
26
|
+
scope = scope.touching(@touching) if @touching
|
|
27
|
+
scope.exists?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def failure_message
|
|
31
|
+
"expected #{@record.class}##{@record.id} to have an audit log entry#{qualifier}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def failure_message_when_negated
|
|
35
|
+
"expected #{@record.class}##{@record.id} not to have an audit log entry#{qualifier}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def description
|
|
39
|
+
"have an audit log entry#{qualifier}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def qualifier
|
|
45
|
+
parts = []
|
|
46
|
+
parts << " with event '#{@event}'" if @event
|
|
47
|
+
parts << " touching '#{@touching}'" if @touching
|
|
48
|
+
parts.join
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class CreateAuditLogEntry
|
|
53
|
+
def initialize(event:, touching:)
|
|
54
|
+
@event = event
|
|
55
|
+
@touching = touching
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def touching(attribute)
|
|
59
|
+
@touching = attribute
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def supports_block_expectations?
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def matches?(block)
|
|
68
|
+
@before = matching_scope.count
|
|
69
|
+
block.call
|
|
70
|
+
@after = matching_scope.count
|
|
71
|
+
@after > @before
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def failure_message
|
|
75
|
+
"expected block to create an audit log entry#{qualifier}, but none was created"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def failure_message_when_negated
|
|
79
|
+
"expected block not to create an audit log entry#{qualifier}, but one was created"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def description
|
|
83
|
+
"create an audit log entry#{qualifier}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def matching_scope
|
|
89
|
+
scope = RailsAuditLog::AuditLogEntry.all
|
|
90
|
+
scope = scope.where(event: @event.to_s) if @event
|
|
91
|
+
scope = scope.touching(@touching) if @touching
|
|
92
|
+
scope
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def qualifier
|
|
96
|
+
parts = []
|
|
97
|
+
parts << " with event '#{@event}'" if @event
|
|
98
|
+
parts << " touching '#{@touching}'" if @touching
|
|
99
|
+
parts.join
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
module MinitestAssertions
|
|
3
|
+
def assert_audit_log_entry(record, event: nil, touching: nil, message: nil)
|
|
4
|
+
scope = build_scope(record, event, touching)
|
|
5
|
+
msg = message || default_message("to have", record, event, touching)
|
|
6
|
+
assert scope.exists?, msg
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def refute_audit_log_entry(record, event: nil, touching: nil, message: nil)
|
|
10
|
+
scope = build_scope(record, event, touching)
|
|
11
|
+
msg = message || default_message("not to have", record, event, touching)
|
|
12
|
+
refute scope.exists?, msg
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def build_scope(record, event, touching)
|
|
18
|
+
scope = record.audit_log_entries
|
|
19
|
+
scope = scope.where(event: event.to_s) if event
|
|
20
|
+
scope = scope.touching(touching) if touching
|
|
21
|
+
scope
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def default_message(polarity, record, event, touching)
|
|
25
|
+
parts = []
|
|
26
|
+
parts << " with event '#{event}'" if event
|
|
27
|
+
parts << " touching '#{touching}'" if touching
|
|
28
|
+
"Expected #{record.class}##{record.id} #{polarity} an audit log entry#{parts.join}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -5,11 +5,21 @@ module RailsAuditLog
|
|
|
5
5
|
# Global default columns to ignore across all audited models.
|
|
6
6
|
# Override in an initializer: RailsAuditLog.ignored_attributes = %w[updated_at cached_at]
|
|
7
7
|
mattr_accessor :ignored_attributes, default: %w[updated_at]
|
|
8
|
+
|
|
9
|
+
def self.configure
|
|
10
|
+
yield self
|
|
11
|
+
end
|
|
8
12
|
mattr_accessor :store_snapshot, default: true
|
|
9
13
|
mattr_accessor :capture_request_metadata, default: false
|
|
10
14
|
mattr_accessor :version_limit, default: nil
|
|
11
15
|
mattr_accessor :async, default: false
|
|
12
16
|
mattr_accessor :connects_to, default: nil
|
|
17
|
+
mattr_accessor :page_size, default: 25
|
|
18
|
+
|
|
19
|
+
def self.authenticate(&block)
|
|
20
|
+
@authenticate = block if block_given?
|
|
21
|
+
@authenticate
|
|
22
|
+
end
|
|
13
23
|
mattr_accessor :whodunnit_display, default: ->(actor) {
|
|
14
24
|
actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
|
|
15
25
|
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_audit_log
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -23,6 +23,48 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: turbo-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: importmap-rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.2'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.2'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: pagy
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '43.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '43.0'
|
|
26
68
|
description: A modern Rails engine that tracks ActiveRecord create, update, and destroy
|
|
27
69
|
events with JSON-first storage, whodunnit actor context, and a clean query API.
|
|
28
70
|
Drop-in replacement for PaperTrail with no legacy baggage.
|
|
@@ -35,21 +77,45 @@ files:
|
|
|
35
77
|
- MIT-LICENSE
|
|
36
78
|
- README.md
|
|
37
79
|
- Rakefile
|
|
80
|
+
- app/assets/stylesheets/rails_audit_log/_01_base.css
|
|
81
|
+
- app/assets/stylesheets/rails_audit_log/_02_layout.css
|
|
82
|
+
- app/assets/stylesheets/rails_audit_log/_03_table.css
|
|
83
|
+
- app/assets/stylesheets/rails_audit_log/_04_badges.css
|
|
84
|
+
- app/assets/stylesheets/rails_audit_log/_05_diff.css
|
|
85
|
+
- app/assets/stylesheets/rails_audit_log/_06_timeline.css
|
|
86
|
+
- app/assets/stylesheets/rails_audit_log/_07_detail.css
|
|
87
|
+
- app/assets/stylesheets/rails_audit_log/_08_pagination.css
|
|
88
|
+
- app/assets/stylesheets/rails_audit_log/_09_filters.css
|
|
38
89
|
- app/assets/stylesheets/rails_audit_log/application.css
|
|
39
90
|
- app/concerns/rails_audit_log/auditable.rb
|
|
40
91
|
- app/concerns/rails_audit_log/controller.rb
|
|
41
92
|
- app/controllers/rails_audit_log/application_controller.rb
|
|
93
|
+
- app/controllers/rails_audit_log/audit_log_entries_controller.rb
|
|
94
|
+
- app/controllers/rails_audit_log/resources_controller.rb
|
|
42
95
|
- app/helpers/rails_audit_log/application_helper.rb
|
|
96
|
+
- app/javascript/rails_audit_log/application.js
|
|
97
|
+
- app/javascript/rails_audit_log/diff_controller.js
|
|
98
|
+
- app/javascript/rails_audit_log/search_controller.js
|
|
43
99
|
- app/jobs/rails_audit_log/application_job.rb
|
|
44
100
|
- app/jobs/rails_audit_log/write_audit_log_job.rb
|
|
45
101
|
- app/models/rails_audit_log/application_record.rb
|
|
46
102
|
- app/models/rails_audit_log/audit_log_entry.rb
|
|
47
103
|
- app/views/layouts/rails_audit_log/application.html.erb
|
|
104
|
+
- app/views/rails_audit_log/audit_log_entries/_diff.html.erb
|
|
105
|
+
- app/views/rails_audit_log/audit_log_entries/index.html.erb
|
|
106
|
+
- app/views/rails_audit_log/audit_log_entries/show.html.erb
|
|
107
|
+
- app/views/rails_audit_log/resources/show.html.erb
|
|
108
|
+
- config/importmap.rb
|
|
48
109
|
- config/routes.rb
|
|
110
|
+
- lib/generators/rails_audit_log/initializer/initializer_generator.rb
|
|
111
|
+
- lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb
|
|
49
112
|
- lib/generators/rails_audit_log/install/install_generator.rb
|
|
50
113
|
- lib/generators/rails_audit_log/install/templates/create_audit_log_entries.rb
|
|
51
114
|
- lib/rails_audit_log.rb
|
|
52
115
|
- lib/rails_audit_log/engine.rb
|
|
116
|
+
- lib/rails_audit_log/matchers.rb
|
|
117
|
+
- lib/rails_audit_log/minitest_assertions.rb
|
|
118
|
+
- lib/rails_audit_log/test_helpers.rb
|
|
53
119
|
- lib/rails_audit_log/version.rb
|
|
54
120
|
- lib/tasks/rails_audit_log_tasks.rake
|
|
55
121
|
homepage: https://github.com/eclectic-coding/rails_audit_log
|