Fixes #1553: Calendar as filter condition for Trigger/Automatization.
This commit is contained in:
parent
ddd49271e2
commit
ec23915bd0
14 changed files with 132 additions and 50 deletions
|
@ -37,7 +37,7 @@ Metrics/BlockNesting:
|
||||||
|
|
||||||
# Offense count: 340
|
# Offense count: 340
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 97
|
Max: 107
|
||||||
|
|
||||||
# Offense count: 27
|
# Offense count: 27
|
||||||
# Configuration parameters: CountComments.
|
# Configuration parameters: CountComments.
|
||||||
|
@ -46,7 +46,7 @@ Metrics/ModuleLength:
|
||||||
|
|
||||||
# Offense count: 274
|
# Offense count: 274
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 115
|
Max: 125
|
||||||
|
|
||||||
# Offense count: 2
|
# Offense count: 2
|
||||||
# Cop supports --auto-correct.
|
# Cop supports --auto-correct.
|
||||||
|
|
|
@ -17,6 +17,10 @@ class App.UiElement.ticket_selector
|
||||||
name: 'Organization'
|
name: 'Organization'
|
||||||
model: 'Organization'
|
model: 'Organization'
|
||||||
|
|
||||||
|
if attribute.executionTime
|
||||||
|
groups.execution_time =
|
||||||
|
name: 'Execution Time'
|
||||||
|
|
||||||
operators_type =
|
operators_type =
|
||||||
'^datetime$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
|
'^datetime$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
|
||||||
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
|
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
|
||||||
|
@ -71,24 +75,36 @@ class App.UiElement.ticket_selector
|
||||||
operator: ['is', 'is not']
|
operator: ['is', 'is not']
|
||||||
|
|
||||||
for groupKey, groupMeta of groups
|
for groupKey, groupMeta of groups
|
||||||
for row in App[groupMeta.model].configure_attributes
|
if groupKey is 'execution_time'
|
||||||
|
if attribute.executionTime
|
||||||
|
elements['execution_time.calendar_id'] =
|
||||||
|
name: 'calendar_id'
|
||||||
|
display: 'Calendar'
|
||||||
|
tag: 'select'
|
||||||
|
relation: 'Calendar'
|
||||||
|
null: false
|
||||||
|
translate: false
|
||||||
|
operator: ['is in working time', 'is not in working time']
|
||||||
|
|
||||||
# ignore passwords and relations
|
else
|
||||||
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false
|
for row in App[groupMeta.model].configure_attributes
|
||||||
config = _.clone(row)
|
|
||||||
for operatorRegEx, operator of operators_type
|
|
||||||
myRegExp = new RegExp(operatorRegEx, 'i')
|
|
||||||
if config.tag && config.tag.match(myRegExp)
|
|
||||||
config.operator = operator
|
|
||||||
elements["#{groupKey}.#{config.name}"] = config
|
|
||||||
for operatorRegEx, operator of operators_name
|
|
||||||
myRegExp = new RegExp(operatorRegEx, 'i')
|
|
||||||
if config.name && config.name.match(myRegExp)
|
|
||||||
config.operator = operator
|
|
||||||
elements["#{groupKey}.#{config.name}"] = config
|
|
||||||
|
|
||||||
if config.tag == 'select'
|
# ignore passwords and relations
|
||||||
config.multiple = true
|
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false
|
||||||
|
config = _.clone(row)
|
||||||
|
for operatorRegEx, operator of operators_type
|
||||||
|
myRegExp = new RegExp(operatorRegEx, 'i')
|
||||||
|
if config.tag && config.tag.match(myRegExp)
|
||||||
|
config.operator = operator
|
||||||
|
elements["#{groupKey}.#{config.name}"] = config
|
||||||
|
for operatorRegEx, operator of operators_name
|
||||||
|
myRegExp = new RegExp(operatorRegEx, 'i')
|
||||||
|
if config.name && config.name.match(myRegExp)
|
||||||
|
config.operator = operator
|
||||||
|
elements["#{groupKey}.#{config.name}"] = config
|
||||||
|
|
||||||
|
if config.tag == 'select'
|
||||||
|
config.multiple = true
|
||||||
|
|
||||||
if attribute.out_of_office
|
if attribute.out_of_office
|
||||||
elements['ticket.out_of_office_replacement_id'] =
|
elements['ticket.out_of_office_replacement_id'] =
|
||||||
|
|
|
@ -5,7 +5,7 @@ class App.Job extends App.Model
|
||||||
@configure_attributes = [
|
@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: 'timeplan', display: 'When should the job run?', tag: 'timer', null: true },
|
||||||
{ name: 'condition', display: 'Conditions for effected objects', tag: 'ticket_selector', null: true },
|
{ name: 'condition', display: 'Conditions for effected objects', tag: 'ticket_selector', null: true, executionTime: true },
|
||||||
{ name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true, notification: true, ticket_delete: true },
|
{ name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true, notification: true, ticket_delete: true },
|
||||||
{ name: 'disable_notification', display: 'Disable Notifications', tag: 'boolean', default: true },
|
{ name: 'disable_notification', 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: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
|
||||||
|
|
|
@ -4,7 +4,7 @@ class App.Trigger extends App.Model
|
||||||
@url: @apiPath + '/triggers'
|
@url: @apiPath + '/triggers'
|
||||||
@configure_attributes = [
|
@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: 'condition', display: 'Conditions for effected objects', tag: 'ticket_selector', null: false, preview: false, action: true, hasChanged: true },
|
{ name: 'condition', display: 'Conditions for effected objects', tag: 'ticket_selector', null: false, preview: false, action: true, hasChanged: true, executionTime: true },
|
||||||
{ name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true, notification: true, trigger: true },
|
{ name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true, notification: true, trigger: true },
|
||||||
{ name: 'active', display: 'Active', tag: 'active', default: true },
|
{ name: 'active', display: 'Active', tag: 'active', default: true },
|
||||||
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
|
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
|
||||||
|
|
|
@ -498,7 +498,7 @@ class TicketsController < ApplicationController
|
||||||
def selector
|
def selector
|
||||||
permission_check('admin.*')
|
permission_check('admin.*')
|
||||||
|
|
||||||
ticket_count, tickets = Ticket.selectors(params[:condition], limit: 6)
|
ticket_count, tickets = Ticket.selectors(params[:condition], limit: 6, execution_time: true)
|
||||||
|
|
||||||
assets = {}
|
assets = {}
|
||||||
ticket_ids = []
|
ticket_ids = []
|
||||||
|
|
|
@ -68,6 +68,7 @@ get assets and record_ids of selector
|
||||||
attribute_class = attribute[0].to_classname.constantize
|
attribute_class = attribute[0].to_classname.constantize
|
||||||
rescue => e
|
rescue => e
|
||||||
next if attribute[0] == 'article'
|
next if attribute[0] == 'article'
|
||||||
|
next if attribute[0] == 'execution_time'
|
||||||
|
|
||||||
logger.error "Unable to get asset for '#{attribute[0]}': #{e.inspect}"
|
logger.error "Unable to get asset for '#{attribute[0]}': #{e.inspect}"
|
||||||
next
|
next
|
||||||
|
|
|
@ -327,6 +327,21 @@ returns
|
||||||
holidays
|
holidays
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def biz
|
||||||
|
Biz::Schedule.new do |config|
|
||||||
|
|
||||||
|
# get business hours
|
||||||
|
hours = business_hours_to_hash
|
||||||
|
raise "No configured hours found in calendar #{inspect}" if hours.blank?
|
||||||
|
|
||||||
|
config.hours = hours
|
||||||
|
|
||||||
|
# get holidays
|
||||||
|
config.holidays = public_holidays_to_array
|
||||||
|
config.time_zone = timezone
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# if changed calendar is default, set all others default to false
|
# if changed calendar is default, set all others default to false
|
||||||
|
|
|
@ -13,6 +13,7 @@ module ChecksConditionValidation
|
||||||
|
|
||||||
# check if a valid condition got inserted.
|
# check if a valid condition got inserted.
|
||||||
validate_condition.delete('ticket.action')
|
validate_condition.delete('ticket.action')
|
||||||
|
validate_condition.delete('execution_time.calendar_id')
|
||||||
validate_condition.each do |key, value|
|
validate_condition.each do |key, value|
|
||||||
next if !value
|
next if !value
|
||||||
next if !value['operator']
|
next if !value['operator']
|
||||||
|
|
|
@ -74,7 +74,7 @@ job.run(true)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
ticket_count, tickets = Ticket.selectors(condition, limit: 2_000)
|
ticket_count, tickets = Ticket.selectors(condition, limit: 2_000, execution_time: true)
|
||||||
|
|
||||||
logger.debug { "Job #{name} with #{ticket_count} tickets" }
|
logger.debug { "Job #{name} with #{ticket_count} tickets" }
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ job.run(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def matching_count
|
def matching_count
|
||||||
ticket_count, _tickets = Ticket.selectors(condition, limit: 1)
|
ticket_count, _tickets = Ticket.selectors(condition, limit: 1, execution_time: true)
|
||||||
ticket_count || 0
|
ticket_count || 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -23,21 +23,22 @@ returns
|
||||||
=end
|
=end
|
||||||
|
|
||||||
def assets(data)
|
def assets(data)
|
||||||
|
|
||||||
app_model = Job.to_app_model
|
app_model = Job.to_app_model
|
||||||
|
|
||||||
if !data[ app_model ]
|
data[ app_model ] ||= {}
|
||||||
data[ app_model ] = {}
|
|
||||||
end
|
|
||||||
return data if data[ app_model ][ id ]
|
return data if data[ app_model ][ id ]
|
||||||
|
|
||||||
data[ app_model ][ id ] = attributes_with_association_ids
|
data[ app_model ][ id ] = attributes_with_association_ids
|
||||||
data = assets_of_selector('condition', data)
|
data = assets_of_selector('condition', data)
|
||||||
data = assets_of_selector('perform', data)
|
data = assets_of_selector('perform', data)
|
||||||
|
|
||||||
if !data[ User.to_app_model ]
|
app_model_calendar = Calendar.to_app_model
|
||||||
data[ User.to_app_model ] = {}
|
data[ app_model_calendar ] ||= {}
|
||||||
|
Calendar.find_each do |calendar|
|
||||||
|
data = calendar.assets(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
data[ User.to_app_model ] ||= {}
|
||||||
%w[created_by_id updated_by_id].each do |local_user_id|
|
%w[created_by_id updated_by_id].each do |local_user_id|
|
||||||
next if !self[ local_user_id ]
|
next if !self[ local_user_id ]
|
||||||
next if data[ User.to_app_model ][ self[ local_user_id ] ]
|
next if data[ User.to_app_model ][ self[ local_user_id ] ]
|
||||||
|
|
|
@ -434,7 +434,7 @@ get count of tickets and tickets which match on selector
|
||||||
access = options[:access] || 'full'
|
access = options[:access] || 'full'
|
||||||
raise 'no selectors given' if !selectors
|
raise 'no selectors given' if !selectors
|
||||||
|
|
||||||
query, bind_params, tables = selector2sql(selectors, current_user: current_user)
|
query, bind_params, tables = selector2sql(selectors, current_user: current_user, execution_time: options[:execution_time])
|
||||||
return [] if !query
|
return [] if !query
|
||||||
|
|
||||||
ActiveRecord::Base.transaction(requires_new: true) do
|
ActiveRecord::Base.transaction(requires_new: true) do
|
||||||
|
@ -528,6 +528,7 @@ condition example
|
||||||
selector = attribute.split(/\./)
|
selector = attribute.split(/\./)
|
||||||
next if !selector[1]
|
next if !selector[1]
|
||||||
next if selector[0] == 'ticket'
|
next if selector[0] == 'ticket'
|
||||||
|
next if selector[0] == 'execution_time'
|
||||||
next if tables.include?(selector[0])
|
next if tables.include?(selector[0])
|
||||||
|
|
||||||
if query != ''
|
if query != ''
|
||||||
|
@ -554,6 +555,7 @@ condition example
|
||||||
end
|
end
|
||||||
|
|
||||||
# add conditions
|
# add conditions
|
||||||
|
no_result = false
|
||||||
selectors.each do |attribute, selector_raw|
|
selectors.each do |attribute, selector_raw|
|
||||||
|
|
||||||
# validation
|
# validation
|
||||||
|
@ -562,7 +564,7 @@ condition example
|
||||||
|
|
||||||
selector = selector_raw.stringify_keys
|
selector = selector_raw.stringify_keys
|
||||||
raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
|
raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
|
||||||
raise "Invalid selector, operator #{selector['operator']} is invalid #{selector.inspect}" if !selector['operator'].match?(/^(is|is\snot|contains|contains\s(not|all|one|all\snot|one\snot)|(after|before)\s\(absolute\)|(within\snext|within\slast|after|before)\s\(relative\))$/)
|
raise "Invalid selector, operator #{selector['operator']} is invalid #{selector.inspect}" if !selector['operator'].match?(/^(is|is\snot|contains|contains\s(not|all|one|all\snot|one\snot)|(after|before)\s\(absolute\)|(within\snext|within\slast|after|before)\s\(relative\))|(is\sin\sworking\stime|is\snot\sin\sworking\stime)$/)
|
||||||
|
|
||||||
# validate value / allow blank but only if pre_condition exists and is not specific
|
# validate value / allow blank but only if pre_condition exists and is not specific
|
||||||
if !selector.key?('value') ||
|
if !selector.key?('value') ||
|
||||||
|
@ -826,11 +828,24 @@ condition example
|
||||||
raise "Unknown selector attributes '#{selector.inspect}'"
|
raise "Unknown selector attributes '#{selector.inspect}'"
|
||||||
end
|
end
|
||||||
bind_params.push time
|
bind_params.push time
|
||||||
|
elsif selector['operator'].include?('in working time')
|
||||||
|
next if attributes[1] != 'calendar_id'
|
||||||
|
raise 'Please enable execution_time feature to use it (currently only allowed for triggers and schedulers)' if !options[:execution_time]
|
||||||
|
|
||||||
|
biz = Calendar.lookup(id: selector['value'])&.biz
|
||||||
|
next if biz.blank?
|
||||||
|
|
||||||
|
if ( selector['operator'] == 'is in working time' && !biz.in_hours?(Time.zone.now) ) || ( selector['operator'] == 'is not in working time' && biz.in_hours?(Time.zone.now) )
|
||||||
|
no_result = true
|
||||||
|
break
|
||||||
|
end
|
||||||
else
|
else
|
||||||
raise "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
|
raise "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return if no_result
|
||||||
|
|
||||||
[query, bind_params, tables]
|
[query, bind_params, tables]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1106,7 +1121,7 @@ perform active triggers on ticket
|
||||||
end
|
end
|
||||||
|
|
||||||
# verify is condition is matching
|
# verify is condition is matching
|
||||||
ticket_count, tickets = Ticket.selectors(condition, limit: 1)
|
ticket_count, tickets = Ticket.selectors(condition, limit: 1, execution_time: true)
|
||||||
|
|
||||||
next if ticket_count.blank?
|
next if ticket_count.blank?
|
||||||
next if ticket_count.zero?
|
next if ticket_count.zero?
|
||||||
|
|
|
@ -154,18 +154,7 @@ returns
|
||||||
self.update_escalation_at = nil
|
self.update_escalation_at = nil
|
||||||
self.close_escalation_at = nil
|
self.close_escalation_at = nil
|
||||||
end
|
end
|
||||||
biz = Biz::Schedule.new do |config|
|
biz = calendar.biz
|
||||||
|
|
||||||
# get business hours
|
|
||||||
hours = calendar.business_hours_to_hash
|
|
||||||
raise "No configured hours found in calendar #{calendar.inspect}" if hours.blank?
|
|
||||||
|
|
||||||
config.hours = hours
|
|
||||||
|
|
||||||
# get holidays
|
|
||||||
config.holidays = calendar.public_holidays_to_array
|
|
||||||
config.time_zone = calendar.timezone
|
|
||||||
end
|
|
||||||
|
|
||||||
# get history data
|
# get history data
|
||||||
history_data = nil
|
history_data = nil
|
||||||
|
|
|
@ -25,21 +25,23 @@ returns
|
||||||
def assets(data)
|
def assets(data)
|
||||||
|
|
||||||
app_model_trigger = Trigger.to_app_model
|
app_model_trigger = Trigger.to_app_model
|
||||||
|
data[ app_model_trigger ] ||= {}
|
||||||
|
|
||||||
if !data[ app_model_trigger ]
|
|
||||||
data[ app_model_trigger ] = {}
|
|
||||||
end
|
|
||||||
return data if data[ app_model_trigger ][ id ]
|
return data if data[ app_model_trigger ][ id ]
|
||||||
|
|
||||||
data[ app_model_trigger ][ id ] = attributes_with_association_ids
|
data[ app_model_trigger ][ id ] = attributes_with_association_ids
|
||||||
data = assets_of_selector('condition', data)
|
data = assets_of_selector('condition', data)
|
||||||
data = assets_of_selector('perform', data)
|
data = assets_of_selector('perform', data)
|
||||||
|
|
||||||
app_model_user = User.to_app_model
|
app_model_calendar = Calendar.to_app_model
|
||||||
if !data[ app_model_user ]
|
data[ app_model_calendar ] ||= {}
|
||||||
data[ app_model_user ] = {}
|
Calendar.find_each do |calendar|
|
||||||
|
data = calendar.assets(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
app_model_user = User.to_app_model
|
||||||
|
data[ app_model_user ] ||= {}
|
||||||
|
|
||||||
%w[created_by_id updated_by_id].each do |local_user_id|
|
%w[created_by_id updated_by_id].each do |local_user_id|
|
||||||
next if !self[ local_user_id ]
|
next if !self[ local_user_id ]
|
||||||
next if data[ app_model_user ][ self[ local_user_id ] ]
|
next if data[ app_model_user ][ self[ local_user_id ] ]
|
||||||
|
|
|
@ -156,5 +156,47 @@ RSpec.describe Trigger, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with condition execution_time.calendar_id' do
|
||||||
|
let(:calendar) { create(:calendar) }
|
||||||
|
let(:perform) do
|
||||||
|
{ 'ticket.title'=>{ 'value'=>'triggered' } }
|
||||||
|
end
|
||||||
|
let!(:ticket) { create(:ticket, title: 'Test Ticket') }
|
||||||
|
|
||||||
|
context 'is in working time' do
|
||||||
|
let(:condition) do
|
||||||
|
{ 'execution_time.calendar_id' => { 'operator' => 'is in working time', 'value' => calendar.id } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does trigger only in working time' do
|
||||||
|
travel_to Time.zone.parse('2020-02-12T12:00:00Z0')
|
||||||
|
expect { Observer::Transaction.commit }.to change { ticket.reload.title }.to('triggered')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not trigger out of working time' do
|
||||||
|
travel_to Time.zone.parse('2020-02-12T02:00:00Z0')
|
||||||
|
Observer::Transaction.commit
|
||||||
|
expect(ticket.reload.title).to eq('Test Ticket')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'is not in working time' do
|
||||||
|
let(:condition) do
|
||||||
|
{ 'execution_time.calendar_id' => { 'operator' => 'is not in working time', 'value' => calendar.id } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not trigger in working time' do
|
||||||
|
travel_to Time.zone.parse('2020-02-12T12:00:00Z0')
|
||||||
|
Observer::Transaction.commit
|
||||||
|
expect(ticket.reload.title).to eq('Test Ticket')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does trigger out of working time' do
|
||||||
|
travel_to Time.zone.parse('2020-02-12T02:00:00Z0')
|
||||||
|
expect { Observer::Transaction.commit }.to change { ticket.reload.title }.to('triggered')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue