diff --git a/README.md b/README.md index 0317783bc8..3b7b32fb23 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,14 @@ from your `Gemfile.plugins` in your OpenProject installation folder and run: to uninstall the ReportingEngine and the OpenProject Reporting plugin. +Configuration +------------- + +* `cost_reporting_cache_filter_classes: true` + +OpenProject Reporting, when not configured otherwise, optimizes response time by caching the filters and group by options generated for work package custom fields. Only when the custom fields are invalidated, does reporting recreate the elements by information from the database. In some scenarios, such a behavior might not be desirable. Especially, when databases are switched between requests to serve information from another installation, caching will almost always fail as the information is outdated and in some edge cases, filters and group by options are displayed erroneously. In such a setting, it is advisible to deactivate the caching by setting `cost_reporting_cache_filter_classes` to `false` in OpenProject's `config/configuration.yml` + + Bug Reporting ------------- diff --git a/app/controllers/cost_reports_controller.rb b/app/controllers/cost_reports_controller.rb index 22d51177a4..0984ccae70 100644 --- a/app/controllers/cost_reports_controller.rb +++ b/app/controllers/cost_reports_controller.rb @@ -46,29 +46,12 @@ class CostReportsController < ApplicationController helper_method :private_queries attr_accessor :cost_types, :unit_id, :cost_type - cattr_accessor :custom_fields_updated_on, :custom_fields_id_sum # Checks if custom fields have been updated, added or removed since we # last saw them, to rebuild the filters and group bys. # Called once per request. - def check_cache(force_update = false) - custom_fields_updated_on = WorkPackageCustomField.maximum(:updated_at) - custom_fields_id_sum = WorkPackageCustomField.sum(:id) + WorkPackageCustomField.count - - if force_update or (custom_fields_updated_on && custom_fields_id_sum) - if force_update or ( - self.class.custom_fields_updated_on != custom_fields_updated_on || - self.class.custom_fields_id_sum != custom_fields_id_sum) - - self.class.custom_fields_updated_on = custom_fields_updated_on - self.class.custom_fields_id_sum = custom_fields_id_sum - - CostQuery::Filter.reset! - CostQuery::Filter::CustomFieldEntries.reset! - CostQuery::GroupBy.reset! - CostQuery::GroupBy::CustomFieldEntries.reset! - end - end + def check_cache + CostQuery::Cache.check end ## diff --git a/app/models/cost_query/cache.rb b/app/models/cost_query/cache.rb new file mode 100644 index 0000000000..d7136038ca --- /dev/null +++ b/app/models/cost_query/cache.rb @@ -0,0 +1,75 @@ +#-- copyright +# OpenProject Reporting Plugin +# +# Copyright (C) 2010 - 2014 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. +# +# 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. +#++ + +module CostQuery::Cache + class << self + + def check + reset! if reset_required? + end + + def reset! + update_reset_on + + CostQuery::Filter.reset! + CostQuery::Filter::CustomFieldEntries.reset! + CostQuery::GroupBy.reset! + CostQuery::GroupBy::CustomFieldEntries.reset! + end + + protected + + attr_accessor :latest_custom_field_change, + :custom_field_count + + def invalid? + changed_on = fetch_latest_custom_field_change + field_count = fetch_current_custom_field_count + + latest_custom_field_change != changed_on || + custom_field_count != field_count + end + + def update_reset_on + return if caching_disabled? + + self.latest_custom_field_change = fetch_latest_custom_field_change + self.custom_field_count = fetch_current_custom_field_count + end + + def fetch_latest_custom_field_change + WorkPackageCustomField.maximum(:updated_at) + end + + def fetch_current_custom_field_count + WorkPackageCustomField.count + end + + def caching_disabled? + !OpenProject::Configuration.cost_reporting_cache_filter_classes + end + + def reset_required? + caching_disabled? || invalid? + end + end + + # initialize to 0 to avoid forced cache reset on first request + self.custom_field_count = 0 +end diff --git a/app/models/cost_query/custom_field_mixin.rb b/app/models/cost_query/custom_field_mixin.rb index c7fbdd2f61..9f0747e02b 100644 --- a/app/models/cost_query/custom_field_mixin.rb +++ b/app/models/cost_query/custom_field_mixin.rb @@ -27,7 +27,8 @@ module CostQuery::CustomFieldMixin 'text' => mysql? ? 'char' : 'text', 'bool' => mysql? ? 'unsigned' : 'boolean', 'date' => 'date', - 'int' => 'decimal(60,3)', 'float' => 'decimal(60,3)' } + 'int' => 'decimal(60,3)', + 'float' => 'decimal(60,3)' } def self.extended(base) base.inherited_attribute :factory @@ -41,6 +42,8 @@ module CostQuery::CustomFieldMixin def reset! @all = nil + + remove_subclasses end def generate_subclasses @@ -52,6 +55,14 @@ module CostQuery::CustomFieldMixin end end + def remove_subclasses + parent.constants.each do |constant| + if constant.to_s.match /^CustomField\d+/ + parent.send(:remove_const, constant) + end + end + end + def factory? factory == self end diff --git a/lib/open_project/reporting/engine.rb b/lib/open_project/reporting/engine.rb index 7d1a67c0ac..5fd43b719a 100644 --- a/lib/open_project/reporting/engine.rb +++ b/lib/open_project/reporting/engine.rb @@ -98,6 +98,9 @@ module OpenProject::Reporting require_dependency 'cost_query/group_by' end - patches [:CostlogController, :TimelogController, :CustomFieldsController] + patches [:CostlogController, + :TimelogController, + :CustomFieldsController, + :'OpenProject::Configuration'] end end diff --git a/lib/open_project/reporting/patches/open_project/configuration_patch.rb b/lib/open_project/reporting/patches/open_project/configuration_patch.rb new file mode 100644 index 0000000000..60faa865a4 --- /dev/null +++ b/lib/open_project/reporting/patches/open_project/configuration_patch.rb @@ -0,0 +1,31 @@ +#-- copyright +# OpenProject Reporting Plugin +# +# Copyright (C) 2010 - 2014 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. +# +# 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. +#++ + + +require_dependency 'open_project/configuration' + +module OpenProject::Reporting::Patches + module OpenProject::ConfigurationPatch + def self.included(base) + base.class_eval do + @defaults['cost_reporting_cache_filter_classes'] = true + end + end + end +end diff --git a/spec/lib/open_project/configuration_spec.rb b/spec/lib/open_project/configuration_spec.rb new file mode 100644 index 0000000000..228cc82973 --- /dev/null +++ b/spec/lib/open_project/configuration_spec.rb @@ -0,0 +1,47 @@ +#-- copyright +# OpenProject Reporting Plugin +# +# Copyright (C) 2010 - 2014 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. +# +# 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. +#++ + +require 'spec_helper' + +describe 'OpenProject::Configuration' do + context '.cost_reporting_cache_filter_classes' do + before do + # This prevents the values from the actual configuration file to influence + # the test outcome. + # + # TODO: I propose to port this over to the core to always prevent this for specs. + OpenProject::Configuration.load(file: 'bogus') + end + + after do + # resetting for now to avoid braking specs, who by now rely on having the file read. + OpenProject::Configuration.load + end + + it 'is a true by default via the method' do + expect(OpenProject::Configuration.cost_reporting_cache_filter_classes).to be_truthy + end + + + it 'is true by default via the hash' do + expect(OpenProject::Configuration['cost_reporting_cache_filter_classes']).to be_truthy + end + + end +end diff --git a/spec/models/cost_query/cache_spec.rb b/spec/models/cost_query/cache_spec.rb new file mode 100644 index 0000000000..5594fe1183 --- /dev/null +++ b/spec/models/cost_query/cache_spec.rb @@ -0,0 +1,124 @@ +#-- copyright +# OpenProject Reporting Plugin +# +# Copyright (C) 2010 - 2014 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. +# +# 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. +#++ + +require 'spec_helper' +require File.join(File.dirname(__FILE__), '..', '..', 'support', 'configuration_helper') + +describe CostQuery::Cache do + include OpenProject::Reporting::SpecHelper::ConfigurationHelper + + def all_caches + [ CostQuery::GroupBy::CustomFieldEntries, + CostQuery::GroupBy, + CostQuery::Filter::CustomFieldEntries, + CostQuery::Filter ] + end + + def expect_reset_on_caches + all_caches.each do |klass| + expect(klass).to receive(:reset!) + end + end + + def expect_no_reset_on_caches + all_caches.each do |klass| + expect(klass).to_not receive(:reset!) + end + end + + def reset_cache_keys + # resetting internal caching keys to avoid dependencies with other specs + described_class.send(:latest_custom_field_change=, nil) + described_class.send(:custom_field_count=, 0) + end + + def custom_fields_exist + allow(WorkPackageCustomField).to receive(:maximum).and_return(Time.now) + allow(WorkPackageCustomField).to receive(:count).and_return(23) + end + + def no_custom_fields_exist + allow(WorkPackageCustomField).to receive(:maximum).and_return(nil) + allow(WorkPackageCustomField).to receive(:count).and_return(0) + end + + before do + reset_cache_keys + end + + after do + reset_cache_keys + end + + describe '.check' do + + context 'with cache_classes configuration enabled' do + before do + mock_cache_classes_setting_with(true) + end + + it 'resets the caches on filters and group by' do + custom_fields_exist + expect_reset_on_caches + + described_class.check + end + + it 'stores when the last update was made and does not reset again if nothing changed' do + custom_fields_exist + expect_reset_on_caches + + described_class.check + + expect_no_reset_on_caches + + described_class.check + end + + it 'does reset the cache if last CustomField is removed' do + custom_fields_exist + expect_reset_on_caches + + described_class.check + + no_custom_fields_exist + expect_reset_on_caches + + described_class.check + end + end + + context 'with_cache_classes configuration disabled' do + before do + mock_cache_classes_setting_with(false) + end + + it 'resets the cache again even if nothing changed' do + custom_fields_exist + expect_reset_on_caches + + described_class.check + + expect_reset_on_caches + + described_class.check + end + end + end +end diff --git a/spec/models/cost_query/filter_spec.rb b/spec/models/cost_query/filter_spec.rb index c0ed74068a..8de4237353 100644 --- a/spec/models/cost_query/filter_spec.rb +++ b/spec/models/cost_query/filter_spec.rb @@ -18,6 +18,7 @@ #++ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') +require File.join(File.dirname(__FILE__), '..', '..', 'support', 'custom_field_filter') describe CostQuery, type: :model, reporting_query_helper: true do minimal_query @@ -324,17 +325,23 @@ describe CostQuery, type: :model, reporting_query_helper: true do cf end + let(:custom_field2) do + FactoryGirl.build(:work_package_custom_field, name: 'Database', + field_format: "list", + possible_values: ['value']) + end + after(:all) do clear_cache end def clear_cache - CostReportsController.new.check_cache(true) + CostQuery::Cache.reset! CostQuery::Filter::CustomFieldEntries.all end - def delete_work_package_custom_field(name) - WorkPackageCustomField.find_by_name(name).destroy + def delete_work_package_custom_field(cf) + cf.destroy clear_cache end @@ -345,44 +352,40 @@ describe CostQuery, type: :model, reporting_query_helper: true do clear_cache end - def class_name_for(name) - "CostQuery::Filter::CustomField#{WorkPackageCustomField.find_by_name(name).id}" - end + include OpenProject::Reporting::SpecHelper::CustomFieldFilterHelper it "should create classes for custom fields that get added after starting the server" do custom_field - expect { class_name_for('My custom field').constantize }.not_to raise_error + expect { filter_class_name_string(custom_field).constantize }.not_to raise_error end it "should remove the custom field classes after it is deleted" do custom_field - class_name = class_name_for('My custom field') - delete_work_package_custom_field("My custom field") - expect(CostQuery::Filter.all).not_to include class_name.constantize + class_name = filter_class_name_string(custom_field) + delete_work_package_custom_field(custom_field) + expect { filter_class_name_string(custom_field).constantize }.to raise_error NameError end it "should provide the correct available values" do - FactoryGirl.create(:work_package_custom_field, name: 'Database', - field_format: "list", - possible_values: ['value']) + custom_field2.save + clear_cache - ao = class_name_for('Database').constantize.available_operators.map(&:name) + ao = filter_class_name_string(custom_field2).constantize.available_operators.map(&:name) CostQuery::Operator.null_operators.each do |o| expect(ao).to include o.name end end it "should update the available values on change" do - FactoryGirl.create(:work_package_custom_field, name: 'Database', - field_format: "list", - possible_values: ['value']) + custom_field2.save + update_work_package_custom_field("Database", field_format: "string") - ao = class_name_for('Database').constantize.available_operators.map(&:name) + ao = filter_class_name_string(custom_field2).constantize.available_operators.map(&:name) CostQuery::Operator.string_operators.each do |o| expect(ao).to include o.name end update_work_package_custom_field("Database", field_format: "int") - ao = class_name_for('Database').constantize.available_operators.map(&:name) + ao = filter_class_name_string(custom_field2).constantize.available_operators.map(&:name) CostQuery::Operator.integer_operators.each do |o| expect(ao).to include o.name end @@ -391,13 +394,13 @@ describe CostQuery, type: :model, reporting_query_helper: true do it "includes custom fields classes in CustomFieldEntries.all" do custom_field expect(CostQuery::Filter::CustomFieldEntries.all). - to include(class_name_for('My custom field').constantize) + to include(filter_class_name_string(custom_field).constantize) end it "includes custom fields classes in Filter.all" do custom_field expect(CostQuery::Filter.all). - to include(class_name_for('My custom field').constantize) + to include(filter_class_name_string(custom_field).constantize) end def create_searchable_fields_and_values diff --git a/spec/models/cost_query/group_by_spec.rb b/spec/models/cost_query/group_by_spec.rb index 2e4a8de424..c090f91554 100644 --- a/spec/models/cost_query/group_by_spec.rb +++ b/spec/models/cost_query/group_by_spec.rb @@ -18,6 +18,7 @@ #++ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') +require File.join(File.dirname(__FILE__), '..', '..', 'support', 'custom_field_filter') describe CostQuery, type: :model, reporting_query_helper: true do let!(:type) { FactoryGirl.create(:type) } @@ -216,74 +217,75 @@ describe CostQuery, type: :model, reporting_query_helper: true do describe CostQuery::GroupBy::CustomFieldEntries do let!(:project){ FactoryGirl.create(:project_with_types) } + let!(:custom_field) do + FactoryGirl.create(:work_package_custom_field) + end + + let(:custom_field2) do + FactoryGirl.build(:work_package_custom_field) + end before do - create_work_package_custom_field("Searchable Field") + check_cache CostQuery::GroupBy.all.merge CostQuery::GroupBy::CustomFieldEntries.all end def check_cache - CostReportsController.new.check_cache + CostQuery::Cache.reset! CostQuery::GroupBy::CustomFieldEntries.all end - def create_work_package_custom_field(name) - WorkPackageCustomField.create(name: name, - min_length: 1, - regexp: "", - is_for_all: true, - max_length: 100, - possible_values: "", - is_required: false, - field_format: "string", - searchable: true, - default_value: "Default string", - editable: true) + def delete_work_package_custom_field(custom_field) + custom_field.destroy check_cache end - def delete_work_package_custom_field(name) - WorkPackageCustomField.find_by_name(name).destroy - check_cache - end - - def class_name_for(name) - "CostQuery::GroupBy::CustomField#{WorkPackageCustomField.find_by_name(name).id}" - end + include OpenProject::Reporting::SpecHelper::CustomFieldFilterHelper it "should create classes for custom fields" do # Would raise a name error - expect { class_name_for('Searchable Field').constantize }.to_not raise_error + expect { group_by_class_name_string(custom_field).constantize }.to_not raise_error end it "should create new classes for custom fields that get added after starting the server" do - create_work_package_custom_field("AFreshCustomField") + custom_field2.save! + + check_cache + # Would raise a name error - expect { class_name_for('AFreshCustomField').constantize }.to_not raise_error - WorkPackageCustomField.find_by_name("AFreshCustomField").destroy + expect { group_by_class_name_string(custom_field2).constantize }.to_not raise_error + + custom_field2.destroy end it "should remove the custom field classes after it is deleted" do - create_work_package_custom_field("AFreshCustomField") - name = class_name_for('AFreshCustomField') - delete_work_package_custom_field("AFreshCustomField") - expect(CostQuery::GroupBy.all).not_to include name.constantize + custom_field2.save! + + check_cache + + custom_field2.destroy + + check_cache + + expect { group_by_class_name_string(custom_field2).constantize }.to raise_error NameError end it "includes custom fields classes in CustomFieldEntries.all" do expect(CostQuery::GroupBy::CustomFieldEntries.all). - to include(class_name_for('Searchable Field').constantize) + to include(group_by_class_name_string(custom_field).constantize) end it "includes custom fields classes in GroupBy.all" do expect(CostQuery::GroupBy.all). - to include(class_name_for('Searchable Field').constantize) + to include(group_by_class_name_string(custom_field).constantize) end it "is usable as filter" do - create_work_package_custom_field("Database") - id = WorkPackageCustomField.find_by_name('Database').id - @query.group_by "custom_field_#{id}".to_sym + custom_field2.save! + + check_cache + + @query.group_by "custom_field_#{custom_field2.id}".to_sym footprint = @query.result.each_direct_result.map { |c| [c.count, c.units.to_i] }.sort expect(footprint).to eq([[8, 8]]) end diff --git a/spec/requests/custom_field_cache_spec.rb b/spec/requests/custom_field_cache_spec.rb new file mode 100644 index 0000000000..4e9119c185 --- /dev/null +++ b/spec/requests/custom_field_cache_spec.rb @@ -0,0 +1,129 @@ +#-- copyright +# OpenProject Reporting Plugin +# +# Copyright (C) 2010 - 2014 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. +# +# 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. +#++ + +require 'spec_helper' +require File.join(File.dirname(__FILE__), '..', 'support', 'custom_field_filter') +require File.join(File.dirname(__FILE__), '..', 'support', 'configuration_helper') + +describe 'Custom field filter and group by caching', type: :request do + include OpenProject::Reporting::SpecHelper::CustomFieldFilterHelper + include OpenProject::Reporting::SpecHelper::ConfigurationHelper + + let(:project) { FactoryGirl.create(:valid_project) } + let(:user) { FactoryGirl.create(:admin) } + let(:custom_field) { FactoryGirl.build(:work_package_custom_field) } + let(:custom_field2) { FactoryGirl.build(:work_package_custom_field) } + + before do + allow(User).to receive(:current).and_return(user) + + custom_field.save! + end + + def expect_group_by_all_to_include(custom_field) + expect(CostQuery::GroupBy.all).to include(group_by_class_name_string(custom_field).constantize) + end + + def expect_filter_all_to_include(custom_field) + expect(CostQuery::Filter.all).to include(filter_class_name_string(custom_field).constantize) + end + + def expect_group_by_all_to_not_exist(custom_field) + # can not check for whether the element is included in CostQuery::GroupBy.all if it does not exist + expect { group_by_class_name_string(custom_field).constantize }.to raise_error NameError + end + + def expect_filter_all_to_not_exist(custom_field) + # can not check for whether the element is included in CostQuery::Filter.all if it does not exist + expect { filter_class_name_string(custom_field).constantize }.to raise_error NameError + end + + def visit_cost_reports_index + get "projects/#{project.id}/cost_reports" + end + + it 'removes the filter/group_by if the custom field is removed' do + custom_field2.save! + + visit_cost_reports_index + + expect_group_by_all_to_include(custom_field) + expect_group_by_all_to_include(custom_field2) + + expect_filter_all_to_include(custom_field) + expect_filter_all_to_include(custom_field2) + + custom_field2.destroy + + visit_cost_reports_index + + expect_group_by_all_to_include(custom_field) + expect_group_by_all_to_not_exist(custom_field2) + + expect_filter_all_to_include(custom_field) + expect_filter_all_to_not_exist(custom_field2) + end + + it 'removes the filter/group_by if the last custom field is removed' do + visit_cost_reports_index + + expect_group_by_all_to_include(custom_field) + expect_filter_all_to_include(custom_field) + + custom_field.destroy + + visit_cost_reports_index + + expect_group_by_all_to_not_exist(custom_field) + expect_filter_all_to_not_exist(custom_field) + end + + it 'allows for changing the db table between requests if no caching is done' do + old_table_name = WorkPackageCustomField.table_name + new_table_name = 'custom_fields_clone' + new_id = custom_field.id + 1 + + begin + mock_cache_classes_setting_with(false) + + visit_cost_reports_index + + expect_group_by_all_to_include(custom_field) + expect_filter_all_to_include(custom_field) + + ActiveRecord::Base.connection.execute("CREATE TABLE #{new_table_name} AS SELECT * from custom_fields;") + ActiveRecord::Base.connection.execute("UPDATE #{new_table_name} SET id = #{new_id} WHERE id = #{custom_field.id};") + CustomField::Translation.where(custom_field_id: custom_field.id).update_all(custom_field_id: new_id) + + WorkPackageCustomField.table_name = new_table_name + + visit_cost_reports_index + + expect_group_by_all_to_not_exist(custom_field) + expect_filter_all_to_not_exist(custom_field) + + expect_group_by_all_to_include(new_id) + expect_filter_all_to_include(new_id) + + ensure + WorkPackageCustomField.table_name = old_table_name + ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{new_table_name}") + end + end +end diff --git a/spec/support/configuration_helper.rb b/spec/support/configuration_helper.rb new file mode 100644 index 0000000000..0714ebbe68 --- /dev/null +++ b/spec/support/configuration_helper.rb @@ -0,0 +1,31 @@ +#-- copyright +# OpenProject Reporting Plugin +# +# Copyright (C) 2010 - 2014 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. +# +# 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. +#++ + +module OpenProject::Reporting::SpecHelper + module ConfigurationHelper + def mock_cache_classes_setting_with(value) + allow(OpenProject::Configuration).to receive(:[]).and_call_original + allow(OpenProject::Configuration).to receive(:[]) + .with('cost_reporting_cache_filter_classes') + .and_return(value) + allow(OpenProject::Configuration).to receive(:cost_reporting_cache_filter_classes) + .and_return(value) + end + end +end diff --git a/spec/support/custom_field_filter.rb b/spec/support/custom_field_filter.rb new file mode 100644 index 0000000000..ce7aabb3c7 --- /dev/null +++ b/spec/support/custom_field_filter.rb @@ -0,0 +1,34 @@ +#-- copyright +# OpenProject Reporting Plugin +# +# Copyright (C) 2010 - 2014 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. +# +# 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. +#++ + +module OpenProject::Reporting::SpecHelper + module CustomFieldFilterHelper + def group_by_class_name_string(custom_field) + id = custom_field.is_a?(ActiveRecord::Base) ? custom_field.id : custom_field + + "CostQuery::GroupBy::CustomField#{id}" + end + + def filter_class_name_string(custom_field) + id = custom_field.is_a?(ActiveRecord::Base) ? custom_field.id : custom_field + + "CostQuery::Filter::CustomField#{id}" + end + end +end