mobilize-base 1.36 → 1.293

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/README.md +666 -1
  2. data/lib/mobilize-base.rb +1 -12
  3. data/lib/mobilize-base/extensions/array.rb +3 -8
  4. data/lib/mobilize-base/extensions/google_drive/acl.rb +1 -1
  5. data/lib/mobilize-base/extensions/google_drive/client_login_fetcher.rb +1 -2
  6. data/lib/mobilize-base/extensions/google_drive/file.rb +37 -11
  7. data/lib/mobilize-base/extensions/string.rb +6 -11
  8. data/lib/mobilize-base/extensions/yaml.rb +7 -10
  9. data/lib/mobilize-base/handlers/gbook.rb +38 -25
  10. data/lib/mobilize-base/handlers/gdrive.rb +4 -20
  11. data/lib/mobilize-base/handlers/gfile.rb +10 -64
  12. data/lib/mobilize-base/handlers/gridfs.rb +24 -19
  13. data/lib/mobilize-base/handlers/gsheet.rb +29 -45
  14. data/lib/mobilize-base/handlers/resque.rb +10 -17
  15. data/lib/mobilize-base/jobtracker.rb +196 -22
  16. data/lib/mobilize-base/models/job.rb +77 -107
  17. data/lib/mobilize-base/models/runner.rb +122 -36
  18. data/lib/mobilize-base/models/stage.rb +37 -18
  19. data/lib/mobilize-base/tasks.rb +13 -50
  20. data/lib/mobilize-base/version.rb +1 -1
  21. data/lib/samples/gdrive.yml +0 -15
  22. data/lib/samples/gridfs.yml +3 -0
  23. data/lib/samples/gsheet.yml +4 -4
  24. data/lib/samples/jobtracker.yml +6 -0
  25. data/mobilize-base.gemspec +3 -3
  26. data/test/base_job_rows.yml +11 -0
  27. data/test/mobilize-base_test.rb +106 -0
  28. data/test/test_base_1.yml +3 -0
  29. data/test/test_helper.rb +0 -155
  30. metadata +24 -36
  31. data/lib/mobilize-base/extensions/time.rb +0 -20
  32. data/lib/mobilize-base/helpers/job_helper.rb +0 -54
  33. data/lib/mobilize-base/helpers/jobtracker_helper.rb +0 -143
  34. data/lib/mobilize-base/helpers/runner_helper.rb +0 -83
  35. data/lib/mobilize-base/helpers/stage_helper.rb +0 -38
  36. data/lib/samples/gfile.yml +0 -9
  37. data/test/fixtures/base1_stage1.in.yml +0 -10
  38. data/test/fixtures/integration_expected.yml +0 -25
  39. data/test/fixtures/integration_jobs.yml +0 -12
  40. data/test/fixtures/is_due.yml +0 -97
  41. data/test/integration/mobilize-base_test.rb +0 -57
  42. data/test/unit/mobilize-base_test.rb +0 -33
@@ -0,0 +1,3 @@
1
+ - {date: 2013-01-01, snake_date: 2013-02-01, camelDate: 2013-03-01}
2
+ - {date: 2013-01-01, snake_date: 2013-02-02, camelDate: 2013-03-02}
3
+ - {date: 2013-01-01, snake_date: 2013-02-03, camelDate: 2013-03-03}
data/test/test_helper.rb CHANGED
@@ -8,158 +8,3 @@ $dir = File.dirname(File.expand_path(__FILE__))
8
8
  ENV['MOBILIZE_ENV'] = 'test'
9
9
  require 'mobilize-base'
10
10
  $TESTING = true
