Module: Whodunit

Defined in:
lib/whodunit.rb,
lib/whodunit/current.rb,
lib/whodunit/railtie.rb,
lib/whodunit/version.rb,
lib/whodunit/generator.rb,
lib/whodunit/stampable.rb,
lib/whodunit/migration_helpers.rb,
lib/whodunit/controller_methods.rb,
lib/whodunit/table_definition_extension.rb,
lib/whodunit/generator/application_record_integration.rb

Overview

Lightweight creator/updater/deleter tracking for ActiveRecord models.

Whodunit provides simple auditing by tracking who created, updated, and deleted ActiveRecord models. It features smart soft-delete detection and zero performance overhead.

Examples:

Basic usage

class Post < ApplicationRecord
  include Whodunit::Stampable
end

# In controller
Whodunit::Current.user = current_user
post = Post.create(title: "Hello")
post.creator_id # => current_user.id

Configuration

Whodunit.configure do |config|
  config.user_class = "Account"
  config.creator_column = :created_by_id
  config.column_data_type = :integer
end

Author:

  • Ken C. Demanawa

Since:

  • 0.1.0

Defined Under Namespace

Modules: ControllerMethods, MigrationHelpers, Stampable, TableDefinitionExtension Classes: Current, Error, Generator, Railtie

Constant Summary collapse

VERSION =

Since:

  • 0.1.0

"0.5.0"

Configuration collapse

Data Type Configuration collapse

Reverse Association Configuration collapse

Model Registry collapse

Data Type Helpers collapse

Model Registration & Reverse Associations collapse

Class Method Details

.configure {|self| ... } ⇒ void

This method returns an undefined value.

Configure Whodunit settings

Examples:

Whodunit.configure do |config|
  config.user_class = "Account"
  config.creator_column = :created_by_id
  config.column_data_type = :integer
end

Yields:

  • (self)

    configuration block

Raises:

  • (Whodunit::Error)

    if both creator_column and updater_column are set to nil

Since:

  • 0.1.0



132
133
134
135
# File 'lib/whodunit.rb', line 132

def self.configure
  yield self
  validate_column_configuration!
end

.creator_data_typeSymbol

Get the data type for the creator column

Returns:

  • (Symbol)

    the creator column data type

Since:

  • 0.1.0



163
164
165
# File 'lib/whodunit.rb', line 163

def self.creator_data_type
  creator_column_type || column_data_type
end

.creator_enabled?Boolean

Check if creator column is enabled

Returns:

  • (Boolean)

    true if creator_column is not nil

Since:

  • 0.1.0



187
188
189
# File 'lib/whodunit.rb', line 187

def self.creator_enabled?
  !creator_column.nil?
end

.deleter_data_typeSymbol

Get the data type for the deleter column

Returns:

  • (Symbol)

    the deleter column data type

Since:

  • 0.1.0



175
176
177
# File 'lib/whodunit.rb', line 175

def self.deleter_data_type
  deleter_column_type || column_data_type
end

.deleter_enabled?Boolean

Check if deleter column is enabled

Returns:

  • (Boolean)

    true if deleter_column is not nil

Since:

  • 0.1.0



199
200
201
# File 'lib/whodunit.rb', line 199

def self.deleter_enabled?
  !deleter_column.nil?
end

.generate_reverse_association_name(action, model_plural) ⇒ String

Generate a reverse association name based on action and model name

Parameters:

  • action (String)

    the action (created, updated, deleted)

  • model_plural (String)

    the pluralized model name

Returns:

  • (String)

    the generated association name

Since:

  • 0.1.0



248
249
250
# File 'lib/whodunit.rb', line 248

def self.generate_reverse_association_name(action, model_plural)
  "#{reverse_association_prefix}#{action}_#{model_plural}#{reverse_association_suffix}"
end

.register_model(model_class) ⇒ void

This method returns an undefined value.

Register a model class that includes Whodunit::Stampable This is called automatically when Stampable is included

Parameters:

  • model_class (Class)

    the model class to register

Since:

  • 0.1.0



209
210
211
212
213
214
215
216
217
# File 'lib/whodunit.rb', line 209

def self.register_model(model_class)
  return unless auto_setup_reverse_associations
  return if registered_models.include?(model_class)
  return if model_class.respond_to?(:whodunit_reverse_associations_enabled?) &&
            !model_class.whodunit_reverse_associations_enabled?

  registered_models << model_class
  setup_reverse_associations_for_model(model_class)
