Fixes #3523 - Escalation overview and all escalation related triggers/schedulers broken in Zammad 4.0.

This commit is contained in:
Rolf Schmidt 2021-04-29 15:34:27 +00:00 committed by Thorsten Eckel
parent 34bcc21296
commit 1e35eaf770
10 changed files with 256 additions and 32 deletions

View file

@ -332,6 +332,8 @@ class App.UiElement.ticket_perform_action
'within next (relative)', 'within next (relative)',
'within last (relative)', 'within last (relative)',
'after (relative)', 'after (relative)',
'till (relative)',
'from (relative)',
'relative' 'relative'
] ]

View file

@ -22,8 +22,8 @@ class App.UiElement.ticket_selector
name: '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)', 'till (relative)', 'from (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)', 'till (relative)', 'from (relative)']
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)'] '^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
'boolean$': ['is', 'is not'] 'boolean$': ['is', 'is not']
'integer$': ['is', 'is not'] 'integer$': ['is', 'is not']
@ -37,9 +37,9 @@ class App.UiElement.ticket_selector
if attribute.hasChanged if attribute.hasChanged
operators_type = operators_type =
'^datetime$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed'] '^datetime$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)', 'has changed']
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed'] '^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)', 'has changed']
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed'] '^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)', 'has changed']
'boolean$': ['is', 'is not', 'has changed'] 'boolean$': ['is', 'is not', 'has changed']
'integer$': ['is', 'is not', 'has changed'] 'integer$': ['is', 'is not', 'has changed']
'^radio$': ['is', 'is not', 'has changed'] '^radio$': ['is', 'is not', 'has changed']
@ -441,7 +441,7 @@ class App.UiElement.ticket_selector
item = App.UiElement[tagSearch].render(config, {}) item = App.UiElement[tagSearch].render(config, {})
else else
item = App.UiElement[config.tag].render(config, {}) item = App.UiElement[config.tag].render(config, {})
if meta.operator is 'before (relative)' || meta.operator is 'within next (relative)' || meta.operator is 'within last (relative)' || meta.operator is 'after (relative)' if meta.operator is 'before (relative)' || meta.operator is 'within next (relative)' || meta.operator is 'within last (relative)' || meta.operator is 'after (relative)' || meta.operator is 'from (relative)' || meta.operator is 'till (relative)'
config['name'] = "#{attribute.name}::#{groupAndAttribute}" config['name'] = "#{attribute.name}::#{groupAndAttribute}"
if attribute.value && attribute.value[groupAndAttribute] if attribute.value && attribute.value[groupAndAttribute]
config['value'] = _.clone(attribute.value[groupAndAttribute]) config['value'] = _.clone(attribute.value[groupAndAttribute])

View file