11
- module TestHelper
12
- def TestHelper.confirm_expected_jobs(expected_fixture_name,time_limit=600)
13
- jobs = {}
14
- jobs['expected'] = TestHelper.load_fixture(expected_fixture_name)
15
- jobs['pending'] = jobs['expected'].select{|j| j['confirmed_ats'].length < j['count']}
16
- start_time = Time.now.utc
17
- total_time = 0
18
- while (jobs['pending'].length>0 or Mobilize::Resque.workers('working').length>0) and total_time < time_limit
19
- #working jobs are running on the queue at this instant
20
- jobs['working'] = Mobilize::Resque.workers('working').map{|w| w.job}.select{|j| j and j['payload'] and j['payload']['args']}
21
- #failed jobs are in the failure queue
22
- jobs['failed'] = Mobilize::Resque.failures.select{|j| j and j['payload'] and j['payload']['args']}
23
-
24
- #unexpected jobs are not supposed to be run in this test, includes leftover failures
25
- jobs['unexpected'] = {}
26
- error_msg = ""
27
- ['working','failed'].each do |state|
28
- jobs['unexpected'][state] = jobs[state].reject{|j|
29
- jobs['expected'].select{|ej|
30
- ej['state']==state and j['payload']['args'].first == ej['path']}.first}
31
- if jobs['unexpected'][state].length>0
32
- error_msg += state + ": " + jobs['unexpected'][state].map{|j| j['payload']['args'].first}.join(";") + "\n"
33
- end
34
- end
35
- #clear out unexpected paths or there will be failure
36
- if error_msg.length>0
37
- raise "Found unexpected results:\n" + error_msg
38
- end
39
-
40
- #now make sure pending jobs get done
41
- jobs['expected'].each do |j|
42
- start_confirmed_ats = j['confirmed_ats']
43
- resque_timestamps = jobs[j['state']].select{|sj| sj['payload']['args'].first == j['path']}.map{|sj| sj['run_at'] || sj['failed_at']}
44
- new_timestamps = (resque_timestamps - start_confirmed_ats).uniq
45
- if new_timestamps.length>0 and j['confirmed_ats'].length < j['count']
46
- j['confirmed_ats'] += new_timestamps
47
- puts "#{Time.now.utc.to_s}: #{new_timestamps.length.to_s} #{j['state']} added to #{j['path']}; total #{j['confirmed_ats'].length.to_s} of #{j['count']}"
48
- end
49
- end
50
-
51
- #figure out who's still pending
52
- jobs['pending'] = jobs['expected'].select{|j| j['confirmed_ats'].length < j['count']}
53
- sleep 1
54
- total_time = Time.now.utc - start_time
55
- puts "#{total_time.to_s} seconds elapsed" if total_time.to_s.ends_with?("0")
56
- end
57
- end
58
-
59
- #test methods
60
- def TestHelper.restart_test_redis
61
- TestHelper.stop_test_redis
62
- if !system("which redis-server")
63
- raise "** can't find `redis-server` in your path, you need redis to run Resque and Mobilize"
64
- end
65
- "redis-server #{Mobilize::Base.root}/test/redis-test.conf".bash
66
- end
67
-
68
- def TestHelper.stop_test_redis
69
- processes = `ps -A -o pid,command | grep [r]edis-test`.split($/)
70
- pids = processes.map { |process| process.split(" ")[0] }
71
- puts "Killing test redis server..."
72
- pids.each { |pid| Process.kill("TERM", pid.to_i) }
73
- puts "removing redis db dump file"
74
- sleep 5
75
- `rm -f #{Mobilize::Base.root}/test/dump.rdb #{Mobilize::Base.root}/test/dump-cluster.rdb`
76
- end
77
-
78
- def TestHelper.set_test_env
79
- ENV['MOBILIZE_ENV']='test'
80
- ::Resque.redis="localhost:9736"
81
- mongoid_config_path = "#{Mobilize::Base.root}/config/mobilize/mongoid.yml"
82
- Mongoid.load!(mongoid_config_path, Mobilize::Base.env)
83
- end
84
-
85
- def TestHelper.drop_test_db
86
- TestHelper.set_test_env
87
- Mongoid.session(:default).collections.each do |collection|
88
- unless collection.name =~ /^system\./
89
- collection.drop
90
- end
91
- end
92
- end
93
-
94
- def TestHelper.build_test_runner(user_name)
95
- TestHelper.set_test_env
96
- u = Mobilize::User.where(:name=>user_name).first
97
- Mobilize::Jobtracker.update_status("delete old books and datasets")
98
- # delete any old runner from previous test runs
99
- gdrive_slot = Mobilize::Gdrive.owner_email
100
- u.runner.gsheet(gdrive_slot).spreadsheet.delete
101
- Mobilize::Dataset.find_by_handler_and_path('gbook',u.runner.title).delete
102
- Mobilize::Jobtracker.update_status("enqueue jobtracker, wait 45s")
103
- Mobilize::Jobtracker.start
104
- sleep 45
105
- end
106
-
107
- def TestHelper.owner_user
108
- gdrive_slot = Mobilize::Gdrive.owner_email
109
- user_name = gdrive_slot.split("@").first
110
- return Mobilize::User.find_or_create_by_name(user_name)
111
- end
112
-
113
- def TestHelper.load_fixture(name)
114
- #assume yml, check
115
- yml_file_path = "#{Mobilize::Base.root}/test/fixtures/#{name}.yml"
116
- standard_file_path = "#{Mobilize::Base.root}/test/fixtures/#{name}"
117
- if File.exists?(yml_file_path)
118
- YAML.load_file(yml_file_path)
119
- elsif File.exists?(standard_file_path)
120
- File.read(standard_file_path)
121
- else
122
- raise "Could not find #{standard_file_path}"
123
- end
124
- end
125
-
126
- def TestHelper.write_fixture(fixture_name, target_url, options={})
127
- u = TestHelper.owner_user
128
- fixture_raw = TestHelper.load_fixture(fixture_name)
129
- if options['replace']
130
- fixture_data = if fixture_raw.class == Array
131
- fixture_raw.hash_array_to_tsv
132
- elsif fixture_raw.class == String
133
- fixture_raw
134
- end
135
- Mobilize::Dataset.write_by_url(target_url,fixture_data,u.name,u.email)
136
- elsif options['update']
137
- handler, sheet_path = target_url.split("://")
138
- raise "update only works for gsheet, not #{handler}" unless handler=='gsheet'
139
- sheet = Mobilize::Gsheet.find_or_create_by_path(sheet_path,u.email)
140
- sheet.add_or_update_rows(fixture_raw)
141
- else
142
- raise "unknown options #{options.to_s}"
143
- end
144
- return true
145
- end
146
-
147
- #checks output sheet for matching string or minimum length
148
- def TestHelper.check_output(target_url, options={})
149
- u = TestHelper.owner_user
150
- handler, sheet_path = target_url.split("://")
151
- handler = nil
152
- sheet = Mobilize::Gsheet.find_by_path(sheet_path,u.email)
153
- raise "no output found" if sheet.nil?
154
- output = sheet.to_tsv
155
- if options['match']
156
- return true if output == options['match']
157
- elsif options['min_length']
158
- return true if output.length >= options['min_length']
159
- else
160
- raise "unknown check options #{options.to_s}"
161
- end
162
- return true
163
- end
164
-
165
- end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mobilize-base
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.36'
4
+ version: '1.293'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-21 00:00:00.000000000 Z
12
+ date: 2013-03-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -34,7 +34,7 @@ dependencies:
34
34
  requirements:
