Module: ActiveVersion::Audits::HasAudits::ClassMethods

Defined in:
lib/active_version/audits/has_audits.rb

Instance Method Summary collapse

Instance Method Details

#audit_on_createObject

Manual callback installation methods



585
586
587
# File 'lib/active_version/audits/has_audits.rb', line 585

def audit_on_create
  after_create :audit_create
end

#audit_on_destroyObject



593
594
595
# File 'lib/active_version/audits/has_audits.rb', line 593

def audit_on_destroy
  before_destroy :audit_destroy, if: :should_audit?
end

#audit_on_touchObject



597
598
599
# File 'lib/active_version/audits/has_audits.rb', line 597

def audit_on_touch
  after_touch :audit_touch if ::ActiveRecord::VERSION::MAJOR >= 6
end

#audit_on_updateObject



589
590
591
# File 'lib/active_version/audits/has_audits.rb', line 589

def audit_on_update
  before_update :audit_update, if: :should_audit?, prepend: true
end

#audited?Boolean

Check if model is audited (has been set up with has_audits)

Returns:

  • (Boolean)


98
99
100
101
# File 'lib/active_version/audits/has_audits.rb', line 98

def audited?
  # Check if audited_options is set, which means set_audit has been called
  audited_options.present?
end

#class_auditing_enabled?Boolean

Returns:

  • (Boolean)


508
509
510
# File 'lib/active_version/audits/has_audits.rb', line 508

def class_auditing_enabled?
  @class_auditing_enabled != false
end

#has_audits(options = {}) ⇒ Object

Declare that a model has audits



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/active_version/audits/has_audits.rb', line 73

def has_audits(options = {})
  # For dynamically created classes, require class_name to be explicitly specified
  is_dynamic = name.nil?
  if is_dynamic && !options[:class_name]
    raise ConfigurationError, "Dynamically created classes must specify class_name option. Example: has_audits as: PostAudit, class_name: 'Post'"
  end
  if is_dynamic
    explicit_name = options[:class_name].to_s
    define_singleton_method(:name) { explicit_name } if explicit_name.present? && name.nil?
  end

  # For dynamically created classes, always call set_audit to ensure callbacks are set up
  # For regular classes, update options if already audited
  if audited? && !is_dynamic
    update_audited_options(options)
  else
    set_audit(options)
    # Verify association was set up
    unless reflect_on_association(:audits)
      raise ConfigurationError, "has_audits failed to set up association for #{name || options[:class_name]}. Audit class should be: #{audit_class.inspect}"
    end
  end
end

#instance_methods(all = true) ⇒ Object



413
414
415
416
417
# File 'lib/active_version/audits/has_audits.rb', line 413

def instance_methods(all = true)
  methods = super
  methods -= [:audit_revision, :audit_revision_at]
  methods
end

#revision_at(date_or_time) ⇒ Object

Get revision at specific time



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/active_version/audits/has_audits.rb', line 137

def revision_at(date_or_time)
  time_obj = ActiveVersion.parse_time_to_time(date_or_time)
  # Don't raise error for future times, just return nil (let HasRevisions handle it)
  return nil if time_obj.future?

  version_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :version)
  # Reload audits to ensure we get fresh data from database
  audits.reset if respond_to?(:audits) && audits.loaded?
  # Query audits up to and including the time
  # Use < instead of <= to exclude audits created exactly at the time (they represent state after that time)
  # But we want to include audits created at or before the time, so we need to use <=
  # Actually, we want audits created at or before the time, so <= is correct
  audits_list = audits.where("created_at <= ?", time_obj).order(version_column => :asc).to_a
  return nil if audits_list.empty?

  revision_with audit_class.reconstruct_attributes(audits_list)
end

#revision_with(attributes, id: nil) ⇒ Object



603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/active_version/audits/has_audits.rb', line 603