end

.resolve_foreign_key(model_class, foreign_key_column) ⇒ Symbol

Resolve the actual foreign key column name from model configuration

Parameters:

  • model_class (Class)

    the model class

  • foreign_key_column (Symbol)

    the default foreign key column

Returns:

  • (Symbol)

    the actual foreign key column name

Since:

  • 0.1.0



319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/whodunit.rb', line 319

def self.resolve_foreign_key(model_class, foreign_key_column)
  return foreign_key_column unless model_class.respond_to?(:whodunit_setting)

  column_mapping = {
    creator_id: :creator_column,
    updater_id: :updater_column,
    deleter_id: :deleter_column
  }

  setting_key = column_mapping[foreign_key_column]
  setting_key ? (model_class.whodunit_setting(setting_key) || foreign_key_column) : foreign_key_column
end

.resolve_user_classClass?

Resolve the user class constant

Returns:

  • (Class, nil)

    the user class or nil if not found

Since:

  • 0.1.0



254
255
256
257
258
# File 'lib/whodunit.rb', line 254

def self.resolve_user_class
  user_class.constantize
rescue StandardError
  nil
end

.setup_all_reverse_associationsvoid

This method returns an undefined value.

Set up all reverse associations for all registered models This can be called manually if needed (e.g., after configuration changes)

Since:

  • 0.1.0



238
239
240
241
242
# File 'lib/whodunit.rb', line 238

def self.setup_all_reverse_associations
  registered_models.each do |model_class|
    setup_reverse_associations_for_model(model_class)
  end
end

.setup_creator_reverse_association(user_class_instance, model_class, model_plural) ⇒ void

This method returns an undefined value.

Set up creator reverse association

Parameters:

  • user_class_instance (Class)

    the user class

  • model_class (Class)

    the model class

  • model_plural (String)

    the pluralized model name

Since:

  • 0.1.0



265
266
267
268
269
270
# File 'lib/whodunit.rb', line 265

def self.setup_creator_reverse_association(user_class_instance, model_class, model_plural)
  return unless model_class.respond_to?(:model_creator_enabled?) && model_class.model_creator_enabled?

  association_name = generate_reverse_association_name("created", model_plural)
  setup_user_reverse_association(user_class_instance, association_name, model_class, :creator_id)
end

.setup_deleter_reverse_association(user_class_instance, model_class, model_plural) ⇒ void

This method returns an undefined value.

Set up deleter reverse association

Parameters:

  • user_class_instance (Class)

    the user class

  • model_class (Class)

    the model class

  • model_plural (String)

    the pluralized model name

Since:

  • 0.1.0



289
290
291
292
293
294
295
# File 'lib/whodunit.rb', line 289

def self.setup_deleter_reverse_association(user_class_instance, model_class, model_plural)
  return unless model_class.respond_to?(:model_deleter_enabled?) && model_class.model_deleter_enabled?
  return unless model_class.respond_to?(:soft_delete_enabled?) && model_class.soft_delete_enabled?

  association_name = generate_reverse_association_name("deleted", model_plural)
  setup_user_reverse_association(user_class_instance, association_name, model_class, :deleter_id)
end

.setup_reverse_associations_for_model(model_class) ⇒ void

This method returns an undefined value.

Set up reverse associations on the user class for a specific model

Parameters:

  • model_class (Class)

    the model class to set up reverse associations for

Since:

  • 0.1.0



222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/whodunit.rb', line 222

def self.setup_reverse_associations_for_model(model_class)
  return unless auto_setup_reverse_associations

  user_class_instance = resolve_user_class
  return unless user_class_instance.respond_to?(:has_many)

  model_plural = model_class.name.underscore.pluralize

  setup_creator_reverse_association(user_class_instance, model_class, model_plural)
  setup_updater_reverse_association(user_class_instance, model_class, model_plural)
  setup_deleter_reverse_association(user_class_instance, model_class, model_plural)
end

.setup_updater_reverse_association(user_class_instance, model_class, model_plural) ⇒ void

This method returns an undefined value.

Set up updater reverse association

Parameters:

  • user_class_instance (Class)

    the user class

  • model_class (Class)

    the model class

  • model_plural (String)

    the pluralized model name

