historiographer 4.1.5 → 4.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +71 -4
- data/lib/historiographer/history.rb +149 -6
- data/lib/historiographer/history_migration.rb +4 -3
- data/lib/historiographer/version.rb +1 -1
- data/lib/historiographer.rb +19 -55
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fdfddfba56fcb69bb91c73edcf7c1ddff76adc25f517d4b225bcf87bdd356540
|
4
|
+
data.tar.gz: 01e398edc375e2bcf17f22a81c4eb06ab2d507483b05db80b8014ea291f2ac82
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64b288645843cad4862b06724619ec8c98405745b4d6ba572d00baabbc1ce0d4f706cbf95b42c7e1e6214c870ac0c335fb37ea760f599830037f11a94cf5443e
|
7
|
+
data.tar.gz: '09663a6452efd27ce2f566b3764e7dd9f29499a61cb5eecc7b488e8fe31270d57a6ceb84489de983cbd4851ac8906cf6140127113ef06751fc6cb19839821906'
|
data/README.md
CHANGED
@@ -137,7 +137,7 @@ Historiographer fully supports Single Table Inheritance, both with the default `
|
|
137
137
|
### Default STI with `type` column
|
138
138
|
|
139
139
|
```ruby
|
140
|
-
class Post <
|
140
|
+
class Post < ActiveRecord::Base
|
141
141
|
include Historiographer
|
142
142
|
end
|
143
143
|
|
@@ -145,7 +145,7 @@ class PrivatePost < Post
|
|
145
145
|
end
|
146
146
|
|
147
147
|
# The history classes follow the same inheritance pattern:
|
148
|
-
class PostHistory <
|
148
|
+
class PostHistory < ActiveRecord::Base
|
149
149
|
include Historiographer::History
|
150
150
|
end
|
151
151
|
|
@@ -170,9 +170,9 @@ history.type #=> "PrivatePostHistory"
|
|
170
170
|
You can also use a custom column for STI instead of the default `type`:
|
171
171
|
|
172
172
|
```ruby
|
173
|
-
class MLModel <
|
174
|
-
include Historiographer
|
173
|
+
class MLModel < ActiveRecord::Base
|
175
174
|
self.inheritance_column = :model_type
|
175
|
+
include Historiographer
|
176
176
|
end
|
177
177
|
|
178
178
|
class XGBoost < MLModel
|
@@ -291,12 +291,45 @@ trained_version = model.histories.find_by(metadata: { stage: "post_training" })
|
|
291
291
|
```
|
292
292
|
|
293
293
|
This combination of STI and snapshots is particularly valuable for:
|
294
|
+
|
294
295
|
- Model governance and compliance
|
295
296
|
- A/B testing different model types
|
296
297
|
- Debugging model behavior
|
297
298
|
- Reproducing historical predictions
|
298
299
|
- Maintaining audit trails for regulatory requirements
|
299
300
|
|
301
|
+
## Namespaced Models
|
302
|
+
|
303
|
+
When using namespaced models, Rails handles foreign key naming differently than with non-namespaced models. For example, if you have a model namespaced like this:
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
module EasyML
|
307
|
+
class Dataset
|
308
|
+
self.table_name = "easy_ml_datasets"
|
309
|
+
end
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
Rails will expect foreign keys to be formatted using just the model name (without the namespace) like this:
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
:dataset_id
|
317
|
+
```
|
318
|
+
|
319
|
+
Therefore, when creating history migrations for namespaced models, you need to specify the foreign key name explicitly:
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
class CreateEasyMLDatasetHistories < ActiveRecord::Migration
|
323
|
+
def change
|
324
|
+
create_table :easy_ml_dataset_histories do |t|
|
325
|
+
t.histories(foreign_key: :dataset_id) # instead of using the table name — easy_ml_dataset_id
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
```
|
330
|
+
|
331
|
+
This ensures that the foreign key relationships are properly established between your namespaced models and their history tables.
|
332
|
+
|
300
333
|
## Getting Started
|
301
334
|
|
302
335
|
Whenever you include the `Historiographer` gem in your ActiveRecord model, it allows you to insert, update, or delete data as you normally would.
|
@@ -305,6 +338,11 @@ Whenever you include the `Historiographer` gem in your ActiveRecord model, it al
|
|
305
338
|
class Post < ActiveRecord::Base
|
306
339
|
include Historiographer
|
307
340
|
end
|
341
|
+
|
342
|
+
class PostHistory < ActiveRecord::Base
|
343
|
+
self.table_name = "post_histories"
|
344
|
+
include Historiographer::History
|
345
|
+
end
|
308
346
|
```
|
309
347
|
|
310
348
|
### History Modes
|
@@ -337,6 +375,29 @@ class Comment < ActiveRecord::Base
|
|
337
375
|
end
|
338
376
|
```
|
339
377
|
|
378
|
+
The class-level mode setting takes precedence over the global configuration. This allows you to:
|
379
|
+
|
380
|
+
- Have different history tracking strategies for different models
|
381
|
+
- Set most models to use snapshots while keeping detailed history for critical models
|
382
|
+
- Optimize storage by only tracking detailed history where needed
|
383
|
+
|
384
|
+
For example:
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
# Global setting for most models
|
388
|
+
Historiographer::Configuration.mode = :snapshot_only
|
389
|
+
|
390
|
+
class Order < ActiveRecord::Base
|
391
|
+
include Historiographer
|
392
|
+
# Uses global :snapshot_only mode
|
393
|
+
end
|
394
|
+
|
395
|
+
class Payment < ActiveRecord::Base
|
396
|
+
include Historiographer
|
397
|
+
historiographer_mode :histories # Override to record histories of every change
|
398
|
+
end
|
399
|
+
```
|
400
|
+
|
340
401
|
## Create A Migration
|
341
402
|
|
342
403
|
You need a separate table to store histories for each model.
|
@@ -391,12 +452,18 @@ The primary model should include `Historiographer`:
|
|
391
452
|
class Post < ActiveRecord::Base
|
392
453
|
include Historiographer
|
393
454
|
end
|
455
|
+
|
456
|
+
class PostHistory < ActiveRecord::Base
|
457
|
+
self.table_name = "post_histories"
|
458
|
+
include Historiographer::History
|
459
|
+
end
|
394
460
|
```
|
395
461
|
|
396
462
|
You should also make a `PostHistory` class if you're going to query `PostHistory` from Rails:
|
397
463
|
|
398
464
|
```ruby
|
399
465
|
class PostHistory < ActiveRecord::Base
|
466
|
+
self.table_name = "post_histories"
|
400
467
|
end
|
401
468
|
```
|
402
469
|
|
@@ -61,6 +61,7 @@ module Historiographer
|
|
61
61
|
extend ActiveSupport::Concern
|
62
62
|
|
63
63
|
included do |base|
|
64
|
+
clear_validators!
|
64
65
|
#
|
65
66
|
# A History class (e.g. RetailerProductHistory) will gain
|
66
67
|
# access to a current scope, returning
|
@@ -91,6 +92,67 @@ module Historiographer
|
|
91
92
|
foreign_class = foreign_class_name.constantize
|
92
93
|
association_name = foreign_class_name.split("::").last.underscore.to_sym # e.g. "RetailerProduct" => :retailer_product
|
93
94
|
|
95
|
+
# Store the original class for method delegation
|
96
|
+
class_variable_set(:@@original_class, foreign_class)
|
97
|
+
|
98
|
+
# Add method_added hook to the original class
|
99
|
+
foreign_class.singleton_class.class_eval do
|
100
|
+
# Keep track of original method_added if it exists
|
101
|
+
if method_defined?(:method_added)
|
102
|
+
alias_method :original_method_added, :method_added
|
103
|
+
end
|
104
|
+
|
105
|
+
method_map = Hash.new(0)
|
106
|
+
define_method(:method_added) do |method_name|
|
107
|
+
# Skip if we're already in the process of defining a method
|
108
|
+
return if Thread.current[:defining_historiographer_method]
|
109
|
+
|
110
|
+
begin
|
111
|
+
Thread.current[:defining_historiographer_method] = true
|
112
|
+
|
113
|
+
# Call original method_added if it exists
|
114
|
+
original_method_added(method_name) if respond_to?(:original_method_added)
|
115
|
+
|
116
|
+
# Get the method object to check if it's from our class (not inherited)
|
117
|
+
method_obj = instance_method(method_name)
|
118
|
+
return unless method_obj.owner == self
|
119
|
+
|
120
|
+
# Skip if we've already defined this method in the history class
|
121
|
+
return if foreign_class.history_class.method_defined?(method_name)
|
122
|
+
|
123
|
+
# Define the method in the history class
|
124
|
+
foreign_class.history_class.class_eval do
|
125
|
+
define_method(method_name) do |*args, &block|
|
126
|
+
forward_method(method_name, *args, &block)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
ensure
|
130
|
+
Thread.current[:defining_historiographer_method] = false
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
foreign_class.columns.map(&:name).each do |method_name|
|
136
|
+
define_method(method_name) do |*args, &block|
|
137
|
+
forward_method(method_name, *args, &block)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Add method_missing for any methods we might have missed
|
142
|
+
def method_missing(method_name, *args, &block)
|
143
|
+
original_class = self.class.class_variable_get(:@@original_class)
|
144
|
+
if original_class.method_defined?(method_name)
|
145
|
+
forward_method(method_name, *args, &block)
|
146
|
+
else
|
147
|
+
super
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def respond_to_missing?(method_name, include_private = false)
|
152
|
+
original_class = self.class.class_variable_get(:@@original_class)
|
153
|
+
original_class.method_defined?(method_name) || super
|
154
|
+
end
|
155
|
+
|
94
156
|
#
|
95
157
|
# Historiographer will automatically setup the association
|
96
158
|
# to the primary class (e.g. RetailerProduct)
|
@@ -147,19 +209,19 @@ module Historiographer
|
|
147
209
|
#
|
148
210
|
# If the record was not already persisted, proceed as normal.
|
149
211
|
#
|
150
|
-
def save(*args)
|
212
|
+
def save(*args, **kwargs)
|
151
213
|
if persisted? && (changes.keys - %w(history_ended_at snapshot_id)).any?
|
152
214
|
false
|
153
215
|
else
|
154
|
-
super
|
216
|
+
super(*args, **kwargs)
|
155
217
|
end
|
156
218
|
end
|
157
219
|
|
158
|
-
def save!(*args)
|
220
|
+
def save!(*args, **kwargs)
|
159
221
|
if persisted? && (changes.keys - %w(history_ended_at snapshot_id)).any?
|
160
222
|
false
|
161
223
|
else
|
162
|
-
super
|
224
|
+
super(*args, **kwargs)
|
163
225
|
end
|
164
226
|
end
|
165
227
|
|
@@ -167,11 +229,66 @@ module Historiographer
|
|
167
229
|
# Orders by history_started_at and id to handle cases where multiple records
|
168
230
|
# have the same history_started_at timestamp
|
169
231
|
scope :latest_snapshot, -> {
|
170
|
-
where.not(snapshot_id: nil)
|
232
|
+
where.not(snapshot_id: nil)
|
233
|
+
.select('DISTINCT ON (snapshot_id) *')
|
234
|
+
.order('snapshot_id, history_started_at DESC, id DESC')
|
171
235
|
}
|
236
|
+
|
237
|
+
# Dynamically define associations on the history class
|
238
|
+
foreign_class.reflect_on_all_associations.each do |association|
|
239
|
+
define_history_association(association)
|
240
|
+
end
|
241
|
+
|
172
242
|
end
|
173
243
|
|
174
244
|
class_methods do
|
245
|
+
def original_class
|
246
|
+
unless class_variable_defined?(:@@original_class)
|
247
|
+
class_variable_set(:@@original_class, self.name.gsub(/History$/, '').constantize)
|
248
|
+
end
|
249
|
+
|
250
|
+
class_variable_get(:@@original_class)
|
251
|
+
end
|
252
|
+
|
253
|
+
def define_history_association(association)
|
254
|
+
if association.is_a?(Symbol) || association.is_a?(String)
|
255
|
+
association = original_class.reflect_on_association(association)
|
256
|
+
end
|
257
|
+
assoc_name = association.name
|
258
|
+
assoc_history_class_name = "#{association.class_name}History"
|
259
|
+
assoc_foreign_key = association.foreign_key
|
260
|
+
|
261
|
+
# Skip if the association is already defined
|
262
|
+
return if method_defined?(assoc_name)
|
263
|
+
|
264
|
+
# Skip through associations to history classes to avoid infinite loops
|
265
|
+
return if association.class_name.end_with?('History')
|
266
|
+
|
267
|
+
# We're writing a belongs_to
|
268
|
+
# The dataset belongs_to the datasource
|
269
|
+
# dataset#datasource_id => datasource.id
|
270
|
+
#
|
271
|
+
# For the history class, we're writing a belongs_to
|
272
|
+
# the DatasetHistory belongs_to the DatasourceHistory
|
273
|
+
# dataset_history#datasource_id => datasource_history.easy_ml_datasource_id
|
274
|
+
#
|
275
|
+
# The missing piece for us here is whatever DatasourceHistory would call easy_ml_datasource_id (history foreign key?)
|
276
|
+
|
277
|
+
case association.macro
|
278
|
+
when :belongs_to
|
279
|
+
belongs_to assoc_name, ->(history_instance) {
|
280
|
+
where(snapshot_id: history_instance.snapshot_id)
|
281
|
+
}, class_name: assoc_history_class_name, foreign_key: assoc_foreign_key, primary_key: assoc_foreign_key
|
282
|
+
when :has_one
|
283
|
+
has_one assoc_name, ->(history_instance) {
|
284
|
+
where(snapshot_id: history_instance.snapshot_id)
|
285
|
+
}, class_name: assoc_history_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
|
286
|
+
when :has_many
|
287
|
+
has_many assoc_name, ->(history_instance) {
|
288
|
+
where(snapshot_id: history_instance.snapshot_id)
|
289
|
+
}, class_name: assoc_history_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
|
290
|
+
end
|
291
|
+
end
|
175
292
|
#
|
176
293
|
# The foreign key to the primary class.
|
177
294
|
#
|
@@ -180,7 +297,8 @@ module Historiographer
|
|
180
297
|
def history_foreign_key
|
181
298
|
return @history_foreign_key if @history_foreign_key
|
182
299
|
|
183
|
-
|
300
|
+
# CAN THIS BE TABLE OR MODEL?
|
301
|
+
@history_foreign_key = sti_base_class.name.singularize.foreign_key
|
184
302
|
end
|
185
303
|
|
186
304
|
def sti_base_class
|
@@ -194,5 +312,30 @@ module Historiographer
|
|
194
312
|
@sti_base_class = base_class
|
195
313
|
end
|
196
314
|
end
|
315
|
+
|
316
|
+
def original_class
|
317
|
+
self.class.original_class
|
318
|
+
end
|
319
|
+
|
320
|
+
private
|
321
|
+
def dummy_instance
|
322
|
+
return @dummy_instance if @dummy_instance
|
323
|
+
|
324
|
+
cannot_keep_cols = %w(history_started_at history_ended_at history_user_id snapshot_id)
|
325
|
+
cannot_keep_cols += [self.class.inheritance_column.to_sym] if self.original_class.sti_enabled?
|
326
|
+
cannot_keep_cols += [self.class.history_foreign_key]
|
327
|
+
cannot_keep_cols.map!(&:to_s)
|
328
|
+
|
329
|
+
attrs = attributes.clone
|
330
|
+
attrs[original_class.primary_key] = attrs[self.class.history_foreign_key]
|
331
|
+
|
332
|
+
instance = original_class.find_or_initialize_by(original_class.primary_key => attrs[original_class.primary_key])
|
333
|
+
instance.assign_attributes(attrs.except(*cannot_keep_cols))
|
334
|
+
@dummy_instance = instance
|
335
|
+
end
|
336
|
+
|
337
|
+
def forward_method(method_name, *args, &block)
|
338
|
+
dummy_instance.send(method_name, *args, &block)
|
339
|
+
end
|
197
340
|
end
|
198
341
|
end
|
@@ -16,10 +16,11 @@ module Historiographer
|
|
16
16
|
# Will automatically add user_id, history_started_at,
|
17
17
|
# and history_ended_at columns
|
18
18
|
#
|
19
|
-
def histories(except: [], only: [], no_business_columns: false, index_names: {})
|
19
|
+
def histories(except: [], only: [], no_business_columns: false, index_names: {}, foreign_key: nil)
|
20
20
|
index_names.symbolize_keys!
|
21
|
-
|
22
|
-
|
21
|
+
history_table_name = self.name
|
22
|
+
original_table_name = history_table_name.gsub(/_histories$/) {}.pluralize
|
23
|
+
foreign_key ||= original_table_name.singularize.foreign_key
|
23
24
|
|
24
25
|
class_definer = Class.new(ActiveRecord::Base) do
|
25
26
|
end
|
data/lib/historiographer.rb
CHANGED
@@ -85,7 +85,6 @@ module Historiographer
|
|
85
85
|
after_save :record_history, if: :should_record_history?
|
86
86
|
validate :validate_history_user_id_present, if: :should_validate_history_user_id_present?
|
87
87
|
|
88
|
-
|
89
88
|
def should_alert_history_user_id_present?
|
90
89
|
!snapshot_mode? && !is_history_class? && Thread.current[:skip_history_user_id_validation] != true
|
91
90
|
end
|
@@ -184,15 +183,15 @@ module Historiographer
|
|
184
183
|
|
185
184
|
begin
|
186
185
|
class_name.constantize
|
187
|
-
rescue
|
186
|
+
rescue NameError
|
188
187
|
# Get the base table name without _histories suffix
|
189
|
-
base_table = base.table_name.sub(/_histories$/, '')
|
188
|
+
base_table = base.table_name.singularize.sub(/_histories$/, '')
|
190
189
|
|
191
|
-
history_class_initializer = Class.new(
|
190
|
+
history_class_initializer = Class.new(ActiveRecord::Base) do
|
192
191
|
self.table_name = "#{base_table}_histories"
|
193
192
|
|
194
193
|
# Handle STI properly
|
195
|
-
self.inheritance_column = base.inheritance_column if base.
|
194
|
+
self.inheritance_column = base.inheritance_column if base.sti_enabled?
|
196
195
|
end
|
197
196
|
|
198
197
|
# Split the class name into module parts and the actual class name
|
@@ -217,63 +216,22 @@ module Historiographer
|
|
217
216
|
base.singleton_class.prepend(Module.new do
|
218
217
|
def belongs_to(name, scope = nil, **options, &extension)
|
219
218
|
super
|
220
|
-
define_history_association(name
|
219
|
+
history_class.define_history_association(name)
|
221
220
|
end
|
222
221
|
|
223
222
|
def has_one(name, scope = nil, **options, &extension)
|
224
223
|
super
|
225
|
-
define_history_association(name
|
224
|
+
history_class.define_history_association(name)
|
226
225
|
end
|
227
226
|
|
228
227
|
def has_many(name, scope = nil, **options, &extension)
|
229
228
|
super
|
230
|
-
define_history_association(name
|
229
|
+
history_class.define_history_association(name)
|
231
230
|
end
|
232
231
|
|
233
232
|
def has_and_belongs_to_many(name, scope = nil, **options, &extension)
|
234
233
|
super
|
235
|
-
define_history_association(name
|
236
|
-
end
|
237
|
-
|
238
|
-
private
|
239
|
-
|
240
|
-
def define_history_association(name, type, options)
|
241
|
-
return if is_history_class?
|
242
|
-
return if @defining_association
|
243
|
-
return if %i[histories current_history].include?(name)
|
244
|
-
@defining_association = true
|
245
|
-
|
246
|
-
history_class = "#{self.name}History".constantize
|
247
|
-
history_class_name = "#{name.to_s.singularize.camelize}History"
|
248
|
-
|
249
|
-
# Get the original association's foreign key
|
250
|
-
original_reflection = self.reflect_on_association(name)
|
251
|
-
foreign_key = original_reflection.foreign_key
|
252
|
-
|
253
|
-
if type == :has_many || type == :has_and_belongs_to_many
|
254
|
-
history_class.send(
|
255
|
-
type,
|
256
|
-
name,
|
257
|
-
-> (owner) { where("#{name.to_s.singularize}_histories.snapshot_id = ?", owner.snapshot_id) },
|
258
|
-
**options.merge(
|
259
|
-
class_name: history_class_name,
|
260
|
-
foreign_key: foreign_key,
|
261
|
-
primary_key: foreign_key
|
262
|
-
)
|
263
|
-
)
|
264
|
-
else
|
265
|
-
history_class.send(
|
266
|
-
type,
|
267
|
-
name,
|
268
|
-
-> (owner) { where("#{name}_histories.snapshot_id = ?", owner.snapshot_id) },
|
269
|
-
**options.merge(
|
270
|
-
class_name: history_class_name,
|
271
|
-
foreign_key: foreign_key,
|
272
|
-
primary_key: foreign_key
|
273
|
-
)
|
274
|
-
)
|
275
|
-
end
|
276
|
-
@defining_association = false
|
234
|
+
history_class.define_history_association(name)
|
277
235
|
end
|
278
236
|
end)
|
279
237
|
|
@@ -318,7 +276,6 @@ module Historiographer
|
|
318
276
|
save!(*args, &block)
|
319
277
|
@no_history = false
|
320
278
|
end
|
321
|
-
|
322
279
|
|
323
280
|
def snapshot(tree = {}, snapshot_id = nil)
|
324
281
|
return if is_history_class?
|
@@ -334,10 +291,11 @@ module Historiographer
|
|
334
291
|
return if existing_snapshot.present?
|
335
292
|
|
336
293
|
null_snapshot = history_class.where(foreign_key => attrs[primary_key], snapshot_id: nil)
|
294
|
+
snapshot = nil
|
337
295
|
if null_snapshot.present?
|
338
|
-
null_snapshot.update(snapshot_id: snapshot_id)
|
296
|
+
snapshot = null_snapshot.update(snapshot_id: snapshot_id)
|
339
297
|
else
|
340
|
-
record_history(snapshot_id: snapshot_id)
|
298
|
+
snapshot = record_history(snapshot_id: snapshot_id)
|
341
299
|
end
|
342
300
|
|
343
301
|
# Recursively snapshot associations, avoiding infinite loops
|
@@ -356,6 +314,8 @@ module Historiographer
|
|
356
314
|
record.snapshot(new_tree, snapshot_id) if record.respond_to?(:snapshot)
|
357
315
|
end
|
358
316
|
end
|
317
|
+
|
318
|
+
snapshot
|
359
319
|
end
|
360
320
|
end
|
361
321
|
|
@@ -383,8 +343,12 @@ module Historiographer
|
|
383
343
|
attrs
|
384
344
|
end
|
385
345
|
|
346
|
+
def snapshots
|
347
|
+
histories.where.not(snapshot_id: nil)
|
348
|
+
end
|
349
|
+
|
386
350
|
def latest_snapshot
|
387
|
-
|
351
|
+
snapshots.order('id DESC').limit(1)&.first || history_class.none
|
388
352
|
end
|
389
353
|
|
390
354
|
private
|
@@ -407,7 +371,7 @@ module Historiographer
|
|
407
371
|
current_history = histories.where(history_ended_at: nil).order('id desc').limit(1).last
|
408
372
|
|
409
373
|
if history_class.history_foreign_key.present? && history_class.present?
|
410
|
-
history_class.create!(attrs).tap do |
|
374
|
+
history_class.create!(attrs).tap do |new_history|
|
411
375
|
current_history.update!(history_ended_at: now) if current_history.present?
|
412
376
|
end
|
413
377
|
else
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: historiographer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.1.
|
4
|
+
version: 4.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- brettshollenberger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-11-
|
11
|
+
date: 2024-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|