diff --git a/app/assets/javascripts/app/models/job.coffee b/app/assets/javascripts/app/models/job.coffee index 571080bac..36fd9611b 100644 --- a/app/assets/javascripts/app/models/job.coffee +++ b/app/assets/javascripts/app/models/job.coffee @@ -3,26 +3,28 @@ class App.Job extends App.Model @extend Spine.Model.Ajax @url: @apiPath + '/jobs' @configure_attributes = [ - { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, + { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, { name: 'timeplan', display: 'When should the job run?', tag: 'timer', null: true }, { name: 'condition', display: 'Conditions for effected objects', tag: 'ticket_selector', null: true }, { name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true }, { name: 'disable_notiifcation', display: 'Disable Notifications', tag: 'boolean', default: true }, - { name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true }, - { name: 'active', display: 'Active', tag: 'active', default: true }, - { name: 'matching', display: 'Matching', readonly: 1 }, - { name: 'processed', display: 'Processed', readonly: 1 }, - { name: 'last_run_at', display: 'Last run', tag: 'datetime', readonly: 1 }, - { name: 'running', display: 'Running', tag: 'boolean', readonly: 1 }, - { name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 }, - { name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 }, - { name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 }, - { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, + { name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true }, + { name: 'active', display: 'Active', tag: 'active', default: true }, + { name: 'matching', display: 'Will process', readonly: 1 }, + { name: 'processed', display: 'Has processed', readonly: 1 }, + { name: 'last_run_at', display: 'Last run', tag: 'datetime', readonly: 1 }, + { name: 'next_run_at', display: 'Scheduled for', tag: 'datetime', readonly: 1 }, + { name: 'running', display: 'Running', tag: 'boolean', readonly: 1 }, + { name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 }, + { name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 }, + { name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 }, + { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, ] @configure_delete = true @configure_overview = [ 'name', 'last_run_at', - 'matching', 'processed', + 'next_run_at', + 'matching', ] diff --git a/app/models/channel/email_parser.rb b/app/models/channel/email_parser.rb index 15e8a6fe4..e449ea0b5 100644 --- a/app/models/channel/email_parser.rb +++ b/app/models/channel/email_parser.rb @@ -507,7 +507,7 @@ retrns end end - # execute ticket events + # execute ticket notification events Observer::Ticket::Notification.transaction # run postmaster post filter diff --git a/app/models/job.rb b/app/models/job.rb index 256800c9f..7d4a66bf7 100644 --- a/app/models/job.rb +++ b/app/models/job.rb @@ -6,8 +6,8 @@ class Job < ApplicationModel store :perform validates :name, presence: true - before_create :updated_matching - before_update :updated_matching + before_create :updated_matching, :update_next_run_at + before_update :updated_matching, :update_next_run_at notify_clients_support @@ -37,21 +37,31 @@ class Job < ApplicationModel if tickets tickets.each do |ticket| - logger.debug "Perform job #{job.perform.inspect} in Ticket.find(#{ticket.id})" - changed = false - job.perform.each do |key, value| - (object_name, attribute) = key.split('.', 2) - raise "Unable to update object #{object_name}.#{attribute}, only can update tickets!" if object_name != 'ticket' - next if ticket[attribute].to_s == value['value'].to_s - changed = true + # use transaction + ActiveRecord::Base.transaction do + UserInfo.current_user_id = 1 - ticket[attribute] = value['value'] - logger.debug "set #{object_name}.#{attribute} = #{value['value'].inspect}" + logger.debug "Perform job #{job.perform.inspect} in Ticket.find(#{ticket.id})" + changed = false + job.perform.each do |key, value| + (object_name, attribute) = key.split('.', 2) + raise "Unable to update object #{object_name}.#{attribute}, only can update tickets!" if object_name != 'ticket' + + next if ticket[attribute].to_s == value['value'].to_s + changed = true + + ticket[attribute] = value['value'] + logger.debug "set #{object_name}.#{attribute} = #{value['value'].inspect}" + end + next if !changed + ticket.save + + # execute ticket notification events + if !job.disable_notification + Observer::Ticket::Notification.transaction + end end - next if !changed - ticket.updated_by_id = 1 - ticket.save end end @@ -63,6 +73,7 @@ class Job < ApplicationModel end def executable? + return false if !active # only execute jobs, older then 1 min, to give admin posibility to change return false if updated_at > Time.zone.now - 1.minute @@ -74,8 +85,7 @@ class Job < ApplicationModel true end - def in_timeplan? - time = Time.zone.now + def in_timeplan?(time = Time.zone.now) day_map = { 0 => 'Sun', 1 => 'Mon', @@ -106,12 +116,117 @@ class Job < ApplicationModel ticket_count || 0 end + def next_run_at_calculate(time = Time.zone.now) + if last_run_at + diff = time - last_run_at + if diff > 0 + time = time + 10.minutes + end + end + day_map = { + 0 => 'Sun', + 1 => 'Mon', + 2 => 'Tue', + 3 => 'Wed', + 4 => 'Thu', + 5 => 'Fri', + 6 => 'Sat', + } + return nil if !active + return nil if !timeplan['days'] + return nil if !timeplan['hours'] + return nil if !timeplan['minutes'] + + # loop week days + (0..7).each do |day_counter| + time_to_check = nil + day_to_check = if day_counter == 0 + time + else + time + 1.day + end + if !timeplan['days'][day_map[day_to_check.wday]] + + # start on next day at 00:00:00 + time = day_to_check - day_to_check.sec.seconds + time = time - day_to_check.min.minutes + time = time - day_to_check.hour.hours + next + end + + min = day_to_check.min + if min < 9 + min = 0 + elsif min < 20 + min = 10 + elsif min < 30 + min = 20 + elsif min < 40 + min = 30 + elsif min < 50 + min = 40 + elsif min < 60 + min = 50 + end + + # move to [0-5]0:00 time stamps + day_to_check = day_to_check - day_to_check.min.minutes + min.minutes + day_to_check = day_to_check - day_to_check.sec.seconds + + # loop minutes till next full hour + if day_to_check.min != 0 + (0..5).each do |minute_counter| + if minute_counter != 0 + break if day_to_check.min == 0 + day_to_check = day_to_check + 10.minutes + end + next if !timeplan['hours'][day_to_check.hour] && !timeplan['hours'][day_to_check.hour.to_s] + next if !timeplan['minutes'][match_minutes(day_to_check.min)] && !timeplan['minutes'][match_minutes(day_to_check.min).to_s] + return day_to_check + end + end + + # loop hours + hour_to_check = nil + (0..23).each do |hour_counter| + hour_to_check = day_to_check + hour_counter.hours + + # start on next day + if hour_to_check.day != day_to_check.day + time = day_to_check - day_to_check.hour.hours + break + end + + # ignore not configured hours + next if !timeplan['hours'][hour_to_check.hour] && !timeplan['hours'][hour_to_check.hour.to_s] + return nil if !hour_to_check + + # loop minutes + minute_to_check = nil + (0..5).each do |minute_counter| + minute_to_check = hour_to_check + minute_counter.minutes * 10 + next if !timeplan['minutes'][match_minutes(minute_to_check.min)] && !timeplan['minutes'][match_minutes(minute_to_check.min).to_s] + time_to_check = minute_to_check + break + end + next if !minute_to_check + return time_to_check + end + + end + nil + end + private def updated_matching self.matching = matching_count end + def update_next_run_at + self.next_run_at = next_run_at_calculate + end + def match_minutes(minutes) return 0 if minutes < 10 "#{minutes.to_s.gsub(/(\d)\d/, '\\1')}0".to_i diff --git a/db/migrate/20120101000010_create_ticket.rb b/db/migrate/20120101000010_create_ticket.rb index 4968a4cb9..b2359d099 100644 --- a/db/migrate/20120101000010_create_ticket.rb +++ b/db/migrate/20120101000010_create_ticket.rb @@ -238,6 +238,7 @@ class CreateTicket < ActiveRecord::Migration t.column :perform, :string, limit: 2500, null: false t.column :disable_notification, :boolean, null: false, default: true t.column :last_run_at, :timestamp, null: true + t.column :next_run_at, :timestamp, null: true t.column :running, :boolean, null: false, default: false t.column :processed, :integer, null: false, default: 0 t.column :matching, :integer, null: false diff --git a/db/migrate/20160316000005_renew_triggers.rb b/db/migrate/20160316000006_renew_triggers.rb similarity index 96% rename from db/migrate/20160316000005_renew_triggers.rb rename to db/migrate/20160316000006_renew_triggers.rb index 4c6825293..5469f2d1e 100644 --- a/db/migrate/20160316000005_renew_triggers.rb +++ b/db/migrate/20160316000006_renew_triggers.rb @@ -22,6 +22,7 @@ class RenewTriggers < ActiveRecord::Migration t.column :perform, :string, limit: 2500, null: false t.column :disable_notification, :boolean, null: false, default: true t.column :last_run_at, :timestamp, null: true + t.column :next_run_at, :timestamp, null: true t.column :running, :boolean, null: false, default: false t.column :processed, :integer, null: false, default: 0 t.column :matching, :integer, null: false, default: 0 diff --git a/test/unit/job_test.rb b/test/unit/job_test.rb index 7a15bc67b..63c8c1b13 100644 --- a/test/unit/job_test.rb +++ b/test/unit/job_test.rb @@ -130,6 +130,7 @@ class JobTest < ActiveSupport::TestCase updated_by_id: 1, updated_at: Time.zone.now, ) + assert_not(job1.next_run_at) assert_not(job1.executable?) job1.last_run_at = Time.zone.now - 15.minutes @@ -140,6 +141,14 @@ class JobTest < ActiveSupport::TestCase job1.save assert(job1.executable?) + job1.active = false + job1.save + assert_not(job1.executable?) + + job1.active = true + job1.save + assert_not(job1.executable?) + assert_not(job1.in_timeplan?) time = Time.zone.now day_map = { @@ -168,7 +177,7 @@ class JobTest < ActiveSupport::TestCase min = 30 elsif min < 50 min = 40 - elsif min < 59 + elsif min < 60 min = 50 end job1.timeplan['minutes'][min.to_s] = true @@ -187,6 +196,7 @@ class JobTest < ActiveSupport::TestCase job1.save Job.run + assert(job1.next_run_at) assert(job1.executable?) assert(job1.in_timeplan?) @@ -422,4 +432,294 @@ class JobTest < ActiveSupport::TestCase end + test 'case 3' do + + # create jobs + job1 = Job.create_or_update( + name: 'Test Job1', + timeplan: { + days: { + Mon: true, + Tue: false, + Wed: false, + Thu: false, + Fri: true, + Sat: false, + Sun: false, + }, + hours: { + 0 => false, + 1 => true, + 2 => false, + 3 => false, + 4 => false, + 5 => false, + 6 => false, + 7 => false, + 8 => false, + 9 => false, + 10 => true, + 11 => false, + 12 => false, + 13 => false, + 14 => false, + 15 => false, + 16 => false, + 17 => false, + 18 => false, + 19 => false, + 20 => false, + 21 => false, + 22 => false, + 23 => false, + }, + minutes: { + 0 => true, + 10 => false, + 20 => false, + 30 => false, + 40 => true, + 50 => false, + }, + }, + condition: { + 'ticket.state_id' => { 'operator' => 'is', 'value' => [Ticket::State.lookup(name: 'new').id.to_s, Ticket::State.lookup(name: 'open').id.to_s] }, + 'ticket.created_at' => { 'operator' => 'before (relative)', 'value' => '2', 'range' => 'day' }, + }, + perform: { + 'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s } + }, + disable_notification: true, + last_run_at: nil, + active: true, + created_by_id: 1, + created_at: Time.zone.now, + updated_by_id: 1, + updated_at: Time.zone.now, + ) + + time_now = Time.zone.parse('2016-03-18 09:17:13 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-18 10:00:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-18 10:37:13 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-18 10:40:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-17 09:17:13 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-18 01:00:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-17 11:17:13 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-18 01:00:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-19 11:17:13 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-21 01:00:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-22 00:59:59 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-25 01:00:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-25 00:59:59 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-25 01:00:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-24 00:59:59 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-25 01:00:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-24 23:59:59 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-25 01:00:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-25 01:00:01 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-25 01:00:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-25 01:09:01 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-25 01:40:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-25 01:09:59 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-25 01:40:00 UTC', next_run_at.to_s) + + job1.last_run_at = Time.zone.parse('2016-03-18 10:00:01 UTC') + job1.save + time_now = Time.zone.parse('2016-03-18 10:00:02 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-18 10:40:00 UTC', next_run_at.to_s) + + job1.last_run_at = Time.zone.parse('2016-03-18 10:40:01 UTC') + job1.save + time_now = Time.zone.parse('2016-03-18 10:40:02 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-21 01:00:00 UTC', next_run_at.to_s) + + end + + test 'case 4' do + + # create jobs + job1 = Job.create_or_update( + name: 'Test Job1', + timeplan: { + days: { + Mon: true, + Tue: false, + Wed: false, + Thu: false, + Fri: true, + Sat: false, + Sun: false, + }, + hours: { + 0 => true, + 1 => false, + 2 => false, + 3 => false, + 4 => false, + 5 => false, + 6 => false, + 7 => false, + 8 => false, + 9 => false, + 10 => true, + 11 => false, + 12 => false, + 13 => false, + 14 => false, + 15 => false, + 16 => false, + 17 => false, + 18 => false, + 19 => false, + 20 => false, + 21 => false, + 22 => false, + 23 => false, + }, + minutes: { + 0 => true, + 10 => false, + 20 => false, + 30 => false, + 40 => true, + 50 => false, + }, + }, + condition: { + 'ticket.state_id' => { 'operator' => 'is', 'value' => [Ticket::State.lookup(name: 'new').id.to_s, Ticket::State.lookup(name: 'open').id.to_s] }, + 'ticket.created_at' => { 'operator' => 'before (relative)', 'value' => '2', 'range' => 'day' }, + }, + perform: { + 'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s } + }, + disable_notification: true, + last_run_at: nil, + active: true, + created_by_id: 1, + created_at: Time.zone.now, + updated_by_id: 1, + updated_at: Time.zone.now, + ) + + time_now = Time.zone.parse('2016-03-17 23:51:23 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-18 00:00:00 UTC', next_run_at.to_s) + + job1.last_run_at = Time.zone.parse('2016-03-17 23:45:01 UTC') + job1.save + time_now = Time.zone.parse('2016-03-17 23:51:23 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-18 00:00:00 UTC', next_run_at.to_s) + + job1.last_run_at = Time.zone.parse('2016-03-17 23:59:01 UTC') + job1.save + time_now = Time.zone.parse('2016-03-17 23:59:23 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-18 00:40:00 UTC', next_run_at.to_s) + + time_now = Time.zone.parse('2016-03-17 23:59:23 UTC') + assert_not(job1.in_timeplan?(time_now)) + + time_now = Time.zone.parse('2016-03-18 00:01:23 UTC') + assert(job1.in_timeplan?(time_now)) + + end + + test 'case 5' do + + # create jobs + job1 = Job.create_or_update( + name: 'Test Job1', + timeplan: { + days: { + Mon: true, + Tue: false, + Wed: false, + Thu: false, + Fri: false, + Sat: false, + Sun: false, + }, + hours: { + '0' => true, + '1' => false, + '2' => false, + '3' => false, + '4' => false, + '5' => false, + '6' => false, + '7' => false, + '8' => false, + '9' => false, + '10' => false, + '11' => false, + '12' => false, + '13' => false, + '14' => false, + '15' => false, + '16' => false, + '17' => false, + '18' => false, + '19' => false, + '20' => false, + '21' => false, + '22' => false, + '23' => false, + }, + minutes: { + '0' => true, + '10' => false, + '20' => false, + '30' => false, + '40' => false, + '50' => false, + }, + }, + condition: { + 'ticket.state_id' => { 'operator' => 'is', 'value' => [Ticket::State.lookup(name: 'new').id.to_s, Ticket::State.lookup(name: 'open').id.to_s] }, + 'ticket.created_at' => { 'operator' => 'before (relative)', 'value' => '2', 'range' => 'day' }, + }, + perform: { + 'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s } + }, + disable_notification: true, + last_run_at: nil, + active: true, + created_by_id: 1, + created_at: Time.zone.now, + updated_by_id: 1, + updated_at: Time.zone.now, + ) + + time_now = Time.zone.parse('2016-03-17 23:51:23 UTC') + next_run_at = job1.next_run_at_calculate(time_now) + assert_equal('2016-03-21 00:00:00 UTC', next_run_at.to_s) + + end + end