Module: ActiveVersion::Audits::HasAudits::ClassMethods
- Defined in:
- lib/active_version/audits/has_audits.rb
Instance Method Summary collapse
-
#audit_on_create ⇒ Object
Manual callback installation methods.
- #audit_on_destroy ⇒ Object
- #audit_on_touch ⇒ Object
- #audit_on_update ⇒ Object
-
#audited? ⇒ Boolean
Check if model is audited (has been set up with has_audits).
- #class_auditing_enabled? ⇒ Boolean
-
#has_audits(options = {}) ⇒ Object
Declare that a model has audits.
- #instance_methods(all = true) ⇒ Object
-
#revision_at(date_or_time) ⇒ Object
Get revision at specific time.
- #revision_with(attributes, id: nil) ⇒ Object
-
#revisions(from_version = 1) ⇒ Object
Get revisions (reconstructed from audits).
- #with_audited_options(options = {}) ⇒ Object
-
#with_auditing ⇒ Object
Enable auditing for a block.
-
#without_auditing ⇒ Object
Disable auditing for a block.
Instance Method Details
#audit_on_create ⇒ Object
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_destroy ⇒ Object
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_touch ⇒ Object
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_update ⇒ Object
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)
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 .present? end |
#class_auditing_enabled? ⇒ 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( = {}) # For dynamically created classes, require class_name to be explicitly specified is_dynamic = name.nil? if is_dynamic && ![: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 = [: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 () else set_audit() # Verify association was set up unless reflect_on_association(:audits) raise ConfigurationError, "has_audits failed to set up association for #{name || [: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 ( = {}) thread_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 .is_a?(Hash) elsif .respond_to?(:to_h) .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_auditing ⇒ Object
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_auditing ⇒ Object
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 |