Skip to content

Commit

Permalink
Fixes zammad#3523 - Escalation overview and all escalation related tr…
Browse files Browse the repository at this point in the history
…iggers/schedulers broken in Zammad 4.0.
  • Loading branch information
rolfschmidt authored and thorsteneckel committed Apr 29, 2021
1 parent 34bcc21 commit 1e35eaf
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ class App.UiElement.ticket_perform_action
'within next (relative)',
'within last (relative)',
'after (relative)',
'till (relative)',
'from (relative)',
'relative'
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class App.UiElement.ticket_selector
name: 'Execution Time'

operators_type =
'^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)']
'^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)', 'till (relative)', 'from (relative)']
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
'boolean$': ['is', 'is not']
'integer$': ['is', 'is not']
Expand All @@ -37,9 +37,9 @@ class App.UiElement.ticket_selector

if attribute.hasChanged
operators_type =
'^datetime$': ['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)', 'has changed']
'^date$': ['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)', 'till (relative)', 'from (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']
'integer$': ['is', 'is not', 'has changed']
'^radio$': ['is', 'is not', 'has changed']
Expand Down Expand Up @@ -441,7 +441,7 @@ class App.UiElement.ticket_selector
item = App.UiElement[tagSearch].render(config, {})
else
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}"
if attribute.value && attribute.value[groupAndAttribute]
config['value'] = _.clone(attribute.value[groupAndAttribute])
Expand Down
78 changes: 57 additions & 21 deletions app/models/ticket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ def self.selector2sql(selectors, options = {})

selector = selector_raw.stringify_keys
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
if !selector.key?('value') ||
Expand Down Expand Up @@ -861,15 +861,15 @@ def self.selector2sql(selectors, options = {})
time = nil
case selector['range']
when 'minute'
time = Time.zone.now - selector['value'].to_i.minutes
time = selector['value'].to_i.minutes.ago
when 'hour'
time = Time.zone.now - selector['value'].to_i.hours
time = selector['value'].to_i.hours.ago
when 'day'
time = Time.zone.now - selector['value'].to_i.days
time = selector['value'].to_i.days.ago
when 'month'
time = Time.zone.now - selector['value'].to_i.months
time = selector['value'].to_i.months.ago
when 'year'
time = Time.zone.now - selector['value'].to_i.years
time = selector['value'].to_i.years.ago
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
Expand All @@ -880,15 +880,15 @@ def self.selector2sql(selectors, options = {})
time = nil
case selector['range']
when 'minute'
time = Time.zone.now + selector['value'].to_i.minutes
time = selector['value'].to_i.minutes.from_now
when 'hour'
time = Time.zone.now + selector['value'].to_i.hours
time = selector['value'].to_i.hours.from_now
when 'day'
time = Time.zone.now + selector['value'].to_i.days
time = selector['value'].to_i.days.from_now
when 'month'
time = Time.zone.now + selector['value'].to_i.months
time = selector['value'].to_i.months.from_now
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
Expand All @@ -899,15 +899,15 @@ def self.selector2sql(selectors, options = {})
time = nil
case selector['range']
when 'minute'
time = Time.zone.now - selector['value'].to_i.minutes
time = selector['value'].to_i.minutes.ago
when 'hour'
time = Time.zone.now - selector['value'].to_i.hours
time = selector['value'].to_i.hours.ago
when 'day'
time = Time.zone.now - selector['value'].to_i.days
time = selector['value'].to_i.days.ago
when 'month'
time = Time.zone.now - selector['value'].to_i.months
time = selector['value'].to_i.months.ago
when 'year'
time = Time.zone.now - selector['value'].to_i.years
time = selector['value'].to_i.years.ago
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
Expand All @@ -917,15 +917,51 @@ def self.selector2sql(selectors, options = {})
time = nil
case selector['range']
when 'minute'
time = Time.zone.now + selector['value'].to_i.minutes
time = selector['value'].to_i.minutes.from_now
when 'hour'
time = Time.zone.now + selector['value'].to_i.hours
time = selector['value'].to_i.hours.from_now
when 'day'
time = Time.zone.now + selector['value'].to_i.days
time = selector['value'].to_i.days.from_now
when 'month'
time = Time.zone.now + selector['value'].to_i.months
time = selector['value'].to_i.months.from_now
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
raise "Unknown selector attributes '#{selector.inspect}'"
end
Expand Down
4 changes: 2 additions & 2 deletions db/migrate/20201202080338_issue3270_selector_update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ def fix_selector(object)
next if attribute_condition['operator'] != 'within next (relative)' && attribute_condition['operator'] != 'within last (relative)'

attribute_condition['operator'] = if attribute_condition['operator'] == 'within next (relative)'
'before (relative)'
'till (relative)'
else
'before (after)'
'from (relative)'
end

fixed = true
Expand Down
13 changes: 13 additions & 0 deletions db/migrate/20210428125300_issue_3523_new_operator.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion db/seeds/overviews.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
role_ids: [overview_role.id],
condition: {
'ticket.escalation_at' => {
operator: 'before (relative)',
operator: 'till (relative)',
value: '10',
range: 'minute',
},
Expand Down
16 changes: 16 additions & 0 deletions lib/search_index_backend.rb
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,22 @@ def self.selector2query(selector, options, aggs_interval)
end
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)
when 'before (absolute)', 'after (absolute)'
t[:range] = {}
Expand Down
45 changes: 43 additions & 2 deletions spec/lib/search_index_backend_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,10 @@

before do
Ticket.destroy_all # needed to remove not created tickets
travel(-1.hour)
create(:mention, mentionable: ticket1, user: agent1)
ticket1.search_index_update_backend
travel 1.second
travel 1.hour
ticket2.search_index_update_backend
travel 1.second
ticket3.search_index_update_backend
Expand All @@ -207,12 +208,52 @@
ticket6.search_index_update_backend
travel 1.second
ticket7.search_index_update_backend
travel 1.second
travel 1.hour
article8.ticket.search_index_update_backend
described_class.refresh
end

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
result = described_class.selectors('Ticket',
{ 'ticket.tags'=>{ 'operator' => 'contains all', 'value' => 't1, t2' } },
Expand Down
70 changes: 70 additions & 0 deletions spec/models/ticket_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,76 @@
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
let(:first_response_time) { 5 }
let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
Expand Down
Loading

0 comments on commit 1e35eaf

Please sign in to comment.