taskr 0.1.0

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.
@@ -0,0 +1,48 @@
1
+ # This file is part of Taskr.
2
+ #
3
+ # Taskr is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # Taskr is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with Taskr. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ $: << File.dirname(File.expand_path(__FILE__))
17
+
18
+ # Try to load local versions of Picnic and Reststop if possible...
19
+ $: << File.dirname(File.expand_path(__FILE__))+"/../../../picnic/lib"
20
+ $: << File.dirname(File.expand_path(__FILE__))+"/../../vendor/picnic/lib"
21
+ $: << File.dirname(File.expand_path(__FILE__))+"/../../../reststop/lib"
22
+ $: << File.dirname(File.expand_path(__FILE__))+"/../../vendor/reststop/lib"
23
+
24
+ # active_resource needs newer versions of active_support, but this conflicts
25
+ # with active_record, so we need a newer version of that as well (yes, it's a mess)
26
+ #$: << File.dirname(File.expand_path(__FILE__))+"/../../vendor/activeresource/lib"
27
+ #$: << File.dirname(File.expand_path(__FILE__))+"/../../vendor/activesupport/lib"
28
+ #$: << File.dirname(File.expand_path(__FILE__))+"/../../vendor/activerecord/lib"
29
+
30
+ require 'rubygems'
31
+
32
+ require 'active_support'
33
+ #require 'active_resource'
34
+ require 'active_record'
35
+
36
+
37
+ # make things backwards-compatible for rubygems < 0.9.0
38
+ unless Object.method_defined? :gem
39
+ alias gem require_gem
40
+ end
41
+
42
+ require 'picnic.rb'
43
+ require 'camping/db'
44
+
45
+ require 'reststop'
46
+
47
+ gem 'openwferu-scheduler', '~> 0.9.16'
48
+ require 'openwfe/util/scheduler'
@@ -0,0 +1,79 @@
1
+ # This file is part of Taskr.
2
+ #
3
+ # Taskr is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # Taskr is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with Taskr. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ module Taskr::Helpers
17
+ def taskr_response_xml(result, &block)
18
+ instruct!
19
+ tag!("response", 'result' => result, 'xmlns:taskr' => "http://taskr.googlecode.com") do
20
+ yield
21
+ end
22
+ end
23
+
24
+ def html_task_action_li(ta)
25
+ li ta.action_class_name
26
+ ul do
27
+ ta.action_parameters.each do |ap|
28
+ li do
29
+ label "#{ap.name}:"
30
+ pre ap.value
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def html_scaffold
37
+ html do
38
+ head do
39
+ title "Taskr"
40
+ link(:rel => 'stylesheet', :type => 'text/css', :href => '/public/taskr.css')
41
+ script(:type => 'text/javascript', :src => '/public/prototype.js')
42
+ end
43
+ body do
44
+ yield
45
+ end
46
+ end
47
+ end
48
+
49
+ # Taken from Rails
50
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
51
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
52
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
53
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
54
+ distance_in_seconds = ((to_time - from_time).abs).round
55
+
56
+ case distance_in_minutes
57
+ when 0..1
58
+ return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
59
+ case distance_in_seconds
60
+ when 0..4 then 'less than 5 seconds'
61
+ when 5..9 then 'less than 10 seconds'
62
+ when 10..19 then 'less than 20 seconds'
63
+ when 20..39 then 'half a minute'
64
+ when 40..59 then 'less than a minute'
65
+ else '1 minute'
66
+ end
67
+
68
+ when 2..44 then "#{distance_in_minutes} minutes"
69
+ when 45..89 then 'about 1 hour'
70
+ when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
71
+ when 1440..2879 then '1 day'
72
+ when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
73
+ when 43200..86399 then 'about 1 month'
74
+ when 86400..525959 then "#{(distance_in_minutes / 43200).round} months"
75
+ when 525960..1051919 then 'about 1 year'
76
+ else "over #{(distance_in_minutes / 525960).round} years"
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,238 @@
1
+ # This file is part of Taskr.
2
+ #
3
+ # Taskr is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # Taskr is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with Taskr. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ require 'camping/db'
17
+ require 'openwfe/util/scheduler'
18
+ require 'date'
19
+
20
+ class OpenWFE::Scheduler
21
+ public :duration_to_f
22
+ end
23
+
24
+
25
+ module Taskr::Models
26
+
27
+ class Task < Base
28
+ has_many :task_actions,
29
+ :include => :action_parameters,
30
+ :dependent => :destroy
31
+
32
+ serialize :schedule_options
33
+ serialize :last_triggered_error
34
+
35
+ validates_presence_of :schedule_method
36
+ validates_presence_of :schedule_when
37
+ validates_presence_of :name
38
+ validates_uniqueness_of :name
39
+ validates_presence_of :task_actions
40
+ validates_associated :task_actions
41
+
42
+ def schedule!(scheduler)
43
+ case schedule_method
44
+ when 'cron'
45
+ method = :schedule
46
+ when 'at'
47
+ method = :schedule_at
48
+ when 'in'
49
+ method = :schedule_in
50
+ when 'every'
51
+ method = :schedule_every
52
+ end
53
+
54
+ if method == :schedule_at || method == :schedule_in
55
+ t = next_trigger_time
56
+ method = :schedule_at
57
+ if t < Time.now
58
+ $LOG.warn "Task #{name.inspect} will not be scheduled because its trigger time is in the past (#{t.inspect})."
59
+ return nil
60
+ end
61
+ end
62
+
63
+ $LOG.debug "Scheduling task #{name.inspect}: #{self.inspect}"
64
+
65
+ if task_actions.length == 1
66
+ ta = task_actions.first
67
+
68
+ parameters = {}
69
+ ta.action_parameters.each{|p| parameters[p.name] = p.value}
70
+
71
+ action = (ta.action_class.kind_of?(Class) ? ta.action_class : ta.action_class.constantize).new(parameters)
72
+ action.task = self
73
+ elsif task_actions.length > 1
74
+ action = Taskr::Actions::Multi.new
75
+ task_actions.each do |ta|
76
+ parameters = {}
77
+ ta.action_parameters.each{|p| parameters[p.name] = p.value}
78
+
79
+ a = (ta.action_class.kind_of?(Class) ? ta.action_class : ta.action_class.constantize).new(parameters)
80
+ a.task = self
81
+
82
+ action.actions << a
83
+ end
84
+ action.task = self
85
+ else
86
+ $LOG.warn "Task #{name.inspect} has no actions and as a result will not be scheduled!"
87
+ return false
88
+ end
89
+
90
+ job_id = scheduler.send(method, t || schedule_when, :schedulable => action)
91
+
92
+ if job_id
93
+ $LOG.debug "Task #{name.inspect} scheduled with job id #{job_id}"
94
+ else
95
+ $LOG.error "Task #{name.inspect} was NOT scheduled!"
96
+ return nil
97
+ end
98
+
99
+ self.update_attribute(:scheduler_job_id, job_id)
100
+ if method == :schedule_at || method == :schedule_in
101
+ job = scheduler.get_job(job_id)
102
+ at = job.schedule_info
103
+ self.update_attribute(:schedule_when, at)
104
+ self.update_attribute(:schedule_method, 'at')
105
+ end
106
+
107
+ return job_id
108
+ end
109
+
110
+ def next_trigger_time
111
+ # TODO: need to figure out how to calulate trigger_time for these.. for now return :unknown
112
+ return :unknown unless schedule_method == 'at' || schedule_method == 'in'
113
+
114
+ if schedule_method == 'in'
115
+ return (created_on || Time.now) + Taskr.scheduler.duration_to_f(schedule_when)
116
+ end
117
+
118
+ # Time parsing code from Rails
119
+ time_hash = Date._parse(schedule_when)
120
+ time_hash[:sec_fraction] = ((time_hash[:sec_fraction].to_f % 1) * 1_000_000).to_i
121
+ time_array = time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)
122
+ # treat 0000-00-00 00:00:00 as nil
123
+ Time.send(Base.default_timezone, *time_array) rescue DateTime.new(*time_array[0..5]) rescue nil
124
+ end
125
+
126
+ def to_s
127
+ "#<#{self.class}:#{self.id}>"
128
+ end
129
+ end
130
+
131
+ class TaskAction < Base
132
+ belongs_to :task
133
+
134
+ has_many :action_parameters,
135
+ :class_name => 'TaskActionParameter',
136
+ :foreign_key => :task_action_id,
137
+ :dependent => :destroy
138
+
139
+ validates_associated :action_parameters
140
+
141
+ def action_class=(class_name)
142
+ if class_name.kind_of? Class
143
+ self[:action_class_name] = class_name.to_s
144
+ else
145
+ self[:action_class_name] = class_name
146
+ end
147
+ end
148
+
149
+ def action_class
150
+ self[:action_class_name].constantize
151
+ end
152
+
153
+ def to_xml(options = {})
154
+ options[:indent] ||= 2
155
+ xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
156
+ xml.instruct! unless options[:skip_instruct]
157
+ xml.tag!('task-action', :type => self.class) do
158
+ xml.tag!('id', {:type => 'integer'}, id)
159
+ xml.tag!('action-class-name', action_class_name)
160
+ xml.tag!('order', {:type => 'integer'}, order) unless order.blank?
161
+ xml.tag!('task-id', {:type => 'integer'}, task_id)
162
+ xml.tag!('action-parameters', {:type => 'array'}) do
163
+ action_parameters.each {|ap| ap.to_xml(options)}
164
+ end
165
+ end
166
+ end
167
+
168
+ def to_s
169
+ "#<#{self.class}:#{self.id}>"
170
+ end
171
+ end
172
+
173
+ class TaskActionParameter < Base
174
+ belongs_to :task_action
175
+ serialize :value
176
+
177
+ def to_xml(options = {})
178
+ options[:indent] ||= 2
179
+ xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
180
+ xml.instruct! unless options[:skip_instruct]
181
+ xml.tag!('action-parameter', :type => self.class) do
182
+ xml.tag!('id', {:type => 'integer'}, id)
183
+ xml.tag!('name', name)
184
+ xml.tag!('value') do
185
+ xml.cdata!(value)
186
+ end
187
+ end
188
+ end
189
+
190
+ def to_s
191
+ "#<#{self.class}:#{self.id}>"
192
+ end
193
+ end
194
+
195
+ class CreateTaskr < V 0.01
196
+ def self.up
197
+ $LOG.info("Migrating database")
198
+
199
+ create_table :taskr_tasks, :force => true do |t|
200
+ t.column :name, :string, :null => false
201
+ t.column :created_on, :timestamp, :null => false
202
+ t.column :created_by, :string
203
+
204
+ t.column :schedule_method, :string, :null => false
205
+ t.column :schedule_when, :string, :null => false
206
+ t.column :schedule_options, :text
207
+
208
+ t.column :scheduler_job_id, :integer
209
+ t.column :last_triggered, :datetime
210
+ t.column :last_triggered_error, :text
211
+ end
212
+
213
+ add_index :taskr_tasks, [:name], :unique => true
214
+
215
+ create_table :taskr_task_actions, :force => true do |t|
216
+ t.column :task_id, :integer, :null => false
217
+ t.column :action_class_name, :string, :null => false
218
+ t.column :order, :integer
219
+ end
220
+
221
+ add_index :taskr_task_actions, [:task_id]
222
+
223
+ create_table :taskr_task_action_parameters, :force => true do |t|
224
+ t.column :task_action_id, :integer, :null => false
225
+ t.column :name, :string, :null => false
226
+ t.column :value, :text
227
+ end
228
+
229
+ add_index :taskr_task_action_parameters, [:task_action_id]
230
+ add_index :taskr_task_action_parameters, [:task_action_id, :name]
231
+ end
232
+
233
+ def self.down
234
+ drop_table :taskr_task_action_parameters
235
+ drop_table :taskr_tasks
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,9 @@
1
+ module Taskr #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,276 @@
1
+ # This file is part of Taskr.
2
+ #
3
+ # Taskr is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # Taskr is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with Taskr. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ # need auto_validation off to render non-XHTML XML
17
+ Markaby::Builder.set(:auto_validation, false)
18
+ Markaby::Builder.set(:indent, 2)
19
+
20
+ module Taskr::Views
21
+ module XML
22
+ include Taskr::Models
23
+
24
+ CONTENT_TYPE = 'text/xml'
25
+
26
+ def tasks_list
27
+ @tasks.to_xml(:root => 'tasks', :include => [:task_actions])
28
+ end
29
+
30
+ def view_task
31
+ @task.to_xml(:root => 'task', :include => [:task_actions])
32
+ end
33
+
34
+ def create_task_result
35
+ taskr_response_xml(@task.valid? ? 'success' : 'failure') do
36
+ text @task.to_xml
37
+ text @task.errors.to_xml unless @task.valid?
38
+ end
39
+ end
40
+ end
41
+
42
+ module HTML
43
+ CONTENT_TYPE = 'text/html'
44
+
45
+ def tasks_list
46
+ html_scaffold do
47
+ h1 {"Tasks"}
48
+
49
+ p{a(:href => R(Taskr::Controllers::Tasks, 'new')) {"Schedule New Task"}}
50
+
51
+ table do
52
+ thead do
53
+ tr do
54
+ th "Name"
55
+ th "Schedule"
56
+ th "Last Triggered"
57
+ th "Job ID"
58
+ th "Created On"
59
+ th "Created By"
60
+ end
61
+ end
62
+ tbody do
63
+ @tasks.each do |t|
64
+ tr_css = []
65
+ tr_css << "error" if t.last_triggered_error
66
+ tr_css << "expired" if t.next_trigger_time != :unknown && t.next_trigger_time < Time.now
67
+
68
+ tr(:class => tr_css.join(" ")) do
69
+ td {a(:href => R(t)) {strong{t.name}}}
70
+ td "#{t.schedule_method} #{t.schedule_when}"
71
+ td do
72
+ if t.last_triggered
73
+ "#{distance_of_time_in_words(t.last_triggered, Time.now, true)} ago"
74
+ else
75
+ em "Not yet triggered"
76
+ end
77
+ end
78
+ td(:class => "job-id") {t.scheduler_job_id}
79
+ td t.created_on
80
+ td t.created_by
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ br
87
+ div {scheduler_status}
88
+ end
89
+ end
90
+
91
+ def new_task
92
+ html_scaffold do
93
+ script(:type => 'text/javascript') do
94
+ %{
95
+ function show_action_parameters(num) {
96
+ new Ajax.Updater('parameters_'+num, '/actions', {
97
+ method: 'get',
98
+ parameters: {
99
+ id: $F('action_class_name_'+num),
100
+ action: 'parameters_form',
101
+ num: num
102
+ }
103
+ });
104
+ }
105
+ }
106
+ end
107
+
108
+ form :method => 'post', :action => self/"/tasks?format=#{@format}" do
109
+ h1 "New Task"
110
+ input :type => 'hidden', :name => '_method', :value => 'post'
111
+
112
+ p do
113
+ label 'name'
114
+ br
115
+ input :type => 'text', :name => 'name', :size => 40
116
+ end
117
+
118
+ p do
119
+ label 'schedule'
120
+ br
121
+ select(:name => 'schedule_method') do
122
+ ['every','at','in','cron'].each do |method|
123
+ option(:value => method) {method}
124
+ end
125
+ end
126
+ input :type => 'text', :name => 'schedule_when', :size => 30
127
+ end
128
+
129
+ action_form
130
+
131
+ p do
132
+ a(:id => 'add_action', :href => '#'){'Add another action'}
133
+ end
134
+ script(:type => 'text/javascript') do
135
+ %{
136
+ Event.observe('add_action', 'click', function() {
137
+ new Ajax.Updater('add_action', '#{self/'/actions'}', {
138
+ method: 'get',
139
+ parameters: { action: 'new', num: $$('select.action_class_name').size() },
140
+ insertion: Insertion.Before
141
+ });
142
+ return false;
143
+ })
144
+ }
145
+ end
146
+
147
+ button(:type => 'submit') {"submit"}
148
+ end
149
+ end
150
+ end
151
+
152
+ def view_task
153
+ html_scaffold do
154
+ form(:method => 'delete', :style => 'display: inline') do
155
+ button(:type => 'submit', :value => 'delete') {"Delete"}
156
+ end
157
+ br
158
+ a(:href => self/'tasks') {"Back to Task List"}
159
+
160
+ h1 "Task #{@task.id}"
161
+ table do
162
+ tr do
163
+ th "Name:"
164
+ td @task.name
165
+ end
166
+ tr do
167
+ th "Schedule:"
168
+ td "#{@task.schedule_method} #{@task.schedule_when}"
169
+ end
170
+ tr do
171
+ th "Triggered:"
172
+ td do
173
+ if @task.last_triggered
174
+ span "#{distance_of_time_in_words(@task.last_triggered, Time.now, true)} ago"
175
+ span(:style => 'font-size: 8pt; color: #bbb'){"(#{@task.last_triggered})"}
176
+ else
177
+ em "Not yet triggered"
178
+ end
179
+ end
180
+ end
181
+ if @task.last_triggered_error
182
+ th "Error:"
183
+ td(:style => 'color: #e00;') do
184
+ strong "#{@task.last_triggered_error[:type]}"
185
+ br
186
+ span @task.last_triggered_error[:message]
187
+ end
188
+ end
189
+ tr do
190
+ th "Actions:"
191
+ td do
192
+ if @task.task_actions.length > 1
193
+ ol(:style => 'padding-left: 20px') do
194
+ @task.task_actions.each do |ta|
195
+ html_task_action_li(ta)
196
+ end
197
+ end
198
+ else
199
+ html_task_action_li(@task.task_actions.first)
200
+ end
201
+ end
202
+ end
203
+ tr do
204
+ th "Created By:"
205
+ td @task.created_by
206
+ end
207
+ tr do
208
+ th "Created On:"
209
+ td @task.created_on
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ def scheduler_status
216
+ s = Taskr.scheduler
217
+ h3(:style => "margin-bottom: 8px;") {"Scheduler Status"}
218
+ strong "Running?"
219
+ span(:style => 'margin-right: 10px') {s.instance_variable_get(:@stopped) ? "NO" : "Yes"}
220
+ strong "Precision:"
221
+ span(:style => 'margin-right: 10px') {"#{s.instance_variable_get(:@precision)}s"}
222
+ strong "Pending Jobs:"
223
+ span(:style => 'margin-right: 10px') {s.instance_variable_get(:@pending_jobs).size}
224
+ strong "Thread Status:"
225
+ span(:style => 'margin-right: 10px') {s.instance_variable_get(:@scheduler_thread).status}
226
+ end
227
+
228
+ def action_list
229
+ h1 "Actions"
230
+ ul do
231
+ @actions.each do |a|
232
+ li a
233
+ end
234
+ end
235
+ end
236
+
237
+ def action_parameters_form
238
+ @num ||= 0
239
+
240
+ p {em @action.description}
241
+
242
+ @action.parameters.each do |param|
243
+ p do
244
+ label param
245
+ br
246
+ input :type => 'textarea', :name => "action[#{@num}][#{param}]", :size => 40
247
+ end
248
+ end
249
+ end
250
+
251
+ def action_form
252
+ @num ||= 0
253
+
254
+ p do
255
+ label 'action_class_name'
256
+ br
257
+ select(:name => "action[#{@num}][action_class_name]",
258
+ :id => "action_class_name_#{@num}",
259
+ :class => "action_class_name",
260
+ :onchange => "show_action_parameters(#{@num})") do
261
+ option(:value => "")
262
+ @actions.each do |a|
263
+ a.to_s =~ /Taskr::Actions::([^:]*?)$/
264
+ option(:value => $~[1]) {$~[1]}
265
+ end
266
+ end
267
+ end
268
+
269
+ div(:id => "parameters_#{@num}")
270
+
271
+ end
272
+
273
+ end
274
+
275
+ default_format :HTML
276
+ end