Module: ChronoModel::TimeMachine
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/chrono_model/time_machine.rb
Defined Under Namespace
Modules: ClassMethods, HistoryMethods, QueryMethods, 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. -
#as_of_time ⇒ Object
Read the virtual ‘as_of_time’ attribute and return it as an UTC timestamp.
-
#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.
Class Method Details
.chrono_models ⇒ Object
Returns an Hash keyed by table name of ChronoModels
33 34 35 |
# File 'lib/chrono_model/time_machine.rb', line 33 def self.chrono_models (@chrono_models ||= {}) end |
.define_history_model_for(model) ⇒ Object
37 38 39 40 41 42 43 44 45 46 47 48 49 50 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 |
# File 'lib/chrono_model/time_machine.rb', line 37 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 # 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 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
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/chrono_model/time_machine.rb', line 170 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 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.
192 193 194 |
# File 'lib/chrono_model/time_machine.rb', line 192 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.
199 200 201 |
# File 'lib/chrono_model/time_machine.rb', line 199 def as_of!(time) _as_of(time).first! end |
#as_of_time ⇒ Object
Read the virtual ‘as_of_time’ attribute and return it as an UTC timestamp.
233 234 235 |
# File 'lib/chrono_model/time_machine.rb', line 233 def as_of_time Conversions.string_to_utc_time attributes_before_type_cast['as_of_time'] 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.
306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/chrono_model/time_machine.rb', line 306 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
289 290 291 |
# File 'lib/chrono_model/time_machine.rb', line 289 def current_version self.historical? ? self.class.find(self.id) : self end |
#destroy ⇒ Object
Inhibit destroy of historical records
239 240 241 242 |
# File 'lib/chrono_model/time_machine.rb', line 239 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.
227 228 229 |
# File 'lib/chrono_model/time_machine.rb', line 227 def historical? self.attributes.key?('as_of_time') || self.kind_of?(self.class.history) end |
#history ⇒ Object
Return the complete read-only history of this instance.
214 215 216 |
# File 'lib/chrono_model/time_machine.rb', line 214 def history self.class.history.of(self) end |
#last_changes ⇒ Object
Returns the differences between this entry and the previous history one. See: changes_against.
296 297 298 299 |
# File 'lib/chrono_model/time_machine.rb', line 296 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.
247 248 249 250 251 252 253 254 |
# File 'lib/chrono_model/time_machine.rb', line 247 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(%[ #{options[: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.
259 260 261 262 263 264 265 266 |
# File 'lib/chrono_model/time_machine.rb', line 259 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.
270 271 272 273 274 275 |
# File 'lib/chrono_model/time_machine.rb', line 270 def succ( = {}) unless self.class.timeline_associations.empty? return nil unless (ts = ()) self.class.as_of(ts).order(%[ #{options[: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.
280 281 282 283 284 285 |
# File 'lib/chrono_model/time_machine.rb', line 280 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.
221 222 223 |
# File 'lib/chrono_model/time_machine.rb', line 221 def timeline( = {}) self.class.history.timeline(self, ) end |