35
35
  - - '='
36
36
  - !ruby/object:Gem::Version
37
- version: 1.8.4
37
+ version: 1.6.1
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +42,7 @@ dependencies:
42
42
  requirements:
43
43
  - - '='
44
44
  - !ruby/object:Gem::Version
45
- version: 1.8.4
45
+ version: 1.6.1
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: bson_ext
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -50,7 +50,7 @@ dependencies:
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 1.8.4
53
+ version: 1.6.1
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
@@ -58,39 +58,39 @@ dependencies:
58
58
  requirements:
59
59
  - - '='
60
60
  - !ruby/object:Gem::Version
61
- version: 1.8.4
61
+ version: 1.6.1
62
62
  - !ruby/object:Gem::Dependency
63
- name: mongoid
63
+ name: mongo
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  none: false
66
66
  requirements:
67
- - - ~>
67
+ - - '='
68
68
  - !ruby/object:Gem::Version
69
- version: 3.0.0
69
+ version: 1.6.1
70
70
  type: :runtime
71
71
  prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
73
73
  none: false
74
74
  requirements:
75
- - - ~>
75
+ - - '='
76
76
  - !ruby/object:Gem::Version
77
- version: 3.0.0
77
+ version: 1.6.1
78
78
  - !ruby/object:Gem::Dependency
