#-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2018 the OpenProject Foundation (OPF) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. # # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: # Copyright (C) 2006-2017 Jean-Philippe Lang # Copyright (C) 2010-2013 the ChiliProject Team # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See docs/COPYRIGHT.rdoc for more details. #++ require 'spec_helper' describe ::Query::Results, type: :model do let(:query) do FactoryBot.build :query, show_hierarchies: false end let(:query_results) do ::Query::Results.new query, include: %i( assigned_to type priority category fixed_version ), order: 'work_packages.root_id DESC, work_packages.lft ASC' end let(:project_1) { FactoryBot.create :project } let(:role_pm) do FactoryBot.create(:role, permissions: %i( view_work_packages edit_work_packages create_work_packages delete_work_packages )) end let(:role_dev) do FactoryBot.create(:role, permissions: [:view_work_packages]) end let(:user_1) do FactoryBot.create(:user, firstname: 'user', lastname: '1', member_in_project: project_1, member_through_role: [role_dev, role_pm]) end let(:wp_p1) do (1..3).map do FactoryBot.create(:work_package, project: project_1, assigned_to_id: user_1.id) end end describe '#work_package_count_by_group' do let(:query) do FactoryBot.build :query, show_hierarchies: false, group_by: group_by end context 'when grouping by responsible' do let(:group_by) { 'responsible' } it 'should produce a valid SQL statement' do expect { query_results.work_package_count_by_group }.not_to raise_error end end end describe '#work_packages' do let!(:project_1) { FactoryBot.create :project } let!(:project_2) { FactoryBot.create :project } let!(:member) do FactoryBot.create(:member, project: project_2, principal: user_1, roles: [role_pm]) end let!(:user_2) do FactoryBot.create(:user, firstname: 'user', lastname: '2', member_in_project: project_2, member_through_role: role_dev) end let!(:wp_p2) do FactoryBot.create(:work_package, project: project_2, assigned_to_id: user_2.id) end let!(:wp2_p2) do FactoryBot.create(:work_package, project: project_2, assigned_to_id: user_1.id) end before do wp_p1 end context 'when filtering for assigned_to_role' do before do allow(User).to receive(:current).and_return(user_2) allow(project_2.descendants).to receive(:active).and_return([]) query.add_filter('assigned_to_role', '=', [role_dev.id.to_s]) end context 'when a project is set' do let(:query) { FactoryBot.build :query, project: project_2 } it 'should display only wp for selected project and selected role' do expect(query_results.work_packages).to match_array([wp_p2]) end end context 'when no project is set' do let(:query) { FactoryBot.build :query, project: nil } it 'should display all wp from projects where User.current has access' do expect(query_results.work_packages).to match_array([wp_p2, wp2_p2]) end end end # this tests some unfortunate combination of filters where wrong # sql statements where produced. context 'with a custom field being returned and paginating' do let(:group_by) { nil } let(:query) do FactoryBot.build_stubbed :query, show_hierarchies: false, group_by: group_by, project: project_2 end let!(:custom_field) { FactoryBot.create(:work_package_custom_field, is_for_all: true) } before do allow(User).to receive(:current).and_return(user_2) # reload in order to have the custom field as an available # custom field query.project = Project.find(query.project.id) end context 'when grouping by assignees' do before do query.column_names = [:assigned_to, :"cf_#{custom_field.id}"] query.group_by = 'assigned_to' end it 'returns all work packages of project 2' do work_packages = query .results .work_packages .page(1) .per_page(10) expect(work_packages).to match_array([wp_p2, wp2_p2]) end end context 'when grouping by responsibles' do let(:group_by) { 'responsible' } before do query.column_names = [:responsible, :"cf_#{custom_field.id}"] end it 'returns all work packages of project 2' do work_packages = query .results .work_packages .page(1) .per_page(10) expect(work_packages).to match_array([wp_p2, wp2_p2]) end end end context 'when grouping by responsible' do let(:query) do FactoryBot.build :query, show_hierarchies: false, group_by: group_by, project: project_1 end let(:group_by) { 'responsible' } before do allow(User).to receive(:current).and_return(user_1) wp_p1[0].update_attribute(:responsible, user_1) wp_p1[1].update_attribute(:responsible, user_2) end it 'outputs the work package count in the schema { => count }' do expect(query_results.work_package_count_by_group) .to eql(user_1 => 1, user_2 => 1, nil => 1) end end context 'when filtering by precedes and ordering by parent' do let(:query) do FactoryBot.build :query, project: project_1 end before do login_as(user_1) wp_p1[1].precedes << wp_p1[0] query.add_filter('precedes', '=', [wp_p1[0].id.to_s]) query.sort_criteria = [['parent', 'asc']] end it 'returns the work packages preceding the filtered for work package' do expect(query.results.work_packages) .to match_array(wp_p1[1]) end end end describe '#sorted_work_packages' do let(:work_package1) { FactoryBot.create(:work_package, project: project_1, id: 1) } let(:work_package2) { FactoryBot.create(:work_package, project: project_1, id: 2) } let(:work_package3) { FactoryBot.create(:work_package, project: project_1, id: 3) } let(:sort_by) { [['parent', 'asc']] } let(:columns) { %i(id subject) } let(:group_by) { '' } let(:query) do FactoryBot.build_stubbed :query, show_hierarchies: false, group_by: group_by, sort_criteria: sort_by, project: project_1, column_names: columns end let(:query_results) do ::Query::Results.new query end let(:user_a) { FactoryBot.create(:user, firstname: 'AAA', lastname: 'AAA') } let(:user_m) { FactoryBot.create(:user, firstname: 'MMM', lastname: 'MMM') } let(:user_z) { FactoryBot.create(:user, firstname: 'ZZZ', lastname: 'ZZZ') } context 'grouping by assigned_to, having the author column selected' do let(:group_by) { 'assigned_to' } let(:columns) { %i(id subject author) } before do allow(User).to receive(:current).and_return(user_1) work_package1.assigned_to = user_m work_package1.author = user_m work_package1.save(validate: false) work_package2.assigned_to = user_z work_package2.author = user_a work_package2.save(validate: false) work_package3.assigned_to = user_m work_package3.author = user_a work_package3.save(validate: false) end it 'sorts first by assigned_to (group by), then by sort criteria' do # Would look like this in the table # # user_m # work_package 1 # work_package 3 # user_z # work_package 2 expect(query_results.sorted_work_packages) .to match [work_package1, work_package3, work_package2] end end context 'sorting by author, grouping by assigned_to' do let(:group_by) { 'assigned_to' } let(:sort_by) { [['author', 'asc']] } before do allow(User).to receive(:current).and_return(user_1) work_package1.assigned_to = user_m work_package1.author = user_m work_package1.save(validate: false) work_package2.assigned_to = user_z work_package2.author = user_a work_package2.save(validate: false) work_package3.assigned_to = user_m work_package3.author = user_a work_package3.save(validate: false) end it 'sorts first by group by, then by assigned_to' do # Would look like this in the table # # user_m # work_package 3 # work_package 1 # user_z # work_package 2 expect(query_results.sorted_work_packages) .to match [work_package3, work_package1, work_package2] query.sort_criteria = [['author', 'desc']] # Would look like this in the table # # user_m # work_package 1 # work_package 3 # user_z # work_package 2 expect(query_results.sorted_work_packages) .to match [work_package1, work_package3, work_package2] end end context 'sorting by author and responsible, grouping by assigned_to' do let(:group_by) { 'assigned_to' } let(:sort_by) { [['author', 'asc'], ['responsible', 'desc']] } before do allow(User).to receive(:current).and_return(user_1) work_package1.assigned_to = user_m work_package1.author = user_m work_package1.responsible = user_a work_package1.save(validate: false) work_package2.assigned_to = user_z work_package2.author = user_m work_package3.responsible = user_m work_package2.save(validate: false) work_package3.assigned_to = user_m work_package3.author = user_m work_package3.responsible = user_z work_package3.save(validate: false) end it 'sorts first by group by, then by assigned_to (neutral as equal), then by responsible' do # Would look like this in the table # # user_m # work_package 3 # work_package 1 # user_z # work_package 2 expect(query_results.sorted_work_packages) .to match [work_package3, work_package1, work_package2] query.sort_criteria = [['author', 'desc'], ['responsible', 'asc']] # Would look like this in the table # # user_m # work_package 1 # work_package 3 # user_z # work_package 2 expect(query_results.sorted_work_packages) .to match [work_package1, work_package3, work_package2] end end context 'sorting by parent' do let(:work_package1) { FactoryBot.create(:work_package, project: project_1, subject: '1') } let(:work_package2) { FactoryBot.create(:work_package, project: project_1, parent: work_package1, subject: '2') } let(:work_package3) { FactoryBot.create(:work_package, project: project_1, parent: work_package2, subject: '3') } let(:work_package4) { FactoryBot.create(:work_package, project: project_1, parent: work_package1, subject: '4') } let(:work_package5) { FactoryBot.create(:work_package, project: project_1, parent: work_package4, subject: '5') } let(:work_package6) { FactoryBot.create(:work_package, project: project_1, parent: work_package4, subject: '6') } let(:work_package7) { FactoryBot.create(:work_package, project: project_1, subject: '7') } let(:work_package8) { FactoryBot.create(:work_package, project: project_1, subject: '8') } let(:work_package9) { FactoryBot.create(:work_package, project: project_1, parent: work_package8, subject: '9') } let(:work_packages) do [work_package1, work_package2, work_package3, work_package4, work_package5, work_package6, work_package7, work_package8, work_package9] end # While we set a second sort criteria, it will be ignored as the sorting works solely on the id of the ancestors and # the work package itself let(:sort_by) { [['parent', 'asc'], ['subject', 'asc']] } before do allow(User).to receive(:current).and_return(user_1) end it 'sorts depth first by parent (id) where the second criteria is unfortunately ignored' do # Reimplementing the algorithm of how the production code sorts lexically on ids (e.g. '15' before '7'). # This is necessary as the ids are not fixed and might span order of magnitude boundaries. paths = work_packages.map do |wp| # Only need to include 'relations.hierarchy' in the projection # to satisfy PG needing to have all ORDER BY columns included on DISTINCT. [(wp.ancestors.order("relations.hierarchy DESC").pluck(:id, 'relations.hierarchy').map(&:first) << wp.id).join(' '), wp] end expected_order = paths.sort_by(&:first).map(&:second).flatten expect(query_results.sorted_work_packages) .to match expected_order query.sort_criteria = [['parent', 'desc'], ['subject', 'asc']] expect(query_results.sorted_work_packages) .to match expected_order.reverse end end context 'filtering by bool cf' do let(:bool_cf) { FactoryBot.create(:bool_wp_custom_field, is_filter: true) } let(:custom_value) do FactoryBot.create(:custom_value, custom_field: bool_cf, customized: work_package1, value: value) end let(:value) { 't' } let(:filter_value) { 't' } let(:activate_cf) do work_package1.project.work_package_custom_fields << bool_cf work_package1.type.custom_fields << bool_cf work_package1.reload project_1.reload end before do allow(User).to receive(:current).and_return(user_1) custom_value activate_cf query.add_filter(:"cf_#{bool_cf.id}", '=', [filter_value]) end shared_examples_for 'is empty' do it 'is empty' do expect(query.results.work_packages) .to be_empty end end shared_examples_for 'returns the wp' do it 'returns the wp' do expect(query.results.work_packages) .to match_array(work_package1) end end context 'with the wp having true for the cf and filtering for true' do it_behaves_like 'returns the wp' end context 'with the wp having true for the cf and filtering for false' do let(:filter_value) { 'f' } it_behaves_like 'is empty' end context 'with the wp having false for the cf and filtering for false' do let(:value) { 'f' } let(:filter_value) { 'f' } it_behaves_like 'returns the wp' end context 'with the wp having false for the cf and filtering for true' do let(:value) { 'f' } it_behaves_like 'is empty' end context 'with the wp having no value for the cf and filtering for true' do let(:custom_value) { nil } it_behaves_like 'is empty' end context 'with the wp having no value for the cf and filtering for false' do let(:custom_value) { nil } let(:filter_value) { 'f' } it_behaves_like 'returns the wp' end context 'with the wp having no value for the cf and filtering for false and the cf not being active in the project' do let(:custom_value) { nil } let(:filter_value) { 'f' } let(:activate_cf) do work_package1.type.custom_fields << bool_cf work_package1.reload project_1.reload end it_behaves_like 'is empty' end context 'with the wp having no value for the cf and filtering for false and the cf not being active for the type' do let(:custom_value) { nil } let(:filter_value) { 'f' } let(:activate_cf) do work_package1.type.custom_fields << bool_cf work_package1.reload project_1.reload end it_behaves_like 'is empty' end context 'with the wp having no value for the cf and filtering for false and the cf not being active in the project and the cf being for all' do let(:custom_value) { nil } let(:filter_value) { 'f' } let(:bool_cf) do FactoryBot.create(:bool_wp_custom_field, is_filter: true, is_for_all: true) end let(:activate_cf) do work_package1.project.work_package_custom_fields << bool_cf work_package1.reload project_1.reload end it_behaves_like 'is empty' end end end # Introduced to ensure being able to group by custom fields # when running on a MySQL server. # When upgrading to rails 5, the sql_mode passed on with the connection # does include the "only_full_group_by" flag by default which causes our queries to become # invalid because (mysql error): # "SELECT list is not in GROUP BY clause and contains nonaggregated column # 'config_myproject_test.work_packages.id' which is not functionally # dependent on columns in GROUP BY clause" context 'when grouping by custom field' do let!(:custom_field) do FactoryBot.create(:int_wp_custom_field, is_for_all: true, is_filter: true) end before do allow(User).to receive(:current).and_return(user_1) wp_p1[0].type.custom_fields << custom_field project_1.work_package_custom_fields << custom_field wp_p1[0].update_attribute(:"custom_field_#{custom_field.id}", 42) wp_p1[0].save wp_p1[1].update_attribute(:"custom_field_#{custom_field.id}", 42) wp_p1[1].save query.project = project_1 query.group_by = "cf_#{custom_field.id}" end describe '#work_package_count_by_group' do it 'returns a hash of counts by value' do expect(query.results.work_package_count_by_group).to eql(42 => 2, nil => 1) end end end context 'when grouping by list custom field and filtering for it at the same time' do let!(:custom_field) do FactoryBot.create(:list_wp_custom_field, is_for_all: true, is_filter: true, multi_value: true).tap do |cf| work_package1.type.custom_fields << cf end end let(:first_value) do custom_field.custom_options.first end let(:last_value) do custom_field.custom_options.last end let(:work_package1) do FactoryBot.create(:work_package, project: project_1) end let(:work_package2) do FactoryBot.create(:work_package, type: work_package1.type, project: project_1) end before do allow(User).to receive(:current).and_return(user_1) query.group_by = "cf_#{custom_field.id}" query.project = project_1 work_package1.send(:"custom_field_#{custom_field.id}=", first_value) work_package1.save! work_package2.send(:"custom_field_#{custom_field.id}=", [first_value, last_value]) work_package2.save! end describe '#work_package_count_by_group' do it 'yields no error but rather returns the result' do expect { query.results.work_package_count_by_group }.not_to raise_error group_count = query.results.work_package_count_by_group expected_groups = [[first_value], [first_value, last_value]] group_count.each do |key, count| expect(count).to eql 1 expect(expected_groups.any? { |group| group & key == key & group }).to be_truthy end end end end end