Class: PaperTrailHistory::TrackableModel

Inherits:
Object
  • Object
show all
Defined in:
app/models/paper_trail_history/trackable_model.rb

Overview

Wraps an ActiveRecord class that uses PaperTrail and answers the questions that the interface asks about it: which version class holds its versions, how many versions exist, and which of them are the newest.

The list of trackable classes is expensive to build, thus the class caches it. TrackableModel.clear_cache! empties the cache, and the engine calls that method on each code reload.

Examples:

Read the versions of one model

trackable = PaperTrailHistory::TrackableModel.find('User')
trackable.total_versions_count # => 1234
trackable.recent_versions(5)   # => the five newest versions

Constant Summary collapse

LOCK =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Guards the cache. A Monitor is reentrant, thus the discovery can reach this class again without a deadlock.

Monitor.new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(klass, cached_version_count = nil) ⇒ TrackableModel

Returns a new instance of TrackableModel.

Parameters:

  • klass (Class)

    an ActiveRecord class that uses has_paper_trail

  • cached_version_count (Integer, nil) (defaults to: nil)

    a count that all_with_counts read for every model at once



31
32
33
34
35
# File 'app/models/paper_trail_history/trackable_model.rb', line 31

def initialize(klass, cached_version_count = nil)
  @klass = klass
  @name = klass.name
  @cached_version_count = cached_version_count
end

Instance Attribute Details

#cached_version_countString, ... (readonly)

Returns:

  • (String)

    name of the wrapped class

  • (Class)

    the wrapped ActiveRecord class

  • (Integer, nil)

    version count from a bulk count, or nil



26
27
28
# File 'app/models/paper_trail_history/trackable_model.rb', line 26

def cached_version_count
  @cached_version_count
end

#klassString, ... (readonly)

Returns:

  • (String)

    name of the wrapped class

  • (Class)

    the wrapped ActiveRecord class

  • (Integer, nil)

    version count from a bulk count, or nil



26
27
28
# File 'app/models/paper_trail_history/trackable_model.rb', line 26

def klass
  @klass
end

#nameString, ... (readonly)

Returns:

  • (String)

    name of the wrapped class

  • (Class)

    the wrapped ActiveRecord class

  • (Integer, nil)

    version count from a bulk count, or nil



26
27
28
# File 'app/models/paper_trail_history/trackable_model.rb', line 26

def name
  @name
end

Class Method Details

.allArray<TrackableModel>

All classes of the application that use PaperTrail, sorted by name.

The result comes from the cache after the first call.

Returns:



42
43
44
# File 'app/models/paper_trail_history/trackable_model.rb', line 42

def self.all
  @all_models || LOCK.synchronize { @all_models ||= discover_all }
end

.all_with_countsArray<TrackableModel>

All trackable models, each one with its version count.

The counts come from one query for each version table. Use this method for a list of models, because #total_versions_count on each model alone makes one query for each model.

Returns:



82
83
84
85
86
87
88
# File 'app/models/paper_trail_history/trackable_model.rb', line 82

def self.all_with_counts
  models = all
  return models if models.empty?

  count_cache = build_count_cache(models)
  models_with_cached_counts(models, count_cache)
end

.build_count_cache(models) ⇒ Hash{String => Integer}

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns count for each model name.

Parameters:

Returns:

  • (Hash{String => Integer})

    count for each model name



93
94
95
96
97
98
# File 'app/models/paper_trail_history/trackable_model.rb', line 93

def self.build_count_cache(models)
  models_by_version_class = models.group_by(&:version_class)
  count_cache = {}
  populate_count_cache(models_by_version_class, count_cache)
  count_cache
end

.cache_counts_for_models(models_for_class, counts, count_cache) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Parameters:

  • models_for_class (Array<TrackableModel>)
  • counts (Hash)
  • count_cache (Hash{String => Integer})

    gets the counts



135
136
137
138
139
# File 'app/models/paper_trail_history/trackable_model.rb', line 135

