Class: PaperTrailHistory::TrackableModel
- Inherits:
-
Object
- Object
- PaperTrailHistory::TrackableModel
- 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.
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
- #cached_version_count ⇒ String, ... readonly
- #klass ⇒ String, ... readonly
- #name ⇒ String, ... readonly
Class Method Summary collapse
-
.all ⇒ Array<TrackableModel>
All classes of the application that use PaperTrail, sorted by name.
-
.all_with_counts ⇒ Array<TrackableModel>
All trackable models, each one with its version count.
-
.build_count_cache(models) ⇒ Hash{String => Integer}
private
Count for each model name.
- .cache_counts_for_models(models_for_class, counts, count_cache) ⇒ void private
-
.clear_cache! ⇒ void
Clears the cached models.
-
.count_for(model, counts) ⇒ Integer
private
Reads the count of one model out of the grouped result.
-
.discover_all ⇒ Array<TrackableModel>
private
Searches the application for classes that use PaperTrail.
-
.fetch_version_counts(version_class, models_for_class) ⇒ Hash
private
Counts the versions of a version table with one query.
-
.find(model_name) ⇒ TrackableModel?
Finds a trackable model by class name.
-
.models_with_cached_counts(models, count_cache) ⇒ Array<TrackableModel>
private
New wrappers that hold their count.
- .populate_count_cache(models_by_version_class, count_cache) ⇒ void private
Instance Method Summary collapse
-
#human_name ⇒ String
The name of the model for a person, in plural and in the current language.
-
#initialize(klass, cached_version_count = nil) ⇒ TrackableModel
constructor
A new instance of TrackableModel.
-
#item_subtype_available? ⇒ Boolean
Tells if the version table can separate the subclasses.
-
#item_type_for_versions ⇒ String
The value that PaperTrail writes into the item_type column for this model.
-
#recent_versions(limit = 10) ⇒ ActiveRecord::Relation
The newest versions of this model.
-
#sti_subclass? ⇒ Boolean
Tells if this model is a single table inheritance subclass.
-
#table_name ⇒ String
Name of the table of the wrapped class.
-
#total_versions_count ⇒ Integer
The number of versions of this model.
-
#version_class ⇒ Class
The class that holds the versions of this model.
-
#version_table_name ⇒ String
Name of the table that holds the versions.
-
#versions ⇒ ActiveRecord::Relation
The versions of this model.
Constructor Details
#initialize(klass, cached_version_count = nil) ⇒ TrackableModel
Returns a new instance of TrackableModel.
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_count ⇒ String, ... (readonly)
26 27 28 |
# File 'app/models/paper_trail_history/trackable_model.rb', line 26 def cached_version_count @cached_version_count end |
#klass ⇒ String, ... (readonly)
26 27 28 |
# File 'app/models/paper_trail_history/trackable_model.rb', line 26 def klass @klass end |
#name ⇒ String, ... (readonly)
26 27 28 |
# File 'app/models/paper_trail_history/trackable_model.rb', line 26 def name @name end |
Class Method Details
.all ⇒ Array<TrackableModel>
All classes of the application that use PaperTrail, sorted by name.
The result comes from the cache after the first call.
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_counts ⇒ Array<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.
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.
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.
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.
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_all ⇒ 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.
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.
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.
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.
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.
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.
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_name ⇒ String
The name of the model for a person, in plural and in the current language.
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.
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_versions ⇒ String
The value that PaperTrail writes into the item_type column for this model.
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.
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.
210 211 212 |
# File 'app/models/paper_trail_history/trackable_model.rb', line 210 def sti_subclass? klass != klass.base_class end |
#table_name ⇒ String
Returns 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_count ⇒ Integer
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.
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_class ⇒ Class
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' }.
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_name ⇒ String
Returns 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 |
#versions ⇒ ActiveRecord::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.
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 |