Module: ChronoModel::TimeMachine
- Extended by:
- ActiveSupport::Concern
- Includes:
- Patches::AsOfTimeHolder
- Defined in:
- lib/chrono_model/time_machine.rb
Defined Under Namespace
Modules: ClassMethods, HistoryMethods, TimeQuery
Class Method Summary collapse
-
.chrono_models ⇒ Object
Returns an Hash keyed by table name of ChronoModels.
- .define_history_model_for(model) ⇒ Object
- .define_inherited_history_model_for(subclass) ⇒ Object
Instance Method Summary collapse
-
#as_of(time) ⇒ Object
Returns a read-only representation of this record as it was
timeago. -
#as_of!(time) ⇒ Object
Returns a read-only representation of this record as it was
timeago. -
#changes_against(ref) ⇒ Object
Returns the differences between this record and an arbitrary reference record.
-
#current_version ⇒ Object
Returns the current history version.
-
#destroy ⇒ Object
Inhibit destroy of historical records.
-
#historical? ⇒ Boolean
Returns a boolean indicating whether this record is an history entry.
-
#history ⇒ Object
Return the complete read-only history of this instance.
-
#last_changes ⇒ Object
Returns the differences between this entry and the previous history one.
-
#pred(options = {}) ⇒ Object
Returns the previous record in the history, or nil if this is the only recorded entry.
-
#pred_timestamp(options = {}) ⇒ Object
Returns the previous timestamp in this record’s timeline.
-
#succ(options = {}) ⇒ Object
Returns the next record in the history timeline.
-
#succ_timestamp(options = {}) ⇒ Object
Returns the next timestamp in this record’s timeline.
-
#timeline(options = {}) ⇒ Object
Returns an Array of timestamps for which this instance has an history record.
Methods included from Patches::AsOfTimeHolder
Class Method Details
.chrono_models ⇒ Object
Returns an Hash keyed by table name of ChronoModels
47 48 49 |
# File 'lib/chrono_model/time_machine.rb', line 47 def self.chrono_models (@chrono_models ||= {}) end |
.define_history_model_for(model) ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/chrono_model/time_machine.rb', line 51 def self.define_history_model_for(model) history = Class.new(model) do self.table_name = [Adapter::HISTORY_SCHEMA, model.table_name].join('.') extend TimeMachine::HistoryMethods scope :chronological, -> { order(:recorded_at, :hid) } # The history id is `hid`, but this cannot set as primary key # or temporal assocations will break. Solutions are welcome. def id hid end # Referenced record ID. # def rid attributes[self.class.primary_key] end # HACK. find() and save() require the real history ID. So we are # setting it now and ensuring to reset it to the original one after # execution completes. # def self.with_hid_pkey(&block) old = self.primary_key self.primary_key = :hid block.call ensure self.primary_key = old end def self.find(*) with_hid_pkey { super } end if RUBY_VERSION.to_f < 2.0 # PLEASE UPDATE YOUR RUBY <3 # def save_with_pkey(*) self.class.with_hid_pkey { save_without_pkey } end def save_with_pkey!(*) self.class.with_hid_pkey { save_without_pkey! } end alias_method_chain :save, :pkey else def save(*) self.class.with_hid_pkey { super } end def save!(*) self.class.with_hid_pkey { super } end end # Returns the previous history entry, or nil if this # is the first one. # def pred return if self.valid_from.nil? if self.class.timeline_associations.empty? self.class.where('id = ? AND upper(validity) = ?', rid, valid_from).first else super(:id => rid, :before => valid_from, :table => self.class.superclass.quoted_table_name) end end # Returns the next history entry, or nil if this is the # last one. # def succ return if self.valid_to.nil? if self.class.timeline_associations.empty? self.class.where('id = ? AND lower(validity) = ?', rid, valid_to).first else super(:id => rid, :after => valid_to, :table => self.class.superclass.quoted_table_name) end end alias :next :succ # Returns the first history entry # def first self.class.where(:id => rid).order('lower(validity)').first end # Returns the last history entry # def last self.class.where(:id => rid).order('lower(validity)').last end # Returns this history entry's current record # def current_version self.class.non_history_superclass.find(rid) end def record #:nodoc: ActiveSupport::Deprecation.warn '.record is deprecated in favour of .current_version' self.current_version end def valid_from validity.first end def valid_to validity.last end alias as_of_time valid_to def recorded_at Conversions.string_to_utc_time attributes_before_type_cast['recorded_at'] end end 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
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/chrono_model/time_machine.rb', line 187 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.
219 220 221 |
# File 'lib/chrono_model/time_machine.rb', line 219 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.
226 227 228 |
# File 'lib/chrono_model/time_machine.rb', line 226 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.
327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/chrono_model/time_machine.rb', line 327 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_version ⇒ Object
Returns the current history version
310 311 312 |
# File 'lib/chrono_model/time_machine.rb', line 310 def current_version self.historical? ? self.class.find(self.id) : self end |
#destroy ⇒ Object
Inhibit destroy of historical records
260 261 262 263 |
# File 'lib/chrono_model/time_machine.rb', line 260 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.
254 255 256 |
# File 'lib/chrono_model/time_machine.rb', line 254 def historical? self.as_of_time.present? || self.kind_of?(self.class.history) end |
#history ⇒ Object
Return the complete read-only history of this instance.
241 242 243 |
# File 'lib/chrono_model/time_machine.rb', line 241 def history self.class.history.chronological.of(self) end |
#last_changes ⇒ Object
Returns the differences between this entry and the previous history one. See: changes_against.
317 318 319 320 |
# File 'lib/chrono_model/time_machine.rb', line 317 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.
268 269 270 271 272 273 274 275 |
# File 'lib/chrono_model/time_machine.rb', line 268 def pred( = {}) if self.class.timeline_associations.empty? history.order('upper(validity) DESC').offset(1).first else return nil unless (ts = ()) self.class.as_of(ts).order(%[ #{[:table] || self.class.quoted_table_name}."hid" DESC ]).find([:id] || id) end end |
#pred_timestamp(options = {}) ⇒ Object
Returns the previous timestamp in this record’s timeline. Includes temporal associations.
280 281 282 283 284 285 286 287 |
# File 'lib/chrono_model/time_machine.rb', line 280 def ( = {}) if historical? [:before] ||= as_of_time timeline(.merge(:limit => 1, :reverse => true)).first else timeline(.merge(:limit => 2, :reverse => true)).second end end |
#succ(options = {}) ⇒ Object
Returns the next record in the history timeline.
291 292 293 294 295 296 |
# File 'lib/chrono_model/time_machine.rb', line 291 def succ( = {}) unless self.class.timeline_associations.empty? return nil unless (ts = ()) self.class.as_of(ts).order(%[ #{[:table] || self.class.quoted_table_name}."hid" DESC ]).find([:id] || id) end end |
#succ_timestamp(options = {}) ⇒ Object
Returns the next timestamp in this record’s timeline. Includes temporal associations.
301 302 303 304 305 306 |
# File 'lib/chrono_model/time_machine.rb', line 301 def ( = {}) return nil unless historical? [:after] ||= as_of_time timeline(.merge(:limit => 1, :reverse => false)).first end |
#timeline(options = {}) ⇒ Object
Returns an Array of timestamps for which this instance has an history record. Takes temporal associations into account.
248 249 250 |
# File 'lib/chrono_model/time_machine.rb', line 248 def timeline( = {}) self.class.history.timeline(self, ) end |