79
- name: mongoid-grid_fs
79
+ name: mongoid
80
80
  requirement: !ruby/object:Gem::Requirement
81
81
  none: false
82
82
  requirements:
83
- - - ! '>='
83
+ - - ~>
84
84
  - !ruby/object:Gem::Version
85
- version: '0'
85
+ version: 3.0.0
86
86
  type: :runtime
87
87
  prerelease: false
88
88
  version_requirements: !ruby/object:Gem::Requirement
89
89
  none: false
90
90
  requirements:
91
- - - ! '>='
91
+ - - ~>
92
92
  - !ruby/object:Gem::Version
93
- version: '0'
93
+ version: 3.0.0
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: resque
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -182,7 +182,6 @@ files:
182
182
  - lib/mobilize-base/extensions/resque-server/views/queues.erb
183
183
  - lib/mobilize-base/extensions/resque-server/views/working.erb
184
184
  - lib/mobilize-base/extensions/string.rb
185
- - lib/mobilize-base/extensions/time.rb
186
185
  - lib/mobilize-base/extensions/yaml.rb
187
186
  - lib/mobilize-base/handlers/email.rb
188
187
  - lib/mobilize-base/handlers/gbook.rb
@@ -191,10 +190,6 @@ files:
191
190
  - lib/mobilize-base/handlers/gridfs.rb
192
191
  - lib/mobilize-base/handlers/gsheet.rb
193
192
  - lib/mobilize-base/handlers/resque.rb
194
- - lib/mobilize-base/helpers/job_helper.rb
195
- - lib/mobilize-base/helpers/jobtracker_helper.rb
196
- - lib/mobilize-base/helpers/runner_helper.rb
197
- - lib/mobilize-base/helpers/stage_helper.rb
198
193
  - lib/mobilize-base/jobtracker.rb
199
194
  - lib/mobilize-base/models/dataset.rb
200
195
  - lib/mobilize-base/models/job.rb
@@ -204,7 +199,6 @@ files:
204
199
  - lib/mobilize-base/tasks.rb
205
200
  - lib/mobilize-base/version.rb
206
201
  - lib/samples/gdrive.yml
207
- - lib/samples/gfile.yml
208
202
  - lib/samples/gridfs.yml
209
203
  - lib/samples/gsheet.yml
210
204
  - lib/samples/jobtracker.yml
@@ -212,14 +206,11 @@ files:
212
206
  - lib/samples/resque.yml
213
207
  - lib/samples/resque_web.rb
214
208
  - mobilize-base.gemspec
215
- - test/fixtures/base1_stage1.in.yml
216
- - test/fixtures/integration_expected.yml
217
- - test/fixtures/integration_jobs.yml
218
- - test/fixtures/is_due.yml
219
- - test/integration/mobilize-base_test.rb
209
+ - test/base_job_rows.yml
210
+ - test/mobilize-base_test.rb
220
211
  - test/redis-test.conf
212
+ - test/test_base_1.yml
221
213
  - test/test_helper.rb
222
- - test/unit/mobilize-base_test.rb
223
214
  homepage: http://github.com/ngmoco/mobilize-base
224
215
  licenses: []
225
216
  post_install_message:
@@ -234,7 +225,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
234
225
  version: '0'
235
226
  segments:
236
227
  - 0
237
- hash: -579148248180029294
228
+ hash: 3027972477721057312
238
229
  required_rubygems_version: !ruby/object:Gem::Requirement
239
230
  none: false
240
231
  requirements:
@@ -243,7 +234,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
234
  version: '0'
244
235
  segments:
245
236
  - 0
