taskr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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