acts_as_flux_capacitor 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.5 2008-05-18
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/License.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Jim
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,35 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ PostInstall.txt
5
+ README.txt
6
+ Rakefile
7
+ config/hoe.rb
8
+ config/requirements.rb
9
+ lib/acts_as_flux_capacitor.rb
10
+ lib/acts_as_flux_capacitor/acts_as_flux_capacitor.rb
11
+ lib/acts_as_flux_capacitor/core_class_extensions.rb
12
+ lib/acts_as_flux_capacitor/range_overlap.rb
13
+ lib/acts_as_flux_capacitor/temporal.rb
14
+ lib/acts_as_flux_capacitor/version.rb
15
+ script/console
16
+ script/destroy
17
+ script/generate
18
+ script/txt2html
19
+ setup.rb
20
+ spec/acts_as_flux_capacitor_spec.rb
21
+ spec/database.yml
22
+ spec/fixtures.rb
23
+ spec/report/report.html
24
+ spec/schema.rb
25
+ spec/spec.opts
26
+ spec/spec_helper.rb
27
+ tasks/deployment.rake
28
+ tasks/environment.rake
29
+ tasks/rspec.rake
30
+ tasks/website.rake
31
+ website/index.html
32
+ website/index.txt
33
+ website/javascripts/rounded_corners_lite.inc.js
34
+ website/stylesheets/screen.css
35
+ website/template.html.erb
data/PostInstall.txt ADDED
@@ -0,0 +1,7 @@
1
+
2
+ For more information on acts_as_flux_capacitor, see http://acts_as_flux_capacitor.rubyforge.org
3
+
4
+ NOTE: Change this information in PostInstall.txt
5
+ You can also delete it if you don't want it.
6
+
7
+
data/README.txt ADDED
@@ -0,0 +1,48 @@
1
+ = acts_as_flux_capacitor
2
+
3
+ * FIX (url)
4
+
5
+ == DESCRIPTION:
6
+
7
+ FIX (describe your package)
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * FIX (list of features or problems)
12
+
13
+ == SYNOPSIS:
14
+
15
+ FIX (code sample of usage)
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * FIX (list of requirements)
20
+
21
+ == INSTALL:
22
+
23
+ * FIX (sudo gem install, anything else)
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2008 FIX
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
data/config/hoe.rb ADDED
@@ -0,0 +1,73 @@
1
+ require 'acts_as_flux_capacitor/version'
2
+
3
+ AUTHOR = 'Jim Cropcho' # can also be an array of Authors
4
+ EMAIL = "jim.cropcho@gmail.com"
5
+ DESCRIPTION = "Acts as Flux Capacitor is a better way to work with time-centric ActiveRecord models. Make it feel good to manipulate objects representing real-world events and/or objects with a finite period of database persistence!"
6
+ GEM_NAME = 'acts_as_flux_capacitor' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'aafc' # The unix name for your project
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+ EXTRA_DEPENDENCIES = [
11
+ # ['activesupport', '>= 1.3.1']
12
+ ] # An array of rubygem dependencies [name, version]
13
+
14
+ @config_file = "~/.rubyforge/user-config.yml"
15
+ @config = nil
16
+ RUBYFORGE_USERNAME = "JimCropcho"
17
+ def rubyforge_username
18
+ unless @config
19
+ begin
20
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
21
+ rescue
22
+ puts <<-EOS
23
+ ERROR: No rubyforge config file found: #{@config_file}
24
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
25
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
26
+ EOS
27
+ exit
28
+ end
29
+ end
30
+ RUBYFORGE_USERNAME.replace @config["username"]
31
+ end
32
+
33
+
34
+ REV = nil
35
+ # UNCOMMENT IF REQUIRED:
36
+ # REV = YAML.load(`svn info`)['Revision']
37
+ VERS = ActsAsFluxCapacitor::VERSION::STRING + (REV ? ".#{REV}" : "")
38
+ RDOC_OPTS = ['--quiet', '--title', 'acts_as_flux_capacitor documentation',
39
+ "--opname", "index.html",
40
+ "--line-numbers",
41
+ "--main", "README",
42
+ "--inline-source"]
43
+
44
+ class Hoe
45
+ def extra_deps
46
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
47
+ @extra_deps
48
+ end
49
+ end
50
+
51
+ # Generate all the Rake tasks
52
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
53
+ $hoe = Hoe.new(GEM_NAME, VERS) do |p|
54
+ p.developer(AUTHOR, EMAIL)
55
+ p.description = DESCRIPTION
56
+ p.summary = DESCRIPTION
57
+ p.url = HOMEPATH
58
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
59
+ p.test_globs = ["test/**/test_*.rb"]
60
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
61
+
62
+ # == Optional
63
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
64
+ #p.extra_deps = EXTRA_DEPENDENCIES
65
+
66
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
67
+ end
68
+
69
+ CHANGES = $hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
70
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
71
+ $hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
72
+ $hoe.rsync_args = '-av --delete --ignore-errors'
73
+ $hoe.spec.post_install_message = File.open(File.dirname(__FILE__) + "/../PostInstall.txt").read rescue ""
@@ -0,0 +1,15 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
@@ -0,0 +1,9 @@
1
+ SUBDIRECTORY = 'acts_as_flux_capacitor'
2
+
3
+ require 'date'
4
+ require File.join(SUBDIRECTORY, 'version')
5
+ require File.join(SUBDIRECTORY, 'temporal')
6
+ require File.join(SUBDIRECTORY, 'range_overlap')
7
+ require File.join(SUBDIRECTORY, 'core_class_extensions')
8
+ require File.join(SUBDIRECTORY, 'acts_as_flux_capacitor')
9
+
@@ -0,0 +1,352 @@
1
+ module HolmesLabs #:nodoc:
2
+ module Acts #:nodoc:
3
+ module FluxCapacitor
4
+ module ActiveRecord #:nodoc:
5
+ def self.included(base) # :nodoc:
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def acts_as_flux_capacitor
11
+ unless acts_as_flux_capacitor?
12
+ class << self
13
+ alias_method :original_find, :find
14
+ end
15
+ end
16
+ include Temporal
17
+ include InstanceMethods
18
+ include OperateOnEvents
19
+ include OperateOnDurations
20
+ include CompareEvents
21
+ include Validation
22
+ end
23
+
24
+ def flux_capacitor?
25
+ included_modules.include?(InstanceMethods)
26
+ end
27
+ alias acts_as_flux_capacitor? flux_capacitor?
28
+ end
29
+
30
+ protected
31
+
32
+ module InstanceMethods #:nodoc:
33
+ def self.included(base) # :nodoc:
34
+ base.extend ClassMethods
35
+ base.extend QueryGenerators
36
+ end
37
+
38
+ module ClassMethods
39
+
40
+ CUSTOM_FIND_OPTIONS = [
41
+ :before, # raw sql
42
+ :after, # raw sql
43
+
44
+ :now,
45
+ :at,
46
+
47
+ :past,
48
+ :present,
49
+ :future,
50
+
51
+ :from,
52
+ :to,
53
+
54
+ :starts_before, # raw sql
55
+ :ends_after # raw sql
56
+ ]
57
+
58
+ def find(*args)
59
+ options = args.extract_options!
60
+
61
+ current_time = options.delete(:current_time)
62
+ temporal_conditions = extract_temporal_conditions!(options)
63
+ sql_temporal = sql_generate_temporal(temporal_conditions,current_time)
64
+
65
+ puts temporal_conditions.size
66
+ puts sql_temporal
67
+
68
+ with_scope( :find => {
69
+ :conditions => sql_temporal ,
70
+ :order => sql_oldest_first }) do
71
+ original_find(*(args << options))
72
+ end
73
+ end
74
+
75
+ def overlap(an_event,another_event)
76
+ an_event.to_range.overlap(another_event.to_range)
77
+ end
78
+
79
+ def overlap?(an_event,another_event)
80
+ an_event.to_range.overlap?(another_event.to_range)
81
+ end
82
+ private
83
+ def extract_temporal_conditions!(options)
84
+ temporal_conditions = {}
85
+
86
+ options.each do |key,val|
87
+ temporal_conditions.merge!({key, options.delete(key)}) if CUSTOM_FIND_OPTIONS.include?(key)
88
+ end
89
+
90
+
91
+ require 'pp'# ; pp "(#{temporal_conditions.class}) #{temporal_conditions.first.keys}: #{temporal_conditions.first.values}"
92
+ pp temporal_conditions
93
+ temporal_conditions
94
+ end
95
+ end
96
+
97
+ module QueryGenerators
98
+ private
99
+
100
+ def sql_before(some_time, current_time = nil)
101
+ # table_name comes from ActiveRecord.
102
+ "#{table_name}.ends_at < '#{some_time.to_s(:db)}'"
103
+ end
104
+ alias sql_to sql_before
105
+ alias sql_ends_before sql_before
106
+
107
+ def sql_after(some_time , current_time = nil)
108
+ # table_name comes from ActiveRecord.
109
+ "#{table_name}.begins_at > '#{some_time.to_s(:db)}'"
110
+ end
111
+ alias sql_from sql_after
112
+ alias sql_begins_after sql_after
113
+
114
+ def sql_at(some_time , current_time = Time.now)
115
+ sql_join_conditions([ sql_starts_before( some_time),
116
+ sql_ends_after( some_time)])
117
+ end
118
+
119
+ def sql_ends_after(some_time , current_time = nil)
120
+ "#{table_name}.ends_at > '#{some_time.to_s(:db)}'"
121
+ end
122
+
123
+ def sql_starts_before(some_time , current_time = nil)
124
+ "#{table_name}.begins_at < '#{some_time.to_s(:db)}'"
125
+ end
126
+
127
+ def sql_past(is_true , current_time = Time.now)
128
+ if is_true
129
+ sql_before(current_time)
130
+ else
131
+ sql_join_conditions_or([ sql_present( Time.now),
132
+ sql_future( Time.now)])
133
+ end
134
+ end
135
+ alias sql_ended sql_past
136
+
137
+ def sql_present(is_true , current_time = Time.now)
138
+ if is_true
139
+ sql_at(current_time)
140
+ else
141
+ sql_join_conditions_or([ sql_past( Time.now),
142
+ sql_future( Time.now)])
143
+ end
144
+ end
145
+ alias sql_now sql_present
146
+
147
+ def sql_future(is_true, current_time = Time.now)
148
+ if is_true
149
+ sql_after(current_time)
150
+ else
151
+ sql_join_conditions_or([ sql_present( Time.now),
152
+ sql_past( Time.now)])
153
+ end
154
+ end
155
+
156
+ def sql_oldest_first(attrib = :begins_at)
157
+ "#{table_name}.#{attrib.to_s} ASC"
158
+ end
159
+
160
+ def sql_join_conditions(conditions)
161
+ "#{conditions.join(") AND (")}"
162
+ end
163
+
164
+ def sql_join_conditions_or(conditions)
165
+ "#{conditions.join(") OR (")}"
166
+ end
167
+
168
+ def sql_generate_temporal(temporal_conditions,current_time)
169
+ sql_conditions = []
170
+ temporal_conditions.each do |condition,parameter|
171
+ puts "#{condition}: #{parameter}"
172
+ sql_conditions << send("sql_#{condition}" , parameter , current_time)
173
+ end
174
+
175
+ require 'pp' ; pp sql_conditions
176
+ pp sql_join_conditions(sql_conditions)
177
+ sql_join_conditions(sql_conditions)
178
+ end
179
+ end
180
+
181
+ def to_range
182
+ (start..finish)
183
+ end
184
+ alias range to_range
185
+
186
+ def start ; read_attribute(:begins_at) ; end
187
+ def start=(val) ; write_attribute(:begins_at,val); end
188
+
189
+ def finish ; read_attribute(:ends_at) ; end
190
+ def finish=(val) ; write_attribute(:ends_at,val) ; end
191
+
192
+ alias start_time start
193
+ alias start_time= start=
194
+ alias beginning start
195
+ alias beginning= start=
196
+ alias end_time finish
197
+ alias end_time= finish=
198
+ alias from start
199
+ alias from= start=
200
+ alias to finish
201
+ alias to= finish=
202
+ alias ending finish
203
+ alias ending= finish=
204
+ end # InstanceMethods
205
+
206
+ module OperateOnEvents
207
+ def at?(obj=Time.now)
208
+ bigger_than = self.start_time.before?(extract_time(obj,:start_time)) &&
209
+ self.end_time.after?( extract_time(obj,:end_time ))
210
+ smaller_than = self.start_time.after?( extract_time(obj,:start_time)) &&
211
+ self.end_time.before?( extract_time(obj,:end_time))
212
+ same_size = self.start_time == extract_time(obj,:start_time) &&
213
+ self.end_time == extract_time(obj,:end_time)
214
+ return bigger_than || smaller_than || same_size
215
+ end
216
+ alias happening_at? at?
217
+ alias happened_at? at?
218
+
219
+ def happening_now?(time = Time.now)
220
+ at?(time)
221
+ end
222
+ alias in_progress? happening_now?
223
+ alias now? happening_now?
224
+ alias present? happening_now?
225
+
226
+ def started?(time = Time.now)
227
+ self.start_time.before?(time)
228
+ end
229
+ alias began? started?
230
+
231
+ def ended?(time = Time.now)
232
+ self.end_time.before?(time)
233
+ end
234
+ alias finished? ended?
235
+
236
+ def <=>(obj)
237
+ if self.before?(obj)
238
+ return -1
239
+ elsif at?(obj)
240
+ return 0
241
+ elsif self.after?(obj)
242
+ return 1
243
+ else
244
+ return 'ActsAsEvent: non-fatal error in #status'
245
+ end
246
+ end
247
+
248
+ def transpose(duration)
249
+ new_event = self.clone
250
+ new_event.transpose!(duration)
251
+ end
252
+ alias shift transpose
253
+ alias reschedule transpose
254
+ alias move transpose
255
+
256
+ def transpose!(duration)
257
+ self.beginning += duration
258
+ self.ending += duration
259
+ return self
260
+ end
261
+ alias shift! transpose!
262
+ alias reschedule! transpose!
263
+ alias move! transpose!
264
+ end # OperateOnEvents
265
+
266
+ module OperateOnDurations
267
+ def elapsed(time = Time.now)
268
+ time_since_beginning = self.class.length_of_time_since(self.beginning,time)
269
+ self.now?(time) ? time_since_beginning : nil
270
+ end
271
+
272
+ def remaining(time = Time.now)
273
+ time_until_end = self.class.length_of_time_until(self.ending,time)
274
+ self.now?(time) ? time_until_end : nil
275
+ end
276
+
277
+ def percent_complete(time = Time.now)
278
+ amount_complete = self.elapsed(time)
279
+ amount_complete ? 100 * ((amount_complete * 1.0) / duration) : nil
280
+ end
281
+ alias percent_elapsed percent_complete
282
+ alias percent_finished percent_complete
283
+
284
+ def percent_remaining(time = Time.now)
285
+ amount_remaining = self.remaining(time)
286
+ amount_remaining ? 100 * (( amount_remaining * 1.0) / duration) : nil
287
+ end
288
+ end # OperateOnDurations
289
+
290
+ module CompareEvents
291
+ def overlaps_with?(other_event)
292
+ #this method possesses the associative property.
293
+ self.to_range.overlap?(other_event.to_range)
294
+ end
295
+
296
+ def overlap_with(other_event)
297
+ (self.to_range.overlap(other_event.to_range)).size
298
+ end
299
+ alias overlap overlap_with
300
+
301
+ def back_to_back_with?(other_event,tolerance = 0)
302
+ # tolerance is in seconds.
303
+ # first_event,second_event = self.class.send(:ordered_tuple,self,other_event)
304
+ # first_event.ends_at.approx_eql?(second_event.begins_at,tolerance)
305
+ self.start_time.before?(other_event.start_time) ? first_to_start = self : first_to_start = other_event
306
+ first_to_start.ends_at.approx_eql?(other_event.begins_at,tolerance)
307
+ end
308
+
309
+ def duration
310
+ self.class.length_of_time_until(self.ending,self.beginning)
311
+ end
312
+ alias length_of_time duration
313
+ alias length duration
314
+
315
+ def duration=(new_duration)
316
+ self.ending = self.beginning + new_duration
317
+ end
318
+ alias length_of_time= duration=
319
+ alias length= duration=
320
+ end # CompareEvents
321
+
322
+ module Validation
323
+
324
+ def validate
325
+ if self.begins_at.nil?
326
+ errors.add(:begins_at, "must have a start time")
327
+ if self.ends_at.nil?
328
+ errors.add(:ends_at, "must have an end time")
329
+ end
330
+ else
331
+ self.beginning.after?(self.ending) ? errors.add_to_base("An event cannot end before it begins") : nil
332
+ end
333
+ end
334
+ end
335
+ end # ActiveRecord
336
+ end # FluxCapacitor
337
+ end # Acts
338
+ end # HolmesLabs
339
+ ActiveRecord::Base.send :include, HolmesLabs::Acts::FluxCapacitor::ActiveRecord
340
+
341
+ # TODO
342
+ ###########
343
+ # allow events to be created by start time and duration
344
+ # force exclusivity of events at any given time via parameter to acts_as_event()
345
+ # add new validations
346
+ # rake tasks
347
+ # parameter to comparison operator to still allow true if overlap
348
+
349
+
350
+ # NOTES
351
+ ###########
352
+ # verify #find_* returns ordered results unless specified otherwise.