246
- hash: -579148248180029294
237
+ hash: 3027972477721057312
247
238
  requirements: []
248
239
  rubyforge_project: mobilize-base
249
240
  rubygems_version: 1.8.25
@@ -252,11 +243,8 @@ specification_version: 3
252
243
  summary: Moves datasets and schedules data transfers using MongoDB, Resque and Google
253
244
  Docs
254
245
  test_files:
255
- - test/fixtures/base1_stage1.in.yml
256
- - test/fixtures/integration_expected.yml
257
- - test/fixtures/integration_jobs.yml
258
- - test/fixtures/is_due.yml
259
- - test/integration/mobilize-base_test.rb
246
+ - test/base_job_rows.yml
247
+ - test/mobilize-base_test.rb
260
248
  - test/redis-test.conf
249
+ - test/test_base_1.yml
261
250
  - test/test_helper.rb
262
- - test/unit/mobilize-base_test.rb
@@ -1,20 +0,0 @@
1
- class Time
2
- def Time.at_marks_ago(number=1, unit='day', mark='0000')
3
- curr_time = Time.now.utc
4
- #strip out non-numerical characters from mark, add colon
5
- mark = mark.gsub(/[^0-9]/i,"").rjust(4,'0').ie{|m| [m[0..1],":",m[-2..-1]].join}
6
- #if user passes in 0 for the number, make it 1
7
- number = (number.to_i <= 0 ? 1 : number.to_i)
8
- if unit == 'day'
9
- curr_mark_time = Time.parse(curr_time.strftime("%Y-%m-%d #{mark} UTC"))
10
- elsif unit == 'hour'
11
- if curr_time.strftime("%H%M").to_i > mark.to_i
12
- curr_mark_time = Time.parse(curr_time.strftime("%Y-%m-%d %H:#{mark[-2..-1]} UTC"))
13
- end
14
- end
15
- #last mark time is
16
- mark_ago_increment = (curr_time > curr_mark_time ? (number-1).send(unit) : number.send(unit))
17
- last_mark_time = curr_mark_time - mark_ago_increment
18
- return last_mark_time
19
- end
20
- end
@@ -1,54 +0,0 @@
1
- #this module adds convenience methods to the Job model
2
- module Mobilize
3
- module JobHelper
4
- def name
5
- j = self
6
- j.path.split("/").last
7
- end
8
-
9
- def stages
10
- j = self
11
- #starts with the job path, followed by a slash
12
- Stage.where(:path=>/^#{j.path.escape_regex}\//).to_a.sort_by{|s| s.path}
13
- end
14
-
15
- def status
16
- #last stage status
17
- j = self
18
- j.active_stage.status if j.active_stage
19
- end
20
-
21
- def active_stage
22
- j = self
23
- #latest started at or first
24
- j.stages.select{|s| s.started_at}.sort_by{|s| s.started_at}.last || j.stages.first
25
- end
26
-
27
- def completed_at
28
- j = self
29
- j.stages.last.completed_at if j.stages.last
30
- end
31
-
32
- def failed_at
33
- j = self
34
- j.active_stage.failed_at if j.active_stage
35
- end
36
-
37
- def status_at
38
- j = self
39
- j.active_stage.status_at if j.active_stage
40
- end
41
-
42
- #convenience methods
43
- def runner
44
- j = self
45
- runner_path = j.path.split("/")[0..-2].join("/")
46
- return Runner.where(:path=>runner_path).first
47
- end
48
-
49
- def is_working?
50
- j = self
51
- j.stages.select{|s| s.is_working?}.compact.length>0
52
- end
53
- end
54
- end
@@ -1,143 +0,0 @@
1
- module Mobilize
2
- module Jobtracker
3
- def Jobtracker.config
4
- Base.config('jobtracker')
5
- end
6
-
7
- #modify this to increase the frequency of request cycles
8
- def Jobtracker.cycle_freq
9
- Jobtracker.config['cycle_freq']
10
- end
11
-
12
- #frequency of notifications
13
- def Jobtracker.notification_freq
14
- Jobtracker.config['notification_freq']
15
- end
16
-
17
- def Jobtracker.runner_read_freq
18
- Jobtracker.config['runner_read_freq']
19
- end
20
-
21
- #long running tolerance
22
- def Jobtracker.max_run_time
23
- Jobtracker.config['max_run_time']
24
- end
25
-
26
- def Jobtracker.admins
27
- Jobtracker.config['admins']
28
- end
29
-
30
- def Jobtracker.worker
31
- Resque.find_worker_by_path("jobtracker")
32
- end
33
-
34
- def Jobtracker.workers(state="all")
35
- Resque.workers(state)
36
- end
37
-
38
- def Jobtracker.status
39
- args = Jobtracker.get_args
40
- return args['status'] if args
41
- job = Resque.jobs.select{|j| j['args'].first=='jobtracker'}.first
42
- return 'queued' if job
43
- return 'stopped'
44
- end
45
-
46
- def Jobtracker.update_status(msg)
47
- #this is to keep jobtracker from resisting stop commands
48
- return false if Jobtracker.status=="stopping"
49
- #Jobtracker has no persistent database state
50
- Resque.set_worker_args_by_path("jobtracker",{'status'=>msg})
51
- return true
52
- end
53
-
54
- def Jobtracker.restart
55
- Jobtracker.stop!
56
- Jobtracker.start
57
- end
58
-
59
- def Jobtracker.set_args(args)
60
- Resque.set_worker_args(Jobtracker.worker,args)
61
- return true
62
- end
63
-
64
- def Jobtracker.get_args
65
- Resque.get_worker_args(Jobtracker.worker)
66
- end
67
-
68
- def Jobtracker.kill_workers
69
- Resque.kill_workers
70
- end
71
-
72
- def Jobtracker.kill_idle_workers
73
- Resque.kill_idle_workers
74
- end
75
-
76
- def Jobtracker.kill_idle_and_stale_workers
77
- Resque.kill_idle_and_stale_workers
78
- end
79
-
80
- def Jobtracker.prep_workers
81
- Resque.prep_workers
82
- end
83
-
84
- def Jobtracker.failures
85
- Resque.failures
86
- end
87
-
88
- def Jobtracker.start
89
- if Jobtracker.status!='stopped'
90
- Jobtracker.update_status("Jobtracker still #{Jobtracker.status}")
91
- else
92
- #make sure that workers are running and at the right number
93
- #Resque.prep_workers
94
- #queue up the jobtracker (starts the perform method)
95
- Jobtracker.enqueue!
96
- end
97
- return true
98
- end
99
-
100
- def Jobtracker.enqueue!
101
- ::Resque::Job.create(Resque.queue_name, Jobtracker, 'jobtracker',{})
102
- end
103
-
104
- def Jobtracker.restart!
105
- Jobtracker.stop!
106
- Jobtracker.start
107
- return true
108
- end
109
-
110
- def Jobtracker.restart_workers!
111
- Jobtracker.kill_workers
112
- sleep 10
113
- Jobtracker.prep_workers
114
- Jobtracker.update_status("put workers back on the queue")
115
- end
116
-
117
- def Jobtracker.stop!
118
- #send signal for Jobtracker to check for
119
- Jobtracker.update_status('stopping')
120
- sleep 5
121
- i=0
122
- while Jobtracker.status=='stopping'
123
- puts "#{Jobtracker.to_s} still on queue, waiting"
124
- sleep 5
125
- i+=1
126
- end
127
- return true
128
- end
129
-
130
- def Jobtracker.last_notification
131
- return Jobtracker.get_args["last_notification"] if Jobtracker.get_args
132
- end
133
-
134
- def Jobtracker.last_notification=(time)
135
- Jobtracker.set_args({"last_notification"=>time})
136
- end
137
-
138
- def Jobtracker.notif_due?
139
- last_duetime = Time.now.utc - Jobtracker.notification_freq
140
- return (Jobtracker.last_notification.to_s.length==0 || Jobtracker.last_notification.to_datetime < last_duetime)
141
- end
142
- end
143
- end