Module: ChronoModel::TimeMachine

Extended by:
ActiveSupport::Concern
Includes:
Patches::AsOfTimeHolder
Defined in:
lib/chrono_model/time_machine.rb,
lib/chrono_model/time_machine/timeline.rb,
lib/chrono_model/time_machine/time_query.rb,
lib/chrono_model/time_machine/history_model.rb

Defined Under Namespace

Modules: ClassMethods, HistoryModel, TimeQuery, Timeline

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Patches::AsOfTimeHolder

#as_of_time, #as_of_time!

Class Method Details

.define_history_model_for(model) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/chrono_model/time_machine.rb', line 47

def self.define_history_model_for(model)
  history = Class.new(model) { include ChronoModel::TimeMachine::HistoryModel }

  model.singleton_class.instance_eval do
    define_method(:history) { history }
  end

  history.singleton_class.instance_eval do
    define_method(:sti_name) { model.sti_name }
  end

  model.const_set :History, history

  return history
end

.define_inherited_history_model_for(subclass) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/chrono_model/time_machine.rb', line 63

def self.define_inherited_history_model_for(subclass)
  # Define history model for the subclass
  history = Class.new(subclass.superclass.history)
  history.table_name = subclass.superclass.history.table_name

  # Override the STI name on the history subclass
  history.singleton_class.instance_eval do
    define_method(:sti_name) { subclass.sti_name }
  end

  # Return the subclass history via the .history method
  subclass.singleton_class.instance_eval do
    define_method(:history) { history }
  end

  # Define the History constant inside the subclass
  subclass.const_set :History, history

  history.instance_eval do
    # Monkey patch of ActiveRecord::Inheritance.
    # STI fails when a Foo::History record has Foo as type in the
    # inheritance column; AR expects the type to be an instance of the
    # current class or a descendant (or self).
    def find_sti_class(type_name)
      super(type_name + "::History")
    end
  end
end

Instance Method Details

#as_of(time) ⇒ Object

Returns a read-only representation of this record as it was time ago. Returns nil if no record is found.



126
127
128
# File 'lib/chrono_model/time_machine.rb', line 126

def as_of(time)
  _as_of(time).first
end

#as_of!(time) ⇒ Object

Returns a read-only representation of this record as it was time ago. Raises ActiveRecord::RecordNotFound if no record is found.



133
134
135
# File 'lib/chrono_model/time_machine.rb', line 133

def as_of!(time)
  _as_of(time).first!
end

#changes_against(ref) ⇒ Object

Returns the differences between this record and an arbitrary reference record. The changes representation is an hash keyed by attribute whose values are arrays containing previous and current attributes values - the same format used by ActiveModel::Dirty.



224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/chrono_model/time_machine.rb', line 224

def changes_against(ref)
  self.class.attribute_names_for_history_changes.inject({}) do |changes, attr|
    old, new = ref.public_send(attr), self.public_send(attr)

    changes.tap do |c|
      changed = old.respond_to?(:history_eql?) ?
        !old.history_eql?(new) : old != new

      c[attr] = [old, new] if changed
    end
  end
end

#current_versionObject

Returns the current history version



207
208
209
# File 'lib/chrono_model/time_machine.rb', line 207

def current_version
  self.historical? ? self.class.find(self.id) : self
end

#destroyObject

Inhibit destroy of historical records

Raises:

  • (ActiveRecord::ReadOnlyRecord)


167
168
169
170
# File 'lib/chrono_model/time_machine.rb', line 167

def destroy
  raise ActiveRecord::ReadOnlyRecord, 'Cannot delete historical records' if historical?
  super
end

#historical?Boolean

Returns a boolean indicating whether this record is an history entry.

Returns:

  • (Boolean)


161
162
163
# File 'lib/chrono_model/time_machine.rb', line 161

def historical?
  self.as_of_time.present? || self.kind_of?(self.class.history)
end

#historyObject

Return the complete read-only history of this instance.



148
149
150
# File 'lib/chrono_model/time_machine.rb', line 148

def history
  self.class.history.chronological.of(self)
end

#last_changesObject

Returns the differences between this entry and the previous history one. See: changes_against.



214
215
216
217
# File 'lib/chrono_model/time_machine.rb', line 214

def last_changes
  pred = self.pred
  changes_against(pred) if pred
end

#pred(options = {}) ⇒ Object

Returns the previous record in the history, or nil if this is the only recorded entry.



175
176
177
178
179
180
181
182
183
184
185
# File 'lib/chrono_model/time_machine.rb', line 175

def pred(options = {})
  if self.class.timeline_associations.empty?
    history.order(Arel.sql('upper(validity) DESC')).offset(1).first
  else
    return nil unless (ts = pred_timestamp(options))

    order_clause = Arel.sql %[ LOWER(#{options[:table] || self.class.quoted_table_name}."validity") DESC ]

    self.class.as_of(ts).order(order_clause).find(options[:id] || id)
  end
end

#pred_timestamp(options = {}) ⇒ Object

Returns the previous timestamp in this record’s timeline. Includes temporal associations.



190
191
192
193
194
195
196
197
# File 'lib/chrono_model/time_machine.rb', line 190

def pred_timestamp(options = {})
  if historical?
    options[:before] ||= as_of_time
    timeline(options.merge(limit: 1, reverse: true)).first
  else
    timeline(options.merge(limit: 2, reverse: true)).second
  end
end

#succObject

This is a current record, so its next instance is always nil.



201
202
203
# File 'lib/chrono_model/time_machine.rb', line 201

def succ
  nil
end

#timeline(options = {}) ⇒ Object

Returns an Array of timestamps for which this instance has an history record. Takes temporal associations into account.



155
156
157
# File 'lib/chrono_model/time_machine.rb', line 155

def timeline(options = {})
  self.class.history.timeline(self, options)
end