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.3.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



123
124
125
126
# File 'lib/whodunit.rb', line 123

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



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

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



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

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



151
152
153
# File 'lib/whodunit.rb', line 151

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



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

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



224
225
226
# File 'lib/whodunit.rb', line 224

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



185
186
187
188
189
190
191
192
193
# File 'lib/whodunit.rb', line 185

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



295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/whodunit.rb', line 295

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



230
231
232
233
234
# File 'lib/whodunit.rb', line 230

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



214
215
216
217
218
# File 'lib/whodunit.rb', line 214

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



241
242
243
244
245
246
# File 'lib/whodunit.rb', line 241

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



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

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



198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/whodunit.rb', line 198

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



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

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



279
280
281
282
283
284
285
286
287
288
289
# File 'lib/whodunit.rb', line 279

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



157
158
159
# File 'lib/whodunit.rb', line 157

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



145
146
147
# File 'lib/whodunit.rb', line 145

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



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

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



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

def self.user_class_name
  user_class.to_s
end

.validate_column_configuration!Object

Validate that column configuration is valid

Raises:

Since:

  • 0.1.0



310
311
312
313
314
315
316
# File 'lib/whodunit.rb', line 310

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_inject_whodunit_stampsBoolean

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

Returns:

  • (Boolean)

    auto-injection setting

Since:

  • 0.1.0



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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"