Class: PaperTrailHistory::VersionService

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

Overview

Queries and restores PaperTrail versions for the interface.

The class knows about applications with more than one version table: it resolves the version class through TrackableModel instead of assuming PaperTrail::Version.

Examples:

List the versions of a model with a filter

PaperTrailHistory::VersionService.for_model('User', event: 'update')

Constant Summary collapse

SEARCH_TERM_LIMIT =

The longest search text that the engine sends to the database. A longer text costs time and gives no better result.

100
SEARCH_ESCAPE_CHARACTER =

Marks a wildcard of the user as a normal character. A backslash would need its own escaping in each database, this character does not.

'!'
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

Class Method Summary collapse

Class Method Details

.all_version_classesArray<Class>

Every version class that the application uses.

The result comes from a cache. clear_cache! empties it.

Returns:

  • (Array<Class>)


180
181
182
# File 'app/models/paper_trail_history/version_service.rb', line 180

def self.all_version_classes
  @all_version_classes || LOCK.synchronize { @all_version_classes ||= discover_version_classes }
end

.available_events(model_name = nil) ⇒ Array<String>

The different events that appear in the versions.

Parameters:

  • model_name (String, nil) (defaults to: nil)

    one model, or nil for all models

Returns:

  • (Array<String>)

    sorted, without duplicates



122
123
124
# File 'app/models/paper_trail_history/version_service.rb', line 122

def self.available_events(model_name = nil)
  distinct_column_values(:event, model_name)
end

.clear_cache!Object

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



192
193
194
# File 'app/models/paper_trail_history/version_service.rb', line 192

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

.find_version(version_id, model_name = nil) ⇒ ActiveRecord::Base?

Finds one version.

Give the model name whenever you have it. Without it the method looks in every version table, and an ID is not unique over more than one table.

Parameters:

  • version_id (Integer, String)
  • model_name (String, nil) (defaults to: nil)

    name of the trackable class

Returns:

  • (ActiveRecord::Base, nil)


67
68
69
70
71
72
73
74
75
76
77
78
# File 'app/models/paper_trail_history/version_service.rb', line 67

def self.find_version(version_id, model_name = nil)
  if model_name.present?
    # Direct query when model_name is known (fast)
    trackable_model = TrackableModel.find(model_name)
    return nil unless trackable_model

    trackable_model.version_class.unscoped.find_by(id: version_id)
  else
    # Fall back to searching across all tables (backwards compatible)
    find_version_across_tables(version_id)
  end
end

.for_model(model_name, params = {}) ⇒ ActiveRecord::Relation

The versions of one model, newest first.

Parameters:

  • model_name (String)

    name of a trackable class

  • params (Hash) (defaults to: {})

    filter values, see apply_filters

Options Hash (params):

  • :event (String)

    only this event

  • :whodunnit (String)

    only this person

  • :from_date (String)

    only versions from this date

  • :to_date (String)

    only versions up to this date

  • :search (String)

    only versions whose stored data contain this text

Returns:

  • (ActiveRecord::Relation)

    an empty relation for a model that does not exist or is not trackable



33
34
35
36
37
38
39
# File 'app/models/paper_trail_history/version_service.rb', line 33

def self.for_model(model_name, params = {})
  trackable_model = TrackableModel.find(model_name)
  return PaperTrail::Version.none unless trackable_model

  versions = base_versions_for_model(trackable_model)
  apply_filters(versions, params).order(created_at: :desc)
end

.for_record(model_name, item_id, params = {}) ⇒ ActiveRecord::Relation

The versions of one record, newest first.

Parameters:

  • model_name (String)

    name of a trackable class

  • item_id (Integer, String)

    ID of the record

  • params (Hash) (defaults to: {})

    filter values. This list has no :whodunnit and no :search, because the interface does not offer them for one record.

Options Hash (params):

  • :event (String)

    only this event

  • :from_date (String)

    only versions from this date

  • :to_date (String)

    only versions up to this date

Returns:

  • (ActiveRecord::Relation)


51
52
53
54
55
56
57
# File 'app/models/paper_trail_history/version_service.rb', line 51

def self.for_record(model_name, item_id, params = {})
  trackable_model = TrackableModel.find(model_name)
  return PaperTrail::Version.none unless trackable_model

  versions = base_versions_for_record(trackable_model, item_id)
  apply_record_filters(versions, params).order(created_at: :desc)
end

.restore_version(version) ⇒ Hash

Restores a record to the state of the given version.

Give the version record itself whenever you have it. An ID alone is not unique: an application with more than one version table (PaperTrail's versions plus a custom class such as ProductVersion) can hold the same ID in each table, and a search by ID finds whichever table comes first.

PaperTrail keeps the previous state of a record as YAML. Rails loads only permitted classes from a YAML column, thus the host application has to list the types that its models use in config.active_record.yaml_column_permitted_classes. If a class is missing, this method gives back an error that names the setting.

Parameters:

  • version (ActiveRecord::Base, Integer, String)

    the version record, or its ID for backwards compatibility

Returns:

  • (Hash)

    :success, and either :item and :message or :error



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'app/models/paper_trail_history/version_service.rb', line 96

def self.restore_version(version)
  version = find_version_across_tables(version) unless version.is_a?(ActiveRecord::Base)
  return validate_version_for_restore(version) unless version_restorable?(version)

  perform_version_restore(version)
rescue Psych::DisallowedClass => e
  {
    success: false,
    error: I18n.t('paper_trail_history.errors.yaml_class_not_permitted', message: e.message)
  }
rescue StandardError => e
  { success: false, error: e.message }
end

.unique_whodunnits(model_name = nil) ⇒ Array<String>

The different people that appear in the versions.

Parameters:

  • model_name (String, nil) (defaults to: nil)

    one model, or nil for all models

Returns:

  • (Array<String>)

    sorted, without duplicates



114
115
116
# File 'app/models/paper_trail_history/version_service.rb', line 114

def self.unique_whodunnits(model_name = nil)
  distinct_column_values(:whodunnit, model_name)
end