Browse Source

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

Rolf Schmidt 3 years ago
parent
commit
1e35eaf770

+ 2 - 0
app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee

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

+ 6 - 6
app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee

@@ -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']
@@ -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']
@@ -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])

+ 57 - 21
app/models/ticket.rb

@@ -627,7 +627,7 @@ condition example
 
       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') ||
@@ -861,15 +861,15 @@ condition example
         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
@@ -880,15 +880,15 @@ condition example
         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
@@ -899,15 +899,15 @@ condition example
         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
@@ -917,15 +917,51 @@ condition example
         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

+ 2 - 2
db/migrate/20201202080338_issue3270_selector_update.rb

@@ -21,9 +21,9 @@ class Issue3270SelectorUpdate < ActiveRecord::Migration[5.2]
       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

+ 13 - 0
db/migrate/20210428125300_issue_3523_new_operator.rb

@@ -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

+ 1 - 1
db/seeds/overviews.rb

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

+ 16 - 0
lib/search_index_backend.rb

@@ -644,6 +644,22 @@ example for aggregations within one year
           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] = {}

+ 43 - 2
spec/lib/search_index_backend_spec.rb

@@ -193,9 +193,10 @@ RSpec.describe SearchIndexBackend, searchindex: true do
 
     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
@@ -207,12 +208,52 @@ RSpec.describe SearchIndexBackend, searchindex: true do
       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' } },

+ 70 - 0
spec/models/ticket_spec.rb

@@ -989,6 +989,76 @@ RSpec.describe Ticket, type: :model do
         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) }

+ 46 - 0
test/unit/ticket_selector_test.rb

@@ -409,6 +409,29 @@ class TicketSelectorTest < ActiveSupport::TestCase
     ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @customer2)
     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 = {
       'ticket.group_id'   => {
         operator: 'is',
@@ -544,6 +567,29 @@ class TicketSelectorTest < ActiveSupport::TestCase
     ticket_count, tickets = Ticket.selectors(condition, limit: 10, current_user: @customer2)
     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 = {
       'ticket.group_id'   => {
         operator: 'is',