# frozen_string_literal: truerequire"set"moduleAudited# Audit saves the changes to ActiveRecord models. It has the following attributes:## * <tt>auditable</tt>: the ActiveRecord model that was changed# * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model# * <tt>action</tt>: one of create, update, or delete# * <tt>audited_changes</tt>: a hash of all the changes# * <tt>comment</tt>: a comment set with the audit# * <tt>version</tt>: the version of the model# * <tt>request_uuid</tt>: a uuid based that allows audits from the same controller request# * <tt>created_at</tt>: Time that the change was performed#classYAMLIfTextColumnTypeclass<<selfdefload(obj)iftext_column?ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)elseobjendenddefdump(obj)iftext_column?ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)elseobjendenddeftext_column?Audited.audit_model.columns_hash["audited_changes"].type.to_s=="text"endendendclassAudit<::ActiveRecord::Basebelongs_to:auditable,polymorphic: truebelongs_to:user,polymorphic: truebelongs_to:associated,polymorphic: truebefore_create:set_version_number,:set_audit_user,:set_request_uuid,:set_remote_addresscattr_accessor:audited_class_namesself.audited_class_names=Set.newserialize:audited_changes,YAMLIfTextColumnTypescope:ascending,->{reorder(version: :asc)}scope:descending,->{reorder(version: :desc)}scope:creates,->{where(action: "create")}scope:updates,->{where(action: "update")}scope:destroys,->{where(action: "destroy")}scope:up_until,->(date_or_time){where("created_at <= ?",date_or_time)}scope:from_version,->(version){where("version >= ?",version)}scope:to_version,->(version){where("version <= ?",version)}scope:auditable_finder,->(auditable_id,auditable_type){where(auditable_id: auditable_id,auditable_type: auditable_type)}# Return all audits older than the current one.defancestorsself.class.ascending.auditable_finder(auditable_id,auditable_type).to_version(version)end# Return an instance of what the object looked like at this revision. If# the object has been destroyed, this will be a new record.defrevisionclazz=auditable_type.constantize(clazz.find_by_id(auditable_id)||clazz.new).tapdo|m|self.class.assign_revision_attributes(m,self.class.reconstruct_attributes(ancestors).merge(audit_version: version))endend# Returns a hash of the changed attributes with the new valuesdefnew_attributes(audited_changes||{}).each_with_object({}.with_indifferent_access)do|(attr,values),attrs|attrs[attr]=(action=="update")?values.last:valuesendend# Returns a hash of the changed attributes with the old valuesdefold_attributes(audited_changes||{}).each_with_object({}.with_indifferent_access)do|(attr,values),attrs|attrs[attr]=(action=="update")?values.first:valuesendend# Allows user to undo changesdefundocaseactionwhen"create"# destroys a newly created recordauditable.destroy!when"destroy"# creates a new record with the destroyed record attributesauditable_type.constantize.create!(audited_changes)when"update"# changes back attributesauditable.update!(audited_changes.transform_values(&:first))elseraiseStandardError,"invalid action given #{action}"endend# Allows user to be set to either a string or an ActiveRecord object# @privatedefuser_as_string=(user)# reset both either wayself.user_as_model=self.username=niluser.is_a?(::ActiveRecord::Base)?self.user_as_model=user:self.username=userendalias_method:user_as_model=,:user=alias_method:user=,:user_as_string=# @privatedefuser_as_stringuser_as_model||usernameendalias_method:user_as_model,:useralias_method:user,:user_as_string# Returns the list of classes that are being auditeddefself.audited_classesaudited_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.defself.as_user(user)last_audited_user=::Audited.store[:audited_user]::Audited.store[:audited_user]=useryieldensure::Audited.store[:audited_user]=last_audited_userend# @privatedefself.reconstruct_attributes(audits)audits.each_with_object({})do|audit,all|all.merge!(audit.new_attributes)all[:audit_version]=audit.versionendend# @privatedefself.assign_revision_attributes(record,attributes)attributes.eachdo|attr,val|record=record.dupifrecord.frozen?ifrecord.respond_to?("#{attr}=")record.attributes.key?(attr.to_s)?record[attr]=val:record.send("#{attr}=",val)endendrecordend# use created_at as timestamp cache keydefself.collection_cache_key(collection=all,*)super(collection,:created_at)endprivatedefset_version_numberifaction=="create"self.version=1elsecollection=(Rails::VERSION::MAJOR>=6)?self.class.unscoped:self.classmax=collection.auditable_finder(auditable_id,auditable_type).maximum(:version)||0self.version=max+1endenddefset_audit_userself.user||=::Audited.store[:audited_user]# from .as_userself.user||=::Audited.store[:current_user].try!(:call)# from Sweepernil# prevent stopping callback chainsenddefset_request_uuidself.request_uuid||=::Audited.store[:current_request_uuid]self.request_uuid||=SecureRandom.uuidenddefset_remote_addressself.remote_address||=::Audited.store[:current_remote_address]endendend