Module: ActiveVersion::Audits::HasAudits
- Extended by:
- ActiveSupport::Concern
- Includes:
- AuditCallbacks, AuditCombiner, AuditWriter, ChangeFilters
- Defined in:
- lib/active_version/audits/has_audits.rb,
lib/active_version/audits/has_audits/audit_writer.rb,
lib/active_version/audits/has_audits/audit_combiner.rb,
lib/active_version/audits/has_audits/change_filters.rb,
lib/active_version/audits/has_audits/audit_callbacks.rb,
lib/active_version/audits/has_audits/database_adapter_helper.rb
Overview
Concern for models that have audits
Defined Under Namespace
Modules: AuditCallbacks, AuditCombiner, AuditWriter, ChangeFilters, ClassMethods, DatabaseAdapterHelper
Constant Summary collapse
- REDACTED =
"[REDACTED]"
Instance Method Summary collapse
-
#active_audits ⇒ Object
Get active audits (excludes combined ones - those with empty changes) Filters in Ruby for database-agnostic behavior.
- #active_version_audit_identity_columns ⇒ Object
- #active_version_audit_identity_map ⇒ Object
- #active_version_audit_identity_values ⇒ Object
- #active_version_auditable_id_value ⇒ Object
-
#audit_revision(version: nil) ⇒ Object
Get revision at specific version (from audits) This method is separate from HasRevisions#revision to avoid conflicts.
-
#audit_revision_at(date_or_time) ⇒ Object
Get revision at specific time (from audits) This method is separate from HasRevisions#revision_at to avoid conflicts.
-
#audit_sql(destroy: false) ⇒ Object
Generate SQL for single audit insert.
- #auditing_enabled ⇒ Object
-
#audits ⇒ Object
Override audits method to handle dynamically created classes Uses class_name from options if provided Returns standard ActiveRecord relation Use active_audits for filtered results.
- #clear_rolled_back_audits ⇒ Object
- #default_audit_identity_values ⇒ Object
-
#own_and_associated_audits ⇒ Object
Get own and associated audits.
- #revision_with(attributes) ⇒ Object
- #run_conditional_check(condition, matching: true) ⇒ Object
- #should_audit? ⇒ Boolean
-
#with_auditing(&block) ⇒ Object
Temporarily enable auditing.
-
#without_auditing(&block) ⇒ Object
Temporarily disable auditing.
Instance Method Details
#active_audits ⇒ Object
Get active audits (excludes combined ones - those with empty changes) Filters in Ruby for database-agnostic behavior
887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 |
# File 'lib/active_version/audits/has_audits.rb', line 887 def active_audits changes_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :changes) return audits.to_a unless audit_class.column_names.include?(changes_column.to_s) audits.to_a.reject do |audit| # Check raw column value first (before JSON parsing) # Combined audits have their changes set to "{}" (empty JSON object as string) raw_changes = audit.read_attribute(changes_column) # If raw value is "{}", it's a combined audit if raw_changes.is_a?(String) && raw_changes.strip == "{}" true else # Otherwise check parsed value changes = audit.audited_changes changes.nil? || (changes.is_a?(Hash) && changes.empty?) || (changes.is_a?(String) && changes.strip.empty?) end end end |
#active_version_audit_identity_columns ⇒ Object
844 845 846 847 848 |
# File 'lib/active_version/audits/has_audits.rb', line 844 def active_version_audit_identity_columns auditable_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :auditable).to_s configured = self.class. && self.class.[:identity_columns] Array(configured.presence || "#{auditable_column}_id").map(&:to_s) end |
#active_version_audit_identity_map ⇒ Object
850 851 852 853 854 855 856 857 858 859 860 861 862 |
# File 'lib/active_version/audits/has_audits.rb', line 850 def active_version_audit_identity_map columns = active_version_audit_identity_columns values = active_version_audit_identity_values case values when Hash values.transform_keys(&:to_s).slice(*columns) when Array columns.zip(values).to_h else {columns.first => values} end end |
#active_version_audit_identity_values ⇒ Object
864 865 866 867 868 869 870 871 872 873 874 875 876 |
# File 'lib/active_version/audits/has_audits.rb', line 864 def active_version_audit_identity_values resolver = self.class. && self.class.[:identity_resolver] return default_audit_identity_values if resolver.nil? case resolver when Proc resolver.arity.zero? ? instance_exec(&resolver) : resolver.call(self) when Array resolver.map { |column| public_send(column) } else public_send(resolver) end end |
#active_version_auditable_id_value ⇒ Object
836 837 838 839 840 841 842 |
# File 'lib/active_version/audits/has_audits.rb', line 836 def active_version_auditable_id_value values = active_version_audit_identity_values return values.values.first if values.is_a?(Hash) && values.size == 1 return values.first if values.is_a?(Array) && values.size == 1 values end |
#audit_revision(version: nil) ⇒ Object
Get revision at specific version (from audits) This method is separate from HasRevisions#revision to avoid conflicts
646 647 648 649 650 651 652 653 654 655 |
# File 'lib/active_version/audits/has_audits.rb', line 646 def audit_revision(version: nil) return nil unless version # Get all audits up to and including the specified version version_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :version) audits_list = audits.where("#{version_column} <= ?", version).order(version_column => :asc).to_a return nil if audits_list.empty? self.class.revision_with audit_class.reconstruct_attributes(audits_list), id: id end |
#audit_revision_at(date_or_time) ⇒ Object
Get revision at specific time (from audits) This method is separate from HasRevisions#revision_at to avoid conflicts
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 |
# File 'lib/active_version/audits/has_audits.rb', line 659 def audit_revision_at(date_or_time) time_obj = ActiveVersion.parse_time_to_time(date_or_time) # Always raise error for future times raise ActiveVersion::FutureTimeError, "Future state cannot be known" if time_obj.future? version_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :version) audits_list = audits.up_until(time_obj).order(version_column => :asc).to_a # If no audits found for the time, return the earliest audit if it exists (for times before creation) if audits_list.empty? earliest_audit = audits.order(version_column => :asc).first return nil unless earliest_audit audits_list = [earliest_audit] end self.class.revision_with audit_class.reconstruct_attributes(audits_list), id: id end |
#audit_sql(destroy: false) ⇒ Object
Generate SQL for single audit insert
677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 |
# File 'lib/active_version/audits/has_audits.rb', line 677 def audit_sql(destroy: false) # Allow SQL generation even if no changes (for testing/documentation purposes) # In production, this would typically only be called when there are changes action = if new_record? "create" elsif destroy "destroy" else "update" end attrs = { action: action, audited_changes: audited_changes, comment: audit_comment } attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil? # Build attributes for SQL generation (avoid dangerous attribute error) changes_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :changes) context_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :context) auditable_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :auditable) version_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :version) comment_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :comment) # Build changes hash manually to avoid dangerous attribute error changes = { action: attrs[:action] } if audit_class.column_names.include?(changes_column.to_s) changes[changes_column] = attrs[:audited_changes] elsif [:storage].to_sym == :mirror_columns audited_attributes.each do |attr, value| next unless audit_class.column_names.include?(attr.to_s) changes[attr.to_sym] = value end end changes[comment_column] = attrs[:comment] if attrs[:comment].present? changes[context_column] = attrs[:audited_context] if attrs[:audited_context].present? changes.merge!(active_version_audit_identity_map) changes["#{auditable_column}_type"] = self.class.name changes[version_column] = (attrs[:action] == "create") ? 1 : (audits.maximum(version_column) || 0) + 1 changes[:created_at] = Time.current changes[:updated_at] = Time.current # Prepare SQL-safe values changes = prepare_sql_values(changes) changes["created_at"] ||= Time.current # Build SQL using Arel stmt = Arel::InsertManager.new table = Arel::Table.new(audit_class.table_name) stmt.into(table) changes.keys.each { |key| stmt.columns << table[key] } stmt.values = stmt.create_values(changes.values) sql = stmt.to_sql # Instrument SQL generation ActiveVersion::Instrumentation.instrument_audit_sql_generated(self, sql) sql end |
#auditing_enabled ⇒ Object
797 798 799 |
# File 'lib/active_version/audits/has_audits.rb', line 797 def auditing_enabled should_audit? end |
#audits ⇒ Object
Override audits method to handle dynamically created classes Uses class_name from options if provided Returns standard ActiveRecord relation Use active_audits for filtered results
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 |
# File 'lib/active_version/audits/has_audits.rb', line 809 def audits # Use class_name from options if provided (for dynamically created classes) auditable_type = [:class_name] || self.class.name if auditable_type.nil? raise ConfigurationError, "Cannot determine class name for dynamically created class. Please specify class_name option in has_audits (e.g., has_audits as: PostAudit, class_name: 'Post')" end uses_custom_auditable_id = [:identity_resolver].present? || Array(active_version_audit_identity_columns).length > 1 audit_klass = if !uses_custom_auditable_id && self.class.reflect_on_association(:audits) association(:audits).klass else self.class.audit_class end audit_klass ||= self.class.send(:resolve_audit_class_option, [:as]) if self.class.respond_to?(:resolve_audit_class_option, true) raise ConfigurationError, "No audit class configured for #{self.class.name}" unless audit_klass if !uses_custom_auditable_id && auditable_type == self.class.name && self.class.reflect_on_association(:audits) return super end auditable_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :auditable) audit_klass.where({"#{auditable_column}_type" => auditable_type}.merge(active_version_audit_identity_map)) end |
#clear_rolled_back_audits ⇒ Object
801 802 803 |
# File 'lib/active_version/audits/has_audits.rb', line 801 def clear_rolled_back_audits association(:audits).reset if association_cached?(:audits) end |
#default_audit_identity_values ⇒ Object
878 879 880 881 882 883 |
# File 'lib/active_version/audits/has_audits.rb', line 878 def default_audit_identity_values columns = active_version_audit_identity_columns return id if columns.one? Array(self.class.primary_key).map { |column| self[column] } end |
#own_and_associated_audits ⇒ Object
Get own and associated audits
743 744 745 746 747 |
# File 'lib/active_version/audits/has_audits.rb', line 743 def own_and_associated_audits audit_class.unscoped.where(auditable: self) .or(audit_class.unscoped.where(associated: self)) .order(created_at: :desc) end |
#revision_with(attributes) ⇒ Object
915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 |
# File 'lib/active_version/audits/has_audits.rb', line 915 def revision_with(attributes) # Create a new instance with reconstructed attributes # Keep it as a new record to prevent database reads attrs_to_assign = attributes.except(:audit_version).stringify_keys revision = self.class.new(attrs_to_assign) # Set id but keep as new_record to prevent database reads revision.id = id revision.instance_variable_set(:@new_record, true) revision.instance_variable_set(:@persisted, false) # Mark as readonly to prevent modifications revision.readonly! revision end |
#run_conditional_check(condition, matching: true) ⇒ Object
907 908 909 910 911 912 913 |
# File 'lib/active_version/audits/has_audits.rb', line 907 def run_conditional_check(condition, matching: true) return true if condition.blank? return condition.call(self) == matching if condition.respond_to?(:call) return send(condition) == matching if respond_to?(condition.to_sym, true) true end |
#should_audit? ⇒ Boolean
783 784 785 786 787 788 789 790 791 792 793 794 795 |
# File 'lib/active_version/audits/has_audits.rb', line 783 def should_audit? # Check class-level enabled state return false unless self.class.class_auditing_enabled? # Check global enabled state return false unless ActiveVersion.auditing_enabled # Check if/unless conditions return false unless run_conditional_check([:if]) return false unless run_conditional_check([:unless], matching: false) true end |
#with_auditing(&block) ⇒ Object
Temporarily enable auditing
755 756 757 |
# File 'lib/active_version/audits/has_audits.rb', line 755 def with_auditing(&block) self.class.with_auditing(&block) end |
#without_auditing(&block) ⇒ Object
Temporarily disable auditing
750 751 752 |
# File 'lib/active_version/audits/has_audits.rb', line 750 def without_auditing(&block) self.class.without_auditing(&block) end |