paper-trail-10.3.0/0000755000175000017500000000000013467704405013361 5ustar samyaksamyakpaper-trail-10.3.0/lib/0000755000175000017500000000000013467704405014127 5ustar samyaksamyakpaper-trail-10.3.0/lib/generators/0000755000175000017500000000000013467704405016300 5ustar samyaksamyakpaper-trail-10.3.0/lib/generators/paper_trail/0000755000175000017500000000000013467704405020602 5ustar samyaksamyakpaper-trail-10.3.0/lib/generators/paper_trail/update_item_subtype/0000755000175000017500000000000013467704405024655 5ustar samyaksamyakpaper-trail-10.3.0/lib/generators/paper_trail/update_item_subtype/templates/0000755000175000017500000000000013467704405026653 5ustar samyaksamyak././@LongLink0000644000000000000000000000016400000000000011604 Lustar rootrootpaper-trail-10.3.0/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erbpaper-trail-10.3.0/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item0000644000175000017500000000660413467704405033702 0ustar samyaksamyak# This migration updates existing `versions` that have `item_type` that refers to # the base_class, and changes them to refer to the subclass instead. class UpdateVersionsForItemSubtype < ActiveRecord::Migration<%= migration_version %> include ActionView::Helpers::TextHelper def up <%= # Returns class, column, range def self.parse_custom_entry(text) parts = text.split("):") range = parts.last.split("..").map(&:to_i) range = Range.new(range.first, range.last) parts.first.split("(") + [range] end # Running: # rails g paper_trail:update_item_subtype Animal(species):1..4 Plant(genus):42..1337 # results in: # # Versions of item_type "Animal" with IDs between 1 and 4 will be updated based on `species` # # Versions of item_type "Plant" with IDs between 42 and 1337 will be updated based on `genus` # hints = {"Animal"=>{1..4=>"species"}, "Plant"=>{42..1337=>"genus"}} hint_descriptions = "" hints = args.inject(Hash.new{|h, k| h[k] = {}}) do |s, v| klass, column, range = parse_custom_entry(v) hint_descriptions << " # Versions of item_type \"#{klass}\" with IDs between #{ range.first} and #{range.last} will be updated based on \`#{column}\`\n" s[klass][range] = column s end unless hints.empty? "#{hint_descriptions} hints = #{hints.inspect}\n" end %> # Find all ActiveRecord models mentioned in existing versions changes = Hash.new { |h, k| h[k] = [] } model_names = PaperTrail::Version.select(:item_type).distinct model_names.map(&:item_type).each do |model_name| hint = hints[model_name] if defined?(hints) begin klass = model_name.constantize # Actually implements an inheritance_column? (Usually "type") has_inheritance_column = klass.columns.map(&:name).include?(klass.inheritance_column) # Find domain of types stored in PaperTrail versions PaperTrail::Version.where(item_type: model_name, item_subtype: nil).select(:id, :object, :object_changes).each do |obj| if (object_detail = PaperTrail.serializer.load(obj.object || obj.object_changes)) is_found = false subtype_name = nil hint&.each do |k, v| if k === obj.id && (subtype_name = object_detail[v]) break end end if subtype_name.nil? && has_inheritance_column subtype_name = object_detail[klass.inheritance_column] end if subtype_name subtype_name = subtype_name.last if subtype_name.is_a?(Array) if subtype_name != model_name changes[subtype_name] << obj.id end end end end rescue NameError => ex say "Skipping reference to #{model_name}", subitem: true end end changes.each do |k, v| # Update in blocks of up to 100 at a time block_of_ids = [] id_count = 0 num_updated = 0 v.sort.each do |id| block_of_ids << id if (id_count += 1) % 100 == 0 num_updated += PaperTrail::Version.where(id: block_of_ids).update_all(item_subtype: k) block_of_ids = [] end end num_updated += PaperTrail::Version.where(id: block_of_ids).update_all(item_subtype: k) if num_updated > 0 say "Associated #{pluralize(num_updated, 'record')} to #{k}", subitem: true end end end end paper-trail-10.3.0/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb0000644000175000017500000000100713467704405033321 0ustar samyaksamyak# frozen_string_literal: true require_relative "../migration_generator" module PaperTrail # Updates STI entries for PaperTrail class UpdateItemSubtypeGenerator < MigrationGenerator source_root File.expand_path("templates", __dir__) desc "Generates (but does not run) a migration to update item_subtype for STI entries in an "\ "existing versions table." def create_migration_file add_paper_trail_migration("update_versions_for_item_subtype", sti_type_options: options) end end end paper-trail-10.3.0/lib/generators/paper_trail/update_item_subtype/USAGE0000644000175000017500000000027613467704405025451 0ustar samyaksamyakDescription: Generates (but does not run) a migration to update item_type for STI entries in an existing versions table. See section 5.c. Generators in README.md for more information. paper-trail-10.3.0/lib/generators/paper_trail/install/0000755000175000017500000000000013467704405022250 5ustar samyaksamyakpaper-trail-10.3.0/lib/generators/paper_trail/install/templates/0000755000175000017500000000000013467704405024246 5ustar samyaksamyakpaper-trail-10.3.0/lib/generators/paper_trail/install/templates/create_versions.rb.erb0000644000175000017500000000304513467704405030537 0ustar samyaksamyak# This migration creates the `versions` table, the only schema PT requires. # All other migrations PT provides are optional. class CreateVersions < ActiveRecord::Migration<%= migration_version %> # The largest text column available in all supported RDBMS is # 1024^3 - 1 bytes, roughly one gibibyte. We specify a size # so that MySQL will use `longtext` instead of `text`. Otherwise, # when serializing very large objects, `text` might not be big enough. TEXT_BYTES = 1_073_741_823 def change create_table :versions<%= versions_table_options %> do |t| t.string :item_type<%= item_type_options %> t.integer :item_id, null: false, limit: 8 t.string :event, null: false t.string :whodunnit t.text :object, limit: TEXT_BYTES # Known issue in MySQL: fractional second precision # ------------------------------------------------- # # MySQL timestamp columns do not support fractional seconds unless # defined with "fractional seconds precision". MySQL users should manually # add fractional seconds precision to this migration, specifically, to # the `created_at` column. # (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html) # # MySQL users should also upgrade to at least rails 4.2, which is the first # version of ActiveRecord with support for fractional seconds in MySQL. # (https://github.com/rails/rails/pull/14359) # t.datetime :created_at end add_index :versions, %i(item_type item_id) end end ././@LongLink0000644000000000000000000000014600000000000011604 Lustar rootrootpaper-trail-10.3.0/lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.erbpaper-trail-10.3.0/lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.er0000644000175000017500000000073113467704405033401 0ustar samyaksamyak# This migration adds the optional `object_changes` column, in which PaperTrail # will store the `changes` diff for each update event. See the readme for # details. class AddObjectChangesToVersions < ActiveRecord::Migration<%= migration_version %> # The largest text column available in all supported RDBMS. # See `create_versions.rb` for details. TEXT_BYTES = 1_073_741_823 def change add_column :versions, :object_changes, :text, limit: TEXT_BYTES end end paper-trail-10.3.0/lib/generators/paper_trail/install/install_generator.rb0000644000175000017500000000465313467704405026321 0ustar samyaksamyak# frozen_string_literal: true require_relative "../migration_generator" module PaperTrail # Installs PaperTrail in a rails app. class InstallGenerator < MigrationGenerator # Class names of MySQL adapters. # - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`. # - `Mysql2Adapter` - Used by `mysql2` gem. MYSQL_ADAPTERS = [ "ActiveRecord::ConnectionAdapters::MysqlAdapter", "ActiveRecord::ConnectionAdapters::Mysql2Adapter" ].freeze source_root File.expand_path("templates", __dir__) class_option( :with_changes, type: :boolean, default: false, desc: "Store changeset (diff) with each version" ) desc "Generates (but does not run) a migration to add a versions table." \ " See section 5.c. Generators in README.md for more information." def create_migration_file add_paper_trail_migration("create_versions", item_type_options: item_type_options, versions_table_options: versions_table_options) add_paper_trail_migration("add_object_changes_to_versions") if options.with_changes? end private # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes. # See https://github.com/paper-trail-gem/paper_trail/issues/651 def item_type_options opt = { null: false } opt[:limit] = 191 if mysql? ", #{opt}" end def mysql? MYSQL_ADAPTERS.include?(ActiveRecord::Base.connection.class.name) end # Even modern versions of MySQL still use `latin1` as the default character # encoding. Many users are not aware of this, and run into trouble when they # try to use PaperTrail in apps that otherwise tend to use UTF-8. Postgres, by # comparison, uses UTF-8 except in the unusual case where the OS is configured # with a custom locale. # # - https://dev.mysql.com/doc/refman/5.7/en/charset-applications.html # - http://www.postgresql.org/docs/9.4/static/multibyte.html # # Furthermore, MySQL's original implementation of UTF-8 was flawed, and had # to be fixed later by introducing a new charset, `utf8mb4`. # # - https://mathiasbynens.be/notes/mysql-utf8mb4 # - https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html # def versions_table_options if mysql? ', { options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" }' else "" end end end end paper-trail-10.3.0/lib/generators/paper_trail/install/USAGE0000644000175000017500000000032113467704405023033 0ustar samyaksamyakDescription: Generates (but does not run) a migration to add a versions table. Also generates an initializer file for configuring PaperTrail. See section 5.c. Generators in README.md for more information. paper-trail-10.3.0/lib/generators/paper_trail/migration_generator.rb0000644000175000017500000000202513467704405025165 0ustar samyaksamyak# frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" module PaperTrail # Basic structure to support a generator that builds a migration class MigrationGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration def self.next_migration_number(dirname) ::ActiveRecord::Generators::Base.next_migration_number(dirname) end protected def add_paper_trail_migration(template, extra_options = {}) migration_dir = File.expand_path("db/migrate") if self.class.migration_exists?(migration_dir, template) ::Kernel.warn "Migration already exists: #{template}" else migration_template( "#{template}.rb.erb", "db/migrate/#{template}.rb", { migration_version: migration_version }.merge(extra_options) ) end end def migration_version major = ActiveRecord::VERSION::MAJOR if major >= 5 "[#{major}.#{ActiveRecord::VERSION::MINOR}]" end end end end paper-trail-10.3.0/lib/generators/paper_trail/install_generator.rb0000644000175000017500000000616113467704405024647 0ustar samyaksamyak# frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" module PaperTrail # Installs PaperTrail in a rails app. class InstallGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration # Class names of MySQL adapters. # - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`. # - `Mysql2Adapter` - Used by `mysql2` gem. MYSQL_ADAPTERS = [ "ActiveRecord::ConnectionAdapters::MysqlAdapter", "ActiveRecord::ConnectionAdapters::Mysql2Adapter" ].freeze source_root File.expand_path("templates", __dir__) class_option( :with_changes, type: :boolean, default: false, desc: "Store changeset (diff) with each version" ) desc "Generates (but does not run) a migration to add a versions table." def create_migration_file add_paper_trail_migration("create_versions") add_paper_trail_migration("add_object_changes_to_versions") if options.with_changes? end def self.next_migration_number(dirname) ::ActiveRecord::Generators::Base.next_migration_number(dirname) end protected def add_paper_trail_migration(template) migration_dir = File.expand_path("db/migrate") if self.class.migration_exists?(migration_dir, template) ::Kernel.warn "Migration already exists: #{template}" else migration_template( "#{template}.rb.erb", "db/migrate/#{template}.rb", item_type_options: item_type_options, migration_version: migration_version, versions_table_options: versions_table_options ) end end private # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes. # See https://github.com/paper-trail-gem/paper_trail/issues/651 def item_type_options opt = { null: false } opt[:limit] = 191 if mysql? ", #{opt}" end def migration_version major = ActiveRecord::VERSION::MAJOR if major >= 5 "[#{major}.#{ActiveRecord::VERSION::MINOR}]" end end def mysql? MYSQL_ADAPTERS.include?(ActiveRecord::Base.connection.class.name) end # Even modern versions of MySQL still use `latin1` as the default character # encoding. Many users are not aware of this, and run into trouble when they # try to use PaperTrail in apps that otherwise tend to use UTF-8. Postgres, by # comparison, uses UTF-8 except in the unusual case where the OS is configured # with a custom locale. # # - https://dev.mysql.com/doc/refman/5.7/en/charset-applications.html # - http://www.postgresql.org/docs/9.4/static/multibyte.html # # Furthermore, MySQL's original implementation of UTF-8 was flawed, and had # to be fixed later by introducing a new charset, `utf8mb4`. # # - https://mathiasbynens.be/notes/mysql-utf8mb4 # - https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html # def versions_table_options if mysql? ', { options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" }' else "" end end end end paper-trail-10.3.0/lib/paper_trail.rb0000644000175000017500000001164013467704405016760 0ustar samyaksamyak# frozen_string_literal: true # AR does not require all of AS, but PT does. PT uses core_ext like # `String#squish`, so we require `active_support/all`. Instead of eagerly # loading all of AS here, we could put specific `require`s in only the various # PT files that need them, but this seems easier to troubleshoot, though it may # add a few milliseconds to rails boot time. If that becomes a pain point, we # can revisit this decision. require "active_support/all" # AR is required for, eg. has_paper_trail.rb, so we could put this `require` in # all of those files, but it seems easier to troubleshoot if we just make sure # AR is loaded here before loading *any* of PT. See discussion of # performance/simplicity tradeoff for activesupport above. require "active_record" require "request_store" require "paper_trail/cleaner" require "paper_trail/config" require "paper_trail/has_paper_trail" require "paper_trail/record_history" require "paper_trail/reifier" require "paper_trail/request" require "paper_trail/version_concern" require "paper_trail/version_number" require "paper_trail/serializers/json" require "paper_trail/serializers/yaml" # An ActiveRecord extension that tracks changes to your models, for auditing or # versioning. module PaperTrail E_RAILS_NOT_LOADED = <<-EOS.squish.freeze PaperTrail has been loaded too early, before rails is loaded. This can happen when another gem defines the ::Rails namespace, then PT is loaded, all before rails is loaded. You may want to reorder your Gemfile, or defer the loading of PT by using `require: false` and a manual require elsewhere. EOS E_TIMESTAMP_FIELD_CONFIG = <<-EOS.squish.freeze PaperTrail.timestamp_field= has been removed, without replacement. It is no longer configurable. The timestamp column in the versions table must now be named created_at. EOS extend PaperTrail::Cleaner class << self # Switches PaperTrail on or off, for all threads. # @api public def enabled=(value) PaperTrail.config.enabled = value end # Returns `true` if PaperTrail is on, `false` otherwise. This is the # on/off switch that affects all threads. Enabled by default. # @api public def enabled? !!PaperTrail.config.enabled end # Returns PaperTrail's `::Gem::Version`, convenient for comparisons. This is # recommended over `::PaperTrail::VERSION::STRING`. # # Added in 7.0.0 # # @api public def gem_version ::Gem::Version.new(VERSION::STRING) end # Set variables for the current request, eg. whodunnit. # # All request-level variables are now managed here, as of PT 9. Having the # word "request" right there in your application code will remind you that # these variables only affect the current request, not all threads. # # Given a block, temporarily sets the given `options`, executes the block, # and returns the value of the block. # # Without a block, this currently just returns `PaperTrail::Request`. # However, please do not use `PaperTrail::Request` directly. Currently, # `Request` is a `Module`, but in the future it is quite possible we may # make it a `Class`. If we make such a choice, we will not provide any # warning and will not treat it as a breaking change. You've been warned :) # # @api public def request(options = nil, &block) if options.nil? && !block_given? Request else Request.with(options, &block) end end # Set the field which records when a version was created. # @api public def timestamp_field=(_field_name) raise(E_TIMESTAMP_FIELD_CONFIG) end # Set the PaperTrail serializer. This setting affects all threads. # @api public def serializer=(value) PaperTrail.config.serializer = value end # Get the PaperTrail serializer used by all threads. # @api public def serializer PaperTrail.config.serializer end # Returns PaperTrail's global configuration object, a singleton. These # settings affect all threads. # @api private def config @config ||= PaperTrail::Config.instance yield @config if block_given? @config end alias configure config def version VERSION::STRING end end end # We use the `on_load` "hook" instead of `ActiveRecord::Base.include` because we # don't want to cause all of AR to be autloaded yet. See # https://guides.rubyonrails.org/engines.html#what-are-on-load-hooks-questionmark # to learn more about `on_load`. ActiveSupport.on_load(:active_record) do include PaperTrail::Model end # Require frameworks if defined?(::Rails) # Rails module is sometimes defined by gems like rails-html-sanitizer # so we check for presence of Rails.application. if defined?(::Rails.application) require "paper_trail/frameworks/rails" else ::Kernel.warn(::PaperTrail::E_RAILS_NOT_LOADED) end else require "paper_trail/frameworks/active_record" end paper-trail-10.3.0/lib/paper_trail/0000755000175000017500000000000013467704405016431 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/queries/0000755000175000017500000000000013467704405020106 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/queries/versions/0000755000175000017500000000000013467704405021756 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/queries/versions/where_object_changes.rb0000644000175000017500000000475013467704405026441 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail module Queries module Versions # For public API documentation, see `where_object_changes` in # `paper_trail/version_concern.rb`. # @api private class WhereObjectChanges # - version_model_class - The class that VersionConcern was mixed into. # - attributes - A `Hash` of attributes and values. See the public API # documentation for details. # @api private def initialize(version_model_class, attributes) @version_model_class = version_model_class # Currently, this `deep_dup` is necessary because the `jsonb` branch # modifies `@attributes`, and that would be a nasty suprise for # consumers of this class. # TODO: Stop modifying `@attributes`, then remove `deep_dup`. @attributes = attributes.deep_dup end # @api private def execute if PaperTrail.config.object_changes_adapter&.respond_to?(:where_object_changes) return PaperTrail.config.object_changes_adapter.where_object_changes( @version_model_class, @attributes ) end case @version_model_class.columns_hash["object_changes"].type when :jsonb jsonb when :json json else text end end private # @api private def json predicates = [] values = [] @attributes.each do |field, value| predicates.push( "((object_changes->>? ILIKE ?) OR (object_changes->>? ILIKE ?))" ) values.concat([field, "[#{value.to_json},%", field, "[%,#{value.to_json}]%"]) end sql = predicates.join(" and ") @version_model_class.where(sql, *values) end # @api private def jsonb @attributes.each { |field, value| @attributes[field] = [value] } @version_model_class.where("object_changes @> ?", @attributes.to_json) end # @api private def text arel_field = @version_model_class.arel_table[:object_changes] where_conditions = @attributes.map { |field, value| ::PaperTrail.serializer.where_object_changes_condition(arel_field, field, value) } where_conditions = where_conditions.reduce { |a, e| a.and(e) } @version_model_class.where(where_conditions) end end end end end paper-trail-10.3.0/lib/paper_trail/queries/versions/where_object.rb0000644000175000017500000000353713467704405024753 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail module Queries module Versions # For public API documentation, see `where_object` in # `paper_trail/version_concern.rb`. # @api private class WhereObject # - version_model_class - The class that VersionConcern was mixed into. # - attributes - A `Hash` of attributes and values. See the public API # documentation for details. # @api private def initialize(version_model_class, attributes) @version_model_class = version_model_class @attributes = attributes end # @api private def execute column = @version_model_class.columns_hash["object"] raise "where_object can't be called without an object column" unless column case column.type when :jsonb jsonb when :json json else text end end private # @api private def json predicates = [] values = [] @attributes.each do |field, value| predicates.push "object->>? = ?" values.concat([field, value.to_s]) end sql = predicates.join(" and ") @version_model_class.where(sql, *values) end # @api private def jsonb @version_model_class.where("object @> ?", @attributes.to_json) end # @api private def text arel_field = @version_model_class.arel_table[:object] where_conditions = @attributes.map { |field, value| ::PaperTrail.serializer.where_object_condition(arel_field, field, value) } where_conditions = where_conditions.reduce { |a, e| a.and(e) } @version_model_class.where(where_conditions) end end end end end paper-trail-10.3.0/lib/paper_trail/version_concern.rb0000644000175000017500000002730713467704405022163 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/attribute_serializers/object_changes_attribute" require "paper_trail/queries/versions/where_object" require "paper_trail/queries/versions/where_object_changes" module PaperTrail # Originally, PaperTrail did not provide this module, and all of this # functionality was in `PaperTrail::Version`. That model still exists (and is # used by most apps) but by moving the functionality to this module, people # can include this concern instead of sub-classing the `Version` model. module VersionConcern extend ::ActiveSupport::Concern included do if ::ActiveRecord.gem_version >= Gem::Version.new("5.0") belongs_to :item, polymorphic: true, optional: true else belongs_to :item, polymorphic: true end validates_presence_of :event after_create :enforce_version_limit! end # :nodoc: module ClassMethods def item_subtype_column_present? column_names.include?("item_subtype") end def with_item_keys(item_type, item_id) where item_type: item_type, item_id: item_id end def creates where event: "create" end def updates where event: "update" end def destroys where event: "destroy" end def not_creates where "event <> ?", "create" end def between(start_time, end_time) where( arel_table[:created_at].gt(start_time). and(arel_table[:created_at].lt(end_time)) ).order(timestamp_sort_order) end # Defaults to using the primary key as the secondary sort order if # possible. def timestamp_sort_order(direction = "asc") [arel_table[:created_at].send(direction.downcase)].tap do |array| array << arel_table[primary_key].send(direction.downcase) if primary_key_is_int? end end # Given a hash of attributes like `name: 'Joan'`, query the # `versions.objects` column. # # ``` # SELECT "versions".* # FROM "versions" # WHERE ("versions"."object" LIKE '% # name: Joan # %') # ``` # # This is useful for finding versions where a given attribute had a given # value. Imagine, in the example above, that Joan had changed her name # and we wanted to find the versions before that change. # # Based on the data type of the `object` column, the appropriate SQL # operator is used. For example, a text column will use `like`, and a # jsonb column will use `@>`. # # @api public def where_object(args = {}) raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash) Queries::Versions::WhereObject.new(self, args).execute end # Given a hash of attributes like `name: 'Joan'`, query the # `versions.objects_changes` column. # # ``` # SELECT "versions".* # FROM "versions" # WHERE .. ("versions"."object_changes" LIKE '% # name: # - Joan # %' OR "versions"."object_changes" LIKE '% # name: # -% # - Joan # %') # ``` # # This is useful for finding versions immediately before and after a given # attribute had a given value. Imagine, in the example above, that someone # changed their name to Joan and we wanted to find the versions # immediately before and after that change. # # Based on the data type of the `object` column, the appropriate SQL # operator is used. For example, a text column will use `like`, and a # jsonb column will use `@>`. # # @api public def where_object_changes(args = {}) raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash) Queries::Versions::WhereObjectChanges.new(self, args).execute end def primary_key_is_int? @primary_key_is_int ||= columns_hash[primary_key].type == :integer rescue StandardError # TODO: Rescue something more specific true end # Returns whether the `object` column is using the `json` type supported # by PostgreSQL. def object_col_is_json? %i[json jsonb].include?(columns_hash["object"].type) end # Returns whether the `object_changes` column is using the `json` type # supported by PostgreSQL. def object_changes_col_is_json? %i[json jsonb].include?(columns_hash["object_changes"].try(:type)) end # Returns versions before `obj`. # # @param obj - a `Version` or a timestamp # @param timestamp_arg - boolean - When true, `obj` is a timestamp. # Default: false. # @return `ActiveRecord::Relation` # @api public def preceding(obj, timestamp_arg = false) if timestamp_arg != true && primary_key_is_int? preceding_by_id(obj) else preceding_by_timestamp(obj) end end # Returns versions after `obj`. # # @param obj - a `Version` or a timestamp # @param timestamp_arg - boolean - When true, `obj` is a timestamp. # Default: false. # @return `ActiveRecord::Relation` # @api public def subsequent(obj, timestamp_arg = false) if timestamp_arg != true && primary_key_is_int? subsequent_by_id(obj) else subsequent_by_timestamp(obj) end end private # @api private def preceding_by_id(obj) where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc) end # @api private def preceding_by_timestamp(obj) obj = obj.send(:created_at) if obj.is_a?(self) where(arel_table[:created_at].lt(obj)). order(timestamp_sort_order("desc")) end # @api private def subsequent_by_id(version) where(arel_table[primary_key].gt(version.id)).order(arel_table[primary_key].asc) end # @api private def subsequent_by_timestamp(obj) obj = obj.send(:created_at) if obj.is_a?(self) where(arel_table[:created_at].gt(obj)).order(timestamp_sort_order) end end # @api private def object_deserialized if self.class.object_col_is_json? object else PaperTrail.serializer.load(object) end end # Restore the item from this version. # # Optionally this can also restore all :has_one and :has_many (including # has_many :through) associations as they were "at the time", if they are # also being versioned by PaperTrail. # # Options: # # - :has_one # - `true` - Also reify has_one associations. # - `false - Default. # - :has_many # - `true` - Also reify has_many and has_many :through associations. # - `false` - Default. # - :mark_for_destruction # - `true` - Mark the has_one/has_many associations that did not exist in # the reified version for destruction, instead of removing them. # - `false` - Default. Useful for persisting the reified version. # - :dup # - `false` - Default. # - `true` - Always create a new object instance. Useful for # comparing two versions of the same object. # - :unversioned_attributes # - `:nil` - Default. Attributes undefined in version record are set to # nil in reified record. # - `:preserve` - Attributes undefined in version record are not modified. # def reify(options = {}) unless self.class.column_names.include? "object" raise "reify can't be called without an object column" end return nil if object.nil? ::PaperTrail::Reifier.reify(self, options) end # Returns what changed in this version of the item. # `ActiveModel::Dirty#changes`. returns `nil` if your `versions` table does # not have an `object_changes` text column. def changeset return nil unless self.class.column_names.include? "object_changes" @changeset ||= load_changeset end # Returns who put the item into the state stored in this version. def paper_trail_originator @paper_trail_originator ||= previous.try(:whodunnit) end # Returns who changed the item from the state it had in this version. This # is an alias for `whodunnit`. def terminator @terminator ||= whodunnit end alias version_author terminator def sibling_versions(reload = false) if reload || !defined?(@sibling_versions) || @sibling_versions.nil? @sibling_versions = self.class.with_item_keys(item_type, item_id) end @sibling_versions end def next @next ||= sibling_versions.subsequent(self).first end def previous @previous ||= sibling_versions.preceding(self).first end # Returns an integer representing the chronological position of the # version among its siblings (see `sibling_versions`). The "create" event, # for example, has an index of 0. # @api public def index @index ||= RecordHistory.new(sibling_versions, self.class).index(self) end private # @api private def load_changeset if PaperTrail.config.object_changes_adapter&.respond_to?(:load_changeset) return PaperTrail.config.object_changes_adapter.load_changeset(self) end # First, deserialize the `object_changes` column. changes = HashWithIndifferentAccess.new(object_changes_deserialized) # The next step is, perhaps unfortunately, called "de-serialization", # and appears to be responsible for custom attribute serializers. For an # example of a custom attribute serializer, see # `Person::TimeZoneSerializer` in the test suite. # # Is `item.class` good enough? Does it handle `inheritance_column` # as well as `Reifier#version_reification_class`? We were using # `item_type.constantize`, but that is problematic when the STI parent # is not versioned. (See `Vehicle` and `Car` in the test suite). # # Note: `item` returns nil if `event` is "destroy". unless item.nil? AttributeSerializers::ObjectChangesAttribute. new(item.class). deserialize(changes) end # Finally, return a Hash mapping each attribute name to # a two-element array representing before and after. changes end # If the `object_changes` column is a Postgres JSON column, then # ActiveRecord will deserialize it for us. Otherwise, it's a string column # and we must deserialize it ourselves. # @api private def object_changes_deserialized if self.class.object_changes_col_is_json? object_changes else begin PaperTrail.serializer.load(object_changes) rescue StandardError # TODO: Rescue something more specific {} end end end # Enforces the `version_limit`, if set. Default: no limit. # @api private def enforce_version_limit! limit = version_limit return unless limit.is_a? Numeric previous_versions = sibling_versions.not_creates. order(self.class.timestamp_sort_order("asc")) return unless previous_versions.size > limit excess_versions = previous_versions - previous_versions.last(limit) excess_versions.map(&:destroy) end # See docs section 2.e. Limiting the Number of Versions Created. # The version limit can be global or per-model. # # @api private # # TODO: Duplication: similar `constantize` in Reifier#version_reification_class def version_limit if self.class.item_subtype_column_present? klass = (item_subtype || item_type).constantize if klass&.paper_trail_options&.key?(:limit) return klass.paper_trail_options[:limit] end end PaperTrail.config.version_limit end end end paper-trail-10.3.0/lib/paper_trail/type_serializers/0000755000175000017500000000000013467704405022026 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/type_serializers/postgres_array_serializer.rb0000644000175000017500000000237313467704405027655 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail module TypeSerializers # Provides an alternative method of serialization # and deserialization of PostgreSQL array columns. class PostgresArraySerializer def initialize(subtype, delimiter) @subtype = subtype @delimiter = delimiter end def serialize(array) return serialize_with_ar(array) if active_record_pre_502? array end def deserialize(array) return deserialize_with_ar(array) if active_record_pre_502? case array # Needed for legacy reasons. If serialized array is a string # then it was serialized with Rails < 5.0.2. when ::String then deserialize_with_ar(array) else array end end private def active_record_pre_502? ::ActiveRecord.gem_version < Gem::Version.new("5.0.2") end def serialize_with_ar(array) ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array. new(@subtype, @delimiter). serialize(array) end def deserialize_with_ar(array) ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array. new(@subtype, @delimiter). deserialize(array) end end end end paper-trail-10.3.0/lib/paper_trail/attribute_serializers/0000755000175000017500000000000013467704405023050 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/attribute_serializers/object_changes_attribute.rb0000644000175000017500000000240113467704405030413 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/attribute_serializers/cast_attribute_serializer" module PaperTrail module AttributeSerializers # Serialize or deserialize the `version.object_changes` column. class ObjectChangesAttribute def initialize(item_class) @item_class = item_class end def serialize(changes) alter(changes, :serialize) end def deserialize(changes) alter(changes, :deserialize) end private # Modifies `changes` in place. # TODO: Return a new hash instead. def alter(changes, serialization_method) # Don't serialize before values before inserting into columns of type # `JSON` on `PostgreSQL` databases. return changes if object_changes_col_is_json? serializer = CastAttributeSerializer.new(@item_class) changes.clone.each do |key, change| # `change` is an Array with two elements, representing before and after. changes[key] = Array(change).map do |value| serializer.send(serialization_method, key, value) end end end def object_changes_col_is_json? @item_class.paper_trail.version_class.object_changes_col_is_json? end end end end paper-trail-10.3.0/lib/paper_trail/attribute_serializers/object_attribute.rb0000644000175000017500000000215113467704405026725 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/attribute_serializers/cast_attribute_serializer" module PaperTrail module AttributeSerializers # Serialize or deserialize the `version.object` column. class ObjectAttribute def initialize(model_class) @model_class = model_class end def serialize(attributes) alter(attributes, :serialize) end def deserialize(attributes) alter(attributes, :deserialize) end private # Modifies `attributes` in place. # TODO: Return a new hash instead. def alter(attributes, serialization_method) # Don't serialize before values before inserting into columns of type # `JSON` on `PostgreSQL` databases. return attributes if object_col_is_json? serializer = CastAttributeSerializer.new(@model_class) attributes.each do |key, value| attributes[key] = serializer.send(serialization_method, key, value) end end def object_col_is_json? @model_class.paper_trail.version_class.object_col_is_json? end end end end paper-trail-10.3.0/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb0000644000175000017500000000555413467704405030654 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/attribute_serializers/attribute_serializer_factory" module PaperTrail # :nodoc: module AttributeSerializers # The `CastAttributeSerializer` (de)serializes model attribute values. For # example, the string "1.99" serializes into the integer `1` when assigned # to an attribute of type `ActiveRecord::Type::Integer`. # # This implementation depends on the `type_for_attribute` method, which was # introduced in rails 4.2. As of PT 8, we no longer support rails < 4.2. class CastAttributeSerializer def initialize(klass) @klass = klass end private # Returns a hash mapping attributes to hashes that map strings to # integers. Example: # # ``` # { "status" => { "draft"=>0, "published"=>1, "archived"=>2 } } # ``` # # ActiveRecord::Enum was added in AR 4.1 # http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums def defined_enums @defined_enums ||= (@klass.respond_to?(:defined_enums) ? @klass.defined_enums : {}) end end if ::ActiveRecord::VERSION::MAJOR >= 5 # This implementation uses AR 5's `serialize` and `deserialize`. class CastAttributeSerializer def serialize(attr, val) AttributeSerializerFactory.for(@klass, attr).serialize(val) end def deserialize(attr, val) if defined_enums[attr] && val.is_a?(::String) # Because PT 4 used to save the string version of enums to `object_changes` val else AttributeSerializerFactory.for(@klass, attr).deserialize(val) end end end else # This implementation uses AR 4.2's `type_cast_for_database`. For # versions of AR < 4.2 we provide an implementation of # `type_cast_for_database` in our shim attribute type classes, # `NoOpAttribute` and `SerializedAttribute`. class CastAttributeSerializer def serialize(attr, val) castable_val = val if defined_enums[attr] # `attr` is an enum. Find the number that corresponds to `val`. If `val` is # a number already, there won't be a corresponding entry, just use `val`. castable_val = defined_enums[attr][val] || val end @klass.type_for_attribute(attr).type_cast_for_database(castable_val) end def deserialize(attr, val) if defined_enums[attr] && val.is_a?(::String) # Because PT 4 used to save the string version of enums to `object_changes` val else val = @klass.type_for_attribute(attr).type_cast_from_database(val) if defined_enums[attr] defined_enums[attr].key(val) else val end end end end end end end paper-trail-10.3.0/lib/paper_trail/attribute_serializers/README.md0000644000175000017500000000057613467704405024337 0ustar samyaksamyakAttribute Serializers ===================== "Serialization" here refers to the preparation of data for insertion into a database, particularly the `object` and `object_changes` columns in the `versions` table. Likewise, "deserialization" refers to any processing of data after they have been read from the database, for example preparing the result of `VersionConcern#changeset`. paper-trail-10.3.0/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb0000644000175000017500000000160513467704405031362 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/type_serializers/postgres_array_serializer" module PaperTrail module AttributeSerializers # Values returned by some Active Record serializers are # not suited for writing JSON to a text column. This factory # replaces certain default Active Record serializers # with custom PaperTrail ones. module AttributeSerializerFactory AR_PG_ARRAY_CLASS = "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array" def self.for(klass, attr) active_record_serializer = klass.type_for_attribute(attr) if active_record_serializer.class.name == AR_PG_ARRAY_CLASS TypeSerializers::PostgresArraySerializer.new( active_record_serializer.subtype, active_record_serializer.delimiter ) else active_record_serializer end end end end end paper-trail-10.3.0/lib/paper_trail/model_config.rb0000644000175000017500000002171713467704405021413 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail # Configures an ActiveRecord model, mostly at application boot time, but also # sometimes mid-request, with methods like enable/disable. class ModelConfig E_CANNOT_RECORD_AFTER_DESTROY = <<-STR.strip_heredoc.freeze paper_trail.on_destroy(:after) is incompatible with ActiveRecord's belongs_to_required_by_default. Use on_destroy(:before) or disable belongs_to_required_by_default. STR E_HPT_ABSTRACT_CLASS = <<~STR.squish.freeze An application model (%s) has been configured to use PaperTrail (via `has_paper_trail`), but the version model it has been told to use (%s) is an `abstract_class`. This could happen when an advanced feature called Custom Version Classes (http://bit.ly/2G4ch0G) is misconfigured. When all version classes are custom, PaperTrail::Version is configured to be an `abstract_class`. This is fine, but all application models must be configured to use concrete (not abstract) version models. STR E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE = <<~STR.squish.freeze To use PaperTrail's per-model limit in your %s model, you must have an item_subtype column in your versions table. See documentation sections 2.e.1 Per-model limit, and 4.b.1 The optional item_subtype column. STR DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION = <<~STR.squish Passing versions association name as `has_paper_trail versions: %{versions_name}` is deprecated. Use `has_paper_trail versions: {name: %{versions_name}}` instead. The hash you pass to `versions:` is now passed directly to `has_many`. STR DPR_CLASS_NAME_OPTION = <<~STR.squish Passing Version class name as `has_paper_trail class_name: %{class_name}` is deprecated. Use `has_paper_trail versions: {class_name: %{class_name}}` instead. The hash you pass to `versions:` is now passed directly to `has_many`. STR def initialize(model_class) @model_class = model_class end # Adds a callback that records a version after a "create" event. # # @api public def on_create @model_class.after_create { |r| r.paper_trail.record_create if r.paper_trail.save_version? } return if @model_class.paper_trail_options[:on].include?(:create) @model_class.paper_trail_options[:on] << :create end # Adds a callback that records a version before or after a "destroy" event. # # @api public def on_destroy(recording_order = "before") unless %w[after before].include?(recording_order.to_s) raise ArgumentError, 'recording order can only be "after" or "before"' end if recording_order.to_s == "after" && cannot_record_after_destroy? raise E_CANNOT_RECORD_AFTER_DESTROY end @model_class.send( "#{recording_order}_destroy", lambda do |r| return unless r.paper_trail.save_version? r.paper_trail.record_destroy(recording_order) end ) return if @model_class.paper_trail_options[:on].include?(:destroy) @model_class.paper_trail_options[:on] << :destroy end # Adds a callback that records a version after an "update" event. # # @api public def on_update @model_class.before_save { |r| r.paper_trail.reset_timestamp_attrs_for_update_if_needed } @model_class.after_update { |r| if r.paper_trail.save_version? r.paper_trail.record_update( force: false, in_after_callback: true, is_touch: false ) end } @model_class.after_update { |r| r.paper_trail.clear_version_instance } return if @model_class.paper_trail_options[:on].include?(:update) @model_class.paper_trail_options[:on] << :update end # Adds a callback that records a version after a "touch" event. # @api public def on_touch @model_class.after_touch { |r| r.paper_trail.record_update( force: true, in_after_callback: true, is_touch: true ) } end # Set up `@model_class` for PaperTrail. Installs callbacks, associations, # "class attributes", instance methods, and more. # @api private def setup(options = {}) options[:on] ||= %i[create update destroy touch] options[:on] = Array(options[:on]) # Support single symbol @model_class.send :include, ::PaperTrail::Model::InstanceMethods setup_options(options) setup_associations(options) check_presence_of_item_subtype_column(options) @model_class.after_rollback { paper_trail.clear_rolled_back_versions } setup_callbacks_from_options options[:on] end def version_class @_version_class ||= @model_class.version_class_name.constantize end private def active_record_gem_version Gem::Version.new(ActiveRecord::VERSION::STRING) end # Raises an error if the provided class is an `abstract_class`. # @api private def assert_concrete_activerecord_class(class_name) if class_name.constantize.abstract_class? raise format(E_HPT_ABSTRACT_CLASS, @model_class, class_name) end end def cannot_record_after_destroy? Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5") && ::ActiveRecord::Base.belongs_to_required_by_default end # Some options require the presence of the `item_subtype` column. Currently # only `limit`, but in the future there may be others. # # @api private def check_presence_of_item_subtype_column(options) return unless options.key?(:limit) return if version_class.item_subtype_column_present? raise format(E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE, @model_class.name) end def check_version_class_name(options) # @api private - `version_class_name` @model_class.class_attribute :version_class_name if options[:class_name] ::ActiveSupport::Deprecation.warn( format( DPR_CLASS_NAME_OPTION, class_name: options[:class_name].inspect ), caller(1) ) options[:versions][:class_name] = options[:class_name] end @model_class.version_class_name = options[:versions][:class_name] || "PaperTrail::Version" assert_concrete_activerecord_class(@model_class.version_class_name) end def check_versions_association_name(options) # @api private - versions_association_name @model_class.class_attribute :versions_association_name @model_class.versions_association_name = options[:versions][:name] || :versions end def define_has_many_versions(options) options = ensure_versions_option_is_hash(options) check_version_class_name(options) check_versions_association_name(options) scope = get_versions_scope(options) @model_class.has_many( @model_class.versions_association_name, scope, class_name: @model_class.version_class_name, as: :item, **options[:versions].except(:name, :scope) ) end def ensure_versions_option_is_hash(options) unless options[:versions].is_a?(Hash) if options[:versions] ::ActiveSupport::Deprecation.warn( format( DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION, versions_name: options[:versions].inspect ), caller(1) ) end options[:versions] = { name: options[:versions] } end options end def get_versions_scope(options) options[:versions][:scope] || -> { order(model.timestamp_sort_order) } end def setup_associations(options) # @api private - version_association_name @model_class.class_attribute :version_association_name @model_class.version_association_name = options[:version] || :version # The version this instance was reified from. # @api public @model_class.send :attr_accessor, @model_class.version_association_name # @api public - paper_trail_event @model_class.send :attr_accessor, :paper_trail_event define_has_many_versions(options) end def setup_callbacks_from_options(options_on = []) options_on.each do |event| public_send("on_#{event}") end end def setup_options(options) # @api public - paper_trail_options - Let's encourage plugins to use eg. # `paper_trail_options[:versions][:class_name]` rather than # `version_class_name` because the former is documented and the latter is # not. @model_class.class_attribute :paper_trail_options @model_class.paper_trail_options = options.dup %i[ignore skip only].each do |k| @model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]]. flatten. compact. map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s } end @model_class.paper_trail_options[:meta] ||= {} end end end paper-trail-10.3.0/lib/paper_trail/config.rb0000644000175000017500000000435113467704405020226 0ustar samyaksamyak# frozen_string_literal: true require "singleton" require "paper_trail/serializers/yaml" module PaperTrail # Global configuration affecting all threads. Some thread-specific # configuration can be found in `paper_trail.rb`, others in `controller.rb`. class Config include Singleton E_PT_AT_REMOVED = <<-EOS.squish Association Tracking for PaperTrail has been extracted to a separate gem. To use it, please add `paper_trail-association_tracking` to your Gemfile. If you don't use it (most people don't, that's the default) and you set `track_associations = false` somewhere (probably a rails initializer) you can remove that line now. EOS attr_accessor( :association_reify_error_behaviour, :object_changes_adapter, :serializer, :version_limit, :has_paper_trail_defaults ) def initialize # Variables which affect all threads, whose access is synchronized. @mutex = Mutex.new @enabled = true # Variables which affect all threads, whose access is *not* synchronized. @serializer = PaperTrail::Serializers::YAML @has_paper_trail_defaults = {} end # Indicates whether PaperTrail is on or off. Default: true. def enabled @mutex.synchronize { !!@enabled } end def enabled=(enable) @mutex.synchronize { @enabled = enable } end # In PT 10, the paper_trail-association_tracking gem was changed from a # runtime dependency to a development dependency. We raise an error about # this for the people who don't read changelogs. # # We raise a generic RuntimeError instead of a specific PT error class # because there is no known use case where someone would want to rescue # this. If we think of such a use case in the future we can revisit this # decision. # # @override If PT-AT is `require`d, it will replace this method with its # own implementation. def track_associations=(value) if value raise E_PT_AT_REMOVED else ::Kernel.warn(E_PT_AT_REMOVED) end end # @override If PT-AT is `require`d, it will replace this method with its # own implementation. def track_associations? raise E_PT_AT_REMOVED end end end paper-trail-10.3.0/lib/paper_trail/serializers/0000755000175000017500000000000013467704405020765 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/serializers/json.rb0000644000175000017500000000320113467704405022257 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail module Serializers # An alternate serializer for, e.g. `versions.object`. module JSON extend self # makes all instance methods become module methods as well def load(string) ActiveSupport::JSON.decode string end def dump(object) ActiveSupport::JSON.encode object end # Returns a SQL LIKE condition to be used to match the given field and # value in the serialized object. def where_object_condition(arel_field, field, value) # Convert to JSON to handle strings and nulls correctly. json_value = value.to_json # If the value is a number, we need to ensure that we find the next # character too, which is either `,` or `}`, to ensure that searching # for the value 12 doesn't yield false positives when the value is # 123. if value.is_a? Numeric arel_field.matches("%\"#{field}\":#{json_value},%"). or(arel_field.matches("%\"#{field}\":#{json_value}}%")) else arel_field.matches("%\"#{field}\":#{json_value}%") end end def where_object_changes_condition(*) raise <<-STR.squish.freeze where_object_changes no longer supports reading JSON from a text column. The old implementation was inaccurate, returning more records than you wanted. This feature was deprecated in 7.1.0 and removed in 8.0.0. The json and jsonb datatypes are still supported. See the discussion at https://github.com/paper-trail-gem/paper_trail/issues/803 STR end end end end paper-trail-10.3.0/lib/paper_trail/serializers/yaml.rb0000644000175000017500000000311513467704405022254 0ustar samyaksamyak# frozen_string_literal: true require "yaml" module PaperTrail module Serializers # The default serializer for, e.g. `versions.object`. module YAML extend self # makes all instance methods become module methods as well def load(string) ::YAML.load string end # @param object (Hash | HashWithIndifferentAccess) - Coming from # `recordable_object` `object` will be a plain `Hash`. However, due to # recent [memory optimizations](https://git.io/fjeYv), when coming from # `recordable_object_changes`, it will be a `HashWithIndifferentAccess`. def dump(object) object = object.to_hash if object.is_a?(HashWithIndifferentAccess) ::YAML.dump object end # Returns a SQL LIKE condition to be used to match the given field and # value in the serialized object. def where_object_condition(arel_field, field, value) arel_field.matches("%\n#{field}: #{value}\n%") end # Returns a SQL LIKE condition to be used to match the given field and # value in the serialized `object_changes`. def where_object_changes_condition(*) raise <<-STR.squish.freeze where_object_changes no longer supports reading YAML from a text column. The old implementation was inaccurate, returning more records than you wanted. This feature was deprecated in 8.1.0 and removed in 9.0.0. The json and jsonb datatypes are still supported. See discussion at https://github.com/paper-trail-gem/paper_trail/pull/997 STR end end end end paper-trail-10.3.0/lib/paper_trail/frameworks/0000755000175000017500000000000013467704405020611 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/frameworks/active_record.rb0000644000175000017500000000040313467704405023744 0ustar samyaksamyak# frozen_string_literal: true # This file only needs to be loaded if the gem is being used outside of Rails, # since otherwise the model(s) will get loaded in via the `Rails::Engine`. require "paper_trail/frameworks/active_record/models/paper_trail/version" paper-trail-10.3.0/lib/paper_trail/frameworks/cucumber.rb0000644000175000017500000000133013467704405022740 0ustar samyaksamyak# frozen_string_literal: true # before hook for Cucumber Before do PaperTrail.enabled = false PaperTrail.request.enabled = true PaperTrail.request.whodunnit = nil PaperTrail.request.controller_info = {} if defined?(::Rails) end module PaperTrail module Cucumber # Helper method for enabling PT in Cucumber features. module Extensions # :call-seq: # with_versioning # # enable versioning for specific blocks def with_versioning was_enabled = ::PaperTrail.enabled? ::PaperTrail.enabled = true begin yield ensure ::PaperTrail.enabled = was_enabled end end end end end World PaperTrail::Cucumber::Extensions paper-trail-10.3.0/lib/paper_trail/frameworks/rails.rb0000644000175000017500000000017713467704405022255 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/frameworks/rails/controller" require "paper_trail/frameworks/rails/engine" paper-trail-10.3.0/lib/paper_trail/frameworks/rails/0000755000175000017500000000000013467704405021723 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/frameworks/rails/controller.rb0000644000175000017500000000665513467704405024447 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail module Rails # Extensions to rails controllers. Provides convenient ways to pass certain # information to the model layer, with `controller_info` and `whodunnit`. # Also includes a convenient on/off switch, # `paper_trail_enabled_for_controller`. module Controller def self.included(controller) controller.before_action( :set_paper_trail_enabled_for_controller, :set_paper_trail_controller_info ) end protected # Returns the user who is responsible for any changes that occur. # By default this calls `current_user` and returns the result. # # Override this method in your controller to call a different # method, e.g. `current_person`, or anything you like. # # @api public def user_for_paper_trail return unless defined?(current_user) ActiveSupport::VERSION::MAJOR >= 4 ? current_user.try!(:id) : current_user.try(:id) rescue NoMethodError current_user end # Returns any information about the controller or request that you # want PaperTrail to store alongside any changes that occur. By # default this returns an empty hash. # # Override this method in your controller to return a hash of any # information you need. The hash's keys must correspond to columns # in your `versions` table, so don't forget to add any new columns # you need. # # For example: # # {:ip => request.remote_ip, :user_agent => request.user_agent} # # The columns `ip` and `user_agent` must exist in your `versions` # table. # # Use the `:meta` option to # `PaperTrail::Model::ClassMethods.has_paper_trail` to store any extra # model-level data you need. # # @api public def info_for_paper_trail {} end # Returns `true` (default) or `false` depending on whether PaperTrail # should be active for the current request. # # Override this method in your controller to specify when PaperTrail # should be off. # # ``` # def paper_trail_enabled_for_controller # # Don't omit `super` without a good reason. # super && request.user_agent != 'Disable User-Agent' # end # ``` # # @api public def paper_trail_enabled_for_controller ::PaperTrail.enabled? end private # Tells PaperTrail whether versions should be saved in the current # request. # # @api public def set_paper_trail_enabled_for_controller ::PaperTrail.request.enabled = paper_trail_enabled_for_controller end # Tells PaperTrail who is responsible for any changes that occur. # # @api public def set_paper_trail_whodunnit if ::PaperTrail.request.enabled? ::PaperTrail.request.whodunnit = user_for_paper_trail end end # Tells PaperTrail any information from the controller you want to store # alongside any changes that occur. # # @api public def set_paper_trail_controller_info if ::PaperTrail.request.enabled? ::PaperTrail.request.controller_info = info_for_paper_trail end end end end end if defined?(::ActionController) ::ActiveSupport.on_load(:action_controller) do include ::PaperTrail::Rails::Controller end end paper-trail-10.3.0/lib/paper_trail/frameworks/rails/engine.rb0000644000175000017500000000352713467704405023524 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail module Rails # See http://guides.rubyonrails.org/engines.html class Engine < ::Rails::Engine DPR_CONFIG_ENABLED = <<~EOS.squish.freeze The rails configuration option config.paper_trail.enabled is deprecated. Please use PaperTrail.enabled= instead. People were getting confused that PT has both, specifically regarding *when* each was happening. If you'd like to keep config.paper_trail, join the discussion at https://github.com/paper-trail-gem/paper_trail/pull/1176 EOS private_constant :DPR_CONFIG_ENABLED DPR_RUDELY_ENABLING = <<~EOS.squish.freeze At some point early in the rails boot process, you have set PaperTrail.enabled = false. PT's rails engine is now overriding your setting, and setting it to true. We're not sure why, but this is how PT has worked since 5.0, when the config.paper_trail.enabled option was introduced. This is now deprecated. In the future, PT will not override your setting. See https://github.com/paper-trail-gem/paper_trail/pull/1176 for discussion. EOS private_constant :DPR_RUDELY_ENABLING paths["app/models"] << "lib/paper_trail/frameworks/active_record/models" # --- Begin deprecated section --- config.paper_trail = ActiveSupport::OrderedOptions.new initializer "paper_trail.initialisation" do |app| enable = app.config.paper_trail[:enabled] if enable.nil? unless PaperTrail.enabled? ::ActiveSupport::Deprecation.warn(DPR_RUDELY_ENABLING) PaperTrail.enabled = true end else ::ActiveSupport::Deprecation.warn(DPR_CONFIG_ENABLED) PaperTrail.enabled = enable end end # --- End deprecated section --- end end end paper-trail-10.3.0/lib/paper_trail/frameworks/active_record/0000755000175000017500000000000013467704405023422 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/frameworks/active_record/models/0000755000175000017500000000000013467704405024705 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/frameworks/active_record/models/paper_trail/0000755000175000017500000000000013467704405027207 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb0000644000175000017500000000104013467704405031214 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/version_concern" module PaperTrail # This is the default ActiveRecord model provided by PaperTrail. Most simple # applications will use this model as-is, but it is possible to sub-class, # extend, or even do without this model entirely. See documentation section # 6.a. Custom Version Classes. # # The paper_trail-association_tracking gem provides a related model, # `VersionAssociation`. class Version < ::ActiveRecord::Base include PaperTrail::VersionConcern end end paper-trail-10.3.0/lib/paper_trail/frameworks/rspec.rb0000644000175000017500000000256413467704405022261 0ustar samyaksamyak# frozen_string_literal: true require "rspec/core" require "rspec/matchers" require "paper_trail/frameworks/rspec/helpers" RSpec.configure do |config| config.include ::PaperTrail::RSpec::Helpers::InstanceMethods config.extend ::PaperTrail::RSpec::Helpers::ClassMethods config.before(:each) do ::PaperTrail.enabled = false ::PaperTrail.request.enabled = true ::PaperTrail.request.whodunnit = nil ::PaperTrail.request.controller_info = {} if defined?(::Rails) && defined?(::RSpec::Rails) end config.before(:each, versioning: true) do ::PaperTrail.enabled = true end end RSpec::Matchers.define :be_versioned do # check to see if the model has `has_paper_trail` declared on it match { |actual| actual.is_a?(::PaperTrail::Model::InstanceMethods) } end RSpec::Matchers.define :have_a_version_with do |attributes| # check if the model has a version with the specified attributes match do |actual| versions_association = actual.class.versions_association_name actual.send(versions_association).where_object(attributes).any? end end RSpec::Matchers.define :have_a_version_with_changes do |attributes| # check if the model has a version changes with the specified attributes match do |actual| versions_association = actual.class.versions_association_name actual.send(versions_association).where_object_changes(attributes).any? end end paper-trail-10.3.0/lib/paper_trail/frameworks/rspec/0000755000175000017500000000000013467704405021725 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/frameworks/rspec/helpers.rb0000644000175000017500000000143613467704405023720 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail module RSpec module Helpers # Included in the RSpec configuration in `frameworks/rspec.rb` module InstanceMethods # enable versioning for specific blocks (at instance-level) def with_versioning was_enabled = ::PaperTrail.enabled? ::PaperTrail.enabled = true yield ensure ::PaperTrail.enabled = was_enabled end end # Extended by the RSpec configuration in `frameworks/rspec.rb` module ClassMethods # enable versioning for specific blocks (at class-level) def with_versioning(&block) context "with versioning", versioning: true do class_exec(&block) end end end end end end paper-trail-10.3.0/lib/paper_trail/version_number.rb0000644000175000017500000000122713467704405022015 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail # The version number of the paper_trail gem. Not to be confused with # `PaperTrail::Version`. Ruby constants are case-sensitive, apparently, # and they are two different modules! It would be nice to remove `VERSION`, # because of this confusion, but it's not worth the breaking change. # People are encouraged to use `PaperTrail.gem_version` instead. module VERSION MAJOR = 10 MINOR = 3 TINY = 0 # Set PRE to nil unless it's a pre-release (beta, rc, etc.) PRE = nil STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".").freeze def self.to_s STRING end end end paper-trail-10.3.0/lib/paper_trail/record_trail.rb0000644000175000017500000002330113467704405021426 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/events/create" require "paper_trail/events/destroy" require "paper_trail/events/update" module PaperTrail # Represents the "paper trail" for a single record. class RecordTrail RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1") def initialize(record) @record = record end # Invoked after rollbacks to ensure versions records are not created for # changes that never actually took place. Optimization: Use lazy `reset` # instead of eager `reload` because, in many use cases, the association will # not be used. def clear_rolled_back_versions versions.reset end # Invoked via`after_update` callback for when a previous version is # reified and then saved. def clear_version_instance @record.send("#{@record.class.version_association_name}=", nil) end # Is PT enabled for this particular record? # @api private def enabled? PaperTrail.enabled? && PaperTrail.request.enabled? && PaperTrail.request.enabled_for_model?(@record.class) end # Returns true if this instance is the current, live one; # returns false if this instance came from a previous version. def live? source_version.nil? end # Returns the object (not a Version) as it became next. # NOTE: if self (the item) was not reified from a version, i.e. it is the # "live" item, we return nil. Perhaps we should return self instead? def next_version subsequent_version = source_version.next subsequent_version ? subsequent_version.reify : @record.class.find(@record.id) rescue StandardError # TODO: Rescue something more specific nil end # Returns who put `@record` into its current state. # # @api public def originator (source_version || versions.last).try(:whodunnit) end # Returns the object (not a Version) as it was most recently. # # @api public def previous_version (source_version ? source_version.previous : versions.last).try(:reify) end def record_create return unless enabled? build_version_on_create(in_after_callback: true).tap do |version| version.save! # Because the version object was created using version_class.new instead # of versions_assoc.build?, the association cache is unaware. So, we # invalidate the `versions` association cache with `reset`. versions.reset end end # PT-AT extends this method to add its transaction id. # # @api private def data_for_create {} end # `recording_order` is "after" or "before". See ModelConfig#on_destroy. # # @api private # @return - The created version object, so that plugins can use it, e.g. # paper_trail-association_tracking def record_destroy(recording_order) return unless enabled? && !@record.new_record? in_after_callback = recording_order == "after" event = Events::Destroy.new(@record, in_after_callback) # Merge data from `Event` with data from PT-AT. We no longer use # `data_for_destroy` but PT-AT still does. data = event.data.merge(data_for_destroy) version = @record.class.paper_trail.version_class.create(data) if version.errors.any? log_version_errors(version, :destroy) else assign_and_reset_version_association(version) version end end # PT-AT extends this method to add its transaction id. # # @api private def data_for_destroy {} end # @api private # @return - The created version object, so that plugins can use it, e.g. # paper_trail-association_tracking def record_update(force:, in_after_callback:, is_touch:) return unless enabled? version = build_version_on_update( force: force, in_after_callback: in_after_callback, is_touch: is_touch ) return unless version if version.save # Because the version object was created using version_class.new instead # of versions_assoc.build?, the association cache is unaware. So, we # invalidate the `versions` association cache with `reset`. versions.reset version else log_version_errors(version, :update) end end # PT-AT extends this method to add its transaction id. # # @api private def data_for_update {} end # @api private # @return - The created version object, so that plugins can use it, e.g. # paper_trail-association_tracking def record_update_columns(changes) return unless enabled? event = Events::Update.new(@record, false, false, changes) # Merge data from `Event` with data from PT-AT. We no longer use # `data_for_update_columns` but PT-AT still does. data = event.data.merge(data_for_update_columns) versions_assoc = @record.send(@record.class.versions_association_name) version = versions_assoc.create(data) if version.errors.any? log_version_errors(version, :update) else version end end # PT-AT extends this method to add its transaction id. # # @api private def data_for_update_columns {} end # Invoked via callback when a user attempts to persist a reified # `Version`. def reset_timestamp_attrs_for_update_if_needed return if live? @record.send(:timestamp_attributes_for_update_in_model).each do |column| @record.send("restore_#{column}!") end end # AR callback. # @api private def save_version? if_condition = @record.paper_trail_options[:if] unless_condition = @record.paper_trail_options[:unless] (if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record) end def source_version version end # Save, and create a version record regardless of options such as `:on`, # `:if`, or `:unless`. # # Arguments are passed to `save`. # # This is an "update" event. That is, we record the same data we would in # the case of a normal AR `update`. def save_with_version(*args) ::PaperTrail.request(enabled: false) do @record.save(*args) end record_update(force: true, in_after_callback: false, is_touch: false) end # Like the `update_column` method from `ActiveRecord::Persistence`, but also # creates a version to record those changes. # @api public def update_column(name, value) update_columns(name => value) end # Like the `update_columns` method from `ActiveRecord::Persistence`, but also # creates a version to record those changes. # @api public def update_columns(attributes) # `@record.update_columns` skips dirty-tracking, so we can't just use # `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`. # We need to build our own hash with the changes that will be made # directly to the database. changes = {} attributes.each do |k, v| changes[k] = [@record[k], v] end @record.update_columns(attributes) record_update_columns(changes) end # Returns the object (not a Version) as it was at the given timestamp. def version_at(timestamp, reify_options = {}) # Because a version stores how its object looked *before* the change, # we need to look for the first version created *after* the timestamp. v = versions.subsequent(timestamp, true).first return v.reify(reify_options) if v @record unless @record.destroyed? end # Returns the objects (not Versions) as they were between the given times. def versions_between(start_time, end_time) versions = send(@record.class.versions_association_name).between(start_time, end_time) versions.collect { |version| version_at(version.created_at) } end private # @api private def assign_and_reset_version_association(version) @record.send("#{@record.class.version_association_name}=", version) @record.send(@record.class.versions_association_name).reset end # @api private def build_version_on_create(in_after_callback:) event = Events::Create.new(@record, in_after_callback) # Merge data from `Event` with data from PT-AT. We no longer use # `data_for_create` but PT-AT still does. data = event.data.merge!(data_for_create) # Pure `version_class.new` reduces memory usage compared to `versions_assoc.build` @record.class.paper_trail.version_class.new(data) end # @api private def build_version_on_update(force:, in_after_callback:, is_touch:) event = Events::Update.new(@record, in_after_callback, is_touch, nil) return unless force || event.changed_notably? # Merge data from `Event` with data from PT-AT. We no longer use # `data_for_update` but PT-AT still does. To save memory, we use `merge!` # instead of `merge`. data = event.data.merge!(data_for_update) # Using `version_class.new` reduces memory usage compared to # `versions_assoc.build`. It's a trade-off though. We have to clear # the association cache (see `versions.reset`) and that could cause an # additional query in certain applications. @record.class.paper_trail.version_class.new(data) end def log_version_errors(version, action) version.logger&.warn( "Unable to create version for #{action} of #{@record.class.name}" \ "##{@record.id}: " + version.errors.full_messages.join(", ") ) end def version @record.public_send(@record.class.version_association_name) end def versions @record.public_send(@record.class.versions_association_name) end end end paper-trail-10.3.0/lib/paper_trail/has_paper_trail.rb0000644000175000017500000000744613467704405022126 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/attribute_serializers/object_attribute" require "paper_trail/attribute_serializers/object_changes_attribute" require "paper_trail/model_config" require "paper_trail/record_trail" module PaperTrail # Extensions to `ActiveRecord::Base`. See `frameworks/active_record.rb`. # It is our goal to have the smallest possible footprint here, because # `ActiveRecord::Base` is a very crowded namespace! That is why we introduced # `.paper_trail` and `#paper_trail`. module Model def self.included(base) base.send :extend, ClassMethods end # :nodoc: module ClassMethods # Declare this in your model to track every create, update, and destroy. # Each version of the model is available in the `versions` association. # # Options: # # - :on - The events to track (optional; defaults to all of them). Set # to an array of `:create`, `:update`, `:destroy` and `:touch` as desired. # - :class_name (deprecated) - The name of a custom Version class that # includes `PaperTrail::VersionConcern`. # - :ignore - An array of attributes for which a new `Version` will not be # created if only they change. It can also accept a Hash as an # argument where the key is the attribute to ignore (a `String` or # `Symbol`), which will only be ignored if the value is a `Proc` which # returns truthily. # - :if, :unless - Procs that allow to specify conditions when to save # versions for an object. # - :only - Inverse of `ignore`. A new `Version` will be created only # for these attributes if supplied it can also accept a Hash as an # argument where the key is the attribute to track (a `String` or # `Symbol`), which will only be counted if the value is a `Proc` which # returns truthily. # - :skip - Fields to ignore completely. As with `ignore`, updates to # these fields will not create a new `Version`. In addition, these # fields will not be included in the serialized versions of the object # whenever a new `Version` is created. # - :meta - A hash of extra data to store. You must add a column to the # `versions` table for each key. Values are objects or procs (which # are called with `self`, i.e. the model with the paper trail). See # `PaperTrail::Controller.info_for_paper_trail` for how to store data # from the controller. # - :versions - Either, # - A String (deprecated) - The name to use for the versions # association. Default is `:versions`. # - A Hash - options passed to `has_many`, plus `name:` and `scope:`. # - :version - The name to use for the method which returns the version # the instance was reified from. Default is `:version`. # # Plugins like the experimental `paper_trail-association_tracking` gem # may accept additional options. # # You can define a default set of options via the configurable # `PaperTrail.config.has_paper_trail_defaults` hash in your applications # initializer. The hash can contain any of the following options and will # provide an overridable default for all models. # # @api public def has_paper_trail(options = {}) defaults = PaperTrail.config.has_paper_trail_defaults paper_trail.setup(defaults.merge(options)) end # @api public def paper_trail ::PaperTrail::ModelConfig.new(self) end end # Wrap the following methods in a module so we can include them only in the # ActiveRecord models that declare `has_paper_trail`. module InstanceMethods # @api public def paper_trail ::PaperTrail::RecordTrail.new(self) end end end end paper-trail-10.3.0/lib/paper_trail/reifier.rb0000644000175000017500000001257613467704405020416 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/attribute_serializers/object_attribute" module PaperTrail # Given a version record and some options, builds a new model object. # @api private module Reifier class << self # See `VersionConcern#reify` for documentation. # @api private def reify(version, options) options = apply_defaults_to(options, version) attrs = version.object_deserialized model = init_model(attrs, options, version) reify_attributes(model, version, attrs) model.send "#{model.class.version_association_name}=", version model end private # Given a hash of `options` for `.reify`, return a new hash with default # values applied. # @api private def apply_defaults_to(options, version) { version_at: version.created_at, mark_for_destruction: false, has_one: false, has_many: false, belongs_to: false, has_and_belongs_to_many: false, unversioned_attributes: :nil }.merge(options) end # Initialize a model object suitable for reifying `version` into. Does # not perform reification, merely instantiates the appropriate model # class and, if specified by `options[:unversioned_attributes]`, sets # unversioned attributes to `nil`. # # Normally a polymorphic belongs_to relationship allows us to get the # object we belong to by calling, in this case, `item`. However this # returns nil if `item` has been destroyed, and we need to be able to # retrieve destroyed objects. # # In this situation we constantize the `item_type` to get hold of the # class...except when the stored object's attributes include a `type` # key. If this is the case, the object we belong to is using single # table inheritance (STI) and the `item_type` will be the base class, # not the actual subclass. If `type` is present but empty, the class is # the base class. def init_model(attrs, options, version) if options[:dup] != true && version.item model = version.item if options[:unversioned_attributes] == :nil init_unversioned_attrs(attrs, model) end else klass = version_reification_class(version, attrs) # The `dup` option always returns a new object, otherwise we should # attempt to look for the item outside of default scope(s). find_cond = { klass.primary_key => version.item_id } if options[:dup] || (item_found = klass.unscoped.where(find_cond).first).nil? model = klass.new elsif options[:unversioned_attributes] == :nil model = item_found init_unversioned_attrs(attrs, model) end end model end # Look for attributes that exist in `model` and not in this version. # These attributes should be set to nil. Modifies `attrs`. # @api private def init_unversioned_attrs(attrs, model) (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil } end # Reify onto `model` an attribute named `k` with value `v` from `version`. # # `ObjectAttribute#deserialize` will return the mapped enum value and in # Rails < 5, the []= uses the integer type caster from the column # definition (in general) and thus will turn a (usually) string to 0 # instead of the correct value. # # @api private def reify_attribute(k, v, model, version) enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {} is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k) if model.has_attribute?(k) && !is_enum_without_type_caster model[k.to_sym] = v elsif model.respond_to?("#{k}=") model.send("#{k}=", v) elsif version.logger version.logger.warn( "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})." ) end end # Reify onto `model` all the attributes of `version`. # @api private def reify_attributes(model, version, attrs) AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs) attrs.each do |k, v| reify_attribute(k, v, model, version) end end # Given a `version`, return the class to reify. This method supports # Single Table Inheritance (STI) with custom inheritance columns. # # For example, imagine a `version` whose `item_type` is "Animal". The # `animals` table is an STI table (it has cats and dogs) and it has a # custom inheritance column, `species`. If `attrs["species"]` is "Dog", # this method returns the constant `Dog`. If `attrs["species"]` is blank, # this method returns the constant `Animal`. You can see this particular # example in action in `spec/models/animal_spec.rb`. # # TODO: Duplication: similar `constantize` in VersionConcern#version_limit def version_reification_class(version, attrs) inheritance_column_name = version.item_type.constantize.inheritance_column inher_col_value = attrs[inheritance_column_name] class_name = inher_col_value.blank? ? version.item_type : inher_col_value class_name.constantize end end end end paper-trail-10.3.0/lib/paper_trail/request.rb0000644000175000017500000001202613467704405020447 0ustar samyaksamyak# frozen_string_literal: true require "request_store" module PaperTrail # Manages variables that affect the current HTTP request, such as `whodunnit`. # # Please do not use `PaperTrail::Request` directly, use `PaperTrail.request`. # Currently, `Request` is a `Module`, but in the future it is quite possible # we may make it a `Class`. If we make such a choice, we will not provide any # warning and will not treat it as a breaking change. You've been warned :) # # @api private module Request class InvalidOption < RuntimeError end class << self # Sets any data from the controller that you want PaperTrail to store. # See also `PaperTrail::Rails::Controller#info_for_paper_trail`. # # PaperTrail.request.controller_info = { ip: request_user_ip } # PaperTrail.request.controller_info # => { ip: '127.0.0.1' } # # @api public def controller_info=(value) store[:controller_info] = value end # Returns the data from the controller that you want PaperTrail to store. # See also `PaperTrail::Rails::Controller#info_for_paper_trail`. # # PaperTrail.request.controller_info = { ip: request_user_ip } # PaperTrail.request.controller_info # => { ip: '127.0.0.1' } # # @api public def controller_info store[:controller_info] end # Switches PaperTrail off for the given model. # @api public def disable_model(model_class) enabled_for_model(model_class, false) end # Switches PaperTrail on for the given model. # @api public def enable_model(model_class) enabled_for_model(model_class, true) end # Sets whether PaperTrail is enabled or disabled for the current request. # @api public def enabled=(value) store[:enabled] = value end # Returns `true` if PaperTrail is enabled for the request, `false` otherwise. # See `PaperTrail::Rails::Controller#paper_trail_enabled_for_controller`. # @api public def enabled? !!store[:enabled] end # Sets whether PaperTrail is enabled or disabled for this model in the # current request. # @api public def enabled_for_model(model, value) store[:"enabled_for_#{model}"] = value end # Returns `true` if PaperTrail is enabled for this model in the current # request, `false` otherwise. # @api public def enabled_for_model?(model) model.include?(::PaperTrail::Model::InstanceMethods) && !!store.fetch(:"enabled_for_#{model}", true) end # @api private def merge(options) options.to_h.each do |k, v| store[k] = v end end # @api private def set(options) store.clear merge(options) end # Returns a deep copy of the internal hash from our RequestStore. Keys are # all symbols. Values are mostly primitives, but whodunnit can be a Proc. # We cannot use Marshal.dump here because it doesn't support Proc. It is # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain. # @api private def to_h store.deep_dup end # Temporarily set `options` and execute a block. # @api private def with(options) return unless block_given? validate_public_options(options) before = to_h merge(options) yield ensure set(before) end # Sets who is responsible for any changes that occur during request. You # would normally use this in a migration or on the console, when working # with models directly. # # `value` is usually a string, the name of a person, but you can set # anything that responds to `to_s`. You can also set a Proc, which will # not be evaluated until `whodunnit` is called later, usually right before # inserting a `Version` record. # # @api public def whodunnit=(value) store[:whodunnit] = value end # Returns who is reponsible for any changes that occur during request. # # @api public def whodunnit who = store[:whodunnit] who.respond_to?(:call) ? who.call : who end private # Returns a Hash, initializing with default values if necessary. # @api private def store RequestStore.store[:paper_trail] ||= { enabled: true } end # Provide a helpful error message if someone has a typo in one of their # option keys. We don't validate option values here. That's traditionally # been handled with casting (`to_s`, `!!`) in the accessor method. # @api private def validate_public_options(options) options.each do |k, _v| case k when :controller_info, /enabled_for_/, :enabled, :whodunnit next else raise InvalidOption, "Invalid option: #{k}" end end end end end end paper-trail-10.3.0/lib/paper_trail/record_history.rb0000644000175000017500000000264213467704405022021 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail # Represents the history of a single record. # @api private class RecordHistory # @param versions - ActiveRecord::Relation - All versions of the record. # @param version_class - Class - Usually PaperTrail::Version, # but it could also be a custom version class. # @api private def initialize(versions, version_class) @versions = versions @version_class = version_class end # Returns ordinal position of `version` in `sequence`. # @api private def index(version) sequence.to_a.index(version) end private # Returns `@versions` in chronological order. # @api private def sequence if @version_class.primary_key_is_int? @versions.select(primary_key).order(primary_key.asc) else @versions. select([table[:created_at], primary_key]). order(@version_class.timestamp_sort_order) end end # @return - Arel::Attribute - Attribute representing the primary key # of the version table. The column's data type is usually a serial # integer (the rails convention) but not always. # @api private def primary_key table[@version_class.primary_key] end # @return - Arel::Table - The version table, usually named `versions`, but # not always. # @api private def table @version_class.arel_table end end end paper-trail-10.3.0/lib/paper_trail/events/0000755000175000017500000000000013467704405017735 5ustar samyaksamyakpaper-trail-10.3.0/lib/paper_trail/events/create.rb0000644000175000017500000000144513467704405021531 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/events/base" module PaperTrail module Events # See docs in `Base`. # # @api private class Create < Base # Return attributes of nascent `Version` record. # # @api private def data data = { item: @record, event: @record.paper_trail_event || "create", whodunnit: PaperTrail.request.whodunnit } if @record.respond_to?(:updated_at) data[:created_at] = @record.updated_at end if record_object_changes? && changed_notably? changes = notable_changes data[:object_changes] = prepare_object_changes(changes) end merge_item_subtype_into(data) merge_metadata_into(data) end end end end paper-trail-10.3.0/lib/paper_trail/events/destroy.rb0000644000175000017500000000211213467704405021747 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/events/base" module PaperTrail module Events # See docs in `Base`. # # @api private class Destroy < Base # Return attributes of nascent `Version` record. # # @api private def data data = { item_id: @record.id, item_type: @record.class.base_class.name, event: @record.paper_trail_event || "destroy", whodunnit: PaperTrail.request.whodunnit } if record_object? data[:object] = recordable_object(false) end if record_object_changes? data[:object_changes] = prepare_object_changes(notable_changes) end merge_item_subtype_into(data) merge_metadata_into(data) end private # Rails' implementation (eg. `@record.saved_changes`) returns nothing on # destroy, so we have to build the hash we want. # # @override def changes_in_latest_version @record.attributes.map { |attr, value| [attr, [value, nil]] }.to_h end end end end paper-trail-10.3.0/lib/paper_trail/events/base.rb0000644000175000017500000002622313467704405021201 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail module Events # We refer to times in the lifecycle of a record as "events". There are # three events: # # - create # - `after_create` we call `RecordTrail#record_create` # - update # - `after_update` we call `RecordTrail#record_update` # - `after_touch` we call `RecordTrail#record_update` # - `RecordTrail#save_with_version` calls `RecordTrail#record_update` # - `RecordTrail#update_columns` is also referred to as an update, though # it uses `RecordTrail#record_update_columns` rather than # `RecordTrail#record_update` # - destroy # - `before_destroy` or `after_destroy` we call `RecordTrail#record_destroy` # # The value inserted into the `event` column of the versions table can also # be overridden by the user, with `paper_trail_event`. # # @api private class Base RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1") # @api private def initialize(record, in_after_callback) @record = record @in_after_callback = in_after_callback end # Determines whether it is appropriate to generate a new version # instance. A timestamp-only update (e.g. only `updated_at` changed) is # considered notable unless an ignored attribute was also changed. # # @api private def changed_notably? if ignored_attr_has_changed? timestamps = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s) (notably_changed - timestamps).any? else notably_changed.any? end end private # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See # https://github.com/paper-trail-gem/paper_trail/pull/899 # # @api private def attribute_changed_in_latest_version?(attr_name) if @in_after_callback && RAILS_GTE_5_1 @record.saved_change_to_attribute?(attr_name.to_s) else @record.attribute_changed?(attr_name.to_s) end end # @api private def nonskipped_attributes_before_change(is_touch) cache_changed_attributes do record_attributes = @record.attributes.except(*@record.paper_trail_options[:skip]) record_attributes.each_key do |k| if @record.class.column_names.include?(k) record_attributes[k] = attribute_in_previous_version(k, is_touch) end end end end # Rails 5.1 changed the API of `ActiveRecord::Dirty`. # @api private def cache_changed_attributes if RAILS_GTE_5_1 # Everything works fine as it is yield else # Any particular call to `changed_attributes` produces the huge memory allocation. # Lets use the generic AR workaround for that. @record.send(:cache_changed_attributes) { yield } end end # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See # https://github.com/paper-trail-gem/paper_trail/pull/899 # # Event can be any of the three (create, update, destroy). # # @api private def attribute_in_previous_version(attr_name, is_touch) if RAILS_GTE_5_1 if @in_after_callback && !is_touch # For most events, we want the original value of the attribute, before # the last save. @record.attribute_before_last_save(attr_name.to_s) else # We are either performing a `record_destroy` or a # `record_update(is_touch: true)`. @record.attribute_in_database(attr_name.to_s) end else @record.attribute_was(attr_name.to_s) end end # @api private def changed_and_not_ignored ignore = @record.paper_trail_options[:ignore].dup # Remove Hash arguments and then evaluate whether the attributes (the # keys of the hash) should also get pushed into the collection. ignore.delete_if do |obj| obj.is_a?(Hash) && obj.each { |attr, condition| ignore << attr if condition.respond_to?(:call) && condition.call(@record) } end skip = @record.paper_trail_options[:skip] (changed_in_latest_version - ignore) - skip end # @api private def changed_in_latest_version # Memoized to reduce memory usage @changed_in_latest_version ||= changes_in_latest_version.keys end # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See # https://github.com/paper-trail-gem/paper_trail/pull/899 # # @api private def changes_in_latest_version # Memoized to reduce memory usage @changes_in_latest_version ||= begin if @in_after_callback && RAILS_GTE_5_1 @record.saved_changes else @record.changes end end end # An attributed is "ignored" if it is listed in the `:ignore` option # and/or the `:skip` option. Returns true if an ignored attribute has # changed. # # @api private def ignored_attr_has_changed? ignored = @record.paper_trail_options[:ignore] + @record.paper_trail_options[:skip] ignored.any? && (changed_in_latest_version & ignored).any? end # PT 10 has a new optional column, `item_subtype` # # @api private def merge_item_subtype_into(data) if @record.class.paper_trail.version_class.columns_hash.key?("item_subtype") data.merge!(item_subtype: @record.class.name) end end # Updates `data` from the model's `meta` option and from `controller_info`. # Metadata is always recorded; that means all three events (create, update, # destroy) and `update_columns`. # # @api private def merge_metadata_into(data) merge_metadata_from_model_into(data) merge_metadata_from_controller_into(data) end # Updates `data` from `controller_info`. # # @api private def merge_metadata_from_controller_into(data) data.merge(PaperTrail.request.controller_info || {}) end # Updates `data` from the model's `meta` option. # # @api private def merge_metadata_from_model_into(data) @record.paper_trail_options[:meta].each do |k, v| data[k] = model_metadatum(v, data[:event]) end end # Given a `value` from the model's `meta` option, returns an object to be # persisted. The `value` can be a simple scalar value, but it can also # be a symbol that names a model method, or even a Proc. # # @api private def model_metadatum(value, event) if value.respond_to?(:call) value.call(@record) elsif value.is_a?(Symbol) && @record.respond_to?(value, true) # If it is an attribute that is changing in an existing object, # be sure to grab the current version. if event != "create" && @record.has_attribute?(value) && attribute_changed_in_latest_version?(value) attribute_in_previous_version(value, false) else @record.send(value) end else value end end # @api private def notable_changes changes_in_latest_version.delete_if { |k, _v| !notably_changed.include?(k) } end # @api private def notably_changed # Memoized to reduce memory usage @notably_changed ||= begin only = @record.paper_trail_options[:only].dup # Remove Hash arguments and then evaluate whether the attributes (the # keys of the hash) should also get pushed into the collection. only.delete_if do |obj| obj.is_a?(Hash) && obj.each { |attr, condition| only << attr if condition.respond_to?(:call) && condition.call(@record) } end only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only) end end # Returns hash of attributes (with appropriate attributes serialized), # omitting attributes to be skipped. # # @api private def object_attrs_for_paper_trail(is_touch) attrs = nonskipped_attributes_before_change(is_touch) AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs) attrs end # @api private def prepare_object_changes(changes) changes = serialize_object_changes(changes) changes = recordable_object_changes(changes) changes end # Returns an object which can be assigned to the `object_changes` # attribute of a nascent version record. If the `object_changes` column is # a postgres `json` column, then a hash can be used in the assignment, # otherwise the column is a `text` column, and we must perform the # serialization here, using `PaperTrail.serializer`. # # @api private # @param changes HashWithIndifferentAccess def recordable_object_changes(changes) if PaperTrail.config.object_changes_adapter&.respond_to?(:diff) # We'd like to avoid the `to_hash` here, because it increases memory # usage, but that would be a breaking change because # `object_changes_adapter` expects a plain `Hash`, not a # `HashWithIndifferentAccess`. changes = PaperTrail.config.object_changes_adapter.diff(changes.to_hash) end if @record.class.paper_trail.version_class.object_changes_col_is_json? changes else PaperTrail.serializer.dump(changes) end end # Returns a boolean indicating whether to store serialized version diffs # in the `object_changes` column of the version record. # # @api private def record_object_changes? @record.class.paper_trail.version_class.column_names.include?("object_changes") end # Returns a boolean indicating whether to store the original object during save. # # @api private def record_object? @record.class.paper_trail.version_class.column_names.include?("object") end # Returns an object which can be assigned to the `object` attribute of a # nascent version record. If the `object` column is a postgres `json` # column, then a hash can be used in the assignment, otherwise the column # is a `text` column, and we must perform the serialization here, using # `PaperTrail.serializer`. # # @api private def recordable_object(is_touch) if @record.class.paper_trail.version_class.object_col_is_json? object_attrs_for_paper_trail(is_touch) else PaperTrail.serializer.dump(object_attrs_for_paper_trail(is_touch)) end end # @api private def serialize_object_changes(changes) AttributeSerializers::ObjectChangesAttribute. new(@record.class). serialize(changes) # We'd like to convert this `HashWithIndifferentAccess` to a plain # `Hash`, but we don't, to save memory. changes end end end end paper-trail-10.3.0/lib/paper_trail/events/update.rb0000644000175000017500000000347113467704405021551 0ustar samyaksamyak# frozen_string_literal: true require "paper_trail/events/base" module PaperTrail module Events # See docs in `Base`. # # @api private class Update < Base # - is_touch - [boolean] - Used in the two situations that are touch-like: # - `after_touch` we call `RecordTrail#record_update` # - force_changes - [Hash] - Only used by `RecordTrail#update_columns`, # because there dirty-tracking is off, so it has to track its own changes. # # @api private def initialize(record, in_after_callback, is_touch, force_changes) super(record, in_after_callback) @is_touch = is_touch @force_changes = force_changes end # Return attributes of nascent `Version` record. # # @api private def data data = { item: @record, event: @record.paper_trail_event || "update", whodunnit: PaperTrail.request.whodunnit } if @record.respond_to?(:updated_at) data[:created_at] = @record.updated_at end if record_object? data[:object] = recordable_object(@is_touch) end if record_object_changes? changes = @force_changes.nil? ? notable_changes : @force_changes data[:object_changes] = prepare_object_changes(changes) end merge_item_subtype_into(data) merge_metadata_into(data) end private # `touch` cannot record `object_changes` because rails' `touch` does not # perform dirty-tracking. Specifically, methods from `Dirty`, like # `saved_changes`, return the same values before and after `touch`. # # See https://github.com/rails/rails/issues/33429 # # @api private def record_object_changes? !@is_touch && super end end end end paper-trail-10.3.0/lib/paper_trail/cleaner.rb0000644000175000017500000000472313467704405020375 0ustar samyaksamyak# frozen_string_literal: true module PaperTrail # Utilities for deleting version records. module Cleaner # Destroys all but the most recent version(s) for items on a given date # (or on all dates). Useful for deleting drafts. # # Options: # # - :keeping - An `integer` indicating the number of versions to be kept for # each item per date. Defaults to `1`. The most recent matching versions # are kept. # - :date - Should either be a `Date` object specifying which date to # destroy versions for or `:all`, which will specify that all dates # should be cleaned. Defaults to `:all`. # - :item_id - The `id` for the item to be cleaned on, or `nil`, which # causes all items to be cleaned. Defaults to `nil`. # def clean_versions!(options = {}) options = { keeping: 1, date: :all }.merge(options) gather_versions(options[:item_id], options[:date]).each do |_item_id, item_versions| group_versions_by_date(item_versions).each do |_date, date_versions| # Remove the number of versions we wish to keep from the collection # of versions prior to destruction. date_versions.pop(options[:keeping]) date_versions.map(&:destroy) end end end private # Returns a hash of versions grouped by the `item_id` attribute formatted # like this: {:item_id => PaperTrail::Version}. If `item_id` or `date` is # set, versions will be narrowed to those pointing at items with those ids # that were created on specified date. Versions are returned in # chronological order. def gather_versions(item_id = nil, date = :all) unless date == :all || date.respond_to?(:to_date) raise ArgumentError, "Expected date to be a Timestamp or :all" end versions = item_id ? PaperTrail::Version.where(item_id: item_id) : PaperTrail::Version versions = versions.order(PaperTrail::Version.timestamp_sort_order) versions = versions.between(date.to_date, date.to_date + 1.day) unless date == :all # If `versions` has not been converted to an ActiveRecord::Relation yet, # do so now. versions = PaperTrail::Version.all if versions == PaperTrail::Version versions.group_by(&:item_id) end # Given an array of versions, returns a hash mapping dates to arrays of # versions. # @api private def group_versions_by_date(versions) versions.group_by { |v| v.created_at.to_date } end end end paper-trail-10.3.0/paper_trail.gemspec0000644000175000017500000001456613467704405017244 0ustar samyaksamyak######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: paper_trail 10.3.0 ruby lib Gem::Specification.new do |s| s.name = "paper_trail".freeze s.version = "10.3.0" s.required_rubygems_version = Gem::Requirement.new(">= 1.3.6".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Andy Stewart".freeze, "Ben Atkins".freeze, "Jared Beck".freeze] s.date = "2019-04-09" s.description = "Track changes to your models, for auditing or versioning. See how a model looked\nat any stage in its lifecycle, revert it to any version, or restore it after it\nhas been destroyed.\n".freeze s.email = "jared@jaredbeck.com".freeze s.files = ["lib/generators/paper_trail/install/USAGE".freeze, "lib/generators/paper_trail/install/install_generator.rb".freeze, "lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.erb".freeze, "lib/generators/paper_trail/install/templates/create_versions.rb.erb".freeze, "lib/generators/paper_trail/install_generator.rb".freeze, "lib/generators/paper_trail/migration_generator.rb".freeze, "lib/generators/paper_trail/update_item_subtype/USAGE".freeze, "lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb".freeze, "lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb".freeze, "lib/paper_trail.rb".freeze, "lib/paper_trail/attribute_serializers/README.md".freeze, "lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb".freeze, "lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb".freeze, "lib/paper_trail/attribute_serializers/object_attribute.rb".freeze, "lib/paper_trail/attribute_serializers/object_changes_attribute.rb".freeze, "lib/paper_trail/cleaner.rb".freeze, "lib/paper_trail/config.rb".freeze, "lib/paper_trail/events/base.rb".freeze, "lib/paper_trail/events/create.rb".freeze, "lib/paper_trail/events/destroy.rb".freeze, "lib/paper_trail/events/update.rb".freeze, "lib/paper_trail/frameworks/active_record.rb".freeze, "lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb".freeze, "lib/paper_trail/frameworks/cucumber.rb".freeze, "lib/paper_trail/frameworks/rails.rb".freeze, "lib/paper_trail/frameworks/rails/controller.rb".freeze, "lib/paper_trail/frameworks/rails/engine.rb".freeze, "lib/paper_trail/frameworks/rspec.rb".freeze, "lib/paper_trail/frameworks/rspec/helpers.rb".freeze, "lib/paper_trail/has_paper_trail.rb".freeze, "lib/paper_trail/model_config.rb".freeze, "lib/paper_trail/queries/versions/where_object.rb".freeze, "lib/paper_trail/queries/versions/where_object_changes.rb".freeze, "lib/paper_trail/record_history.rb".freeze, "lib/paper_trail/record_trail.rb".freeze, "lib/paper_trail/reifier.rb".freeze, "lib/paper_trail/request.rb".freeze, "lib/paper_trail/serializers/json.rb".freeze, "lib/paper_trail/serializers/yaml.rb".freeze, "lib/paper_trail/type_serializers/postgres_array_serializer.rb".freeze, "lib/paper_trail/version_concern.rb".freeze, "lib/paper_trail/version_number.rb".freeze] s.homepage = "https://github.com/paper-trail-gem/paper_trail".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.3.0".freeze) s.rubygems_version = "2.7.6.2".freeze s.summary = "Track changes to your models.".freeze if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q.freeze, ["< 6.1", ">= 4.2"]) s.add_development_dependency(%q.freeze, ["~> 2.2"]) s.add_development_dependency(%q.freeze, ["~> 10.0"]) s.add_development_dependency(%q.freeze, ["~> 2.8"]) s.add_development_dependency(%q.freeze, ["~> 0.9.4"]) s.add_development_dependency(%q.freeze, ["~> 0.9.12"]) s.add_development_dependency(%q.freeze, ["~> 0.5.2"]) s.add_development_dependency(%q.freeze, ["~> 2.0.0"]) s.add_development_dependency(%q.freeze, ["~> 1.0"]) s.add_development_dependency(%q.freeze, ["~> 12.3"]) s.add_runtime_dependency(%q.freeze, ["~> 1.1"]) s.add_development_dependency(%q.freeze, ["~> 3.8"]) s.add_development_dependency(%q.freeze, ["~> 0.62.0"]) s.add_development_dependency(%q.freeze, ["~> 1.28.0"]) s.add_development_dependency(%q.freeze, ["~> 1.3.13"]) else s.add_dependency(%q.freeze, ["< 6.1", ">= 4.2"]) s.add_dependency(%q.freeze, ["~> 2.2"]) s.add_dependency(%q.freeze, ["~> 10.0"]) s.add_dependency(%q.freeze, ["~> 2.8"]) s.add_dependency(%q.freeze, ["~> 0.9.4"]) s.add_dependency(%q.freeze, ["~> 0.9.12"]) s.add_dependency(%q.freeze, ["~> 0.5.2"]) s.add_dependency(%q.freeze, ["~> 2.0.0"]) s.add_dependency(%q.freeze, ["~> 1.0"]) s.add_dependency(%q.freeze, ["~> 12.3"]) s.add_dependency(%q.freeze, ["~> 1.1"]) s.add_dependency(%q.freeze, ["~> 3.8"]) s.add_dependency(%q.freeze, ["~> 0.62.0"]) s.add_dependency(%q.freeze, ["~> 1.28.0"]) s.add_dependency(%q.freeze, ["~> 1.3.13"]) end else s.add_dependency(%q.freeze, ["< 6.1", ">= 4.2"]) s.add_dependency(%q.freeze, ["~> 2.2"]) s.add_dependency(%q.freeze, ["~> 10.0"]) s.add_dependency(%q.freeze, ["~> 2.8"]) s.add_dependency(%q.freeze, ["~> 0.9.4"]) s.add_dependency(%q.freeze, ["~> 0.9.12"]) s.add_dependency(%q.freeze, ["~> 0.5.2"]) s.add_dependency(%q.freeze, ["~> 2.0.0"]) s.add_dependency(%q.freeze, ["~> 1.0"]) s.add_dependency(%q.freeze, ["~> 12.3"]) s.add_dependency(%q.freeze, ["~> 1.1"]) s.add_dependency(%q.freeze, ["~> 3.8"]) s.add_dependency(%q.freeze, ["~> 0.62.0"]) s.add_dependency(%q.freeze, ["~> 1.28.0"]) s.add_dependency(%q.freeze, ["~> 1.3.13"]) end end