@ -627,7 +627,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\))|(is\sin\sworking\stime|is\snot\sin\sworking\stime)$/) 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|till|from)\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') ||
@ -861,15 +861,15 @@ condition example
time = nil time = nil
case selector['range'] case selector['range']
when 'minute' when 'minute'
time = Time.zone.now - selector['value'].to_i.minutes time = selector['value'].to_i.minutes.ago
when 'hour' when 'hour'
time = Time.zone.now - selector['value'].to_i.hours time = selector['value'].to_i.hours.ago
when 'day' when 'day'
time = Time.zone.now - selector['value'].to_i.days time = selector['value'].to_i.days.ago
when 'month' when 'month'
time = Time.zone.now - selector['value'].to_i.months time = selector['value'].to_i.months.ago
when 'year' when 'year'
time = Time.zone.now - selector['value'].to_i.years time = selector['value'].to_i.years.ago
else else
raise "Unknown selector attributes '#{selector.inspect}'" raise "Unknown selector attributes '#{selector.inspect}'"
end end
@ -880,15 +880,15 @@ condition example
time = nil time = nil
case selector['range'] case selector['range']
when 'minute' when 'minute'
time = Time.zone.now + selector['value'].to_i.minutes time = selector['value'].to_i.minutes.from_now
when 'hour' when 'hour'
time = Time.zone.now + selector['value'].to_i.hours time = selector['value'].to_i.hours.from_now
when 'day' when 'day'
time = Time.zone.now + selector['value'].to_i.days time = selector['value'].to_i.days.from_now
when 'month' when 'month'
time = Time.zone.now + selector['value'].to_i.months time = selector['value'].to_i.months.from_now
when 'year' when 'year'
time = Time.zone.now + selector['value'].to_i.years time = selector['value'].to_i.years.from_now
else else
raise "Unknown selector attributes '#{selector.inspect}'" raise "Unknown selector attributes '#{selector.inspect}'"
end end
@ -899,15 +899,15 @@ condition example
time = nil time = nil
case selector['range'] case selector['range']
when 'minute' when 'minute'
time = Time.zone.now - selector['value'].to_i.minutes time = selector['value'].to_i.minutes.ago
when 'hour' when 'hour'
time = Time.zone.now - selector['value'].to_i.hours time = selector['value'].to_i.hours.ago
when 'day' when 'day'
time = Time.zone.now - selector['value'].to_i.days time = selector['value'].to_i.days.ago
when 'month' when 'month'
time = Time.zone.now - selector['value'].to_i.months time = selector['value'].to_i.months.ago
when 'year' when 'year'
time = Time.zone.now - selector['value'].to_i.years time = selector['value'].to_i.years.ago
else else
raise "Unknown selector attributes '#{selector.inspect}'" raise "Unknown selector attributes '#{selector.inspect}'"
end end
@ -917,15 +917,51 @@ condition example
time = nil time = nil
case selector['range'] case selector['range']
when 'minute' when 'minute'
time = Time.zone.now + selector['value'].to_i.minutes time = selector['value'].to_i.minutes.from_now
when 'hour' when 'hour'
time = Time.zone.now + selector['value'].to_i.hours time = selector['value'].to_i.hours.from_now
when 'day' when 'day'
time = Time.zone.now + selector['value'].to_i.days time = selector['value'].to_i.days.from_now
when 'month' when 'month'
time = Time.zone.now + selector['value'].to_i.months time = selector['value'].to_i.months.from_now
when 'year' when 'year'
time = Time.zone.now + selector['value'].to_i.years time = selector['value'].to_i.years.from_now
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
elsif selector['operator'] == 'till (relative)'
query += "#{attribute} <= ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.from_now
when 'hour'
time = selector['value'].to_i.hours.from_now
when 'day'
time = selector['value'].to_i.days.from_now
when 'month'
time = selector['value'].to_i.months.from_now
when 'year'
time = selector['value'].to_i.years.from_now
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
elsif selector['operator'] == 'from (relative)'
query += "#{attribute} >= ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.ago
when 'hour'
time = selector['value'].to_i.hours.ago
when 'day'
time = selector['value'].to_i.days.ago
when 'month'
time = selector['value'].to_i.months.ago
when 'year'
time = selector['value'].to_i.years.ago
else else
raise "Unknown selector attributes '#{selector.inspect}'" raise "Unknown selector attributes '#{selector.inspect}'"
end end

View file

@ -21,9 +21,9 @@ class Issue3270SelectorUpdate < ActiveRecord::Migration[5.2]
next if attribute_condition['operator'] != 'within next (relative)' && attribute_condition['operator'] != 'within last (relative)' next if attribute_condition['operator'] != 'within next (relative)' && attribute_condition['operator'] != 'within last (relative)'
attribute_condition['operator'] = if attribute_condition['operator'] == 'within next (relative)' attribute_condition['operator'] = if attribute_condition['operator'] == 'within next (relative)'
'before (relative)' 'till (relative)'
else else
'before (after)' 'from (relative)'
end end
fixed = true fixed = true

