diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee index 2b57a1eea..eaa6fdbab 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee +++ b/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' ] diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee index f194f826b..15fdef091 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee +++ b/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]) diff --git a/app/models/ticket.rb b/app/models/ticket.rb index 006e97895..c2519b245 100644 --- a/app/models/ticket.rb +++ b/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 diff --git a/db/migrate/20201202080338_issue3270_selector_update.rb b/db/migrate/20201202080338_issue3270_selector_update.rb index 766998836..56e76d9c1 100644 --- a/db/migrate/20201202080338_issue3270_selector_update.rb +++ b/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 diff --git a/db/migrate/20210428125300_issue_3523_new_operator.rb b/db/migrate/20210428125300_issue_3523_new_operator.rb new file mode 100644 index 000000000..abfc2b42f --- /dev/null +++ b/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 diff --git a/db/seeds/overviews.rb b/db/seeds/overviews.rb index 77278338c..bd59a8ce9 100644 --- a/db/seeds/overviews.rb +++ b/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', }, diff --git a/lib/search_index_backend.rb b/lib/search_index_backend.rb index bcb121ad9..466ff975d 100644 --- a/lib/search_index_backend.rb +++ b/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] = {} diff --git a/spec/lib/search_index_backend_spec.rb b/spec/lib/search_index_backend_spec.rb index 71bef53ed..4c79cf37b 100644 --- a/spec/lib/search_index_backend_spec.rb +++ b/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' } }, diff --git a/spec/models/ticket_spec.rb b/spec/models/ticket_spec.rb index 78320204b..85293379a 100644 --- a/spec/models/ticket_spec.rb +++ b/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) } diff --git a/test/unit/ticket_selector_test.rb b/test/unit/ticket_selector_test.rb index a64a22245..5e9e47728 100644 --- a/test/unit/ticket_selector_test.rb +++ b/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',