Since:

  • 0.1.0



277
278
279
280
281
282
# File 'lib/whodunit.rb', line 277

def self.setup_updater_reverse_association(user_class_instance, model_class, model_plural)
  return unless model_class.respond_to?(:model_updater_enabled?) && model_class.model_updater_enabled?

  association_name = generate_reverse_association_name("updated", model_plural)
  setup_user_reverse_association(user_class_instance, association_name, model_class, :updater_id)
end

.setup_user_reverse_association(user_class_instance, association_name, model_class, foreign_key_column) ⇒ void

This method returns an undefined value.

Set up a specific reverse association on the user class

Parameters:

  • user_class_instance (Class)

    the user class

  • association_name (String)

    the name of the association

  • model_class (Class)

    the model class

  • foreign_key_column (Symbol)

    the foreign key column name

Since:

  • 0.1.0



303
304
305
306
307
308
309
310
311
312
313
# File 'lib/whodunit.rb', line 303

def self.setup_user_reverse_association(user_class_instance, association_name, model_class, foreign_key_column)
  actual_foreign_key = resolve_foreign_key(model_class, foreign_key_column)

  # Check if association already exists to avoid duplicates
  return if user_class_instance.reflect_on_association(association_name.to_sym)

  user_class_instance.has_many association_name.to_sym,
                               class_name: model_class.name,
                               foreign_key: actual_foreign_key,
                               dependent: :nullify
end

.soft_delete_enabled?Boolean

Check if soft-delete is enabled

Returns:

  • (Boolean)

    true if soft-delete is configured (soft_delete_column is not nil)

Since:

  • 0.1.0



181
182
183
# File 'lib/whodunit.rb', line 181

def self.soft_delete_enabled?
  !soft_delete_column.nil?
end

.updater_data_typeSymbol

Get the data type for the updater column

Returns:

  • (Symbol)

    the updater column data type

Since:

  • 0.1.0



169
170
171
# File 'lib/whodunit.rb', line 169

def self.updater_data_type
  updater_column_type || column_data_type
end

.updater_enabled?Boolean

Check if updater column is enabled

Returns:

  • (Boolean)

    true if updater_column is not nil

Since:

  • 0.1.0



193
194
195
# File 'lib/whodunit.rb', line 193

def self.updater_enabled?
  !updater_column.nil?
end

.user_class_nameString

Get the user class name as a string

Returns:

  • (String)

    the user class name

Since:

  • 0.1.0



140
141
142
# File 'lib/whodunit.rb', line 140

def self.user_class_name
  user_class.to_s
end

.user_table_nameString

Resolve the configured user table name without loading the model. Migration execution can happen before the user table exists, so constantizing the user class here may cause Active Record to query a missing relation. Explicit configuration takes precedence; class objects may still provide their custom Active Record table name directly.

Returns:

  • (String)

    the user table name

Since:

  • 0.1.0



151
152
153
154
155
156
157
# File 'lib/whodunit.rb', line 151

def self.user_table_name
  return user_class_table_name.to_s if user_class_table_name

  return user_class.table_name.to_s if user_class.respond_to?(:table_name)

  user_class_name.underscore.pluralize
end

.validate_column_configuration!Object

Validate that column configuration is valid

Raises:

Since:

  • 0.1.0



334
335
336
337
338
339
340
# File 'lib/whodunit.rb', line 334

def self.validate_column_configuration!
  return if creator_enabled? || updater_enabled?

  raise Whodunit::Error,
        "At least one of creator_column or updater_column must be configured (not nil). " \
        "Setting both to nil would disable all stamping functionality."
end

Instance Method Details

#auto_create_user_fk_constraintsBoolean

Whether migration helpers should create foreign keys to the user table.

Returns:

  • (Boolean)

    automatic user foreign-key constraint setting

Since:

  • 0.1.0



75
# File 'lib/whodunit.rb', line 75

mattr_accessor :auto_create_user_fk_constraints, default: false

#auto_inject_whodunit_stampsBoolean

Whether to automatically add whodunit_stamps to create_table migrations (default: true)

Returns:

  • (Boolean)

    auto-injection setting

Since:

  • 0.1.0



71
# File 'lib/whodunit.rb', line 71

mattr_accessor :auto_inject_whodunit_stamps, default: true

#auto_setup_reverse_associationsBoolean

