Skip to content

Commit

Permalink
Introduce support for audited_class setting per model
Browse files Browse the repository at this point in the history
  • Loading branch information
amkisko committed Oct 29, 2024
1 parent 8dc7184 commit 374e896
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 63 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,9 @@ class CustomAudit < Audited::Audit
end
end
```

Then set it in an initializer:

```ruby
# config/initializers/audited.rb

Expand All @@ -425,6 +427,31 @@ Audited.config do |config|
end
```

You can also specify a custom audit class on a per-model basis, which will override default audit class for the exact model.

```ruby
class User < ActiveRecord::Base
audited as: "CustomAudit"
end

# or with a custom class
class User < ActiveRecord::Base
audited as: CustomAudit
end
```

You can also supply a custom table name for the audit records.

```ruby
class CustomAudit < Audited::Audit
self.table_name = "custom_audits"
end

class User < ActiveRecord::Base
audited as: CustomAudit
end
```

### Enum Storage

In 4.10, the default behavior for enums changed from storing the value synthesized by Rails to the value stored in the DB. You can restore the previous behavior by setting the store_synthesized_enums configuration value:
Expand Down
55 changes: 30 additions & 25 deletions lib/audited/audit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,29 @@ module Audited
#

class YAMLIfTextColumnType
class << self
def load(obj)
if text_column?
ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
else
obj
end
end
def initialize(audit_class)
@audit_class = audit_class
end

def dump(obj)
if text_column?
ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
else
obj
end
def load(obj)
if text_column?
ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
else
obj
end
end

def text_column?
Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
def dump(obj)
if text_column?
ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
else
obj
end
end

def text_column?
@audit_class.columns_hash["audited_changes"].type.to_s == "text"
end
end

class Audit < ::ActiveRecord::Base
Expand All @@ -46,13 +48,21 @@ class Audit < ::ActiveRecord::Base

before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address

cattr_accessor :audited_class_names
self.audited_class_names = Set.new
def self.add_audited_class(audited_class)
@@audited_classes ||= {}
@@audited_classes[name] ||= Set.new
@@audited_classes[name] << audited_class
end

def self.audited_classes
@@audited_classes ||= {}
@@audited_classes[name] ||= Set.new
end

if Rails.gem_version >= Gem::Version.new("7.1")
serialize :audited_changes, coder: YAMLIfTextColumnType
serialize :audited_changes, coder: YAMLIfTextColumnType.new(self)
else
serialize :audited_changes, YAMLIfTextColumnType
serialize :audited_changes, YAMLIfTextColumnType.new(self)
end

scope :ascending, -> { reorder(version: :asc) }
Expand Down Expand Up @@ -129,11 +139,6 @@ def user_as_string
alias_method :user_as_model, :user
alias_method :user, :user_as_string

# Returns the list of classes that are being audited
def self.audited_classes
audited_class_names.map(&:constantize)
end

# All audits made during the block called will be recorded as made
# by +user+. This method is hopefully threadsafe, making it ideal
# for background operations that require audit information.
Expand Down
29 changes: 20 additions & 9 deletions lib/audited/auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,31 @@ def audited(options = {})

class_attribute :audit_associated_with, instance_writer: false
class_attribute :audited_options, instance_writer: false
class_attribute :audit_class, instance_writer: false

attr_accessor :audit_version, :audit_comment

self.audited_options = options
normalize_audited_options

self.audit_associated_with = audited_options[:associated_with]

self.audit_class = case audited_options[:as]
when String, Symbol
audited_options[:as].to_s.safe_constantize
when Class
audited_options[:as]
else
Audited.audit_class
end

if audited_options[:comment_required]
validate :presence_of_audit_comment
before_destroy :require_comment if audited_options[:on].include?(:destroy)
end

has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
Audited.audit_class.audited_class_names << to_s
has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: audit_class.name, inverse_of: :auditable
audit_class.add_audited_class(self)

after_create :audit_create if audited_options[:on].include?(:create)
before_update :audit_update if audited_options[:on].include?(:update)
Expand All @@ -98,7 +109,7 @@ def audited(options = {})
end

def has_associated_audits
has_many :associated_audits, as: :associated, class_name: Audited.audit_class.name
has_many :associated_audits, as: :associated, class_name: audit_class.name
end
end

Expand Down Expand Up @@ -160,14 +171,14 @@ def revisions(from_version = 1)
# Returns nil for versions greater than revisions count
def revision(version)
if version == :previous || audits.last.version >= version
revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
revision_with audit_class.reconstruct_attributes(audits_to(version))
end
end

# Find the oldest revision recorded prior to the date/time provided.
def revision_at(date_or_time)
audits = self.audits.up_until(date_or_time)
revision_with Audited.audit_class.reconstruct_attributes(audits) unless audits.empty?
revision_with audit_class.reconstruct_attributes(audits) unless audits.empty?
end

# List of attributes that are audited.
Expand All @@ -180,8 +191,8 @@ def audited_attributes

# Returns a list combined of record audits and associated audits.
def own_and_associated_audits
Audited.audit_class.unscoped.where(auditable: self)
.or(Audited.audit_class.unscoped.where(associated: self))
audit_class.unscoped.where(auditable: self)
.or(audit_class.unscoped.where(associated: self))
.order(created_at: :desc)
end

Expand Down Expand Up @@ -213,7 +224,7 @@ def revision_with(attributes)
revision.send :instance_variable_set, "@destroyed", false
revision.send :instance_variable_set, "@_destroyed", false
revision.send :instance_variable_set, "@marked_for_destruction", false
Audited.audit_class.assign_revision_attributes(revision, attributes)
audit_class.assign_revision_attributes(revision, attributes)

# Remove any association proxies so that they will be recreated
# and reference the correct object for this revision. The only way
Expand Down Expand Up @@ -492,7 +503,7 @@ def enable_auditing
# convenience wrapper around
# @see Audit#as_user.
def audit_as(user, &block)
Audited.audit_class.as_user(user, &block)
audit_class.as_user(user, &block)
end

def auditing_enabled
Expand Down
2 changes: 1 addition & 1 deletion lib/audited/rspec_matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def reflection
def association_exists?
!reflection.nil? &&
reflection.macro == :has_many &&
reflection.options[:class_name] == Audited.audit_class.name
reflection.options[:class_name] == model_class.audit_class.name
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/generators/audited/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class InstallGenerator < Rails::Generators::Base

class_option :audited_changes_column_type, type: :string, default: "text", required: false
class_option :audited_user_id_column_type, type: :string, default: "integer", required: false
class_option :audited_table_name, type: :string, default: "audits", required: false

source_root File.expand_path("../templates", __FILE__)

Expand Down
15 changes: 8 additions & 7 deletions lib/generators/audited/templates/install.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# frozen_string_literal: true

<% table_name = options[:audited_table_name] %>
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
create_table :audits, :force => true do |t|
create_table :<%= table_name %>, :force => true do |t|
t.column :auditable_id, :integer
t.column :auditable_type, :string
t.column :associated_id, :integer
Expand All @@ -19,14 +20,14 @@ def self.up
t.column :created_at, :datetime
end

add_index :audits, [:auditable_type, :auditable_id, :version], :name => 'auditable_index'
add_index :audits, [:associated_type, :associated_id], :name => 'associated_index'
add_index :audits, [:user_id, :user_type], :name => 'user_index'
add_index :audits, :request_uuid
add_index :audits, :created_at
add_index :<%= table_name %>, [:auditable_type, :auditable_id, :version], :name => '#{table_name}_auditable_index'
add_index :<%= table_name %>, [:associated_type, :associated_id], :name => '#{table_name}_associated_index'
add_index :<%= table_name %>, [:user_id, :user_type], :name => '#{table_name}_user_index'
add_index :<%= table_name %>, :request_uuid
add_index :<%= table_name %>, :created_at
end

def self.down
drop_table :audits
drop_table :<%= table_name %>
end
end
2 changes: 2 additions & 0 deletions lib/generators/audited/upgrade_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class UpgradeGenerator < Rails::Generators::Base
include Audited::Generators::MigrationHelper
extend Audited::Generators::Migration

class_option :audited_table_name, type: :string, default: "audits", required: false

source_root File.expand_path("../templates", __FILE__)

def copy_templates
Expand Down
2 changes: 1 addition & 1 deletion spec/audited/audit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse
end

it "does not unserialize from binary columns" do
allow(Audited::YAMLIfTextColumnType).to receive(:text_column?).and_return(false)
allow_any_instance_of(Audited::YAMLIfTextColumnType).to receive(:text_column?).and_return(false)
audit.audited_changes = {foo: "bar"}
expect(audit.audited_changes).to eq "{:foo=>\"bar\"}"
end
Expand Down
Loading

0 comments on commit 374e896

Please sign in to comment.