From 1950f82322c6b4975b4493c5fdc345724382446e Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Fri, 18 Mar 2016 03:04:49 +0100 Subject: [PATCH] Added ticket job feature. --- .../app/controllers/_ui_element/active.coffee | 6 +- .../controllers/_ui_element/boolean.coffee | 8 +- .../app/controllers/_ui_element/radio.coffee | 14 +- .../app/controllers/_ui_element/select.coffee | 14 +- .../app/controllers/_ui_element/tag.coffee | 2 +- .../controllers/_ui_element/textarea.coffee | 4 +- .../controllers/_ui_element/timezone.coffee | 6 +- .../javascripts/app/controllers/job.coffee | 29 ++ .../app/controllers/trigger.coffee | 104 +---- app/assets/javascripts/app/models/job.coffee | 9 +- .../javascripts/app/models/trigger.coffee | 21 + app/controllers/triggers_controller.rb | 30 ++ app/models/job.rb | 149 +++--- app/models/ticket.rb | 10 +- config/routes/test.rb | 1 + config/routes/trigger.rb | 11 + db/migrate/20120101000010_create_ticket.rb | 44 +- db/migrate/20141221000001_create_job.rb | 25 -- .../20160314000001_overview_user_relation.rb | 13 - db/migrate/20160316000005_renew_triggers.rb | 48 ++ db/seeds.rb | 9 + test/unit/job_test.rb | 425 ++++++++++++++++++ test/unit/ticket_selector_test.rb | 80 ++++ 23 files changed, 841 insertions(+), 221 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/job.coffee create mode 100644 app/assets/javascripts/app/models/trigger.coffee create mode 100644 app/controllers/triggers_controller.rb create mode 100644 config/routes/trigger.rb delete mode 100644 db/migrate/20141221000001_create_job.rb delete mode 100644 db/migrate/20160314000001_overview_user_relation.rb create mode 100644 db/migrate/20160316000005_renew_triggers.rb create mode 100644 test/unit/job_test.rb diff --git a/app/assets/javascripts/app/controllers/_ui_element/active.coffee b/app/assets/javascripts/app/controllers/_ui_element/active.coffee index e7947ed62..56d0b363e 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/active.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/active.coffee @@ -17,13 +17,13 @@ class App.UiElement.active extends App.UiElement.ApplicationUiElement attribute.name = '{boolean}' + attribute.name # build options list based on config - @getConfigOptionList( attribute, params ) + @getConfigOptionList(attribute, params) # sort attribute.options - @sortOptions( attribute, params ) + @sortOptions(attribute, params) # finde selected/checked item of list - @selectedOptions( attribute, params ) + @selectedOptions(attribute, params) # return item $( App.view('generic/select')( attribute: attribute ) ) \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/_ui_element/boolean.coffee b/app/assets/javascripts/app/controllers/_ui_element/boolean.coffee index fac677ef0..04951ae39 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/boolean.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/boolean.coffee @@ -14,13 +14,13 @@ class App.UiElement.boolean extends App.UiElement.ApplicationUiElement attribute.name = '{boolean}' + attribute.name # build options list based on config - @getConfigOptionList( attribute, params ) + @getConfigOptionList(attribute, params) # sort attribute.options - @sortOptions( attribute, params ) + @sortOptions(attribute, params) # finde selected/checked item of list - @selectedOptions( attribute, params ) + @selectedOptions(attribute, params) # return item - $( App.view('generic/select')( attribute: attribute ) ) \ No newline at end of file + $(App.view('generic/select')(attribute: attribute)) \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/_ui_element/radio.coffee b/app/assets/javascripts/app/controllers/_ui_element/radio.coffee index 6b03e62f8..00c5ec5b6 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/radio.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/radio.coffee @@ -3,24 +3,24 @@ class App.UiElement.radio extends App.UiElement.ApplicationUiElement @render: (attribute, params) -> # build options list based on config - @getConfigOptionList( attribute, params ) + @getConfigOptionList(attribute, params) # build options list based on relation - @getRelationOptionList( attribute, params ) + @getRelationOptionList(attribute, params) # add null selection if needed - @addNullOption( attribute, params ) + @addNullOption(attribute, params) # sort attribute.options - @sortOptions( attribute, params ) + @sortOptions(attribute, params) # finde selected/checked item of list - @selectedOptions( attribute, params ) + @selectedOptions(attribute, params) # disable item of list - @disabledOptions( attribute, params ) + @disabledOptions(attribute, params) # filter attributes - @filterOption( attribute, params ) + @filterOption(attribute, params) $( App.view('generic/radio')( attribute: attribute ) ) diff --git a/app/assets/javascripts/app/controllers/_ui_element/select.coffee b/app/assets/javascripts/app/controllers/_ui_element/select.coffee index 91a3ad8db..ada6c0b07 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/select.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/select.coffee @@ -9,25 +9,25 @@ class App.UiElement.select extends App.UiElement.ApplicationUiElement attribute.multiple = '' # build options list based on config - @getConfigOptionList( attribute, params ) + @getConfigOptionList(attribute, params) # build options list based on relation - @getRelationOptionList( attribute, params ) + @getRelationOptionList(attribute, params) # add null selection if needed - @addNullOption( attribute, params ) + @addNullOption(attribute, params) # sort attribute.options - @sortOptions( attribute, params ) + @sortOptions(attribute, params) # finde selected/checked item of list - @selectedOptions( attribute, params ) + @selectedOptions(attribute, params) # disable item of list - @disabledOptions( attribute, params ) + @disabledOptions(attribute, params) # filter attributes - @filterOption( attribute, params ) + @filterOption(attribute, params) # return item $( App.view('generic/select')( attribute: attribute ) ) \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/_ui_element/tag.coffee b/app/assets/javascripts/app/controllers/_ui_element/tag.coffee index 3fb865259..701bae89b 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/tag.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/tag.coffee @@ -5,5 +5,5 @@ class App.UiElement.tag a = -> $('#' + attribute.id ).tokenfield() $('#' + attribute.id ).parent().css('height', 'auto') - App.Delay.set( a, 120, undefined, 'tags' ) + App.Delay.set(a, 120, undefined, 'tags') item \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/_ui_element/textarea.coffee b/app/assets/javascripts/app/controllers/_ui_element/textarea.coffee index f0e7fc601..2a63da75f 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/textarea.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/textarea.coffee @@ -13,7 +13,7 @@ class App.UiElement.textarea if visible && !$( item[0] ).expanding('active') $( item[0] ).expanding().focus() ) - App.Delay.set( a, 80 ) + App.Delay.set(a, 80) if attribute.upload @@ -39,5 +39,5 @@ class App.UiElement.textarea fail: '' debug: false ) - App.Delay.set( u, 100, undefined, 'form_upload' ) + App.Delay.set(u, 100, undefined, 'form_upload') item \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/_ui_element/timezone.coffee b/app/assets/javascripts/app/controllers/_ui_element/timezone.coffee index a72413280..ce78512cd 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/timezone.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/timezone.coffee @@ -15,13 +15,13 @@ class App.UiElement.timezone extends App.UiElement.ApplicationUiElement attribute.options.push item # add null selection if needed - @addNullOption( attribute, params ) + @addNullOption(attribute, params) # sort attribute.options - @sortOptions( attribute, params ) + @sortOptions(attribute, params) # finde selected/checked item of list - @selectedOptions( attribute, params ) + @selectedOptions(attribute, params) attribute.tag = 'searchable_select' attribute.placeholder = App.i18n.translateInline('Enter timzone...') diff --git a/app/assets/javascripts/app/controllers/job.coffee b/app/assets/javascripts/app/controllers/job.coffee new file mode 100644 index 000000000..3d29dc0ee --- /dev/null +++ b/app/assets/javascripts/app/controllers/job.coffee @@ -0,0 +1,29 @@ +class Index extends App.ControllerContent + constructor: -> + super + + # check authentication + return if !@authenticate(false, 'Admin') + + new App.ControllerGenericIndex( + el: @el + id: @id + genericObject: 'Job' + defaultSortBy: 'name' + pageData: + title: 'Scheduler' + home: 'Jobs' + object: 'Scheduler' + objects: 'Schedulers' + navupdate: '#Jobs' + notes: [ + 'Scheduler are ...' + ] + buttons: [ + { name: 'New Scheduler', 'data-type': 'new', class: 'btn--success' } + ] + container: @el.closest('.content') + #large: true + ) + +App.Config.set('Job', { prio: 3400, name: 'Scheduler', parent: '#manage', target: '#manage/job', controller: Index, role: ['Admin'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/trigger.coffee b/app/assets/javascripts/app/controllers/trigger.coffee index 123f79965..4376f2d6f 100644 --- a/app/assets/javascripts/app/controllers/trigger.coffee +++ b/app/assets/javascripts/app/controllers/trigger.coffee @@ -1,88 +1,30 @@ -class Index extends App.ControllerTabs - header: 'Trigger' +class Index extends App.ControllerContent constructor: -> super - @title 'Trigger', true + # check authentication + return if !@authenticate(false, 'Admin') - @tabs = [ - { - name: 'Time Based', - target: 'c-time-based', - controller: App.TriggerTime, - }, - { - name: 'Event Based', - target: 'c-event-based', - controller: App.SettingsArea, - params: { area: 'Email::Base' }, - }, - { - name: 'Notifications', - target: 'c-notification', - controller: App.SettingsArea, - params: { area: 'Email::Base' }, - }, - { - name: 'Web Hooks', - target: 'c-web-hook', - controller: App.SettingsArea, - params: { area: 'Email::Base' }, - }, - ] - - @render() - -App.Config.set( 'Trigger', { prio: 3000, name: 'Trigger', parent: '#manage', target: '#manage/triggers', controller: Index, role: ['Admin'] }, 'NavBarAdmin' ) - -class App.TriggerTime extends App.Controller - events: - 'click .js-new': 'new' - #'click .js-edit': 'edit' - 'click .js-delete': 'delete' - - constructor: -> - super - @interval(@load, 30000) - #@load() - - load: => - @startLoading() - @ajax( - id: 'trigger_time_index' - type: 'GET' - url: @apiPath + '/jobs' - processData: true - success: (data, status, xhr) => - #App.Collection.loadAssets(data.assets) - @stopLoading() - @render(data) - ) - - render: (data = {}) => - - @html App.view('trigger/time/index')( - triggers: [] - ) - - - delete: (e) => - e.preventDefault() - id = $(e.target).closest('.action').data('id') - item = App.Channel.find(id) - new App.ControllerGenericDestroyConfirm( - item: item - container: @el.closest('.content') - callback: @load - ) - - new: (e) => - e.preventDefault() - channel_id = $(e.target).closest('.action').data('id') - new App.ControllerGenericNew( + new App.ControllerGenericIndex( + el: @el + id: @id + genericObject: 'Trigger' + defaultSortBy: 'name' + #groupBy: 'role' pageData: - object: 'Jobs' - genericObject: 'Job' + title: 'Triggers' + home: 'triggers' + object: 'Trigger' + objects: 'Triggers' + navupdate: '#triggers' + notes: [ + 'Triggers are ...' + ] + buttons: [ + { name: 'New Trigger', 'data-type': 'new', class: 'btn--success' } + ] container: @el.closest('.content') - callback: @load + #large: true ) + +App.Config.set('Trigger', { prio: 3300, name: 'Trigger', parent: '#manage', target: '#manage/trigger', controller: Index, role: ['Admin'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/models/job.coffee b/app/assets/javascripts/app/models/job.coffee index 855408151..571080bac 100644 --- a/app/assets/javascripts/app/models/job.coffee +++ b/app/assets/javascripts/app/models/job.coffee @@ -1,12 +1,13 @@ class App.Job extends App.Model - @configure 'Job', 'name', 'timeplan', 'condition', 'execute', 'note', 'active' + @configure 'Job', 'name', 'timeplan', 'condition', 'perform', 'disable_notiifcation', 'note', 'active' @extend Spine.Model.Ajax @url: @apiPath + '/jobs' @configure_attributes = [ { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, -# { name: 'timeplan', display: 'The times where the job should run.', tag: 'timeplan', null: true }, - { name: 'condition', display: 'Conditions for matching objects.', tag: 'ticket_selector', null: true }, - { name: 'execute', display: 'Execute changes on objects.', tag: 'ticket_perform_action', null: true }, + { 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 }, diff --git a/app/assets/javascripts/app/models/trigger.coffee b/app/assets/javascripts/app/models/trigger.coffee new file mode 100644 index 000000000..2c9352d60 --- /dev/null +++ b/app/assets/javascripts/app/models/trigger.coffee @@ -0,0 +1,21 @@ +class App.Trigger extends App.Model + @configure 'Trigger', 'name', 'condition', 'perform', 'active' + @extend Spine.Model.Ajax + @url: @apiPath + '/triggers' + @configure_attributes = [ + { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, + { name: 'condition', display: 'Conditions for effected objects', tag: 'ticket_selector', null: false }, + { name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true }, + { name: 'disable_notiifcation', display: 'Disable Notifications', tag: 'boolean', default: true }, + { name: 'active', display: 'Active', tag: 'active', default: true }, + { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, + ] + @configure_delete = true + @configure_overview = [ + 'name', + ] + + @description = ''' +Trigger are.... + +''' \ No newline at end of file diff --git a/app/controllers/triggers_controller.rb b/app/controllers/triggers_controller.rb new file mode 100644 index 000000000..867f67454 --- /dev/null +++ b/app/controllers/triggers_controller.rb @@ -0,0 +1,30 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + +class TriggersController < ApplicationController + before_action :authentication_check + + def index + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_index_render(Trigger, params) + end + + def show + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_show_render(Trigger, params) + end + + def create + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_create_render(Trigger, params) + end + + def update + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_update_render(Trigger, params) + end + + def destroy + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_destory_render(Trigger, params) + end +end diff --git a/app/models/job.rb b/app/models/job.rb index 352e71972..256800c9f 100644 --- a/app/models/job.rb +++ b/app/models/job.rb @@ -3,85 +3,118 @@ class Job < ApplicationModel store :timeplan store :condition - store :execute + store :perform validates :name, presence: true - before_create :updated_matching - before_update :updated_matching + before_create :updated_matching + before_update :updated_matching notify_clients_support def self.run - time = Time.zone.now - day_map = { - 0 => 'sun', - 1 => 'mon', - 2 => 'tue', - 3 => 'wed', - 4 => 'thu', - 5 => 'fri', - 6 => 'sat', - } - jobs = Job.where( active: true ) + jobs = Job.where(active: true, running: false) jobs.each do |job| + logger.debug "Execute job #{job.inspect}" - # only execute jobs, older then 1 min, to give admin posibility to change - next if job.updated_at > Time.zone.now - 1.minute + next if !job.executable? - # check if jobs need to be executed - # ignore if job was running within last 10 min. - next if job.last_run_at && job.last_run_at > Time.zone.now - 10.minutes - - # check day - next if !job.timeplan['days'].include?( day_map[time.wday] ) - - # check hour - next if !job.timeplan['hours'].include?( time.hour.to_s ) - - # check min - next if !job.timeplan['minutes'].include?( match_minutes(time.min.to_s) ) - - # find tickets to change - tickets = Ticket.where( job.condition.permit! ) - .order( '`tickets`.`created_at` DESC' ) - .limit( 1_000 ) - job.processed = tickets.count - tickets.each do |ticket| - logger.debug "CHANGE #{job.execute.inspect}" - changed = false - job.execute.each do |key, value| - changed = true - attribute = key.split('.', 2).last - logger.debug "-- #{Ticket.columns_hash[ attribute ].type}" - #value = 4 - #if Ticket.columns_hash[ attribute ].type == :integer - # logger.debug "to i #{attribute}/#{value.inspect}/#{value.to_i.inspect}" - # #value = value.to_i - #end - ticket[attribute] = value - logger.debug "set #{attribute} = #{value.inspect}" - end - next if !changed - ticket.updated_by_id = 1 - ticket.save + matching = job.matching_count + if job.matching != matching + job.matching = matching + job.save end + next if !job.in_timeplan? + + # find tickets to change + ticket_count, tickets = Ticket.selectors(job.condition, 2_000) + + logger.debug "Job #{job.name} with #{ticket_count} tickets" + + job.processed = ticket_count || 0 + job.running = true + job.save + + 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 + + ticket[attribute] = value['value'] + logger.debug "set #{object_name}.#{attribute} = #{value['value'].inspect}" + end + next if !changed + ticket.updated_by_id = 1 + ticket.save + end + end + + job.running = false job.last_run_at = Time.zone.now job.save end true end + def executable? + + # only execute jobs, older then 1 min, to give admin posibility to change + return false if updated_at > Time.zone.now - 1.minute + + # check if jobs need to be executed + # ignore if job was running within last 10 min. + return false if last_run_at && last_run_at > Time.zone.now - 10.minutes + + true + end + + def in_timeplan? + time = Time.zone.now + day_map = { + 0 => 'Sun', + 1 => 'Mon', + 2 => 'Tue', + 3 => 'Wed', + 4 => 'Thu', + 5 => 'Fri', + 6 => 'Sat', + } + + # check day + return false if !timeplan['days'] + return false if !timeplan['days'][day_map[time.wday]] + + # check hour + return false if !timeplan['hours'] + return false if !timeplan['hours'][time.hour.to_s] && !timeplan['hours'][time.hour] + + # check min + return false if !timeplan['minutes'] + return false if !timeplan['minutes'][match_minutes(time.min).to_s] && !timeplan['minutes'][match_minutes(time.min)] + + true + end + + def matching_count + ticket_count, tickets = Ticket.selectors(condition, 1) + ticket_count || 0 + end + private def updated_matching - count = Ticket.where( condition.permit! ).count - self.matching = count + self.matching = matching_count end - def self.match_minutes(minutes) - minutes.gsub!(/(\d)\d/, '\\1') - minutes.to_s + '0' + def match_minutes(minutes) + return 0 if minutes < 10 + "#{minutes.to_s.gsub(/(\d)\d/, '\\1')}0".to_i end - private_class_method :match_minutes + end diff --git a/app/models/ticket.rb b/app/models/ticket.rb index 68448d6b9..b89d8b317 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -468,7 +468,7 @@ condition example raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator'] # validate value / allow empty but only if pre_condition exists - if (selector['value'].class == String || selector['value'].class == Array) && (selector['value'].respond_to?(:empty?) && selector['value'].empty?) + if !selector.key?('value') || ((selector['value'].class == String || selector['value'].class == Array) && (selector['value'].respond_to?(:empty?) && selector['value'].empty?)) return nil if selector['pre_condition'].nil? || (selector['pre_condition'].respond_to?(:empty?) && selector['pre_condition'].empty?) end @@ -679,7 +679,7 @@ result return if !customer_id - customer = User.find( customer_id ) + customer = User.find(customer_id) return if organization_id == customer.organization_id self.organization_id = customer.organization_id @@ -691,8 +691,8 @@ result return if !changes['state_id'] # check if new state isn't pending* - current_state = Ticket::State.lookup( id: state_id ) - current_state_type = Ticket::StateType.lookup( id: current_state.state_type_id ) + current_state = Ticket::State.lookup(id: state_id) + current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id) # in case, set pending_time to nil return if current_state_type.name =~ /^pending/i @@ -706,7 +706,7 @@ result articles.destroy_all # destroy online notifications - OnlineNotification.remove( self.class.to_s, id ) + OnlineNotification.remove(self.class.to_s, id) end end diff --git a/config/routes/test.rb b/config/routes/test.rb index 9e2a315ec..639adf2d1 100644 --- a/config/routes/test.rb +++ b/config/routes/test.rb @@ -8,6 +8,7 @@ Zammad::Application.routes.draw do match '/tests_form_find', to: 'tests#form_find', via: :get match '/tests_form_trim', to: 'tests#form_trim', via: :get match '/tests_form_extended', to: 'tests#form_extended', via: :get + match '/tests_form_timer', to: 'tests#form_timer', via: :get match '/tests_form_validation', to: 'tests#form_validation', via: :get match '/tests_form_column_select', to: 'tests#form_column_select', via: :get match '/tests_form_searchable_select', to: 'tests#form_searchable_select', via: :get diff --git a/config/routes/trigger.rb b/config/routes/trigger.rb new file mode 100644 index 000000000..3c34c4d2d --- /dev/null +++ b/config/routes/trigger.rb @@ -0,0 +1,11 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + # triggers + match api_path + '/triggers', to: 'triggers#index', via: :get + match api_path + '/triggers/:id', to: 'triggers#show', via: :get + match api_path + '/triggers', to: 'triggers#create', via: :post + match api_path + '/triggers/:id', to: 'triggers#update', via: :put + match api_path + '/triggers/:id', to: 'triggers#destroy', via: :delete + +end diff --git a/db/migrate/20120101000010_create_ticket.rb b/db/migrate/20120101000010_create_ticket.rb index 2c0912898..4968a4cb9 100644 --- a/db/migrate/20120101000010_create_ticket.rb +++ b/db/migrate/20120101000010_create_ticket.rb @@ -188,7 +188,6 @@ class CreateTicket < ActiveRecord::Migration add_index :ticket_counters, [:generator], unique: true create_table :overviews do |t| - t.references :user, null: true t.references :role, null: false t.column :name, :string, limit: 250, null: false t.column :link, :string, limit: 250, null: false @@ -203,9 +202,15 @@ class CreateTicket < ActiveRecord::Migration t.column :created_by_id, :integer, null: false t.timestamps null: false end - add_index :overviews, [:user_id] add_index :overviews, [:name] + create_table :overviews_users, id: false do |t| + t.integer :overview_id + t.integer :user_id + end + add_index :overviews_users, [:overview_id] + add_index :overviews_users, [:user_id] + create_table :overviews_groups, id: false do |t| t.integer :overview_id t.integer :group_id @@ -214,13 +219,36 @@ class CreateTicket < ActiveRecord::Migration add_index :overviews_groups, [:group_id] create_table :triggers do |t| - t.column :name, :string, limit: 250, null: false - t.column :key, :string, limit: 250, null: false - t.column :value, :string, limit: 250, null: false + t.column :name, :string, limit: 250, null: false + t.column :condition, :string, limit: 2500, null: false + t.column :perform, :string, limit: 2500, null: false + t.column :disable_notification, :boolean, null: false, default: true + t.column :note, :string, limit: 250, null: true + t.column :active, :boolean, null: false, default: true + t.column :updated_by_id, :integer, null: false + t.column :created_by_id, :integer, null: false + t.timestamps null: false end - add_index :triggers, [:name] - add_index :triggers, [:key] - add_index :triggers, [:value] + add_index :triggers, [:name], unique: true + + create_table :jobs do |t| + t.column :name, :string, limit: 250, null: false + t.column :timeplan, :string, limit: 500, null: false + t.column :condition, :string, limit: 2500, null: false + 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 :running, :boolean, null: false, default: false + t.column :processed, :integer, null: false, default: 0 + t.column :matching, :integer, null: false + t.column :pid, :string, limit: 250, null: true + t.column :note, :string, limit: 250, null: true + t.column :active, :boolean, null: false, default: false + t.column :updated_by_id, :integer, null: false + t.column :created_by_id, :integer, null: false + t.timestamps null: false + end + add_index :jobs, [:name], unique: true create_table :notifications do |t| t.column :subject, :string, limit: 250, null: false diff --git a/db/migrate/20141221000001_create_job.rb b/db/migrate/20141221000001_create_job.rb deleted file mode 100644 index 17723d5b5..000000000 --- a/db/migrate/20141221000001_create_job.rb +++ /dev/null @@ -1,25 +0,0 @@ -class CreateJob < ActiveRecord::Migration - def up - create_table :jobs do |t| - t.column :name, :string, limit: 250, null: false - t.column :timeplan, :string, limit: 500, null: false - t.column :condition, :string, limit: 2500, null: false - t.column :execute, :string, limit: 2500, null: false - t.column :last_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 - t.column :pid, :string, limit: 250, null: true - t.column :note, :string, limit: 250, null: true - t.column :active, :boolean, null: false, default: false - t.column :updated_by_id, :integer, null: false - t.column :created_by_id, :integer, null: false - t.timestamps null: false - end - add_index :jobs, [:name], unique: true - end - - def down - drop_table :jobs - end -end diff --git a/db/migrate/20160314000001_overview_user_relation.rb b/db/migrate/20160314000001_overview_user_relation.rb deleted file mode 100644 index 5a7adfe07..000000000 --- a/db/migrate/20160314000001_overview_user_relation.rb +++ /dev/null @@ -1,13 +0,0 @@ - -class OverviewUserRelation < ActiveRecord::Migration - def up - create_table :overviews_users, id: false do |t| - t.integer :overview_id - t.integer :user_id - end - add_index :overviews_users, [:overview_id] - add_index :overviews_users, [:user_id] - remove_column :overviews, :user_id - end - -end diff --git a/db/migrate/20160316000005_renew_triggers.rb b/db/migrate/20160316000005_renew_triggers.rb new file mode 100644 index 000000000..4c6825293 --- /dev/null +++ b/db/migrate/20160316000005_renew_triggers.rb @@ -0,0 +1,48 @@ +class RenewTriggers < ActiveRecord::Migration + def up + drop_table :triggers + create_table :triggers do |t| + t.column :name, :string, limit: 250, null: false + t.column :condition, :string, limit: 2500, null: false + t.column :perform, :string, limit: 2500, null: false + t.column :disable_notification, :boolean, null: false, default: true + t.column :note, :string, limit: 250, null: true + t.column :active, :boolean, null: false, default: true + t.column :updated_by_id, :integer, null: false + t.column :created_by_id, :integer, null: false + t.timestamps null: false + end + add_index :triggers, [:name], unique: true + + drop_table :jobs + create_table :jobs do |t| + t.column :name, :string, limit: 250, null: false + t.column :timeplan, :string, limit: 1000, null: false + t.column :condition, :string, limit: 2500, null: false + 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 :running, :boolean, null: false, default: false + t.column :processed, :integer, null: false, default: 0 + t.column :matching, :integer, null: false, default: 0 + t.column :pid, :string, limit: 250, null: true + t.column :note, :string, limit: 250, null: true + t.column :active, :boolean, null: false, default: false + t.column :updated_by_id, :integer, null: false + t.column :created_by_id, :integer, null: false + t.timestamps null: false + end + add_index :jobs, [:name], unique: true + + Scheduler.create_if_not_exists( + name: 'Execute jobs', + method: 'Job.run', + period: 5 * 60, + prio: 2, + active: true, + updated_by_id: 1, + created_by_id: 1, + ) + + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 17abd8e95..05ea1c932 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -3367,6 +3367,15 @@ Scheduler.create_if_not_exists( updated_by_id: 1, created_by_id: 1, ) +Scheduler.create_if_not_exists( + name: 'Execute jobs', + method: 'Job.run', + period: 5 * 60, + prio: 2, + active: true, + updated_by_id: 1, + created_by_id: 1, +) Scheduler.create_if_not_exists( name: 'Cleanup expired sessions', method: 'SessionHelper.cleanup_expired', diff --git a/test/unit/job_test.rb b/test/unit/job_test.rb new file mode 100644 index 000000000..7a15bc67b --- /dev/null +++ b/test/unit/job_test.rb @@ -0,0 +1,425 @@ +# encoding: utf-8 +require 'test_helper' + +class JobTest < ActiveSupport::TestCase + test 'case 1' do + + # create ticket + group1 = Group.lookup(name: 'Users') + group2 = Group.create_or_update( + name: 'JobTest2', + updated_by_id: 1, + created_by_id: 1, + ) + ticket1 = Ticket.create( + title: 'job test 1', + group: group1, + customer_id: 2, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + created_at: Time.zone.now - 3.days, + updated_at: Time.zone.now - 3.days, + created_by_id: 1, + updated_by_id: 1, + ) + ticket2 = Ticket.create( + title: 'job test 2', + group: group1, + customer_id: 2, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + created_at: Time.zone.now - 1.day, + created_by_id: 1, + updated_at: Time.zone.now - 1.day, + updated_by_id: 1, + ) + ticket3 = Ticket.create( + title: 'job test 3', + group: group2, + customer_id: 2, + state: Ticket::State.lookup(name: 'open'), + priority: Ticket::Priority.lookup(name: '3 high'), + created_at: Time.zone.now - 1.day, + created_by_id: 1, + updated_at: Time.zone.now - 1.day, + updated_by_id: 1, + ) + ticket4 = Ticket.create( + title: 'job test 4', + group: group2, + customer_id: 2, + state: Ticket::State.lookup(name: 'closed'), + priority: Ticket::Priority.lookup(name: '2 normal'), + created_at: Time.zone.now - 3.days, + created_by_id: 1, + updated_at: Time.zone.now - 3.days, + updated_by_id: 1, + ) + ticket5 = Ticket.create( + title: 'job test 5', + group: group2, + customer_id: 2, + state: Ticket::State.lookup(name: 'open'), + priority: Ticket::Priority.lookup(name: '2 normal'), + created_at: Time.zone.now - 3.days, + created_by_id: 1, + updated_by_id: 1, + updated_at: Time.zone.now - 3.days, + ) + + # create jobs + job1 = Job.create_or_update( + name: 'Test Job1', + timeplan: { + days: { + Mon: false, + Tue: false, + Wed: false, + Thu: false, + Fri: false, + Sat: false, + Sun: false, + }, + hours: { + 0 => false, + 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 => false, + 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, + ) + assert_not(job1.executable?) + + job1.last_run_at = Time.zone.now - 15.minutes + job1.save + assert_not(job1.executable?) + + job1.updated_at = Time.zone.now - 15.minutes + job1.save + assert(job1.executable?) + + assert_not(job1.in_timeplan?) + time = Time.zone.now + day_map = { + 0 => 'Sun', + 1 => 'Mon', + 2 => 'Tue', + 3 => 'Wed', + 4 => 'Thu', + 5 => 'Fri', + 6 => 'Sat', + } + job1.timeplan['days'][day_map[time.wday]] = true + job1.save + assert_not(job1.in_timeplan?) + job1.timeplan['hours'][time.hour.to_s] = true + job1.save + assert_not(job1.in_timeplan?) + min = time.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 < 59 + min = 50 + end + job1.timeplan['minutes'][min.to_s] = true + job1.save + assert(job1.in_timeplan?) + + job1.timeplan['hours'][time.hour] = true + job1.save + + job1.timeplan['minutes'][min] = true + job1.save + assert(job1.in_timeplan?) + + # execute jobs + job1.updated_at = Time.zone.now - 15.minutes + job1.save + Job.run + + assert(job1.executable?) + assert(job1.in_timeplan?) + + # verify changes on tickets + ticket1_later = Ticket.find(ticket1.id) + assert_equal('closed', ticket1_later.state.name) + assert_not_equal(ticket1.updated_at.to_s, ticket1_later.updated_at.to_s) + + ticket2_later = Ticket.find(ticket2.id) + assert_equal('new', ticket2_later.state.name) + assert_equal(ticket2.updated_at.to_s, ticket2_later.updated_at.to_s) + + ticket3_later = Ticket.find(ticket3.id) + assert_equal('open', ticket3_later.state.name) + assert_equal(ticket3.updated_at.to_s, ticket3_later.updated_at.to_s) + + ticket4_later = Ticket.find(ticket4.id) + assert_equal('closed', ticket4_later.state.name) + assert_equal(ticket4.updated_at.to_s, ticket4_later.updated_at.to_s) + + ticket5_later = Ticket.find(ticket5.id) + assert_equal('closed', ticket5_later.state.name) + assert_not_equal(ticket5.updated_at.to_s, ticket5_later.updated_at.to_s) + + # execute jobs again + job1.updated_at = Time.zone.now - 15.minutes + job1.save + Job.run + + # verify changes on tickets + ticket1_later_next = Ticket.find(ticket1.id) + assert_equal('closed', ticket1_later_next.state.name) + assert_equal(ticket1_later.updated_at.to_s, ticket1_later_next.updated_at.to_s) + + ticket2_later_next = Ticket.find(ticket2.id) + assert_equal('new', ticket2_later_next.state.name) + assert_equal(ticket2_later.updated_at.to_s, ticket2_later_next.updated_at.to_s) + + ticket3_later_next = Ticket.find(ticket3.id) + assert_equal('open', ticket3_later_next.state.name) + assert_equal(ticket3_later.updated_at.to_s, ticket3_later_next.updated_at.to_s) + + ticket4_later_next = Ticket.find(ticket4.id) + assert_equal('closed', ticket4_later_next.state.name) + assert_equal(ticket4_later.updated_at.to_s, ticket4_later_next.updated_at.to_s) + + ticket5_later_next = Ticket.find(ticket5.id) + assert_equal('closed', ticket5_later_next.state.name) + assert_equal(ticket5_later.updated_at.to_s, ticket5_later_next.updated_at.to_s) + + end + + test 'case 2' do + + # create ticket + group1 = Group.lookup(name: 'Users') + group2 = Group.create_or_update( + name: 'JobTest2', + updated_by_id: 1, + created_by_id: 1, + ) + ticket1 = Ticket.create( + title: 'job test 1', + group: group1, + customer_id: 2, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + created_at: Time.zone.now - 3.days, + updated_at: Time.zone.now - 3.days, + created_by_id: 1, + updated_by_id: 1, + ) + ticket2 = Ticket.create( + title: 'job test 2', + group: group1, + customer_id: 2, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + created_at: Time.zone.now - 1.day, + created_by_id: 1, + updated_at: Time.zone.now - 1.day, + updated_by_id: 1, + ) + + # create jobs + job1 = Job.create_or_update( + name: 'Test Job1', + timeplan: { + days: { + Mon: true, + Tue: true, + Wed: true, + Thu: true, + Fri: true, + Sat: true, + Sun: true, + }, + hours: { + 0 => true, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + 5 => true, + 6 => true, + 7 => true, + 8 => true, + 9 => true, + 10 => true, + 11 => true, + 12 => true, + 13 => true, + 14 => true, + 15 => true, + 16 => true, + 17 => true, + 18 => true, + 19 => true, + 20 => true, + 21 => true, + 22 => true, + 23 => true, + }, + minutes: { + 0 => true, + 10 => true, + 20 => true, + 30 => true, + 40 => true, + 50 => true, + }, + }, + condition: { + 'ticket.state_id' => { 'operator' => 'is', 'value' => '' }, + '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, + updated_at: Time.zone.now - 15.minutes, + active: true, + updated_by_id: 1, + created_by_id: 1, + ) + assert(job1.executable?) + assert(job1.in_timeplan?) + Job.run + + # verify changes on tickets + ticket1_later = Ticket.find(ticket1.id) + assert_equal('new', ticket1_later.state.name) + assert_equal(ticket1.updated_at.to_s, ticket1_later.updated_at.to_s) + + ticket2_later = Ticket.find(ticket2.id) + assert_equal('new', ticket2_later.state.name) + assert_equal(ticket2.updated_at.to_s, ticket2_later.updated_at.to_s) + + job1 = Job.create_or_update( + name: 'Test Job1', + timeplan: { + days: { + Mon: true, + Tue: true, + Wed: true, + Thu: true, + Fri: true, + Sat: true, + Sun: true, + }, + hours: { + 0 => true, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + 5 => true, + 6 => true, + 7 => true, + 8 => true, + 9 => true, + 10 => true, + 11 => true, + 12 => true, + 13 => true, + 14 => true, + 15 => true, + 16 => true, + 17 => true, + 18 => true, + 19 => true, + 20 => true, + 21 => true, + 22 => true, + 23 => true, + }, + minutes: { + 0 => true, + 10 => true, + 20 => true, + 30 => true, + 40 => true, + 50 => true, + }, + }, + condition: { + 'ticket.state_id' => { 'operator' => 'is' }, + '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, + updated_at: Time.zone.now - 15.minutes, + active: true, + updated_by_id: 1, + created_by_id: 1, + ) + assert(job1.executable?) + assert(job1.in_timeplan?) + Job.run + + # verify changes on tickets + ticket1_later = Ticket.find(ticket1.id) + assert_equal('new', ticket1_later.state.name) + assert_equal(ticket1.updated_at.to_s, ticket1_later.updated_at.to_s) + + ticket2_later = Ticket.find(ticket2.id) + assert_equal('new', ticket2_later.state.name) + assert_equal(ticket2.updated_at.to_s, ticket2_later.updated_at.to_s) + + end + +end diff --git a/test/unit/ticket_selector_test.rb b/test/unit/ticket_selector_test.rb index 32d63631e..6e4e69c4e 100644 --- a/test/unit/ticket_selector_test.rb +++ b/test/unit/ticket_selector_test.rb @@ -146,6 +146,86 @@ class TicketSelectorTest < ActiveSupport::TestCase ticket_count, tickets = Ticket.selectors(condition, 10, customer1) assert_equal(ticket_count, 0) + # search matching with empty value / missing key + condition = { + 'ticket.group_id' => { + operator: 'is', + value: group.id, + }, + 'ticket.state_id' => { + operator: 'is', + }, + } + + ticket_count, tickets = Ticket.selectors(condition, 10) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, agent1) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, agent2) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, customer1) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, customer2) + assert_equal(ticket_count, nil) + + # search matching with empty value [] + condition = { + 'ticket.group_id' => { + operator: 'is', + value: group.id, + }, + 'ticket.state_id' => { + operator: 'is', + value: [], + }, + } + + ticket_count, tickets = Ticket.selectors(condition, 10) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, agent1) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, agent2) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, customer1) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, customer2) + assert_equal(ticket_count, nil) + + # search matching with empty value '' + condition = { + 'ticket.group_id' => { + operator: 'is', + value: group.id, + }, + 'ticket.state_id' => { + operator: 'is', + value: '', + }, + } + + ticket_count, tickets = Ticket.selectors(condition, 10) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, agent1) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, agent2) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, customer1) + assert_equal(ticket_count, nil) + + ticket_count, tickets = Ticket.selectors(condition, 10, customer2) + assert_equal(ticket_count, nil) + # search matching condition = { 'ticket.group_id' => {