def revision_with(attributes, id: nil)
  # Create a new instance with reconstructed attributes
  # This ensures we start with a clean slate
  attrs_to_assign = attributes.except(:audit_version).stringify_keys

  # Filter out deleted columns
  attrs_to_assign.slice!(*column_names)

  revision = new
  revision.assign_attributes(attrs_to_assign)

  # Set id and persisted state after attributes are set
  revision.id = id if id
  revision.instance_variable_set(:@new_record, false)
  revision.instance_variable_set(:@persisted, true)

  # Mark as readonly to prevent database reads and ensure attributes stay in memory
  revision.readonly!

  # Ensure attributes are in the @attributes hash and not being read from DB
  # Clear any cached values that might trigger database reads
  revision.instance_variable_set(:@attributes_cache, {})
  revision.clear_changes_information

  # Clear association proxies to prevent stale references
  clear_association_proxies(revision)

  revision
end

#revisions(from_version = 1) ⇒ Object

Get revisions (reconstructed from audits)



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/active_version/audits/has_audits.rb', line 122

def revisions(from_version = 1)
  return [] unless audits.from_version(from_version).exists?

  prior_audits = audits.to_version(from_version - 1).ascending.to_a
  targeted_audits = audits.from_version(from_version).ascending.to_a

  previous_attributes = audit_class.reconstruct_attributes(prior_audits)

  targeted_audits.map do |audit|
    previous_attributes.merge!(audit.new_attributes)
    revision_with(previous_attributes)
  end
end

#with_audited_options(options = {}) ⇒ Object



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/active_version/audits/has_audits.rb', line 353

def with_audited_options(options = {})
  install_thread_local_audited_options_reader!
  thread_key = audited_current_options_key
  current = ActiveVersion.store_get(thread_key)
  # Store only the thread-local overrides (merge with existing if any)
  # Only normalize the provided keys, don't set defaults for missing keys
  # Normalize options - convert to hash and process each key
  # Use paper_trail's simple pattern: options.to_h.each
  normalized = {}
  # Convert options to hash (paper_trail pattern: simple to_h call)
  # Handle both Hash and objects that respond to to_h
  opts_hash = if options.is_a?(Hash)
    options
  elsif options.respond_to?(:to_h)
    options.to_h
  else
    {}
  end

  # Some objects (e.g. Struct.new(:to_h).new({...})) stringify into
  # { to_h: {...} } instead of returning the intended options hash.
  if opts_hash.is_a?(Hash)
    if opts_hash.key?(:to_h) && opts_hash[:to_h].is_a?(Hash)
      opts_hash = opts_hash[:to_h]
    elsif opts_hash.key?("to_h") && opts_hash["to_h"].is_a?(Hash)
      opts_hash = opts_hash["to_h"]
    end
  end

  opts_hash.each do |k, v|
    next if v.nil?
    key = k.is_a?(Symbol) ? k : k.to_sym
    # Normalize based on key type
    normalized[key] = case key
    when :only, :except, :redacted
      Array.wrap(v).map(&:to_s)
    when :on
      Array.wrap(v)
    when :max_audits, :redaction_value, :associated_with, :if, :unless, :auto, :comment_required, :storage, :as, :error_behavior
      v
    when :identity_columns
      normalize_identity_columns(v)
    else
      # Allow any other keys to pass through (for extensibility)
      v
    end
  end

  # Merge normalized options with existing thread-local overrides
  # paper_trail pattern: merge into existing, then set
  thread_local_overrides = (current || {}).dup
  thread_local_overrides.merge!(normalized)
  # Set thread-local value - ensure it's a hash so it can be read back
  # Store the merged overrides in Thread.current (use dup to avoid reference issues)
  ActiveVersion.store_set(thread_key, thread_local_overrides.is_a?(Hash) ? thread_local_overrides.dup : {})
  yield
ensure
  ActiveVersion.store_set(thread_key, current)
end

#with_auditingObject

Enable auditing for a block



113
114
115
116
117
118
119
# File 'lib/active_version/audits/has_audits.rb', line 113

def with_auditing
  auditing_was_enabled = class_auditing_enabled?
  enable_auditing
  yield
ensure
  disable_auditing unless auditing_was_enabled
end

#without_auditingObject

Disable auditing for a block



104
105
106
107
108
109
110
# File 'lib/active_version/audits/has_audits.rb', line 104

def without_auditing
  auditing_was_enabled = class_auditing_enabled?
  disable_auditing
  yield
ensure
  enable_auditing if auditing_was_enabled
end