extract custom field filter into shared mixin

pull/5981/head
Jens Ulferts 7 years ago
parent 8573b63b9e
commit 403bf905e9
No known key found for this signature in database
GPG Key ID: 3CAA4B1182CF5308
  1. 213
      app/models/queries/filters/shared/custom_field_filter.rb
  2. 175
      app/models/queries/projects/filters/custom_field_filter.rb
  3. 174
      app/models/queries/work_packages/filter/custom_field_filter.rb
  4. 87
      spec/factories/custom_field_factory.rb
  5. 2
      spec/features/projects/projects_custom_fields_spec.rb
  6. 107
      spec/features/projects/projects_index_spec.rb
  7. 463
      spec/models/queries/projects/filters/custom_field_filter_spec.rb

@ -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

@ -30,184 +30,51 @@
class Queries::Projects::Filters::CustomFieldFilter <
Queries::Projects::Filters::ProjectFilter
attr_accessor :custom_field
validate :custom_field_valid
include Queries::Filters::Shared::CustomFieldFilter
self.custom_field_class = ProjectCustomField
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 'version', 'list' # TODO: Why list?
# custom_field.possible_values_options(project)
custom_field.possible_values_options
when 'user'
if custom_field.field_format == 'user'
custom_field.possible_values_options(:of_all_projects)
end
end
def type
return nil unless custom_field
case custom_field.field_format
when 'int'
:integer
when 'float'
:float
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 = ProjectCustomField.find_by_id(cf_id.to_i)
super
end
def self.key
/cf_(\d+)/
end
def self.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 self.custom_fields(project)
if project
project.all_work_package_custom_fields
else
ProjectCustomField.all
# .filter
# .for_all
# .where.not(field_format: ['user', 'version'])
end
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)
def type
if custom_field && custom_field.field_format == 'float'
:float
else
super
end
end
def where
db_table = CustomValue.table_name
project_db_table = Project.table_name
<<-SQL
#{project_db_table}.id IN
(SELECT #{project_db_table}.id
FROM #{project_db_table}
#{where_joins(db_table, project_db_table)}
WHERE #{operator_strategy.sql_for_field(values, db_table, 'value')})
SQL
end
def error_messages
messages = errors
.full_messages
.join(" #{I18n.t('support.array.sentence_connector')} ")
human_name + I18n.t(default: ' %{message}', message: messages)
def self.custom_fields(_context)
custom_field_class
.all
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?
try(:project) && invalid_custom_field_for_project? ||
try(: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
strategies = super
strategies[:float] = Queries::Filters::Strategies::CfFloat
# knowing that only bool have list type
strategies[:list] = Queries::Filters::Strategies::BooleanList
strategies
end
def where_joins(db_table, project_db_table)
joins = "LEFT OUTER JOIN #{db_table}
ON #{db_table}.customized_type='Project'
AND #{db_table}.customized_id=#{project_db_table}.id
AND #{db_table}.custom_field_id=#{custom_field.id}"
def where_subselect_joins
cv_db_table = CustomValue.table_name
project_db_table = model.table_name
joins
"LEFT OUTER JOIN #{cv_db_table}
ON #{cv_db_table}.customized_type='#{model.name}'
AND #{cv_db_table}.customized_id=#{project_db_table}.id
AND #{cv_db_table}.custom_field_id=#{custom_field.id}"
end
# compatibility only
def project; end
end

@ -30,179 +30,23 @@
class Queries::WorkPackages::Filter::CustomFieldFilter <
Queries::WorkPackages::Filter::WorkPackageFilter
attr_accessor :custom_field
validate :custom_field_valid
include Queries::Filters::Shared::CustomFieldFilter
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 = WorkPackageCustomField.find_by_id(cf_id.to_i)
super
end
def self.key
/cf_(\d+)/
end
def self.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 self.custom_fields(project)
if project
project.all_work_package_custom_fields
else
WorkPackageCustomField
.filter
.for_all
.where.not(field_format: ['user', 'version'])
end
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
db_table = CustomValue.table_name
work_package_db_table = WorkPackage.table_name
<<-SQL
#{work_package_db_table}.id IN
(SELECT #{work_package_db_table}.id
FROM #{work_package_db_table}
#{where_joins(db_table, work_package_db_table)}
WHERE #{operator_strategy.sql_for_field(values, db_table, 'value')})
SQL
end
def error_messages
messages = errors
.full_messages
.join(" #{I18n.t('support.array.sentence_connector')} ")
human_name + I18n.t(default: ' %{message}', message: messages)
end
self.custom_field_class = WorkPackageCustomField
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
def where_joins(db_table, work_package_db_table)
def where_subselect_joins
cf_types_db_table = 'custom_fields_types'
cf_projects_db_table = 'custom_fields_projects'
cv_db_table = CustomValue.table_name
work_package_db_table = model.table_name
joins = "LEFT OUTER JOIN #{db_table}
ON #{db_table}.customized_type='WorkPackage'
AND #{db_table}.customized_id=#{work_package_db_table}.id
AND #{db_table}.custom_field_id=#{custom_field.id}
joins = "LEFT OUTER JOIN #{cv_db_table}
ON #{cv_db_table}.customized_type='WorkPackage'
AND #{cv_db_table}.customized_id=#{work_package_db_table}.id
AND #{cv_db_table}.custom_field_id=#{custom_field.id}
JOIN #{cf_types_db_table}
ON #{cf_types_db_table}.type_id = #{work_package_db_table}.type_id
AND #{cf_types_db_table}.custom_field_id = #{custom_field.id}"

@ -40,27 +40,57 @@ FactoryGirl.define do
field_format 'bool'
factory :project_custom_field, class: ProjectCustomField do
sequence(:name) do |n| "Project Custom Field #{n}" end
type 'ProjectCustomField'
sequence(:name) { |n| "Project custom field #{n}" }
factory :boolean_project_custom_field do
name 'BooleanProjectCustomField'
factory :list_project_custom_field do
sequence(:name) { |n| "List project custom field #{n}" }
field_format 'list'
possible_values ['A', 'B', 'C', 'D', 'E', 'F', 'G']
end
factory :version_project_custom_field do
sequence(:name) { |n| "Version project custom field #{n}" }
field_format 'version'
end
factory :bool_project_custom_field do
sequence(:name) { |n| "Bool project custom field #{n}" }
field_format 'bool'
end
factory :integer_project_custom_field do
name 'IntegerProjectCustomField'
factory :user_project_custom_field do
sequence(:name) { |n| "User project custom field #{n}" }
field_format 'user'
end
factory :int_project_custom_field do
sequence(:name) { |n| "Int project custom field #{n}" }
field_format 'int'
end
factory :float_project_custom_field do
sequence(:name) { |n| "Float project custom field #{n}" }
field_format 'float'
end
factory :text_project_custom_field do
name 'TextProjectCustomField'
sequence(:name) { |n| "Text project custom field #{n}" }
field_format 'text'
end
factory :string_project_custom_field do
sequence(:name) { |n| "String project custom field #{n}" }
field_format 'string'
end
factory :date_project_custom_field do
sequence(:name) { |n| "Date project custom field #{n}" }
field_format 'date'
end
end
factory :user_custom_field, class: UserCustomField do
sequence(:name) do |n| "User Custom Field #{n}" end
sequence(:name) { |n| "User Custom Field #{n}" }
type 'UserCustomField'
factory :boolean_user_custom_field do
@ -101,58 +131,58 @@ FactoryGirl.define do
end
factory :wp_custom_field, class: WorkPackageCustomField do
sequence(:name) do |n| "Work package custom field #{n}" end
sequence(:name) { |n| "Work package custom field #{n}" }
type 'WorkPackageCustomField'
factory :list_wp_custom_field do
sequence(:name) do |n| "List CF #{n}" end
sequence(:name) { |n| "List CF #{n}" }
field_format 'list'
possible_values ['A', 'B', 'C', 'D', 'E', 'F', 'G']
end
factory :version_wp_custom_field do
sequence(:name) do |n| "Version work package custom field #{n}" end
sequence(:name) { |n| "Version work package custom field #{n}" }
field_format 'version'
end
factory :bool_wp_custom_field do
sequence(:name) do |n| "Bool WP custom field #{n}" end
sequence(:name) { |n| "Bool WP custom field #{n}" }
field_format 'bool'
end
factory :user_wp_custom_field do
sequence(:name) do |n| "User WP custom field #{n}" end
sequence(:name) { |n| "User WP custom field #{n}" }
field_format 'user'
end
factory :int_wp_custom_field do
sequence(:name) do |n| "Int WP custom field #{n}" end
sequence(:name) { |n| "Int WP custom field #{n}" }
field_format 'int'
end
factory :float_wp_custom_field do
sequence(:name) do |n| "Float WP custom field #{n}" end
sequence(:name) { |n| "Float WP custom field #{n}" }
field_format 'float'
end
factory :text_wp_custom_field do
sequence(:name) do |n| "Text WP custom field #{n}" end
sequence(:name) { |n| "Text WP custom field #{n}" }
field_format 'text'
end
factory :string_wp_custom_field do
sequence(:name) do |n| "String WP custom field #{n}" end
sequence(:name) { |n| "String WP custom field #{n}" }
field_format 'string'
end
factory :date_wp_custom_field do
sequence(:name) do |n| "Date WP custom field #{n}" end
sequence(:name) { |n| "Date WP custom field #{n}" }
field_format 'date'
end
end
factory :issue_custom_field, class: WorkPackageCustomField do
sequence(:name) do |n| "Issue Custom Field #{n}" end
sequence(:name) { |n| "Issue Custom Field #{n}" }
factory :user_issue_custom_field do
field_format 'user'
@ -174,24 +204,5 @@ FactoryGirl.define do
field_format 'text'
sequence(:name) { |n| "TimeEntryCustomField #{n}" }
end
factory :project_custom_field, class: ProjectCustomField do
field_format 'text'
type 'ProjectCustomField'
sequence(:name) { |n| "ProjectCustomField #{n}" }
end
factory :list_project_custom_field do
sequence(:name) { |n| "ListProjectCustomField #{n}" }
field_format 'list'
type 'ProjectCustomField'
possible_values ['1', '2', '3', '4', '5', '6', '7']
end
factory :date_project_custom_field do
sequence(:name) { |n| "DateProjectCustomField #{n}" }
field_format 'date'
type 'ProjectCustomField'
end
end
end

@ -33,7 +33,7 @@ describe 'Projects custom fields', type: :feature do
let(:current_user) { FactoryGirl.create(:admin) }
let(:project) { FactoryGirl.create(:project, name: 'Foo project', identifier: 'foo-project') }
let!(:custom_field) do
FactoryGirl.create(:boolean_project_custom_field)
FactoryGirl.create(:bool_project_custom_field)
end
let(:identifier) { "project_custom_field_values_#{custom_field.id}" }

@ -29,7 +29,6 @@
require 'spec_helper'
require 'features/projects/projects_page'
describe 'Projects index page',
type: :feature,
js: true,
@ -72,31 +71,46 @@ describe 'Projects index page',
def set_filter(name, human_name, human_operator = nil, values = [])
select human_name, from: 'add_filter_select'
selected_filter = page.find("li[filter-name='#{name}']")
within(selected_filter) do
select human_operator, from: 'operator'
if values.any?
return unless values.any?
case name
when 'name_and_identifier'
fill_in 'value', with: values.first
set_name_and_identifier_filter(values)
when 'status'
set_status_filter(values)
when 'created_on'
set_created_on_filter(human_operator, values)
when /cf_[\d]+/
set_custom_field_filter(selected_filter, human_operator, values)
end
end
end
def set_name_and_identifier_filter(values)
fill_in 'value', with: values.first
end
def set_status_filter(values)
if values.size == 1
select values.first, from: 'value'
end
when 'created_on'
end
def set_created_on_filter(human_operator, values)
case human_operator
when 'on'
fill_in 'value', with: values.first
when 'less than days ago'
fill_in 'value', with: values.first
when 'more than days ago'
fill_in 'value', with: values.first
when 'days ago'
when 'on', 'less than days ago', 'more than days ago', 'days ago'
fill_in 'value', with: values.first
when 'between'
fill_in 'from_value', with: values.first
fill_in 'to_value', with: values.second
end
when /cf_[\d]+/
end
def set_custom_field_filter(selected_filter, human_operator, values)
if selected_filter[:'filter-type'] == 'list_optional'
if values.size == 1
value_select = find('.single-select select[name="value"]')
@ -108,9 +122,6 @@ describe 'Projects index page',
end
end
end
end
end
end
def allow_enterprise_edition
allow(EnterpriseToken).to receive(:allows_to?).with(:custom_fields_in_projects_list).and_return(true)
@ -130,8 +141,7 @@ describe 'Projects index page',
expect(page).to have_text(public_project.name)
# Test that the 'More' menu stays invisible on hover
page.find('tbody tr').hover
expect(page).to_not have_css('.icon-show-more-horizontal')
expect(page).to_not have_selector('.icon-show-more-horizontal')
end
end
@ -172,15 +182,14 @@ describe 'Projects index page',
expect(page).to have_text(public_project.name)
expect(page).to have_text(project.name)
# Test that the 'More' menu becomes visible on hover
expect(page).to_not have_css('.icon-show-more-horizontal')
page.first('tbody tr').hover
expect(page).to have_css('.icon-show-more-horizontal')
# because we use css opacity we can not test for the visibility changes
expect(page).to have_selector('.icon-show-more-horizontal')
# Test visiblity of 'more' menu list items
page.first('tbody tr .icon-show-more-horizontal').click
menu = page.first('tbody tr .project-actions')
expect(menu).to have_text('Copy')
expect(menu).to have_text('Project settings')
expect(menu).to have_text('New subproject')
expect(menu).to have_text('Delete')
expect(menu).to have_text('Archive')
@ -214,10 +223,8 @@ describe 'Projects index page',
# Admins shall be the only ones to see invisible CFs
expect(page).to have_text(invisible_custom_field.name.upcase)
expect(page).to have_select('add_filter_select', :with_options => [invisible_custom_field.name])
expect(page).to have_select('add_filter_select', with_options: [invisible_custom_field.name])
end
scenario
end
feature 'with a filter set' do
@ -234,7 +241,7 @@ describe 'Projects index page',
expect(page).to_not have_text(public_project.name)
expect(page).to have_text(project.name)
# Filter form is visible and the filter is still set.
expect(page).to have_css('li[filter-name="name_and_identifier"]')
expect(page).to have_selector('li[filter-name="name_and_identifier"]')
end
end
@ -292,7 +299,6 @@ describe 'Projects index page',
end
feature 'when filter of type' do
scenario 'Name and identifier gives results in both, name and identifier' do
load_and_open_filters admin
@ -335,7 +341,7 @@ describe 'Projects index page',
load_and_open_filters admin
# value selection defaults to "active"'
expect(page).to have_css('li[filter-name="status"]')
expect(page).to have_selector('li[filter-name="status"]')
# Filter has three operators 'all', 'active' and 'archived'
expect(page.find('li[filter-name="status"] select[name="operator"] option[value="*"]')).to have_text('all')
@ -374,7 +380,7 @@ describe 'Projects index page',
let(:datetime_of_this_week) do
today = Date.today
# Ensure that the date is not today but still in the middle of the week to not run into week-start-issues here.
date_of_this_week = today + (((today.wday) % 7) > 2 ? -1 : 1)
date_of_this_week = today + ((today.wday % 7) > 2 ? -1 : 1)
DateTime.parse(date_of_this_week.to_s + 'T11:11:11+00:00')
end
let(:fixed_datetime) { DateTime.parse('2017-11-11T11:11:11+00:00') }
@ -383,9 +389,9 @@ describe 'Projects index page',
project = FactoryGirl.create(:project,
name: 'Created today project',
created_on: DateTime.now)
project.custom_field_values = { list_custom_field.id => '3'}
project.custom_field_values = { date_custom_field.id => '2011-11-11'}
project.save
project.custom_field_values = { list_custom_field.id => list_custom_field.possible_values[2],
date_custom_field.id => '2011-11-11' }
project.save!
project
end
let!(:project_created_on_this_week) do
@ -512,7 +518,7 @@ describe 'Projects index page',
set_filter("cf_#{list_custom_field.id}",
list_custom_field.name,
'is',
['3'])
[list_custom_field.possible_values[2].value])
click_on 'Filter'
@ -527,9 +533,9 @@ describe 'Projects index page',
click_on 'Toggle multiselect'
# switching to multiselect keeps the current selection
expect(cf_filter.find(:select, 'value')[:multiple]).to be_truthy
expect(cf_filter).to have_select('value', selected: '3')
expect(cf_filter).to have_select('value', selected: list_custom_field.possible_values[2].value)
select '5', from: 'value'
select list_custom_field.possible_values[3].value, from: 'value'
end
click_on 'Filter'
@ -538,16 +544,19 @@ describe 'Projects index page',
within(cf_filter) do
# Query has two values for that filter, so it shoud show a 'multi select'.
expect(cf_filter.find(:select, 'value')[:multiple]).to be_truthy
expect(cf_filter).to have_select('value', selected: ['3', '5'])
expect(cf_filter)
.to have_select('value',
selected: [list_custom_field.possible_values[2].value,
list_custom_field.possible_values[3].value])
# switching to single select keeps the first selection
select '2', from: 'value'
unselect '3', from: 'value'
select list_custom_field.possible_values[1].value, from: 'value'
unselect list_custom_field.possible_values[2].value, from: 'value'
click_on 'Toggle multiselect'
expect(cf_filter.find(:select, 'value')[:multiple]).to be_falsey
expect(cf_filter).to have_select('value', selected: '2')
expect(cf_filter).to_not have_select('value', selected: '5')
expect(cf_filter).to have_select('value', selected: list_custom_field.possible_values[1].value)
expect(cf_filter).to_not have_select('value', selected: list_custom_field.possible_values[3].value)
end
click_on 'Filter'
@ -587,7 +596,7 @@ describe 'Projects index page',
let!(:parent_project) do
FactoryGirl.create(:project,
name: 'Parent project',
identifier: 'parent-project' )
identifier: 'parent-project')
end
let!(:can_copy_projects_manager) do
@ -621,12 +630,9 @@ describe 'Projects index page',
expect(page).to have_text(parent_project.name)
# 'More' menu should be invisible by default
expect(page).not_to have_css('.icon-show-more-horizontal')
# 'More' does not become visible on hover
page.find('tbody tr').hover
expect(page).to_not have_css('.icon-show-more-horizontal')
expect(page).not_to have_selector('.icon-show-more-horizontal')
# For a project member with :copy_projects privilege the 'More' menu is visible.
login_as(can_copy_projects_manager)
@ -634,12 +640,10 @@ describe 'Projects index page',
expect(page).to have_text(parent_project.name)
# 'More' menu should be invisible by default
expect(page).not_to have_css('.icon-show-more-horizontal')
# 'More' becomes visible on hover
# because we use css opacity we can not test for the visibility changes
page.find('tbody tr').hover
expect(page).to have_css('.icon-show-more-horizontal')
expect(page).to have_selector('.icon-show-more-horizontal')
# Test visiblity of 'more' menu list items
page.find('tbody tr .icon-show-more-horizontal').click
@ -654,12 +658,10 @@ describe 'Projects index page',
login_as(can_add_subprojects_manager)
visit projects_path
# 'More' menu should be invisible by default
expect(page).not_to have_css('.icon-show-more-horizontal')
# 'More' becomes visible on hover
# because we use css opacity we can not test for the visibility changes
page.find('tbody tr').hover
expect(page).to have_css('.icon-show-more-horizontal')
expect(page).to have_selector('.icon-show-more-horizontal')
# Test visiblity of 'more' menu list items
page.find('tbody tr .icon-show-more-horizontal').click
@ -672,4 +674,3 @@ describe 'Projects index page',
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…
Cancel
Save