def self.cache_counts_for_models(models_for_class, counts, count_cache)
  models_for_class.each do |model|
    count_cache[model.name] = count_for(model, counts)
  end
end

.clear_cache!void

This method returns an undefined value.

Clears the cached models. The engine calls this on each code reload, because the cache holds class objects that a reload replaces.



179
180
181
# File 'app/models/paper_trail_history/trackable_model.rb', line 179

def self.clear_cache!
  LOCK.synchronize { @all_models = nil }
end

.count_for(model, counts) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Reads the count of one model out of the grouped result.

A subclass counts only the rows of its own subtype. A base class counts all of its rows, thus the rows of its subclasses belong to it. This is the same rule that ActiveRecord uses for a query on the base class.

Parameters:

Returns:

  • (Integer)


151
152
153
154
155
156
# File 'app/models/paper_trail_history/trackable_model.rb', line 151

def self.count_for(model, counts)
  return counts[model.item_type_for_versions].to_i unless counts.keys.first.is_a?(Array)
  return counts.fetch([model.item_type_for_versions, model.name], 0) if model.sti_subclass?

  counts.sum { |(item_type, _subtype), count| item_type == model.item_type_for_versions ? count : 0 }
end

.discover_allArray<TrackableModel>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Searches the application for classes that use PaperTrail.

The search asks ActiveRecord for its descendants. It does not walk through every object of the process, because that is slow and it also finds classes that Rails removed at a code reload.

The eager load is necessary: Rails loads a class only when the code asks for it, thus a model that no request touched yet is not a descendant. The engine calls the discovery one time for each code reload, thus the cost happens one time and not on each request.

Returns:



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'app/models/paper_trail_history/trackable_model.rb', line 59

def self.discover_all
  Rails.application.eager_load!

  # Only keep one instance per class name to avoid duplicates
  trackable_classes = {}
  ActiveRecord::Base.descendants.each do |klass|
    next if klass.name.nil? # an anonymous class has no name to show
    next if klass.abstract_class?
    next unless klass.included_modules.include?(PaperTrail::Model::InstanceMethods)

    trackable_classes[klass.name] = new(klass)
  end

  trackable_classes.values.sort_by(&:name)
end

.fetch_version_counts(version_class, models_for_class) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Counts the versions of a version table with one query.

The query also groups by item_subtype when the table has that column, thus a subclass of a single table inheritance does not need its own query. One query for each version table is the whole cost.

Parameters:

  • version_class (Class)

    a PaperTrail version class

  • models_for_class (Array<TrackableModel>)

Returns:

  • (Hash)

    count for each item type, or for each pair of item type and item subtype



122
123
124
125
126
127
128
# File 'app/models/paper_trail_history/trackable_model.rb', line 122

def self.fetch_version_counts(version_class, models_for_class)
  item_types = models_for_class.map(&:item_type_for_versions)
  scope = version_class.where(item_type: item_types)
  return scope.group(:item_type).count unless version_class.column_names.include?('item_subtype')

  scope.group(:item_type, :item_subtype).count
end

.find(model_name) ⇒ TrackableModel?

Finds a trackable model by class name.

Parameters:

  • model_name (String)

    name of the class, for example 'User'

Returns:

  • (TrackableModel, nil)

    nil if the class does not exist or does not use PaperTrail



171
172
173
# File 'app/models/paper_trail_history/trackable_model.rb', line 171

def self.find(model_name)
  all.find { |model| model.name == model_name }
end

.models_with_cached_counts(models, count_cache) ⇒ Array<TrackableModel>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns new wrappers that hold their count.

Parameters:

  • models (Array<TrackableModel>)
  • count_cache (Hash{String => Integer})

Returns:



162
163
164
# File 'app/models/paper_trail_history/trackable_model.rb', line 162

def self.models_with_cached_counts(models, count_cache)
  models.map { |model| new(model.klass, count_cache[model.name]) }
end

