Skip to content

Setting whodunnit in the rails console

Todd Lynam edited this page Apr 12, 2021 · 22 revisions

This page describes ways of setting whodunnit for use in the rails console or other rails commands / rake tasks (like rake db:migrate).

Manually setting

In a console session you can manually set who is responsible like this:

PaperTrail.request.whodunnit = 'Andy Stewart'
widget.update :name => 'Wibble'
widget.versions.last.whodunnit              # Andy Stewart

Setting automatically based on current OS user

You can avoid having to do this manually by setting your initializer to pick up the username of the current user from the OS, like this:

# config/initializers/paper_trail.rb

# the following line is required for PaperTrail >= 4.0.0 and < 12.0.0 with Rails
PaperTrail::Rails::Engine.eager_load!

# Defer evaluation in case we're using spring loader (otherwise it would be something like "spring app    | app | started 13 secs ago | development")
PaperTrail.request.whodunnit = ->() {
  if Rails.const_defined?('Console') || File.basename($PROGRAM_NAME) == 'rake'
    "#{`whoami`.strip}: console"
  else
    "#{`whoami`.strip}: #{File.basename($PROGRAM_NAME)} #{ARGV.join ' '}"
  end
}

Require users to enter their name when launching rails console

You can also force every console user to type enter their name:

Rails.application.configure do
  console do
    puts 'Welcome'

    name = nil
    until name.present? do
      puts 'Who are you?'
      name = gets
      puts 'Mmmm?' unless name.present?
    end

    puts "Hi #{name}"

    PaperTrail.request.whodunnit = "#{name.chomp} - from console"
  end
end

Ask for a name only if/when you start changing records in the console

Being prompted for a name every time can be annoying if you frequently use the console to just run read-only queries. We only really need to know who you are if you update any records. Here's a variation that only asks for your name at the moment the first record change occurs ...

# config/initializers/console.rb

Rails.application.configure do
  console do
    PaperTrail.request.whodunnit = ->() {
      @paper_trail_whodunnit ||= (
        name = nil
        until name.present? do
          print "What is your name (used by PaperTrail to record who changed records)? "
          name = gets.chomp
        end
        puts "Thank you, #{name}! Have a wonderful time!"
        name
      )
    }
  end
end

Store command in separate field instead of in whodunnit

If you want to reserve the whodunnit field for only storing the actual name or id of the human who made the change but still want to record which command and which source location the change was made from, you can add some fields:

class VersionsAddSourceLocation < ActiveRecord::Migration[5.1]
  def change
    change_table :versions do |t|
      t.text   :source_location
      t.text   :command
    end
  end
end

and do something like this in an initializer:

# config/initializers/paper_trail.rb

# Store some metadata about where the change came from, even for rake tasks, etc.
def PaperTrail.set_global_metadata
  request.controller_info ||= {}
  request.controller_info[:command] ||= "#{File.basename($PROGRAM_NAME)} #{ARGV.join ' '} (#{$PID})"
  request.controller_info[:source_location] = caller.find { |line|
    line.starts_with? Rails.root.to_s and
   !line.starts_with? __FILE__
  }
end
# There's no way to set up deferred evaluation of PaperTrail.request.controller_info from here like
# we can with whodunnit, so abuse that property of PaperTrail.request.whodunnit to set other
# metadata. Reserve the whodunnit field for storing the actual name or id of the human who made the
# change.
PaperTrail.request.whodunnit = ->() {
  PaperTrail.set_global_metadata
  nil
}

Rails.application.configure do
  console do
    PaperTrail.request.controller_info = { command: "rails console" }
    PaperTrail.request.whodunnit = ->() {
      PaperTrail.set_global_metadata

      @paper_trail_whodunnit ||= (
        name = nil
        until name.present? do
          print "What is your name (used by PaperTrail to record who changed records)? "
          name = gets.chomp
        end
        puts "Thank you, #{name}! Have a wonderful time!"
        name
      )
    }
  end
end