Whether to automatically set up reverse associations on the user class (default: true) When enabled, including Whodunit::Stampable in a model will automatically add has_many associations to the user class (e.g., has_many :created_posts)

Returns:

  • (Boolean)

    auto reverse association setting

Since:

  • 0.1.0



101
# File 'lib/whodunit.rb', line 101

mattr_accessor :auto_setup_reverse_associations, default: true

#column_data_typeSymbol

The default data type for stamp columns (default: :bigint)

Returns:

  • (Symbol)

    the default column data type

Since:

  • 0.1.0



81
# File 'lib/whodunit.rb', line 81

mattr_accessor :column_data_type, default: :bigint

#creator_columnSymbol

The column name for tracking who created the record (default: :creator_id)

Returns:

  • (Symbol)

    the creator column name

Since:

  • 0.1.0



53
# File 'lib/whodunit.rb', line 53

mattr_accessor :creator_column, default: :creator_id

#creator_column_typeSymbol?

Specific data type for creator column (overrides column_data_type if set)

Returns:

  • (Symbol, nil)

    the creator column data type

Since:

  • 0.1.0



85
# File 'lib/whodunit.rb', line 85

mattr_accessor :creator_column_type, default: nil

#deleter_columnSymbol

The column name for tracking who deleted the record (default: :deleter_id)

Returns:

  • (Symbol)

    the deleter column name

Since:

  • 0.1.0



61
# File 'lib/whodunit.rb', line 61

mattr_accessor :deleter_column, default: :deleter_id

#deleter_column_typeSymbol?

Specific data type for deleter column (overrides column_data_type if set)

Returns:

  • (Symbol, nil)

    the deleter column data type

Since:

  • 0.1.0



93
# File 'lib/whodunit.rb', line 93

mattr_accessor :deleter_column_type, default: nil

#registered_modelsArray<Class>

Registry to track models that include Whodunit::Stampable This is used to set up reverse associations on the user class

Returns:

  • (Array<Class>)

    array of model classes that include Stampable

Since:

  • 0.1.0



118
# File 'lib/whodunit.rb', line 118

mattr_accessor :registered_models, default: []

#reverse_association_prefixString

Prefix for reverse association names (default: “”) Used to generate association names like “created_posts”, “updated_comments”

Returns:

  • (String)

    the prefix for reverse association names

Since:

  • 0.1.0



106
# File 'lib/whodunit.rb', line 106

mattr_accessor :reverse_association_prefix, default: ""

#reverse_association_suffixString

Suffix for reverse association names (default: “”) Used to generate association names like “posts_created”, “comments_updated”

Returns:

  • (String)

    the suffix for reverse association names

Since:

  • 0.1.0



111
# File 'lib/whodunit.rb', line 111

mattr_accessor :reverse_association_suffix, default: ""

#soft_delete_columnSymbol?

The column name used for soft-delete timestamps (default: nil) Set to a column name to enable soft-delete support (e.g., :deleted_at, :discarded_at) Set to nil to disable soft-delete support entirely

Returns:

  • (Symbol, nil)

    the soft-delete column name

Since:

  • 0.1.0



67
# File 'lib/whodunit.rb', line 67

mattr_accessor :soft_delete_column, default: nil

#updater_columnSymbol

The column name for tracking who updated the record (default: :updater_id)

Returns:

  • (Symbol)

    the updater column name

Since:

  • 0.1.0



57
# File 'lib/whodunit.rb', line 57

mattr_accessor :updater_column, default: :updater_id

#updater_column_typeSymbol?

Specific data type for updater column (overrides column_data_type if set)

Returns:

  • (Symbol, nil)

    the updater column data type

Since:

  • 0.1.0



89
# File 'lib/whodunit.rb', line 89

mattr_accessor :updater_column_type, default: nil

#user_classString

The class name of the user model (default: “User”)

Returns:

  • (String)

    the user class name

Since:

  • 0.1.0



44
# File 'lib/whodunit.rb', line 44

mattr_accessor :user_class, default: "User"

#user_class_table_nameString, ...

Explicit user table name override. When nil, the table name is inferred from the configured user class.

Returns:

  • (String, Symbol, nil)

    the user table name override

Since:

  • 0.1.0



49
# File 'lib/whodunit.rb', line 49

mattr_accessor :user_class_table_name, default: nil