.populate_count_cache(models_by_version_class, count_cache) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Parameters:

  • models_by_version_class (Hash{Class => Array<TrackableModel>})
  • count_cache (Hash{String => Integer})

    gets the counts



104
105
106
107
108
109
# File 'app/models/paper_trail_history/trackable_model.rb', line 104

def self.populate_count_cache(models_by_version_class, count_cache)
  models_by_version_class.each do |version_class, models_for_class|
    counts = fetch_version_counts(version_class, models_for_class)
    cache_counts_for_models(models_for_class, counts, count_cache)
  end
end

Instance Method Details

#human_nameString

The name of the model for a person, in plural and in the current language.

Returns:

  • (String)


264
265
266
# File 'app/models/paper_trail_history/trackable_model.rb', line 264

def human_name
  klass.model_name.human(count: 2)
end

#item_subtype_available?Boolean

Tells if the version table can separate the subclasses.

Returns:

  • (Boolean)

    true if the version table has an item_subtype column



217
218
219
# File 'app/models/paper_trail_history/trackable_model.rb', line 217

def item_subtype_available?
  version_class.column_names.include?('item_subtype')
end

#item_type_for_versionsString

The value that PaperTrail writes into the item_type column for this model.

Returns:

  • (String)

    name of the base class



203
204
205
# File 'app/models/paper_trail_history/trackable_model.rb', line 203

def item_type_for_versions
  klass.base_class.name
end

#recent_versions(limit = 10) ⇒ ActiveRecord::Relation

The newest versions of this model.

The list of recent versions shows the name of the item of each version. Without the preload, each row makes its own query.

Parameters:

  • limit (Integer) (defaults to: 10)

    how many versions

Returns:

  • (ActiveRecord::Relation)


238
239
240
# File 'app/models/paper_trail_history/trackable_model.rb', line 238

def recent_versions(limit = 10)
  versions.includes(:item).order(created_at: :desc).limit(limit)
end

#sti_subclass?Boolean

Tells if this model is a single table inheritance subclass.

Returns:

  • (Boolean)


210
211
212
# File 'app/models/paper_trail_history/trackable_model.rb', line 210

def sti_subclass?
  klass != klass.base_class
end

#table_nameString

Returns name of the table of the wrapped class.

Returns:

  • (String)

    name of the table of the wrapped class



259
# File 'app/models/paper_trail_history/trackable_model.rb', line 259

delegate :table_name, to: :klass

#total_versions_countInteger

The number of versions of this model.

The method uses the count of all_with_counts if it has one, otherwise it makes a query.

Returns:

  • (Integer)


227
228
229
# File 'app/models/paper_trail_history/trackable_model.rb', line 227

def total_versions_count
  @cached_version_count || versions.count
end

#version_classClass

The class that holds the versions of this model.

A model can keep its versions in its own table with has_paper_trail versions: { class_name: 'ProductVersion' }.

Returns:

  • (Class)


248
249
250
# File 'app/models/paper_trail_history/trackable_model.rb', line 248

def version_class
  @version_class ||= klass.paper_trail.version_class || PaperTrail::Version
end

#version_table_nameString

Returns name of the table that holds the versions.

Returns:

  • (String)

    name of the table that holds the versions



253
254
255
# File 'app/models/paper_trail_history/trackable_model.rb', line 253

def version_table_name
  version_class.table_name
end

#versionsActiveRecord::Relation

The versions of this model.

PaperTrail writes the name of the base class into item_type, thus a query by the name of a subclass finds nothing. The engine asks for the base class and narrows the result with item_subtype, which PaperTrail fills when the version table has that column. A version table without item_subtype cannot tell the subclasses apart, thus a subclass then shows the versions of its base class.

Returns:

  • (ActiveRecord::Relation)


193
194
195
196
197
198
# File 'app/models/paper_trail_history/trackable_model.rb', line 193

def versions
  scope = version_class.where(item_type: item_type_for_versions)
  return scope unless sti_subclass? && item_subtype_available?

  scope.where(item_subtype: klass.name)
end