acts_as_dated_detail 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Paul Gillard
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,39 @@
1
+ = acts_as_dated_detail
2
+
3
+ http://github.com/paulgillard/acts_as_dated_detail
4
+
5
+ == DESCRIPTION:
6
+
7
+ acts_as_dated_detil enables versioning of attributes by timestamp.
8
+
9
+ == FEATURES:
10
+
11
+ == SYNOPSIS:
12
+
13
+ == REQUIREMENTS:
14
+
15
+ == INSTALL:
16
+
17
+ == LICENSE:
18
+
19
+ (The MIT License)
20
+
21
+ Copyright (c) 2009 Paul Gillard
22
+
23
+ Permission is hereby granted, free of charge, to any person obtaining a copy
24
+ of this software and associated documentation files (the "Software"), to deal
25
+ in the Software without restriction, including without limitation the rights
26
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
27
+ copies of the Software, and to permit persons to whom the Software is
28
+ furnished to do so, subject to the following conditions:
29
+
30
+ The above copyright notice and this permission notice shall be included in
31
+ all copies or substantial portions of the Software.
32
+
33
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
39
+ THE SOFTWARE.
@@ -0,0 +1,70 @@
1
+ module ActiveRecord
2
+ module Acts #:nodoc:
3
+ module Dated #:nodoc:
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_dated(options = {})
10
+ acts_as_dated_detail_class = "#{self.name}DatedDetail".constantize
11
+
12
+ tracked_attribute_reader_methods = ''
13
+ tracked_attribute_writer_methods = ''
14
+ acts_as_dated_detail_class.tracked_attributes.each do |attribute|
15
+ tracked_attribute_reader_methods << %(
16
+ def #{attribute}
17
+ dated_detail.send('#{attribute}')
18
+ end
19
+ )
20
+ tracked_attribute_writer_methods << %(
21
+ def #{attribute}=(value)
22
+ dated_detail.send('#{attribute}=', value)
23
+ end
24
+ )
25
+ end
26
+
27
+ class_eval <<-EOV
28
+ after_save :save_dated_detail
29
+
30
+ has_many :dated_details, :class_name => "#{acts_as_dated_detail_class.to_s}"
31
+
32
+ #{tracked_attribute_reader_methods}
33
+ #{tracked_attribute_writer_methods}
34
+
35
+ include ActiveRecord::Acts::Dated::InstanceMethods
36
+
37
+ # def self.columns
38
+ # tracked_columns_hash = #{acts_as_dated_detail_class.to_s}.columns_hash.slice(*#{acts_as_dated_detail_class.to_s}.tracked_attributes)
39
+ # @columns ||= tracked_columns_hash.inject(super.columns) do |columns, (key, value)|
40
+ # columns << value
41
+ # end
42
+ # @columns
43
+ # end
44
+ EOV
45
+ end
46
+ end
47
+
48
+ module InstanceMethods
49
+ def on
50
+ @time ||= Time.now
51
+ end
52
+
53
+ def on=(time)
54
+ @time = time
55
+ @dated_detail = nil
56
+ end
57
+
58
+ def dated_detail
59
+ @dated_detail ||= dated_details.on(on).first || dated_details.build
60
+ end
61
+
62
+ private
63
+
64
+ def save_dated_detail
65
+ dated_detail.save!
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,55 @@
1
+ module ActiveRecord
2
+ module Acts #:nodoc:
3
+ module DatedDetail #:nodoc:
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_dated_detail()
10
+ class_eval <<-EOV
11
+ belongs_to :#{self.name.underscore.sub(/_dated_detail$/, '')}
12
+
13
+ before_update :split!
14
+
15
+ named_scope :on, lambda { |time| { :conditions => "\#{start_on_or_before_condition(time)} AND \#{end_on_or_after_condition(time)}" } }
16
+
17
+ def self.tracked_attributes
18
+ columns_hash.keys - ['id', "#{self.name.underscore.sub(/dated_detail$/, 'id')}", 'start_on', 'end_on', 'created_at', 'updated_at']
19
+ end
20
+
21
+ include ActiveRecord::Acts::DatedDetail::InstanceMethods
22
+
23
+ private
24
+
25
+ # Find all records starting on or before given time
26
+ def self.start_on_or_before_condition(time)
27
+ "(start_on <= '\#{time.to_s :db}')"
28
+ end
29
+
30
+ # Find all records ending on or after given time
31
+ def self.end_on_or_after_condition(time)
32
+ "(end_on IS NULL OR end_on >= '\#{time.to_s :db}')"
33
+ end
34
+ EOV
35
+ end
36
+ end
37
+
38
+ module InstanceMethods
39
+ def initialize(*args)
40
+ super
41
+ self.start_on = Time.now
42
+ end
43
+
44
+ def split!
45
+ if changed?
46
+ self.start_on = Time.now
47
+ original = self.class.find(id).clone
48
+ original.end_on = start_on - 1
49
+ original.save!
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ $:.unshift "#{File.dirname(__FILE__)}"
2
+ require 'active_record/acts/dated'
3
+ require 'active_record/acts/dated_detail'
4
+ ActiveRecord::Base.class_eval { include ActiveRecord::Acts::DatedDetail }
5
+ ActiveRecord::Base.class_eval { include ActiveRecord::Acts::Dated }
@@ -0,0 +1,363 @@
1
+ require 'test/unit'
2
+
3
+ require 'rubygems'
4
+ gem 'activerecord', '>= 1.15.4.7794'
5
+ require 'active_record'
6
+ require 'mocha'
7
+ require 'ruby-debug'
8
+
9
+ require "#{File.dirname(__FILE__)}/../lib/acts_as_dated_detail"
10
+
11
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
12
+
13
+ def setup_db
14
+ ActiveRecord::Schema.define(:version => 1) do
15
+ create_table :parrots do |t|
16
+ t.column :name, :string
17
+ t.column :created_at, :datetime
18
+ t.column :updated_at, :datetime
19
+ end
20
+
21
+ create_table :pirates do |t|
22
+ t.column :name, :string
23
+ t.column :catchphrase, :string
24
+ t.column :ruthlessness, :integer
25
+ t.column :birth_date, :datetime
26
+ t.column :parrot_id, :integer
27
+ t.column :created_at, :datetime
28
+ t.column :updated_at, :datetime
29
+ end
30
+
31
+ create_table :pirate_dated_details do |t|
32
+ # Ensure any attributes added here are added to relevant tests (search for 'catchphrase')
33
+ t.column :pirate_id, :integer
34
+ t.column :start_on, :datetime
35
+ t.column :end_on, :datetime
36
+ t.column :catchphrase, :string
37
+ t.column :ruthlessness, :integer
38
+ t.column :birth_date, :datetime
39
+ t.column :parrot_id, :integer
40
+ t.column :created_at, :datetime
41
+ t.column :updated_at, :datetime
42
+ end
43
+ end
44
+ end
45
+
46
+ def teardown_db
47
+ ActiveRecord::Base.connection.tables.each do |table|
48
+ ActiveRecord::Base.connection.drop_table(table)
49
+ end
50
+ end
51
+
52
+ setup_db
53
+
54
+ class Parrot < ActiveRecord::Base
55
+ has_one :pirate
56
+
57
+ def ==(other)
58
+ name == other.name
59
+ end
60
+ end
61
+
62
+ class PirateDatedDetail < ActiveRecord::Base
63
+ acts_as_dated_detail
64
+ end
65
+
66
+ class Pirate < ActiveRecord::Base
67
+ belongs_to :parrot
68
+
69
+ acts_as_dated
70
+ end
71
+
72
+ teardown_db
73
+
74
+ class ActsAsDatedDetailTest < Test::Unit::TestCase
75
+ def setup
76
+ setup_db
77
+ end
78
+
79
+ def teardown
80
+ teardown_db
81
+ end
82
+
83
+ def default_test
84
+ end
85
+
86
+ private
87
+
88
+ def create_parrot(time)
89
+ parrot = Parrot.create!(:name => time.to_s)
90
+ end
91
+
92
+ def create_pirate(times = [Time.now])
93
+ first_time = times.shift
94
+ now = Time.now
95
+ Time.stubs(:now).returns(first_time)
96
+ pirate = Pirate.create!(:name => pirate_name(first_time), :catchphrase => catchphrase(first_time), :ruthlessness => ruthlessness(first_time), :birth_date => first_time, :parrot => create_parrot(first_time))
97
+ times.each do |time|
98
+ Time.stubs(:now).returns(time)
99
+ pirate.update_attributes(:name => pirate_name(time), :catchphrase => catchphrase(time), :ruthlessness => ruthlessness(time), :birth_date => time, :parrot => create_parrot(time))
100
+ end
101
+ Time.stubs(:now).returns(now)
102
+ pirate
103
+ end
104
+
105
+ def pirate_name(time)
106
+ time.to_s
107
+ end
108
+
109
+ def catchphrase(time)
110
+ time.to_s
111
+ end
112
+
113
+ def ruthlessness(time)
114
+ time.to_i
115
+ end
116
+ end
117
+
118
+ class ParentTest < ActsAsDatedDetailTest
119
+ # Creation
120
+
121
+ def test_related_dated_detail_created_along_with_model
122
+ pirate = Pirate.create!
123
+ assert_equal 1, pirate.dated_details.count
124
+ end
125
+
126
+ # Currently Effective Timestamp
127
+
128
+ def test_default_value_of_currently_effective_timestamp
129
+ now = Time.now
130
+ Time.stubs(:now).returns(now)
131
+ pirate = Pirate.create!
132
+ assert_equal Time.now, pirate.on
133
+ end
134
+
135
+ def test_setting_value_of_currently_effective_timestamp
136
+ now = Time.now
137
+ Time.stubs(:now).returns(now)
138
+ pirate = Pirate.create!
139
+ one_month_ago = Time.now - 1.month
140
+ pirate.on = one_month_ago
141
+ assert_equal one_month_ago, pirate.on
142
+ end
143
+
144
+ # Tracked Attribute Retrieval
145
+
146
+ def test_tracked_attribute_for_oldest_timestamp_set_by_instance_method
147
+ pirate = create_pirate([1.year.ago, 6.months.ago, 1.month.ago])
148
+ pirate.on = 8.months.ago
149
+ assert_equal catchphrase(1.year.ago), pirate.catchphrase
150
+ end
151
+
152
+ def test_tracked_attribute_for_middle_timestamp_set_by_instance_method
153
+ pirate = create_pirate([1.year.ago, 6.months.ago, 1.month.ago])
154
+ pirate.on = 5.months.ago
155
+ assert_equal catchphrase(6.months.ago), pirate.catchphrase
156
+ end
157
+
158
+ def test_tracked_attribute_for_newest_timestamp_set_by_instance_method
159
+ pirate = create_pirate([1.year.ago, 6.months.ago, 1.month.ago])
160
+ pirate.on = 2.weeks.ago
161
+ assert_equal catchphrase(1.month.ago), pirate.catchphrase
162
+ end
163
+
164
+ # def test_start_on
165
+ #
166
+ # end
167
+ #
168
+ # def test_end_on
169
+ #
170
+ # end
171
+ #
172
+ # def test_end_on_for_last_dated_detail
173
+ #
174
+ # end
175
+ #
176
+ # def test_previous
177
+ #
178
+ # end
179
+ #
180
+ # def test_previous_for_first_dated_detail
181
+ #
182
+ # end
183
+ #
184
+ # def test_next
185
+ #
186
+ # end
187
+ #
188
+ # def test_next_for_last_dated_detail
189
+ #
190
+ # end
191
+
192
+ # Tracked Attribute Methods
193
+
194
+ def test_read_tracked_attributes
195
+ # Ensure value from dated_detail is returned
196
+ pirate = Pirate.create!
197
+ dated_detail = pirate.dated_detail
198
+ pirate.stubs(:dated_detail).returns(dated_detail)
199
+ PirateDatedDetail.tracked_attributes.each do |attribute|
200
+ value = 10
201
+ PirateDatedDetail.any_instance.stubs(attribute).returns("Error")
202
+ dated_detail.stubs(attribute).returns(value)
203
+ assert_equal value, pirate.send(attribute)
204
+ end
205
+ end
206
+
207
+ def test_write_tracked_attributes
208
+ pirate = Pirate.create!
209
+ PirateDatedDetail.tracked_attributes.each do |attribute|
210
+ value = 10
211
+ pirate.send("#{attribute}=", value)
212
+ assert_equal value, pirate.dated_detail.send(attribute)
213
+ end
214
+ end
215
+
216
+ # Updating Attributes
217
+
218
+ def test_updating_tracked_integer_attribute
219
+ now = Time.now
220
+ Time.stubs(:now).returns(now - 1.year)
221
+ pirate = Pirate.create!
222
+ Time.stubs(:now).returns(now)
223
+ pirate.update_attribute(:ruthlessness, 10)
224
+ assert_equal 2, pirate.dated_details.count
225
+ end
226
+
227
+ def test_updating_tracked_string_attribute
228
+ now = Time.now
229
+ Time.stubs(:now).returns(now - 1.year)
230
+ pirate = Pirate.create!
231
+ Time.stubs(:now).returns(now)
232
+ pirate.update_attribute(:catchphrase, 'Yar!')
233
+ assert_equal 2, pirate.dated_details.count
234
+ end
235
+
236
+ def test_updating_tracked_datetime_attribute
237
+ now = Time.now
238
+ Time.stubs(:now).returns(now - 1.year)
239
+ pirate = create_pirate
240
+ Time.stubs(:now).returns(now)
241
+ pirate.update_attributes(:birth_date => pirate.birth_date + 1.day)
242
+ assert_equal 2, pirate.dated_details.count
243
+ end
244
+
245
+ def test_updating_tracked_multiparameter_attribute
246
+ now = Time.now
247
+ Time.stubs(:now).returns(now - 1.year)
248
+ pirate = create_pirate
249
+ Time.stubs(:now).returns(now)
250
+ new_birth_date = pirate.birth_date + 1.day
251
+ pirate.update_attributes('birth_date(1i)' => "#{new_birth_date.year}", 'birth_date(2i)' => "#{new_birth_date.month}", 'birth_date(3i)' => "#{new_birth_date.day}")
252
+ assert_equal 2, pirate.dated_details.count
253
+ end
254
+
255
+ def test_updating_untracked_attribute
256
+ now = Time.now
257
+ Time.stubs(:now).returns(now - 1.year)
258
+ pirate = Pirate.create!
259
+ Time.stubs(:now).returns(now)
260
+ pirate.update_attribute(:name, 'Long John Silver')
261
+ assert_equal 1, pirate.dated_details.count
262
+ end
263
+ end
264
+
265
+ class DatedDetailTest < ActsAsDatedDetailTest
266
+ def setup
267
+ setup_db
268
+ # create_pirate_with_dated_detail
269
+ end
270
+
271
+ def teardown
272
+ teardown_db
273
+ end
274
+
275
+ # Associations
276
+
277
+ def test_belongs_to_parent
278
+ assert_equal create_pirate, PirateDatedDetail.first.pirate
279
+ end
280
+
281
+ # Creation
282
+
283
+ def test_initial_start_on_value
284
+ pirate = Pirate.create!
285
+ assert_equal Time.now.to_i, PirateDatedDetail.first.start_on.to_i
286
+ end
287
+
288
+ def test_initial_end_on_value
289
+ pirate = Pirate.create!
290
+ assert_nil PirateDatedDetail.first.end_on
291
+ end
292
+
293
+ # Updating
294
+
295
+ def test_updating
296
+ now = Time.now
297
+ Time.stubs(:now).returns(now - 1.year)
298
+ pirate = Pirate.create!
299
+ Time.stubs(:now).returns(now)
300
+
301
+ dated_detail = pirate.dated_detail
302
+ original_dated_detail = dated_detail.class.find(dated_detail.id) # Cloning would keep millisecond parts of time which would make later comparisons harder
303
+
304
+ dated_detail.update_attribute(:ruthlessness, 10)
305
+
306
+ assert_equal now.to_i, dated_detail.start_on.to_i
307
+ assert_nil dated_detail.end_on
308
+
309
+ previous_dated_detail = PirateDatedDetail.find_by_start_on(original_dated_detail.start_on)
310
+ assert_equal dated_detail.start_on.to_i - 1, previous_dated_detail.end_on.to_i
311
+ assert_equal original_dated_detail.attributes.except('end_on', 'id'), previous_dated_detail.attributes.except('end_on', 'id')
312
+ end
313
+
314
+ # Tracked Attributes
315
+
316
+ def test_tracked_attributes_includes_relevant_columns
317
+ assert PirateDatedDetail.tracked_attributes.include?('catchphrase')
318
+ assert PirateDatedDetail.tracked_attributes.include?('ruthlessness')
319
+ assert PirateDatedDetail.tracked_attributes.include?('birth_date')
320
+ assert PirateDatedDetail.tracked_attributes.include?('parrot_id')
321
+ end
322
+
323
+ def test_tracked_attributes_excludes_id
324
+ assert !PirateDatedDetail.tracked_attributes.include?('id')
325
+ end
326
+
327
+ def test_tracked_attributes_excludes_start_on
328
+ assert !PirateDatedDetail.tracked_attributes.include?('start_on')
329
+ end
330
+
331
+ def test_tracked_attributes_excludes_end_on
332
+ assert !PirateDatedDetail.tracked_attributes.include?('end_on')
333
+ end
334
+
335
+ def test_tracked_attributes_excludes_updated_at
336
+ assert !PirateDatedDetail.tracked_attributes.include?('updated_at')
337
+ end
338
+
339
+ def test_tracked_attributes_excludes_created_at
340
+ assert !PirateDatedDetail.tracked_attributes.include?('created_at')
341
+ end
342
+
343
+ def test_tracked_attributes_excludes_parent_id
344
+ assert !PirateDatedDetail.tracked_attributes.include?('pirate_id')
345
+ end
346
+
347
+ # Dated Detail Retrieval
348
+
349
+ def test_dated_detail_for_oldest_timestamp_retrieved_via_named_scope
350
+ pirate = create_pirate([1.year.ago, 6.months.ago, 1.month.ago])
351
+ assert_equal catchphrase(1.year.ago), pirate.dated_details.on(8.months.ago).first.catchphrase
352
+ end
353
+
354
+ def test_tracked_attribute_for_middle_timestamp_retrieved_via_named_scope
355
+ pirate = create_pirate([1.year.ago, 6.months.ago, 1.month.ago])
356
+ assert_equal catchphrase(6.months.ago), pirate.dated_details.on(5.months.ago).first.catchphrase
357
+ end
358
+
359
+ def test_tracked_attribute_for_newest_timestamp_retrieved_via_named_scope
360
+ pirate = create_pirate([1.year.ago, 6.months.ago, 1.month.ago])
361
+ assert_equal catchphrase(1.month.ago), pirate.dated_details.on(2.weeks.ago).first.catchphrase
362
+ end
363
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_dated_detail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Paul Gillard
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-02 00:00:00 +00:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: paulmgillard@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ - LICENSE
25
+ files:
26
+ - LICENSE
27
+ - README.rdoc
28
+ - lib/acts_as_dated_detail.rb
29
+ - lib/active_record/acts/dated.rb
30
+ - lib/active_record/acts/dated_detail.rb
31
+ - test/dated_detail_test.rb
32
+ has_rdoc: true
33
+ homepage: http://github.com/paulgillard/acts_as_dated_detail
34
+ licenses: []
35
+
36
+ post_install_message:
37
+ rdoc_options:
38
+ - --main
39
+ - README.rdoc
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ requirements: []
55
+
56
+ rubyforge_project:
57
+ rubygems_version: 1.3.5
58
+ signing_key:
59
+ specification_version: 3
60
+ summary: acts_as_dated_detail enables versioning of attributes by timestamp
61
+ test_files: []
62
+