View file

@ -0,0 +1,13 @@
class Issue3523NewOperator < ActiveRecord::Migration[5.2]
def change
return if !Setting.exists?(name: 'system_init_done')
overview = Overview.find_by(link: 'all_escalated')
return if !overview
return if overview.condition['ticket.escalation_at'].blank?
return if overview.condition['ticket.escalation_at'][:operator] != 'before (relative)'
overview.condition['ticket.escalation_at'][:operator] = 'till (relative)'
overview.save!
end
end

View file

@ -161,7 +161,7 @@ Overview.create_if_not_exists(
role_ids: [overview_role.id], role_ids: [overview_role.id],
condition: { condition: {
'ticket.escalation_at' => { 'ticket.escalation_at' => {
operator: 'before (relative)', operator: 'till (relative)',
value: '10', value: '10',
range: 'minute', range: 'minute',
}, },

View file

@ -644,6 +644,22 @@ example for aggregations within one year
end end
query_must.push t query_must.push t
# till/from (relative)
when 'till (relative)', 'from (relative)'
range = relative_map[data['range'].to_sym]
if range.blank?
raise "Invalid relative_map for range '#{data['range']}'."
end
t[:range] = {}
t[:range][key_tmp] = {}
if data['operator'] == 'till (relative)'
t[:range][key_tmp][:lt] = "now+#{data['value']}#{range}"
else
t[:range][key_tmp][:gt] = "now-#{data['value']}#{range}"
end
query_must.push t
# before/after (absolute) # before/after (absolute)
when 'before (absolute)', 'after (absolute)' when 'before (absolute)', 'after (absolute)'
t[:range] = {} t[:range] = {}

View file

@ -193,9 +193,10 @@ RSpec.describe SearchIndexBackend, searchindex: true do
before do before do
Ticket.destroy_all # needed to remove not created tickets Ticket.destroy_all # needed to remove not created tickets
travel(-1.hour)
create(:mention, mentionable: ticket1, user: agent1) create(:mention, mentionable: ticket1, user: agent1)
ticket1.search_index_update_backend ticket1.search_index_update_backend
travel 1.second travel 1.hour
ticket2.search_index_update_backend ticket2.search_index_update_backend
travel 1.second travel 1.second
ticket3.search_index_update_backend ticket3.search_index_update_backend
@ -207,12 +208,52 @@ RSpec.describe SearchIndexBackend, searchindex: true do
ticket6.search_index_update_backend ticket6.search_index_update_backend
travel 1.second travel 1.second
ticket7.search_index_update_backend ticket7.search_index_update_backend
travel 1.second travel 1.hour
article8.ticket.search_index_update_backend article8.ticket.search_index_update_backend
described_class.refresh described_class.refresh
end end
context 'query with contains' do context 'query with contains' do
it 'finds records with till (relative)' do
result = described_class.selectors('Ticket',
{ 'ticket.created_at'=>{ 'operator' => 'till (relative)', 'value' => '30', 'range' => 'minute' } },
{},
{
field: 'created_at', # sort to verify result
})
expect(result).to eq({ count: 7, ticket_ids: [ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
end
it 'finds records with from (relative)' do
result = described_class.selectors('Ticket',
{ 'ticket.created_at'=>{ 'operator' => 'from (relative)', 'value' => '30', 'range' => 'minute' } },
{},
{
field: 'created_at', # sort to verify result
})
expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
end
it 'finds records with till (relative) including +1 hour ticket' do
result = described_class.selectors('Ticket',
{ 'ticket.created_at'=>{ 'operator' => 'till (relative)', 'value' => '120', 'range' => 'minute' } },
{},
{
field: 'created_at', # sort to verify result
})
expect(result).to eq({ count: 8, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
end
it 'finds records with from (relative) including -1 hour ticket' do
result = described_class.selectors('Ticket',
{ 'ticket.created_at'=>{ 'operator' => 'from (relative)', 'value' => '120', 'range' => 'minute' } },
{},
{
field: 'created_at', # sort to verify result
})
expect(result).to eq({ count: 8, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
end
it 'finds records with tags which contains all' do it 'finds records with tags which contains all' do
result = described_class.selectors('Ticket', result = described_class.selectors('Ticket',
{ 'ticket.tags'=>{ 'operator' => 'contains all', 'value' => 't1, t2' } }, { 'ticket.tags'=>{ 'operator' => 'contains all', 'value' => 't1, t2' } },

View file

@ -989,6 +989,76 @@ RSpec.describe Ticket, type: :model do
end end
end end
context 'when till (relative)' do
let(:first_response_time) { 5 }
let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
let(:condition) do
{ 'ticket.escalation_at'=>{ 'operator' => 'till (relative)', 'value' => '30', 'range' => 'minute' } }
end
before do
sla
travel_to '2020-11-05 11:37:00'
ticket = create(:ticket)
create(:ticket_article, :inbound_email, ticket: ticket)
travel_to '2020-11-05 11:50:00'
end
context 'when in range' do
it 'does find the ticket' do
count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
expect(count).to eq(1)
end
end
context 'when out of range' do
let(:first_response_time) { 500 }
it 'does not find the ticket' do
count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
expect(count).to eq(0)
end
end
end
context 'when from (relative)' do
let(:first_response_time) { 5 }
let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
let(:condition) do
{ 'ticket.escalation_at'=>{ 'operator' => 'from (relative)', 'value' => '30', 'range' => 'minute' } }
end
before do
sla
travel_to '2020-11-05 11:37:00'
ticket = create(:ticket)
create(:ticket_article, :inbound_email, ticket: ticket)
end
context 'when in range' do
it 'does find the ticket' do
travel_to '2020-11-05 11:50:00'
count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
expect(count).to eq(1)
end
end
context 'when out of range' do
let(:first_response_time) { 5 }
it 'does not find the ticket' do
travel_to '2020-11-05 13:50:00'
count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
expect(count).to eq(0)
end
end
end
context 'when within next (relative)' do context 'when within next (relative)' do
let(:first_response_time) { 5 } let(:first_response_time) { 5 }
let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) } let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }

View file

@ -409,6 +409,29 @@ class TicketSelectorTest < ActiveSupport::TestCase
ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @customer2) ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @customer2)
assert_equal(ticket_count, 2) assert_equal(ticket_count, 2)
condition = {
'ticket.group_id' => {
operator: 'is',
value: @group.id,
},
'ticket.created_at' => {
operator: 'till (relative)',
range: 'year', # minute|hour|day|month|
value: '10',
},
}
ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @agent1)
assert_equal(ticket_count, 3)
ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @agent2)
assert_equal(ticket_count, 0)
ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @customer1)
assert_equal(ticket_count, 1)
ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @customer2)
assert_equal(ticket_count, 2)
condition = { condition = {
'ticket.group_id' => { 'ticket.group_id' => {
operator: 'is', operator: 'is',
@ -544,6 +567,29 @@ class TicketSelectorTest < ActiveSupport::TestCase
ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @customer2) ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @customer2)
assert_equal(ticket_count, 0) assert_equal(ticket_count, 0)
condition = {
'ticket.group_id' => {
operator: 'is',
value: @group.id,
},
'ticket.updated_at' => {
operator: 'till (relative)',
range: 'year', # minute|hour|day|month|
value: '10',
},
}
ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @agent1)
assert_equal(ticket_count, 3)
ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @agent2)
assert_equal(ticket_count, 0)
ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @customer1)
assert_equal(ticket_count, 1)
ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @customer2)
assert_equal(ticket_count, 2)
condition = { condition = {
'ticket.group_id' => { 'ticket.group_id' => {
operator: 'is', operator: 'is',