acts_as_dated_detail 0.0.1

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.
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
+