forked from paper-trail-gem/paper_trail
/
version.rb
132 lines (111 loc) · 4.35 KB
/
version.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
class Version < ActiveRecord::Base
belongs_to :item, :polymorphic => true
validates_presence_of :event
def self.with_item_keys(item_type, item_id)
scoped(:conditions => { :item_type => item_type, :item_id => item_id })
end
scope :subsequent, lambda { |version|
where(["id > ?", version.is_a?(self) ? version.id : version]).order("id ASC")
}
scope :preceding, lambda { |version|
where(["id < ?", version.is_a?(self) ? version.id : version]).order("id DESC")
}
scope :after, lambda { |timestamp|
# TODO: is this :order necessary, considering its presence on the has_many :versions association?
where(['created_at > ?', timestamp]).order('created_at ASC, id ASC')
}
# Restore the item from this version.
#
# This will automatically restore all :has_one associations as they were "at the time",
# if they are also being versioned by PaperTrail. NOTE: this isn't always guaranteed
# to work so you can either change the lookback period (from the default 3 seconds) or
# opt out.
#
# Options:
# +:has_one+ set to `false` to opt out of has_one reification.
# set to a float to change the lookback time (check whether your db supports
# sub-second datetimes if you want them).
def reify(options = {})
options.reverse_merge! :has_one => 3
unless object.nil?
attrs = YAML::load object
# Normally a polymorphic belongs_to relationship allows us
# to get the object we belong to by calling, in this case,
# +item+. However this returns nil if +item+ has been
# destroyed, and we need to be able to retrieve destroyed
# objects.
#
# In this situation we constantize the +item_type+ to get hold of
# the class...except when the stored object's attributes
# include a +type+ key. If this is the case, the object
# we belong to is using single table inheritance and the
# +item_type+ will be the base class, not the actual subclass.
# If +type+ is present but empty, the class is the base class.
if item
model = item
else
class_name = attrs['type'].blank? ? item_type : attrs['type']
klass = class_name.constantize
model = klass.new
end
attrs.each do |k, v|
begin
model.send :write_attribute, k.to_sym , v
rescue NoMethodError
logger.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
end
end
model.version = self
unless options[:has_one] == false
reify_has_ones model, options[:has_one]
end
model
end
end
# Returns who put the item into the state stored in this version.
def originator
previous.try :whodunnit
end
# Returns who changed the item from the state it had in this version.
# This is an alias for `whodunnit`.
def terminator
whodunnit
end
def sibling_versions
self.class.with_item_keys(item_type, item_id)
end
def next
sibling_versions.subsequent(self).first
end
def previous
sibling_versions.preceding(self).first
end
def index
sibling_versions.select(:id).order("id ASC").map(&:id).index(self.id)
end
private
# Restore the `model`'s has_one associations as they were when this version was
# superseded by the next (because that's what the user was looking at when they
# made the change).
#
# The `lookback` sets how many seconds before the model's change we go.
def reify_has_ones(model, lookback)
model.class.reflect_on_all_associations(:has_one).each do |assoc|
child = model.send assoc.name
if child.respond_to? :version_at
# N.B. we use version of the child as it was `lookback` seconds before the parent was updated.
# Ideally we want the version of the child as it was just before the parent was updated...
# but until PaperTrail knows which updates are "together" (e.g. parent and child being
# updated on the same form), it's impossible to tell when the overall update started;
# and therefore impossible to know when "just before" was.
if (child_as_it_was = child.version_at(created_at - lookback.seconds))
child_as_it_was.attributes.each do |k,v|
model.send(assoc.name).send :write_attribute, k.to_sym, v rescue nil
end
else
model.send "#{assoc.name}=", nil
end
end
end
end
end