Fixes #3616 - Provide meaningful modal if report profile tries to use dates out side the filtered date range
This commit is contained in:
parent
d2f24fc0fc
commit
04d1459121
16 changed files with 1481 additions and 1471 deletions
|
@ -11,7 +11,6 @@
|
|||
- bundle exec rails test test/integration/elasticsearch_active_test.rb
|
||||
- bundle exec rails test test/integration/elasticsearch_test.rb
|
||||
- bundle exec rspec --tag searchindex --tag ~type:system --profile 10
|
||||
- bundle exec rails test test/integration/report_test.rb
|
||||
|
||||
es:7:
|
||||
<<: *template_integration_es
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class App.ControllerTechnicalErrorModal extends App.ControllerModal
|
||||
head: "StatusCode: #{status}"
|
||||
contentCode: ''
|
||||
buttonClose: false
|
||||
buttonSubmit: 'Ok'
|
||||
onSubmit: (e) -> @close(e)
|
||||
|
||||
content: ->
|
||||
"<pre><code>#{@contentCode}</code></pre>"
|
|
@ -171,6 +171,13 @@ class Graph extends App.Controller
|
|||
backends: @params.backendSelected
|
||||
)
|
||||
processData: true
|
||||
error: (xhr) =>
|
||||
return if !_.include([401, 403, 404, 422, 502], xhr.status)
|
||||
|
||||
@bodyModal = new App.ControllerTechnicalErrorModal(
|
||||
head: 'Cannot generate report'
|
||||
contentCode: xhr.responseJSON.error
|
||||
)
|
||||
success: (data) =>
|
||||
@update(data)
|
||||
@delay(@render, interval, 'report-update', 'page')
|
||||
|
|
|
@ -102,12 +102,18 @@ class _ajaxSingleton
|
|||
# do not show any error message with code 502
|
||||
return if status is 502
|
||||
|
||||
try
|
||||
json = JSON.parse(detail)
|
||||
text = json.error_human || json.error
|
||||
|
||||
text = detail if !text
|
||||
|
||||
escaped = App.Utils.htmlEscape(text)
|
||||
|
||||
# show error message
|
||||
new App.ControllerModal(
|
||||
head: "StatusCode: #{status}"
|
||||
contentInline: "<pre>#{App.Utils.htmlEscape(detail)}</pre>"
|
||||
buttonClose: true
|
||||
buttonSubmit: false
|
||||
new App.ControllerTechnicalErrorModal(
|
||||
contentCode: escaped
|
||||
head: "StatusCode: #{status}"
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -358,6 +358,26 @@ ol, ul {
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
code {
|
||||
background: hsla(0, 0%, 0%, 0.2);
|
||||
border-radius: 3px;
|
||||
box-decoration-break: clone;
|
||||
}
|
||||
|
||||
code,
|
||||
.hljs {
|
||||
padding: 2px 4px;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
background: none;
|
||||
}
|
||||
|
||||
pre code.hljs {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
padding: 9.5px;
|
||||
|
@ -371,30 +391,26 @@ pre {
|
|||
border: 1px solid hsl(0,0%,90%);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-content pre {
|
||||
background: hsl(0, 0%, 97%);
|
||||
border: 1px solid hsl(0, 0%, 87%);
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
white-space: pre-wrap;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.hljs,
|
||||
code {
|
||||
background: none;
|
||||
padding: 2px 4px;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
overflow-x: auto;
|
||||
|
||||
code:not(.hljs) {
|
||||
border: 1px solid rgba(0,0,0,.2);
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
pre code.hljs {
|
||||
font-size: 1em;
|
||||
&.hljs {
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea::placeholder,
|
||||
|
|
|
@ -22,26 +22,34 @@ class ReportsController < ApplicationController
|
|||
get_params = params_all
|
||||
return if !get_params
|
||||
|
||||
result = {}
|
||||
get_params[:metric][:backend].each do |backend|
|
||||
condition = get_params[:profile].condition
|
||||
if backend[:condition]
|
||||
backend[:condition].merge(condition)
|
||||
else
|
||||
backend[:condition] = condition
|
||||
end
|
||||
next if !backend[:adapter]
|
||||
begin
|
||||
result = {}
|
||||
get_params[:metric][:backend].each do |backend|
|
||||
condition = get_params[:profile].condition
|
||||
if backend[:condition]
|
||||
backend[:condition].merge(condition)
|
||||
else
|
||||
backend[:condition] = condition
|
||||
end
|
||||
next if !backend[:adapter]
|
||||
|
||||
result[backend[:name]] = backend[:adapter].aggs(
|
||||
range_start: get_params[:start],
|
||||
range_end: get_params[:stop],
|
||||
interval: get_params[:range],
|
||||
selector: backend[:condition],
|
||||
params: backend[:params],
|
||||
timezone: get_params[:timezone],
|
||||
timezone_offset: get_params[:timezone_offset],
|
||||
current_user: current_user
|
||||
)
|
||||
result[backend[:name]] = backend[:adapter].aggs(
|
||||
range_start: get_params[:start],
|
||||
range_end: get_params[:stop],
|
||||
interval: get_params[:range],
|
||||
selector: backend[:condition],
|
||||
params: backend[:params],
|
||||
timezone: get_params[:timezone],
|
||||
timezone_offset: get_params[:timezone_offset],
|
||||
current_user: current_user
|
||||
)
|
||||
end
|
||||
rescue => e
|
||||
if e.message.include? 'Conflicting date range'
|
||||
raise Exceptions::UnprocessableEntity, 'Conflicting date ranges. Please check your selected report profile.'
|
||||
end
|
||||
|
||||
raise e
|
||||
end
|
||||
|
||||
render json: {
|
||||
|
|
|
@ -438,6 +438,8 @@ example for aggregations within one year
|
|||
|
||||
data = selector2query(selectors, options, aggs_interval)
|
||||
|
||||
verify_date_range(url, data)
|
||||
|
||||
response = make_request(url, data: data)
|
||||
|
||||
if !response.success?
|
||||
|
@ -1208,4 +1210,87 @@ helper method for making HTTP calls and raising error if response was not succes
|
|||
)
|
||||
end
|
||||
|
||||
# verifies date range ElasticSearch payload
|
||||
#
|
||||
# @param url [String] of ElasticSearch
|
||||
# @param payload [Hash] Elasticsearch query payload
|
||||
#
|
||||
# @return [Boolean] or raises error
|
||||
def self.verify_date_range(url, payload)
|
||||
ranges_payload = payload.dig(:query, :bool, :must)
|
||||
|
||||
return true if ranges_payload.nil?
|
||||
|
||||
ranges = ranges_payload
|
||||
.select { |elem| elem.key? :range }
|
||||
.map { |elem| [elem[:range].keys.first, convert_es_date_range(elem)] }
|
||||
.each_with_object({}) { |elem, sum| (sum[elem.first] ||= []) << elem.last }
|
||||
|
||||
return true if ranges.all? { |_, ranges_by_key| verify_single_key_range(ranges_by_key) }
|
||||
|
||||
error_prefix = "Unable to process request to elasticsearch URL '#{url}'."
|
||||
error_suffix = "Payload:\n#{payload.to_json}"
|
||||
error_message = 'Conflicting date ranges'
|
||||
|
||||
result = "#{error_prefix} #{error_message} #{error_suffix}"
|
||||
Rails.logger.error result.first(40_000)
|
||||
|
||||
raise result
|
||||
end
|
||||
|
||||
# checks if all ranges are overlaping
|
||||
#
|
||||
# @param ranges [Array<Range<DateTime>>] to use in search
|
||||
#
|
||||
# @return [Boolean]
|
||||
def self.verify_single_key_range(ranges)
|
||||
ranges
|
||||
.each_with_index
|
||||
.all? do |range, i|
|
||||
ranges
|
||||
.slice((i + 1)..)
|
||||
.all? { |elem| elem.overlaps? range }
|
||||
end
|
||||
end
|
||||
|
||||
# Converts paylaod component to dates range
|
||||
#
|
||||
# @param elem [Hash] payload component
|
||||
#
|
||||
# @return [Range<DateTime>]
|
||||
def self.convert_es_date_range(elem)
|
||||
range = elem[:range].first.last
|
||||
from = parse_es_range_date range[:from] || range[:gt] || '-9999-01-01'
|
||||
to = parse_es_range_date range[:to] || range[:lt] || '9999-01-01'
|
||||
|
||||
from..to
|
||||
end
|
||||
|
||||
# Parses absolute date or converts relative date
|
||||
#
|
||||
# @param input [String] string representation of date
|
||||
#
|
||||
# @return [Range<DateTime>]
|
||||
def self.parse_es_range_date(input)
|
||||
match = input.match(%r{^now(-|\+)(\d+)(\w{1})$})
|
||||
|
||||
return DateTime.parse input if !match
|
||||
|
||||
map = {
|
||||
d: 'day',
|
||||
y: 'year',
|
||||
M: 'month',
|
||||
h: 'hour',
|
||||
m: 'minute',
|
||||
}
|
||||
|
||||
range = match.captures[1].to_i.send map[match.captures[2].to_sym]
|
||||
|
||||
case match.captures[0]
|
||||
when '-'
|
||||
range.ago
|
||||
when '+'
|
||||
range.from_now
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,5 +6,20 @@ FactoryBot.define do
|
|||
active { true }
|
||||
created_by_id { 1 }
|
||||
updated_by_id { 1 }
|
||||
|
||||
trait :condition_created_at do
|
||||
transient do
|
||||
ticket_created_at { nil }
|
||||
end
|
||||
|
||||
condition do
|
||||
{
|
||||
'ticket.created_at' => {
|
||||
operator: 'before (absolute)',
|
||||
value: ticket_created_at.iso8601
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
192
spec/lib/report/ticket_first_solution_spec.rb
Normal file
192
spec/lib/report/ticket_first_solution_spec.rb
Normal file
|
@ -0,0 +1,192 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
# rubocop:disable RSpec/ExampleLength
|
||||
|
||||
require 'rails_helper'
|
||||
require 'lib/report_examples'
|
||||
|
||||
RSpec.describe Report::TicketFirstSolution, searchindex: true do
|
||||
include_examples 'with report examples'
|
||||
|
||||
describe '.aggs' do
|
||||
it 'gets monthly aggregated results' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0]
|
||||
end
|
||||
|
||||
it 'gets monthly aggregated results with high priority' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {
|
||||
'ticket.priority_id' => {
|
||||
'operator' => 'is',
|
||||
'value' => [Ticket::Priority.lookup(name: '3 high').id],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
|
||||
end
|
||||
|
||||
it 'gets monthly aggregated results not in merged state' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {
|
||||
'ticket_state.name' => {
|
||||
'operator' => 'is not',
|
||||
'value' => 'merged',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0]
|
||||
end
|
||||
|
||||
it 'gets monthly aggregated results with not high priority' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {
|
||||
'ticket.priority_id' => {
|
||||
'operator' => 'is not',
|
||||
'value' => [Ticket::Priority.lookup(name: '3 high').id],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0]
|
||||
end
|
||||
|
||||
it 'gets weekly aggregated results' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-10-26T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-10-31T23:59:59Z'),
|
||||
interval: 'week',
|
||||
selector: {},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 1, 0, 0, 1, 1]
|
||||
end
|
||||
|
||||
it 'gets daily aggregated results' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-10-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-11-01T23:59:59Z'),
|
||||
interval: 'day',
|
||||
selector: {},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1]
|
||||
end
|
||||
|
||||
it 'gets hourly aggregated results' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-10-28T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-10-28T23:59:59Z'),
|
||||
interval: 'hour',
|
||||
selector: {},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
end
|
||||
end
|
||||
|
||||
describe '.items' do
|
||||
it 'gets items in year range' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {},
|
||||
)
|
||||
expect(result).to match_tickets ticket_5, ticket_6, ticket_7
|
||||
end
|
||||
|
||||
it 'gets items in year range with high priority' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'ticket.priority_id' => {
|
||||
'operator' => 'is',
|
||||
'value' => [Ticket::Priority.lookup(name: '3 high').id],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_5
|
||||
end
|
||||
|
||||
it 'gets items in year range not in merged state' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'ticket_state.name' => {
|
||||
'operator' => 'is not',
|
||||
'value' => 'merged',
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_5, ticket_6, ticket_7
|
||||
end
|
||||
|
||||
it 'gets items in year range with not high priority' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'ticket.priority_id' => {
|
||||
'operator' => 'is not',
|
||||
'value' => [Ticket::Priority.lookup(name: '3 high').id],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_6, ticket_7
|
||||
end
|
||||
|
||||
it 'gets items in week range' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-10-26T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-11-01T23:59:59Z'),
|
||||
selector: {}
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_5, ticket_6, ticket_7
|
||||
end
|
||||
|
||||
it 'gets items in day range' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-10-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-10-31T23:59:59Z'),
|
||||
selector: {}
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_5, ticket_6
|
||||
end
|
||||
|
||||
it 'gets items in hour range' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-10-28T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-10-28T23:59:59Z'),
|
||||
interval: 'hour',
|
||||
selector: {},
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_5
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable RSpec/ExampleLength
|
|
@ -1,33 +1,333 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
require 'lib/report_examples'
|
||||
|
||||
RSpec.describe Report::TicketGenericTime do
|
||||
RSpec.describe Report::TicketGenericTime, searchindex: true do
|
||||
include_examples 'with report examples'
|
||||
|
||||
=begin
|
||||
describe '.aggs' do
|
||||
it 'gets monthly aggregated results by created_at' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month', # year, quarter, month, week, day, hour, minute, second
|
||||
selector: {}, # ticket selector to get only a collection of tickets
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
result = Report::TicketGenericTime.items(
|
||||
range_start: '2015-01-01T00:00:00Z',
|
||||
range_end: '2015-12-31T23:59:59Z',
|
||||
selector: selector, # ticket selector to get only a collection of tickets
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 1, 0]
|
||||
end
|
||||
|
||||
returns
|
||||
it 'gets monthly aggregated results by created_at not merged' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month', # year, quarter, month, week, day, hour, minute, second
|
||||
selector: {
|
||||
'state' => {
|
||||
'operator' => 'is not',
|
||||
'value' => 'merged'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
{
|
||||
count: 123,
|
||||
ticket_ids: [4,5,1,5,0,51,5,56,7,4],
|
||||
assets: assets,
|
||||
}
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 1, 0]
|
||||
end
|
||||
end
|
||||
|
||||
=end
|
||||
describe '.items' do
|
||||
it 'gets items in year range by created_at' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {}, # ticket selector to get only a collection of tickets
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
describe 'items' do
|
||||
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2, ticket_1
|
||||
end
|
||||
|
||||
it 'gets items in year range by created_at not merged' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'state' => {
|
||||
'operator' => 'is not',
|
||||
'value' => 'merged'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2, ticket_1
|
||||
end
|
||||
|
||||
it 'gets items in year range by created_at before oct 31st' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'created_at' => {
|
||||
'operator' => 'before (absolute)',
|
||||
'value' => '2015-10-31T00:00:00Z'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_5, ticket_4, ticket_3, ticket_2, ticket_1
|
||||
end
|
||||
|
||||
it 'gets items in year range by created_at after oct 31st' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'created_at' => {
|
||||
'operator' => 'after (absolute)',
|
||||
'value' => '2015-10-31T00:00:00Z'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_7, ticket_6
|
||||
end
|
||||
|
||||
it 'gets items in 1 day from now' do
|
||||
result = described_class.items(
|
||||
range_start: 1.year.ago.beginning_of_year,
|
||||
range_end: 1.year.from_now.at_end_of_year,
|
||||
selector: {
|
||||
'created_at' => {
|
||||
'operator' => 'after (relative)',
|
||||
'range' => 'day',
|
||||
'value' => '1'
|
||||
}
|
||||
}, # ticket selector to get only a collection of tickets
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_after_72h
|
||||
end
|
||||
|
||||
it 'gets items in 1 month from now' do
|
||||
result = described_class.items(
|
||||
range_start: 1.year.ago.beginning_of_year,
|
||||
range_end: 1.year.from_now.at_end_of_year,
|
||||
selector: {
|
||||
'created_at' => {
|
||||
'operator' => 'after (relative)',
|
||||
'range' => 'month',
|
||||
'value' => '1'
|
||||
}
|
||||
}, # ticket selector to get only a collection of tickets
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets []
|
||||
end
|
||||
|
||||
it 'gets items in 1 month ago' do
|
||||
result = described_class.items(
|
||||
range_start: 1.year.ago.beginning_of_year,
|
||||
range_end: 1.year.from_now.at_end_of_year,
|
||||
selector: {
|
||||
'created_at' => {
|
||||
'operator' => 'before (relative)',
|
||||
'range' => 'month',
|
||||
'value' => '1'
|
||||
}
|
||||
}, # ticket selector to get only a collection of tickets
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_before_40d
|
||||
end
|
||||
|
||||
it 'gets items in 5 months ago' do
|
||||
result = described_class.items(
|
||||
range_start: 1.year.ago.beginning_of_year,
|
||||
range_end: 1.year.from_now.at_end_of_year,
|
||||
selector: {
|
||||
'created_at' => {
|
||||
'operator' => 'before (relative)',
|
||||
'range' => 'month',
|
||||
'value' => '5'
|
||||
}
|
||||
}, # ticket selector to get only a collection of tickets
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets []
|
||||
end
|
||||
|
||||
it 'gets items with aaa+bbb' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'tags' => {
|
||||
'operator' => 'contains all',
|
||||
'value' => 'aaa, bbb'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_1
|
||||
end
|
||||
|
||||
it 'gets items with not aaa+bbb' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'tags' => {
|
||||
'operator' => 'contains all not',
|
||||
'value' => 'aaa, bbb'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2
|
||||
end
|
||||
|
||||
it 'gets items with aaa' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'tags' => {
|
||||
'operator' => 'contains all',
|
||||
'value' => 'aaa'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_2, ticket_1
|
||||
end
|
||||
|
||||
it 'gets items with not aaa' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'tags' => {
|
||||
'operator' => 'contains all not',
|
||||
'value' => 'aaa'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3
|
||||
end
|
||||
|
||||
it 'gets items with one not aaa' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'tags' => {
|
||||
'operator' => 'contains one not',
|
||||
'value' => 'aaa'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3
|
||||
end
|
||||
|
||||
it 'gets items with one not aaa+bbb' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'tags' => {
|
||||
'operator' => 'contains one not',
|
||||
'value' => 'aaa, bbb'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_7, ticket_6, ticket_4, ticket_3
|
||||
end
|
||||
|
||||
it 'gets items with one aaa' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'tags' => {
|
||||
'operator' => 'contains one',
|
||||
'value' => 'aaa'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_2, ticket_1
|
||||
end
|
||||
|
||||
it 'gets items with one aaa+bbb' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'tags' => {
|
||||
'operator' => 'contains one',
|
||||
'value' => 'aaa, bbb'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_5, ticket_2, ticket_1
|
||||
end
|
||||
|
||||
it 'gets items with test' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'title' => {
|
||||
'operator' => 'contains',
|
||||
'value' => 'Test'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2, ticket_1
|
||||
end
|
||||
|
||||
it 'gets items with not test' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'title' => {
|
||||
'operator' => 'contains not',
|
||||
'value' => 'Test'
|
||||
}
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets []
|
||||
end
|
||||
|
||||
# Regression test for issue #2246 - Records in Reporting not updated when single ActiveRecord can not be found
|
||||
it 'correctly handles missing tickets' do
|
||||
class_double('SearchIndexBackend', selectors: { ticket_ids: [-1] }).as_stubbed_const
|
||||
it 'correctly handles missing tickets', searchindex: false do
|
||||
class_double('SearchIndexBackend', selectors: { ticket_ids: [-1] }, drop_index: nil, drop_pipeline: nil).as_stubbed_const
|
||||
|
||||
expect do
|
||||
described_class.items(
|
||||
|
@ -39,4 +339,78 @@ returns
|
|||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when additional attribute exists', db_strategy: :reset do
|
||||
before do
|
||||
ObjectManager::Attribute.add(
|
||||
object: 'Ticket',
|
||||
name: 'test_category',
|
||||
display: 'Test 1',
|
||||
data_type: 'tree_select',
|
||||
data_option: {
|
||||
maxlength: 200,
|
||||
null: false,
|
||||
default: '',
|
||||
options: [
|
||||
{ 'name' => 'aa', 'value' => 'aa', 'children' => [{ 'name' => 'aa', 'value' => 'aa::aa' }, { 'name' => 'bb', 'value' => 'aa::bb' }, { 'name' => 'cc', 'value' => 'aa::cc' }] },
|
||||
{ 'name' => 'bb', 'value' => 'bb', 'children' => [{ 'name' => 'aa', 'value' => 'bb::aa' }, { 'name' => 'bb', 'value' => 'bb::bb' }, { 'name' => 'cc', 'value' => 'bb::cc' }] },
|
||||
{ 'name' => 'cc', 'value' => 'cc', 'children' => [{ 'name' => 'aa', 'value' => 'cc::aa' }, { 'name' => 'bb', 'value' => 'cc::bb' }, { 'name' => 'cc', 'value' => 'cc::cc' }] },
|
||||
]
|
||||
},
|
||||
active: true,
|
||||
screens: {},
|
||||
position: 20,
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1,
|
||||
editable: false,
|
||||
to_migrate: false,
|
||||
)
|
||||
ObjectManager::Attribute.migration_execute
|
||||
|
||||
ticket_with_category
|
||||
|
||||
rebuild_searchindex
|
||||
end
|
||||
|
||||
let(:ticket_with_category) do
|
||||
travel_to DateTime.new 2015, 10, 28, 9, 30
|
||||
ticket = create(:ticket,
|
||||
group: group_2,
|
||||
customer: customer,
|
||||
test_category: 'cc::bb',
|
||||
state_name: 'new',
|
||||
priority_name: '2 normal')
|
||||
|
||||
ticket.tag_add('aaa', 1)
|
||||
ticket.tag_add('bbb', 1)
|
||||
create(:ticket_article,
|
||||
:inbound_email,
|
||||
ticket: ticket)
|
||||
|
||||
travel 5.hours
|
||||
|
||||
ticket.update! group: group_1
|
||||
|
||||
travel_back
|
||||
ticket
|
||||
end
|
||||
|
||||
describe '.items' do
|
||||
it 'gets items with test_category cc:bb' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'test_category' => {
|
||||
'operator' => 'is',
|
||||
'value' => 'cc::bb'
|
||||
},
|
||||
},
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_with_category
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
148
spec/lib/report/ticket_moved_spec.rb
Normal file
148
spec/lib/report/ticket_moved_spec.rb
Normal file
|
@ -0,0 +1,148 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
#
|
||||
# rubocop:disable RSpec/ExampleLength
|
||||
|
||||
require 'rails_helper'
|
||||
require 'lib/report_examples'
|
||||
|
||||
RSpec.describe Report::TicketMoved, searchindex: true do
|
||||
include_examples 'with report examples'
|
||||
|
||||
describe '.aggs' do
|
||||
it 'gets monthly aggregated results not in merged state' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {
|
||||
'ticket_state.name' => {
|
||||
'operator' => 'is not',
|
||||
'value' => 'merged',
|
||||
}
|
||||
},
|
||||
params: {
|
||||
type: 'in',
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
end
|
||||
|
||||
it 'gets monthly aggregated results in users group' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {
|
||||
'ticket.group_id' => {
|
||||
'operator' => 'is',
|
||||
'value' => [Group.lookup(name: 'Users').id],
|
||||
}
|
||||
},
|
||||
params: {
|
||||
type: 'in',
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
|
||||
end
|
||||
|
||||
it 'gets monthly aggregated results not in merged state and outgoing' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {
|
||||
'ticket_state.name' => {
|
||||
'operator' => 'is not',
|
||||
'value' => 'merged',
|
||||
}
|
||||
},
|
||||
params: {
|
||||
type: 'out',
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
end
|
||||
|
||||
it 'gets monthly aggregated results in users group and outgoing' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {
|
||||
'ticket.group_id' => {
|
||||
'operator' => 'is',
|
||||
'value' => [Group.lookup(name: 'Users').id],
|
||||
}
|
||||
},
|
||||
params: {
|
||||
type: 'out',
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe '.items' do
|
||||
it 'gets items in year range in users group' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'ticket.group_id' => {
|
||||
'operator' => 'is',
|
||||
'value' => [Group.lookup(name: 'Users').id],
|
||||
}
|
||||
},
|
||||
params: {
|
||||
type: 'in',
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_1
|
||||
end
|
||||
|
||||
it 'gets items in year range not merged and outgoing' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'ticket_state.name' => {
|
||||
'operator' => 'is not',
|
||||
'value' => 'merged',
|
||||
}
|
||||
}, # ticket selector to get only a collection of tickets
|
||||
params: {
|
||||
type: 'out',
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to match_tickets []
|
||||
end
|
||||
|
||||
it 'gets items in year range in users group and outgoing' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'ticket.group_id' => {
|
||||
'operator' => 'is',
|
||||
'value' => [Group.lookup(name: 'Users').id],
|
||||
}
|
||||
},
|
||||
params: {
|
||||
type: 'out',
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_2
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
# rubocop:enable RSpec/ExampleLength
|
129
spec/lib/report/ticket_reopened_spec.rb
Normal file
129
spec/lib/report/ticket_reopened_spec.rb
Normal file
|
@ -0,0 +1,129 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
# rubocop:disable RSpec/ExampleLength
|
||||
|
||||
require 'rails_helper'
|
||||
require 'lib/report_examples'
|
||||
|
||||
RSpec.describe Report::TicketReopened, searchindex: true do
|
||||
include_examples 'with report examples'
|
||||
|
||||
describe '.aggs' do
|
||||
it 'gets monthly aggregated results' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
|
||||
end
|
||||
|
||||
it 'gets monthly aggregated results with high priority' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {
|
||||
'ticket.priority_id' => {
|
||||
'operator' => 'is',
|
||||
'value' => [Ticket::Priority.lookup(name: '3 high').id],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
|
||||
end
|
||||
|
||||
it 'gets monthly aggregated results with not high priority' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {
|
||||
'ticket.priority_id' => {
|
||||
'operator' => 'is not',
|
||||
'value' => [Ticket::Priority.lookup(name: '3 high').id],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
end
|
||||
|
||||
it 'gets monthly aggregated results with not merged' do
|
||||
result = described_class.aggs(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
interval: 'month',
|
||||
selector: {
|
||||
'ticket_state.name' => {
|
||||
'operator' => 'is not',
|
||||
'value' => 'merged',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
|
||||
end
|
||||
end
|
||||
|
||||
describe '.items' do
|
||||
it 'gets items in year range' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {},
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_5
|
||||
end
|
||||
|
||||
it 'gets items in year range with high priority' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'ticket.priority_id' => {
|
||||
'operator' => 'is',
|
||||
'value' => [Ticket::Priority.lookup(name: '3 high').id],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_5
|
||||
end
|
||||
|
||||
it 'gets items in year range with not high priority' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'ticket.priority_id' => {
|
||||
'operator' => 'is not',
|
||||
'value' => [Ticket::Priority.lookup(name: '3 high').id],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to match_tickets []
|
||||
end
|
||||
|
||||
it 'gets items in year range with not merged' do
|
||||
result = described_class.items(
|
||||
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
|
||||
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
|
||||
selector: {
|
||||
'ticket_state.name' => {
|
||||
'operator' => 'is not',
|
||||
'value' => 'merged',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).to match_tickets ticket_5
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable RSpec/ExampleLength
|
245
spec/lib/report_examples.rb
Normal file
245
spec/lib/report_examples.rb
Normal file
|
@ -0,0 +1,245 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
RSpec.shared_context 'with report examples' do
|
||||
before do |example|
|
||||
next if !example.metadata[:searchindex]
|
||||
|
||||
configure_elasticsearch(required: true, rebuild: true) do
|
||||
ticket_1
|
||||
|
||||
ticket_2
|
||||
|
||||
ticket_3
|
||||
|
||||
ticket_4
|
||||
|
||||
ticket_5
|
||||
|
||||
ticket_6
|
||||
|
||||
ticket_7
|
||||
|
||||
ticket_8
|
||||
|
||||
ticket_9
|
||||
|
||||
ticket_after_72h
|
||||
|
||||
ticket_before_40d
|
||||
end
|
||||
end
|
||||
|
||||
let(:group_1) { Group.lookup(name: 'Users') }
|
||||
let(:group_2) { create(:group) }
|
||||
let(:customer) { User.lookup(email: 'nicole.braun@zammad.org') }
|
||||
|
||||
let(:ticket_1) do
|
||||
travel_to DateTime.new 2015, 10, 28, 9, 30
|
||||
ticket = create(:ticket,
|
||||
group: group_2,
|
||||
customer: customer,
|
||||
state_name: 'new',
|
||||
priority_name: '2 normal')
|
||||
|
||||
ticket.tag_add('aaa', 1)
|
||||
ticket.tag_add('bbb', 1)
|
||||
create(:ticket_article,
|
||||
:inbound_email,
|
||||
ticket: ticket)
|
||||
|
||||
travel 5.hours
|
||||
|
||||
ticket.update! group: group_1
|
||||
|
||||
travel_back
|
||||
ticket
|
||||
end
|
||||
|
||||
let(:ticket_2) do
|
||||
travel_to DateTime.new 2015, 10, 28, 9, 30, 1
|
||||
ticket = create(:ticket,
|
||||
group: group_1,
|
||||
customer: customer,
|
||||
state_name: 'new',
|
||||
priority_name: '2 normal')
|
||||
|
||||
ticket.tag_add('aaa', 1)
|
||||
create(:ticket_article,
|
||||
:inbound_email,
|
||||
ticket: ticket)
|
||||
travel 5.hours - 1.second
|
||||
|
||||
ticket.update! group: group_2
|
||||
|
||||
travel_back
|
||||
ticket
|
||||
end
|
||||
|
||||
let(:ticket_3) do
|
||||
travel_to DateTime.new 2015, 10, 28, 10, 30
|
||||
ticket = create(:ticket,
|
||||
group: group_1,
|
||||
customer: customer,
|
||||
state_name: 'open',
|
||||
priority_name: '3 high')
|
||||
create(:ticket_article,
|
||||
:inbound_email,
|
||||
ticket: ticket)
|
||||
|
||||
travel_back
|
||||
ticket
|
||||
end
|
||||
|
||||
let(:ticket_4) do
|
||||
travel_to DateTime.new 2015, 10, 28, 10, 30, 1
|
||||
ticket = create(:ticket,
|
||||
group: group_1,
|
||||
customer: customer,
|
||||
state_name: 'closed',
|
||||
priority_name: '2 normal',
|
||||
close_at: (1.hour - 1.second).from_now)
|
||||
create(:ticket_article,
|
||||
:inbound_email,
|
||||
ticket: ticket)
|
||||
travel_back
|
||||
ticket
|
||||
end
|
||||
|
||||
let(:ticket_5) do
|
||||
travel_to DateTime.new 2015, 10, 28, 11, 30
|
||||
ticket = create(:ticket,
|
||||
group: group_1,
|
||||
customer: customer,
|
||||
state_name: 'closed',
|
||||
priority_name: '3 high',
|
||||
close_at: 10.minutes.from_now)
|
||||
|
||||
ticket.tag_add('bbb', 1)
|
||||
create(:ticket_article,
|
||||
:outbound_email,
|
||||
ticket: ticket)
|
||||
|
||||
ticket.update! state: Ticket::State.lookup(name: 'open')
|
||||
|
||||
travel 3.hours
|
||||
|
||||
travel_back
|
||||
ticket
|
||||
end
|
||||
|
||||
let(:ticket_6) do
|
||||
travel_to DateTime.new 2015, 10, 31, 12, 30
|
||||
ticket = create(:ticket,
|
||||
group: group_1,
|
||||
customer: customer,
|
||||
state_name: 'closed',
|
||||
priority_name: '2 normal',
|
||||
close_at: 5.minutes.from_now)
|
||||
create(:ticket_article,
|
||||
:outbound_email,
|
||||
ticket: ticket)
|
||||
|
||||
travel_back
|
||||
ticket
|
||||
end
|
||||
|
||||
let(:ticket_7) do
|
||||
travel_to DateTime.new 2015, 11, 1, 12, 30
|
||||
ticket = create(:ticket,
|
||||
group: group_1,
|
||||
customer: customer,
|
||||
state_name: 'closed',
|
||||
priority_name: '2 normal',
|
||||
close_at: Time.zone.now)
|
||||
create(:ticket_article,
|
||||
:inbound_email,
|
||||
ticket: ticket)
|
||||
travel_back
|
||||
ticket
|
||||
end
|
||||
|
||||
let(:ticket_8) do
|
||||
travel_to DateTime.new 2015, 11, 2, 12, 30
|
||||
ticket = create(:ticket,
|
||||
group: group_1,
|
||||
customer: customer,
|
||||
state_name: 'merged',
|
||||
priority_name: '2 normal',
|
||||
close_at: Time.zone.now)
|
||||
|
||||
create(:ticket_article,
|
||||
:inbound_email,
|
||||
ticket: ticket)
|
||||
|
||||
travel_back
|
||||
ticket
|
||||
end
|
||||
|
||||
let(:ticket_9) do
|
||||
travel_to DateTime.new 2037, 11, 2, 12, 30
|
||||
ticket = create(:ticket,
|
||||
group: group_1,
|
||||
customer: customer,
|
||||
state_name: 'merged',
|
||||
priority_name: '2 normal',
|
||||
close_at: Time.zone.now)
|
||||
create(:ticket_article,
|
||||
:inbound_email,
|
||||
ticket: ticket)
|
||||
|
||||
travel_back
|
||||
|
||||
ticket
|
||||
end
|
||||
|
||||
let(:ticket_after_72h) do
|
||||
travel 72.hours do
|
||||
ticket = create(:ticket,
|
||||
group: group_1,
|
||||
customer: customer,
|
||||
state_name: 'closed',
|
||||
priority_name: '2 normal',
|
||||
close_at: 5.minutes.from_now)
|
||||
create(:ticket_article,
|
||||
:outbound_email,
|
||||
ticket: ticket)
|
||||
|
||||
ticket
|
||||
end
|
||||
end
|
||||
|
||||
let(:ticket_before_40d) do
|
||||
travel(-40.days) do
|
||||
ticket = create(:ticket,
|
||||
group: group_1,
|
||||
customer: customer,
|
||||
state_name: 'closed',
|
||||
priority_name: '2 normal',
|
||||
close_at: 5.minutes.from_now)
|
||||
create(:ticket_article,
|
||||
:outbound_email,
|
||||
ticket: ticket)
|
||||
|
||||
ticket
|
||||
end
|
||||
end
|
||||
|
||||
matcher :match_tickets do
|
||||
match do
|
||||
if expected_tickets.blank?
|
||||
actual_ticket_ids.blank?
|
||||
else
|
||||
# GenericTime returns string ids :o
|
||||
actual_ticket_ids.map(&:to_i) == expected_tickets.map(&:id)
|
||||
end
|
||||
end
|
||||
|
||||
def expected_tickets
|
||||
Array(expected)
|
||||
end
|
||||
|
||||
def actual_ticket_ids
|
||||
actual[:ticket_ids]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SearchIndexBackend, searchindex: true do
|
||||
RSpec.describe SearchIndexBackend do
|
||||
|
||||
before do
|
||||
configure_elasticsearch
|
||||
rebuild_searchindex
|
||||
before do |example|
|
||||
next if !example.metadata[:searchindex]
|
||||
|
||||
configure_elasticsearch(required: true, rebuild: true)
|
||||
end
|
||||
|
||||
describe '.build_query' do
|
||||
|
@ -19,7 +20,7 @@ RSpec.describe SearchIndexBackend, searchindex: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.search' do
|
||||
describe '.search', searchindex: true do
|
||||
|
||||
context 'query finds results' do
|
||||
|
||||
|
@ -200,7 +201,8 @@ RSpec.describe SearchIndexBackend, searchindex: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.remove' do
|
||||
describe '.remove', searchindex: true do
|
||||
|
||||
context 'record gets deleted' do
|
||||
|
||||
let(:record_type) { 'Ticket'.freeze }
|
||||
|
@ -239,7 +241,7 @@ RSpec.describe SearchIndexBackend, searchindex: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.selectors' do
|
||||
describe '.selectors', searchindex: true do
|
||||
|
||||
let(:group1) { create :group }
|
||||
let(:organization1) { create :organization, note: 'hihi' }
|
||||
|
@ -845,4 +847,145 @@ RSpec.describe SearchIndexBackend, searchindex: true do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.verify_date_range' do
|
||||
let(:range_1) { { range: { created_at: { from: '2020-01-01T00:00:00.000Z', to: '2021-12-31T23:59:59Z' } } } }
|
||||
let(:range_2) { { range: { created_at: { from: '2020-03-01T00:00:00.000Z', to: '2020-03-31T23:59:59Z' } } } }
|
||||
let(:range_3) { { range: { created_at: { from: '2018-03-01T00:00:00.000Z', to: '2018-03-31T23:59:59Z' } } } }
|
||||
let(:range_4) { { range: { updated_at: { from: '2018-03-01T00:00:00.000Z', to: '2018-03-31T23:59:59Z' } } } }
|
||||
|
||||
def build_payload(*ranges)
|
||||
{
|
||||
query: {
|
||||
bool: {
|
||||
must: ranges
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'verifies single range' do
|
||||
result = described_class.verify_date_range 'url', build_payload(range_1)
|
||||
|
||||
expect(result).to be_truthy
|
||||
end
|
||||
|
||||
it 'verifies multiple intersecting ranges' do
|
||||
result = described_class.verify_date_range 'url', build_payload(range_1, range_2)
|
||||
|
||||
expect(result).to be_truthy
|
||||
end
|
||||
|
||||
it 'verifies non-intersecting ranges on different keys' do
|
||||
result = described_class.verify_date_range 'url', build_payload(range_1, range_4)
|
||||
|
||||
expect(result).to be_truthy
|
||||
end
|
||||
|
||||
it 'verifies payload without any ranges' do
|
||||
result = described_class.verify_date_range 'url', build_payload
|
||||
|
||||
expect(result).to be_truthy
|
||||
end
|
||||
|
||||
it 'verifies payload without payload' do
|
||||
result = described_class.verify_date_range 'url', {}
|
||||
|
||||
expect(result).to be_truthy
|
||||
end
|
||||
|
||||
it 'raises an error with multiple non-intersecting range' do
|
||||
expect { described_class.verify_date_range 'url', build_payload(range_1, range_3) }
|
||||
.to raise_error(%r{Conflicting date ranges})
|
||||
end
|
||||
|
||||
context 'with a stubbed range' do
|
||||
before do
|
||||
allow(described_class).to receive(:convert_es_date_range).and_return(mock_range)
|
||||
end
|
||||
|
||||
let(:mock_range) { instance_double('Range', overlaps?: true) }
|
||||
|
||||
it 'checks overlap once for 2 ranges' do
|
||||
described_class.verify_date_range 'url', build_payload(range_1, range_2)
|
||||
expect(mock_range).to have_received(:overlaps?).exactly(1).times
|
||||
end
|
||||
|
||||
it 'checks overlap 3 times for 3 ranges' do
|
||||
described_class.verify_date_range 'url', build_payload(range_1, range_2, range_3)
|
||||
expect(mock_range).to have_received(:overlaps?).exactly(3).times
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.verify_single_key_range' do
|
||||
let(:range_1) { DateTime.new(2020, 1, 1)..DateTime.new(2021, 12, 31) }
|
||||
let(:range_2) { DateTime.new(2020, 3, 1)..DateTime.new(2020, 3, 31) }
|
||||
let(:range_3) { DateTime.new(2018, 3, 1)..DateTime.new(2018, 3, 31) }
|
||||
|
||||
it 'returns true with a single range' do
|
||||
result = described_class.verify_single_key_range [range_1]
|
||||
expect(result).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns true with overlapping ranges' do
|
||||
result = described_class.verify_single_key_range [range_1, range_2]
|
||||
expect(result).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false with non-overlapping ranges' do
|
||||
result = described_class.verify_single_key_range [range_1, range_3]
|
||||
expect(result).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '.convert_es_date_range' do
|
||||
let(:from) { DateTime.new 2018, 1, 1, 17 }
|
||||
let(:from_placeholder) { DateTime.new(-9999, 1, 1) }
|
||||
let(:to) { DateTime.new 2020, 10, 1, 23 }
|
||||
let(:to_placeholder) { DateTime.new 9999, 1, 1 }
|
||||
|
||||
it 'converts range' do
|
||||
result = described_class.convert_es_date_range(
|
||||
{
|
||||
range: {
|
||||
created_at: {
|
||||
from: '2018-01-01T17:00:00.000Z',
|
||||
to: '2020-10-01T23:00:00Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).to eq from..to
|
||||
end
|
||||
|
||||
it 'converts less than' do
|
||||
result = described_class.convert_es_date_range(
|
||||
{
|
||||
range: {
|
||||
created_at: {
|
||||
lt: '2020-10-01T23:00:00Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).to eq from_placeholder..to
|
||||
end
|
||||
|
||||
it 'converts greater than' do
|
||||
result = described_class.convert_es_date_range(
|
||||
{
|
||||
range: {
|
||||
created_at: {
|
||||
gt: '2018-01-01T17:00:00.000Z',
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).to eq from..to_placeholder
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,4 +33,42 @@ RSpec.describe 'Report', type: :system, searchindex: true do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'report profiles are displayed' do
|
||||
let!(:report_profile_active) { create(:report_profile) }
|
||||
let!(:report_profile_inactive) { create(:report_profile, active: false) }
|
||||
|
||||
it 'shows report profiles' do
|
||||
visit 'report'
|
||||
|
||||
expect(page)
|
||||
.to have_css('ul.checkbox-list .label-text', text: report_profile_active.name)
|
||||
.and have_no_css('ul.checkbox-list .label-text', text: report_profile_inactive.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with report profiles with date-based conditions' do
|
||||
let(:report_profile) { create(:report_profile, :condition_created_at, ticket_created_at: 1.year.ago) }
|
||||
|
||||
before do
|
||||
freeze_time
|
||||
report_profile
|
||||
visit 'report'
|
||||
end
|
||||
|
||||
it 'shows previous year for a profile with matching conditions' do
|
||||
click '.js-timePickerYear', text: Time.zone.now.year - 1
|
||||
click '.label-text', text: report_profile.name
|
||||
|
||||
expect(page).to have_no_css('.modal')
|
||||
end
|
||||
|
||||
it 'throws error for a profile when showing a different year than described in the profile' do
|
||||
click '.label-text', text: report_profile.name
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page).to have_text 'Conflicting date ranges'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue