parent
8573b63b9e
commit
403bf905e9
@ -0,0 +1,213 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2017 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 doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Queries::Filters::Shared::CustomFieldFilter |
||||
def self.included(base) |
||||
base.include(InstanceMethods) |
||||
base.extend(ClassMethods) |
||||
|
||||
base.class_eval do |
||||
attr_accessor :custom_field |
||||
validate :custom_field_valid |
||||
|
||||
class_attribute :custom_field_class |
||||
end |
||||
end |
||||
|
||||
module InstanceMethods |
||||
def allowed_values |
||||
case custom_field.field_format |
||||
when 'bool' |
||||
[[I18n.t(:general_text_yes), CustomValue::BoolStrategy::DB_VALUE_TRUE], |
||||
[I18n.t(:general_text_no), CustomValue::BoolStrategy::DB_VALUE_FALSE]] |
||||
when 'user', 'version', 'list' |
||||
custom_field.possible_values_options(project) |
||||
end |
||||
end |
||||
|
||||
def type |
||||
return nil unless custom_field |
||||
|
||||
case custom_field.field_format |
||||
when 'int', 'float' |
||||
:integer |
||||
when 'text' |
||||
:text |
||||
when 'list', 'user', 'version' |
||||
:list_optional |
||||
when 'date' |
||||
:date |
||||
when 'bool' |
||||
:list |
||||
else |
||||
:string |
||||
end |
||||
end |
||||
|
||||
def order |
||||
20 |
||||
end |
||||
|
||||
def name |
||||
# FIXME this can be nil |
||||
:"cf_#{custom_field.id}" |
||||
end |
||||
|
||||
def human_name |
||||
custom_field ? custom_field.name : '' |
||||
end |
||||
|
||||
def name=(field_name) |
||||
cf_id = self.class.key.match(field_name)[1] |
||||
|
||||
self.custom_field = self.class.custom_field_class.find_by_id(cf_id.to_i) |
||||
|
||||
super |
||||
end |
||||
|
||||
def ar_object_filter? |
||||
%w{user version list}.include? custom_field.field_format |
||||
end |
||||
|
||||
def available? |
||||
custom_field.present? |
||||
end |
||||
|
||||
def value_objects |
||||
case custom_field.field_format |
||||
when 'user' |
||||
User.where(id: values) |
||||
when 'version' |
||||
Version.where(id: values) |
||||
when 'list' |
||||
custom_field.custom_options.where(id: values) |
||||
else |
||||
super |
||||
end |
||||
end |
||||
|
||||
def where |
||||
model_db_table = model.table_name |
||||
cv_db_table = CustomValue.table_name |
||||
|
||||
<<-SQL |
||||
#{model_db_table}.id IN |
||||
(SELECT #{model_db_table}.id |
||||
FROM #{model_db_table} |
||||
#{where_subselect_joins} |
||||
WHERE #{operator_strategy.sql_for_field(values, cv_db_table, 'value')}) |
||||
SQL |
||||
end |
||||
|
||||
def where_subselect_joins |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
def error_messages |
||||
messages = errors |
||||
.full_messages |
||||
.join(" #{I18n.t('support.array.sentence_connector')} ") |
||||
|
||||
human_name + I18n.t(default: ' %<message>s', message: messages) |
||||
end |
||||
|
||||
private |
||||
|
||||
def type_strategy |
||||
@type_strategy ||= (strategies[type] || strategies[:inexistent]).new(self) |
||||
end |
||||
|
||||
def custom_field_valid |
||||
if custom_field.nil? |
||||
errors.add(:base, I18n.t('activerecord.errors.models.query.filters.custom_fields.inexistent')) |
||||
elsif invalid_custom_field_for_context? |
||||
errors.add(:base, I18n.t('activerecord.errors.models.query.filters.custom_fields.invalid')) |
||||
end |
||||
end |
||||
|
||||
def validate_inclusion_of_operator |
||||
super if custom_field |
||||
end |
||||
|
||||
def invalid_custom_field_for_context? |
||||
project && invalid_custom_field_for_project? || |
||||
!project && invalid_custom_field_globally? |
||||
end |
||||
|
||||
def invalid_custom_field_globally? |
||||
!self.class.custom_fields(project) |
||||
.exists?(custom_field.id) |
||||
end |
||||
|
||||
def invalid_custom_field_for_project? |
||||
!self.class.custom_fields(project) |
||||
.map(&:id).include? custom_field.id |
||||
end |
||||
|
||||
def strategies |
||||
strategies = Queries::Filters::STRATEGIES.dup |
||||
strategies[:list_optional] = Queries::Filters::Strategies::CfListOptional |
||||
strategies[:integer] = Queries::Filters::Strategies::CfInteger |
||||
# knowing that only bool have list type |
||||
strategies[:list] = Queries::Filters::Strategies::BooleanList |
||||
|
||||
strategies |
||||
end |
||||
end |
||||
|
||||
module ClassMethods |
||||
def key |
||||
/cf_(\d+)/ |
||||
end |
||||
|
||||
def all_for(context = nil) |
||||
project = context ? context.project : nil |
||||
|
||||
custom_fields(project).map do |cf| |
||||
filter = new |
||||
filter.custom_field = cf |
||||
filter.context = context |
||||
filter |
||||
end |
||||
end |
||||
|
||||
def custom_fields(project) |
||||
if project |
||||
project.all_work_package_custom_fields |
||||
else |
||||
custom_field_class |
||||
.filter |
||||
.for_all |
||||
.where |
||||
.not(field_format: ['user', 'version']) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,463 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2017 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 doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe Queries::Projects::Filters::CustomFieldFilter, type: :model do |
||||
let(:query) { Queries::Projects::ProjectQuery.new } |
||||
let(:instance) do |
||||
filter = described_class.new |
||||
filter.name = "cf_#{custom_field.id}" |
||||
filter.operator = '=' |
||||
filter.context = query |
||||
filter |
||||
end |
||||
let(:instance_key) { nil } |
||||
let(:name) { field.name } |
||||
|
||||
let(:list_project_custom_field) { FactoryGirl.create(:list_project_custom_field) } |
||||
let(:bool_project_custom_field) { FactoryGirl.build_stubbed(:bool_project_custom_field) } |
||||
let(:int_project_custom_field) { FactoryGirl.build_stubbed(:int_project_custom_field) } |
||||
let(:float_project_custom_field) { FactoryGirl.build_stubbed(:float_project_custom_field) } |
||||
let(:text_project_custom_field) { FactoryGirl.build_stubbed(:text_project_custom_field) } |
||||
let(:user_project_custom_field) { FactoryGirl.build_stubbed(:user_project_custom_field) } |
||||
let(:version_project_custom_field) { FactoryGirl.build_stubbed(:version_project_custom_field) } |
||||
let(:date_project_custom_field) { FactoryGirl.build_stubbed(:date_project_custom_field) } |
||||
let(:string_project_custom_field) { FactoryGirl.build_stubbed(:string_project_custom_field) } |
||||
let(:custom_field) { list_project_custom_field } |
||||
|
||||
let(:all_custom_fields) do |
||||
[list_project_custom_field, |
||||
bool_project_custom_field, |
||||
int_project_custom_field, |
||||
float_project_custom_field, |
||||
text_project_custom_field, |
||||
user_project_custom_field, |
||||
version_project_custom_field, |
||||
date_project_custom_field, |
||||
string_project_custom_field] |
||||
end |
||||
|
||||
before do |
||||
all_custom_fields.each do |cf| |
||||
allow(ProjectCustomField) |
||||
.to receive(:find_by_id) |
||||
.with(cf.id) |
||||
.and_return(cf) |
||||
end |
||||
end |
||||
|
||||
describe '.valid?' do |
||||
let(:custom_field) { string_project_custom_field } |
||||
before do |
||||
instance.values = ['bogus'] |
||||
end |
||||
|
||||
before do |
||||
allow(ProjectCustomField) |
||||
.to receive_message_chain(:all, :exists?) |
||||
.and_return(true) |
||||
end |
||||
|
||||
it 'is invalid without a custom field' do |
||||
allow(ProjectCustomField) |
||||
.to receive(:find_by_id) |
||||
.with(100) |
||||
.and_return(nil) |
||||
|
||||
instance.name = 'cf_100' |
||||
|
||||
expect(instance).to_not be_valid |
||||
end |
||||
|
||||
shared_examples_for 'custom field type dependent validity' do |
||||
context 'with a string custom field' do |
||||
it 'is valid' do |
||||
expect(instance).to be_valid |
||||
end |
||||
end |
||||
|
||||
context 'with a list custom field' do |
||||
let(:custom_field) { list_project_custom_field } |
||||
|
||||
before do |
||||
instance.values = [list_project_custom_field.possible_values.first.id] |
||||
end |
||||
|
||||
it 'is valid' do |
||||
expect(instance).to be_valid |
||||
end |
||||
|
||||
it "is invalid if the value is not one of the custom field's possible values" do |
||||
instance.values = ['bogus'] |
||||
|
||||
expect(instance).to_not be_valid |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'without a project' do |
||||
it_behaves_like 'custom field type dependent validity' |
||||
end |
||||
end |
||||
|
||||
describe '.key' do |
||||
it 'is a regular expression' do |
||||
expect(described_class.key).to eql(/cf_(\d+)/) |
||||
end |
||||
end |
||||
|
||||
describe '#name' do |
||||
it 'is the custom fields id prefixed with cf_' do |
||||
all_custom_fields.each do |cf| |
||||
filter = described_class.new |
||||
filter.name = "cf_#{cf.id}" |
||||
expect(filter.name).to eql(:"cf_#{cf.id}") |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#order' do |
||||
it 'is 20' do |
||||
all_custom_fields.each do |cf| |
||||
filter = described_class.new |
||||
filter.name = "cf_#{cf.id}" |
||||
expect(filter.order).to eql(20) |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#type' do |
||||
it 'is integer for an integer' do |
||||
instance.name = "cf_#{int_project_custom_field.id}" |
||||
expect(instance.type) |
||||
.to eql(:integer) |
||||
end |
||||
|
||||
it 'is integer for a float' do |
||||
instance.name = "cf_#{float_project_custom_field.id}" |
||||
expect(instance.type) |
||||
.to eql(:float) |
||||
end |
||||
|
||||
it 'is text for a text' do |
||||
instance.name = "cf_#{text_project_custom_field.id}" |
||||
expect(instance.type) |
||||
.to eql(:text) |
||||
end |
||||
|
||||
it 'is list_optional for a list' do |
||||
instance.name = "cf_#{list_project_custom_field.id}" |
||||
expect(instance.type) |
||||
.to eql(:list_optional) |
||||
end |
||||
|
||||
it 'is list_optional for a user' do |
||||
instance.name = "cf_#{user_project_custom_field.id}" |
||||
expect(instance.type) |
||||
.to eql(:list_optional) |
||||
end |
||||
|
||||
it 'is list_optional for a version' do |
||||
instance.name = "cf_#{version_project_custom_field.id}" |
||||
expect(instance.type) |
||||
.to eql(:list_optional) |
||||
end |
||||
|
||||
it 'is date for a date' do |
||||
instance.name = "cf_#{date_project_custom_field.id}" |
||||
expect(instance.type) |
||||
.to eql(:date) |
||||
end |
||||
|
||||
it 'is list for a bool' do |
||||
instance.name = "cf_#{bool_project_custom_field.id}" |
||||
expect(instance.type) |
||||
.to eql(:list) |
||||
end |
||||
|
||||
it 'is string for a string' do |
||||
instance.name = "cf_#{string_project_custom_field.id}" |
||||
expect(instance.type) |
||||
.to eql(:string) |
||||
end |
||||
end |
||||
|
||||
describe '#human_name' do |
||||
it 'is the field name' do |
||||
expect(instance.human_name) |
||||
.to eql(list_project_custom_field.name) |
||||
end |
||||
end |
||||
|
||||
describe '#allowed_values' do |
||||
it 'is nil for an integer' do |
||||
instance.name = "cf_#{int_project_custom_field.id}" |
||||
expect(instance.allowed_values) |
||||
.to be_nil |
||||
end |
||||
|
||||
it 'is integer for a float' do |
||||
instance.name = "cf_#{float_project_custom_field.id}" |
||||
expect(instance.allowed_values) |
||||
.to be_nil |
||||
end |
||||
|
||||
it 'is text for a text' do |
||||
instance.name = "cf_#{text_project_custom_field.id}" |
||||
expect(instance.allowed_values) |
||||
.to be_nil |
||||
end |
||||
|
||||
it 'is list_optional for a list' do |
||||
instance.name = "cf_#{list_project_custom_field.id}" |
||||
expect(instance.allowed_values) |
||||
.to match_array(list_project_custom_field.custom_options.map { |co| [co.value, co.id.to_s] }) |
||||
end |
||||
|
||||
it 'is list_optional for a user' do |
||||
bogus_return_value = ['user1', 'user2'] |
||||
allow(user_project_custom_field) |
||||
.to receive(:possible_values_options) |
||||
.and_return(bogus_return_value) |
||||
|
||||
instance.name = "cf_#{user_project_custom_field.id}" |
||||
expect(instance.allowed_values) |
||||
.to match_array bogus_return_value |
||||
end |
||||
|
||||
it 'is list_optional for a version' do |
||||
bogus_return_value = ['version1', 'version2'] |
||||
allow(version_project_custom_field) |
||||
.to receive(:possible_values_options) |
||||
.and_return(bogus_return_value) |
||||
|
||||
instance.name = "cf_#{version_project_custom_field.id}" |
||||
expect(instance.allowed_values) |
||||
.to match_array bogus_return_value |
||||
end |
||||
|
||||
it 'is nil for a date' do |
||||
instance.name = "cf_#{date_project_custom_field.id}" |
||||
expect(instance.allowed_values) |
||||
.to be_nil |
||||
end |
||||
|
||||
it 'is list for a bool' do |
||||
instance.name = "cf_#{bool_project_custom_field.id}" |
||||
expect(instance.allowed_values) |
||||
.to match_array [[I18n.t(:general_text_yes), CustomValue::BoolStrategy::DB_VALUE_TRUE], |
||||
[I18n.t(:general_text_no), CustomValue::BoolStrategy::DB_VALUE_FALSE]] |
||||
end |
||||
|
||||
it 'is nil for a string' do |
||||
instance.name = "cf_#{string_project_custom_field.id}" |
||||
expect(instance.allowed_values) |
||||
.to be_nil |
||||
end |
||||
end |
||||
|
||||
describe '#available?' do |
||||
context 'for an existing custom field' do |
||||
it 'is true' do |
||||
instance.custom_field = list_project_custom_field |
||||
expect(instance).to be_available |
||||
end |
||||
end |
||||
|
||||
context 'for a non existing custom field (deleted)' do |
||||
it 'is false' do |
||||
instance.custom_field = nil |
||||
expect(instance).not_to be_available |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '.all_for' do |
||||
before do |
||||
allow(ProjectCustomField) |
||||
.to receive_message_chain(:all) |
||||
.and_return([list_project_custom_field, |
||||
bool_project_custom_field, |
||||
int_project_custom_field, |
||||
float_project_custom_field, |
||||
text_project_custom_field, |
||||
date_project_custom_field, |
||||
string_project_custom_field]) |
||||
end |
||||
|
||||
it 'returns a list with a filter for every custom field' do |
||||
filters = described_class.all_for |
||||
|
||||
[list_project_custom_field, |
||||
bool_project_custom_field, |
||||
int_project_custom_field, |
||||
float_project_custom_field, |
||||
text_project_custom_field, |
||||
date_project_custom_field, |
||||
string_project_custom_field].each do |cf| |
||||
expect(filters.detect { |filter| filter.name == :"cf_#{cf.id}" }).to_not be_nil |
||||
end |
||||
|
||||
expect(filters.detect { |filter| filter.name == :"cf_#{version_project_custom_field.id}" }) |
||||
.to be_nil |
||||
expect(filters.detect { |filter| filter.name == :"cf_#{user_project_custom_field.id}" }) |
||||
.to be_nil |
||||
end |
||||
end |
||||
|
||||
context 'list cf' do |
||||
describe '#ar_object_filter? / #value_objects' do |
||||
let(:custom_field) { list_project_custom_field } |
||||
|
||||
describe '#ar_object_filter?' do |
||||
it 'is true' do |
||||
expect(instance) |
||||
.to be_ar_object_filter |
||||
end |
||||
end |
||||
|
||||
describe '#value_objects' do |
||||
before do |
||||
instance.values = [custom_field.custom_options.last.id, |
||||
custom_field.custom_options.first.id] |
||||
end |
||||
|
||||
it 'returns an array with custom classes' do |
||||
expect(instance.value_objects) |
||||
.to match_array([custom_field.custom_options.last, |
||||
custom_field.custom_options.first]) |
||||
end |
||||
|
||||
it 'ignores invalid values' do |
||||
instance.values = ['invalid', |
||||
custom_field.custom_options.last.id] |
||||
|
||||
expect(instance.value_objects) |
||||
.to match_array([custom_field.custom_options.last]) |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'bool cf' do |
||||
let(:custom_field) { bool_project_custom_field } |
||||
|
||||
it_behaves_like 'non ar filter' |
||||
end |
||||
|
||||
context 'int cf' do |
||||
let(:custom_field) { int_project_custom_field } |
||||
|
||||
it_behaves_like 'non ar filter' |
||||
end |
||||
|
||||
context 'float cf' do |
||||
let(:custom_field) { float_project_custom_field } |
||||
|
||||
it_behaves_like 'non ar filter' |
||||
end |
||||
|
||||
context 'text cf' do |
||||
let(:custom_field) { text_project_custom_field } |
||||
|
||||
it_behaves_like 'non ar filter' |
||||
end |
||||
|
||||
context 'user cf' do |
||||
let(:custom_field) { user_project_custom_field } |
||||
|
||||
describe '#ar_object_filter?' do |
||||
it 'is true' do |
||||
expect(instance) |
||||
.to be_ar_object_filter |
||||
end |
||||
end |
||||
|
||||
describe '#value_objects' do |
||||
let(:user1) { FactoryGirl.build_stubbed(:user) } |
||||
let(:user2) { FactoryGirl.build_stubbed(:user) } |
||||
|
||||
before do |
||||
allow(User) |
||||
.to receive(:where) |
||||
.with(id: [user1.id.to_s, user2.id.to_s]) |
||||
.and_return([user1, user2]) |
||||
|
||||
instance.values = [user1.id.to_s, user2.id.to_s] |
||||
end |
||||
|
||||
it 'returns an array with users' do |
||||
expect(instance.value_objects) |
||||
.to match_array([user1, user2]) |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'version cf' do |
||||
let(:custom_field) { version_project_custom_field } |
||||
|
||||
describe '#ar_object_filter?' do |
||||
it 'is true' do |
||||
expect(instance) |
||||
.to be_ar_object_filter |
||||
end |
||||
end |
||||
|
||||
describe '#value_objects' do |
||||
let(:version1) { FactoryGirl.build_stubbed(:version) } |
||||
let(:version2) { FactoryGirl.build_stubbed(:version) } |
||||
|
||||
before do |
||||
allow(Version) |
||||
.to receive(:where) |
||||
.with(id: [version1.id.to_s, version2.id.to_s]) |
||||
.and_return([version1, version2]) |
||||
|
||||
instance.values = [version1.id.to_s, version2.id.to_s] |
||||
end |
||||
|
||||
it 'returns an array with users' do |
||||
expect(instance.value_objects) |
||||
.to match_array([version1, version2]) |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'date cf' do |
||||
let(:custom_field) { date_project_custom_field } |
||||
|
||||
it_behaves_like 'non ar filter' |
||||
end |
||||
|
||||
context 'string cf' do |
||||
let(:custom_field) { string_project_custom_field } |
||||
|
||||
it_behaves_like 'non ar filter' |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue