Merge pull request #4943 from opf/feature/api_v3_users_index

Add GET api/v3/users
pull/5006/head
Oliver Günther 8 years ago committed by GitHub
commit 1d4b1e21c0
  1. 10
      app/controllers/api/experimental/queries_controller.rb
  2. 15
      app/helpers/issues_helper.rb
  3. 16
      app/helpers/queries_helper.rb
  4. 7
      app/models/custom_value/bool_strategy.rb
  5. 65
      app/models/queries/available_filters.rb
  6. 46
      app/models/queries/available_orders.rb
  7. 181
      app/models/queries/base_filter.rb
  8. 66
      app/models/queries/base_order.rb
  9. 124
      app/models/queries/base_query.rb
  10. 15
      app/models/queries/filter_serializer.rb
  11. 55
      app/models/queries/not_existing_filter.rb
  12. 42
      app/models/queries/not_existing_order.rb
  13. 40
      app/models/queries/register.rb
  14. 125
      app/models/queries/sql_for_field.rb
  15. 38
      app/models/queries/users.rb
  16. 41
      app/models/queries/users/filters/group_filter.rb
  17. 75
      app/models/queries/users/filters/name_filter.rb
  18. 44
      app/models/queries/users/filters/status_filter.rb
  19. 8
      app/models/queries/users/filters/user_filter.rb
  20. 14
      app/models/queries/users/orders/default_order.rb
  21. 50
      app/models/queries/users/orders/group_order.rb
  22. 48
      app/models/queries/users/orders/name_order.rb
  23. 13
      app/models/queries/users/user_query.rb
  24. 52
      app/models/queries/work_packages.rb
  25. 28
      app/models/queries/work_packages/filter.rb
  26. 6
      app/models/queries/work_packages/filter/assigned_to_filter.rb
  27. 2
      app/models/queries/work_packages/filter/author_filter.rb
  28. 12
      app/models/queries/work_packages/filter/category_filter.rb
  29. 2
      app/models/queries/work_packages/filter/created_at_filter.rb
  30. 110
      app/models/queries/work_packages/filter/custom_field_filter.rb
  31. 2
      app/models/queries/work_packages/filter/done_ratio_filter.rb
  32. 2
      app/models/queries/work_packages/filter/due_date_filter.rb
  33. 2
      app/models/queries/work_packages/filter/estimated_hours_filter.rb
  34. 8
      app/models/queries/work_packages/filter/group_filter.rb
  35. 20
      app/models/queries/work_packages/filter/principal_base_filter.rb
  36. 6
      app/models/queries/work_packages/filter/priority_filter.rb
  37. 12
      app/models/queries/work_packages/filter/project_filter.rb
  38. 4
      app/models/queries/work_packages/filter/responsible_filter.rb
  39. 8
      app/models/queries/work_packages/filter/role_filter.rb
  40. 2
      app/models/queries/work_packages/filter/start_date_filter.rb
  41. 6
      app/models/queries/work_packages/filter/status_filter.rb
  42. 2
      app/models/queries/work_packages/filter/subject_filter.rb
  43. 15
      app/models/queries/work_packages/filter/subproject_filter.rb
  44. 13
      app/models/queries/work_packages/filter/type_filter.rb
  45. 2
      app/models/queries/work_packages/filter/updated_at_filter.rb
  46. 14
      app/models/queries/work_packages/filter/version_filter.rb
  47. 6
      app/models/queries/work_packages/filter/watcher_filter.rb
  48. 75
      app/models/queries/work_packages/filter/work_package_filter.rb
  49. 201
      app/models/query.rb
  50. 42
      app/seeders/demo_data/query_seeder.rb
  51. 128
      app/services/api/v3/params_to_query_service.rb
  52. 12
      app/views/queries/_filters.html.erb
  53. 4
      app/views/timelines/_custom_fields.html.erb
  54. 52
      config/constants/query_register.rb
  55. 50
      config/initializers/work_package_filters.rb
  56. 14
      config/locales/en.yml
  57. 2
      doc/apiv3/endpoints/users.apib
  58. 5
      features/step_definitions/work_package_steps.rb
  59. 44
      lib/api/v3/users/paginated_user_collection_representer.rb
  60. 28
      lib/api/v3/users/users_api.rb
  61. 7
      spec/features/timelines/filter_custom_fields_spec.rb
  62. 15
      spec/features/work_packages/select_query_spec.rb
  63. 13
      spec/lib/api/v3/queries/query_representer_spec.rb
  64. 69
      spec/lib/api/v3/users/paginated_user_collection_representer_spec.rb
  65. 14
      spec/lib/api/v3/utilities/path_helper_spec.rb
  66. 130
      spec/models/queries/available_filters_spec.rb
  67. 198
      spec/models/queries/base_filter_spec.rb
  68. 62
      spec/models/queries/users/filters/group_filter_spec.rb
  69. 96
      spec/models/queries/users/filters/name_filter_spec.rb
  70. 53
      spec/models/queries/users/filters/status_filter_spec.rb
  71. 225
      spec/models/queries/users/user_query_spec.rb
  72. 13
      spec/models/queries/work_packages/filter/assigned_to_spec.rb
  73. 9
      spec/models/queries/work_packages/filter/author_filter_spec.rb
  74. 7
      spec/models/queries/work_packages/filter/category_filter_spec.rb
  75. 7
      spec/models/queries/work_packages/filter/created_at_filter_spec.rb
  76. 313
      spec/models/queries/work_packages/filter/custom_field_filter_spec.rb
  77. 7
      spec/models/queries/work_packages/filter/done_ratio_filter_spec.rb
  78. 7
      spec/models/queries/work_packages/filter/due_date_filter_spec.rb
  79. 7
      spec/models/queries/work_packages/filter/estimated_hours_filter_spec.rb
  80. 7
      spec/models/queries/work_packages/filter/group_filter_spec.rb
  81. 1
      spec/models/queries/work_packages/filter/principal_loader_spec.rb
  82. 7
      spec/models/queries/work_packages/filter/priority_filter_spec.rb
  83. 7
      spec/models/queries/work_packages/filter/project_filter_spec.rb
  84. 11
      spec/models/queries/work_packages/filter/responsible_filter_spec.rb
  85. 7
      spec/models/queries/work_packages/filter/role_filter_spec.rb
  86. 71
      spec/models/queries/work_packages/filter/shared.rb
  87. 7
      spec/models/queries/work_packages/filter/start_date_filter_spec.rb
  88. 7
      spec/models/queries/work_packages/filter/status_filter_spec.rb
  89. 7
      spec/models/queries/work_packages/filter/subject_filter_spec.rb
  90. 7
      spec/models/queries/work_packages/filter/subproject_filter_spec.rb
  91. 9
      spec/models/queries/work_packages/filter/type_filter_spec.rb
  92. 7
      spec/models/queries/work_packages/filter/updated_at_filter_spec.rb
  93. 66
      spec/models/queries/work_packages/filter/version_filter_spec.rb
  94. 9
      spec/models/queries/work_packages/filter/watcher_filter_spec.rb
  95. 157
      spec/models/queries/work_packages/filter_spec.rb
  96. 57
      spec/models/query_spec.rb
  97. 6
      spec/models/work_package/planning_comparison_spec.rb
  98. 124
      spec/requests/api/v3/user/user_resource_spec.rb
  99. 237
      spec/support/queries/filters/shared_filter_examples.rb
  100. 131
      spec_legacy/unit/query_spec.rb

@ -160,14 +160,14 @@ module Api::Experimental
end
def fetch_custom_field_filters(project)
filters = Queries::WorkPackages::Filter::CustomFieldFilter.create(project)
filters = Queries::WorkPackages::Filter::CustomFieldFilter.all_for(project)
filters.each_with_object({}) do |(key, filter), hash|
new_key = API::Utilities::PropertyNameConverter.from_ar_name(key)
filters.each_with_object({}) do |filter, hash|
new_key = API::Utilities::PropertyNameConverter.from_ar_name(filter.name)
hash[new_key] = { type: filter.type,
values: filter.values,
values: filter.allowed_values,
order: filter.order,
name: filter.name }
name: filter.human_name }
end
end
end

@ -56,21 +56,6 @@ module IssuesHelper
<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe)
end
# Find the name of an associated record stored in the field attribute
def find_name_by_reflection(field, id)
association = WorkPackage.reflect_on_association(field.to_sym)
if association
record = association.class_name.constantize.find_by(id: id)
return record.name if record
end
end
def entries_for_filter_select_sorted(query)
[['', '']] + query.available_work_package_filters.map { |field| [field[1][:name] || WorkPackage.human_attribute_name(field[0]), field[0]] unless query.has_filter?(field[0]) }.compact.sort_by { |el|
ActiveSupport::Inflector.transliterate(el[0]).downcase
}
end
def last_issue_note(issue)
note_journals = issue.journals.select(&:notes?)
return t(:text_no_notes) if note_journals.empty?

@ -29,11 +29,19 @@
module QueriesHelper
def operators_for_select(filter_type)
Queries::Filter.operators_by_filter_type[filter_type].map { |o| [l(Queries::Filter.operators[o]), o] }
Queries::BaseFilter.operators_by_filter_type[filter_type].map { |o| [l(Queries::BaseFilter.operators[o]), o] }
end
def entries_for_filter_select_sorted(query)
[['', '']] +
query.available_filters
.reject { |filter| query.has_filter?(filter.name) }
.map { |filter| [filter.human_name, filter.name] }
.sort_by { |el| ActiveSupport::Inflector.transliterate(el[0]).downcase }
end
def column_locale(column)
(column.is_a? QueryCustomFieldColumn) ? column.custom_field.name_locale : nil
column.is_a?(QueryCustomFieldColumn) ? column.custom_field.name_locale : nil
end
def add_filter_from_params
@ -62,7 +70,7 @@ module QueriesHelper
if params[:fields] || params[:f]
add_filter_from_params
else
@query.available_work_package_filters.keys.each do |field|
@query.available_filters.map(&:name).each do |field|
@query.add_short_filter(field, params[field]) if params[field]
end
end
@ -163,7 +171,7 @@ module QueriesHelper
return [] if field_names.nil?
context = WorkPackage.new
available_keys = query.available_work_package_filters.keys
available_keys = query.available_filters.map(&:name)
field_names
.map { |name| API::Utilities::PropertyNameConverter.to_ar_name name, context: context }

@ -28,6 +28,9 @@
#++
class CustomValue::BoolStrategy < CustomValue::FormatStrategy
DB_VALUE_FALSE = 'f'.freeze
DB_VALUE_TRUE = 't'.freeze
def value_present?
# can't use :blank? safely, because false.blank? == true
# can't use :present? safely, because false.present? == false
@ -44,9 +47,9 @@ class CustomValue::BoolStrategy < CustomValue::FormatStrategy
if !value_present?
nil
elsif ActiveRecord::Type::Boolean::FALSE_VALUES.include?(value)
'f'
DB_VALUE_FALSE
else
't'
DB_VALUE_TRUE
end
end

@ -27,59 +27,74 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
module Queries::WorkPackages::AvailableFilterOptions
def available_work_package_filters
uninitialized = registered_work_package_filters - already_initialized_work_package_filters
module Queries::AvailableFilters
def available_filters
uninitialized = registered_filters - already_initialized_filters
uninitialized.each do |filter|
initialize_work_package_filter(filter)
initialize_filter(filter)
end
initialized_available_work_package_filters
initialized_filters.select(&:available?)
end
def work_package_filter_available?(key)
def filter_for(key, no_memoization = false)
filter_instance = get_initialized_filter(key, no_memoization) || Queries::NotExistingFilter.new
filter_instance.name = key
filter_instance
end
private
def get_initialized_filter(key, no_memoization)
filter = find_registered_filter(key)
return unless filter
initialize_work_package_filter(filter)
if no_memoization
filter.new
else
initialize_filter(filter)
initialized_available_work_package_filters[key]
find_initialized_filter(key)
end
end
private
def initialize_work_package_filter(filter)
return if already_initialized_work_package_filters.include?(filter)
already_initialized_work_package_filters << filter
new_filters = filter.create(project)
def initialize_filter(filter)
return if already_initialized_filters.include?(filter)
already_initialized_filters << filter
available_filters = new_filters.reject { |_, f| !f.available? }
new_filters = filter.all_for(context)
initialized_available_work_package_filters.merge! available_filters
initialized_filters.push(*Array(new_filters))
end
def find_registered_filter(key)
registered_work_package_filters.detect do |f|
registered_filters.detect do |f|
f.key === key.to_sym
end
end
def already_initialized_work_package_filters
@already_initialized_work_package_filters ||= []
def find_initialized_filter(key)
initialized_filters.detect do |f|
f.name == key.to_sym
end
end
def already_initialized_filters
@already_initialized_filters ||= []
end
def registered_work_package_filters
@registered_work_package_filters ||= filter_register.filters
def registered_filters
@registered_filters ||= filter_register
end
def initialized_available_work_package_filters
@initialized_available_work_package_filters ||= {}.with_indifferent_access
def initialized_filters
@initialized_filters ||= []
end
def filter_register
Queries::WorkPackages::FilterRegister
Queries::Register.filters[self.class]
end
end

@ -0,0 +1,46 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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::AvailableOrders
def order_for(key)
(find_registered_order(key) || Queries::NotExistingOrder).new(key)
end
private
def find_registered_order(key)
orders_register.detect do |s|
s.key === key.to_sym
end
end
def orders_register
Queries::Register.orders[self.class]
end
end

@ -27,18 +27,86 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::Filter
class Queries::BaseFilter
include ActiveModel::Validations
include ActiveModel::Serialization
include Queries::SqlForField
class_attribute :filter_types_by_field, instance_writer: false
attr_accessor :context
self.filter_types_by_field = {
created_at: :date_past,
updated_at: :date_past
}
class_attribute :model
@@filter_params = [:operator, :values]
attr_accessor *@@filter_params
def initialize(options = {})
self.context = options[:context]
@@filter_params.each do |param_field|
send("#{param_field}=", options[param_field])
end
end
def [](name)
send(name)
end
def name
@name || self.class.key
end
alias :field :name
def name=(name)
@name = name.to_sym
end
def human_name
raise NotImplementedError
end
@@filter_params = [:operator, :values] # will be serialized and persisted with the query
def type
raise NotImplementedError
end
def allowed_values
nil
end
def available?
true
end
def scope
scope = model.where(where)
scope = scope.joins(joins) if joins
scope
end
def self.key
name.to_sym
end
def self.name
to_s.demodulize.underscore.gsub(/_filter$/, '')
end
def self.connection
model.connection
end
def self.all_for(context = nil)
filter = new
filter.context = context
filter
end
def where
sql_for_field(self.class.key, operator, values, self.class.model.table_name, self.class.key)
end
def joins
nil
end
@@operators = {
label_equals: '=',
@ -77,87 +145,64 @@ class Queries::Filter
cattr_reader :operators, :operators_by_filter_type
attr_accessor :field, *@@filter_params
attr_accessor *@@filter_params
validates_presence_of :field, :operator
validate :validate_presence_of_values, unless: Proc.new { |filter| @@operators_not_requiring_values.include?(filter.operator) }
validate :validate_inclusion_of_operator
validate :validate_presence_of_values,
unless: Proc.new { |filter| @@operators_not_requiring_values.include?(filter.operator) }
validate :validate_filter_values
def initialize(field = nil, options = {})
self.field = field
values = []
@@filter_params.each do |param_field|
send("#{param_field}=", options[param_field])
end
stringify_values
end
# (de-)serialization
def self.from_hash(filter_hash)
filter_hash.keys.map { |field| new(field, filter_hash[field]) }
end
def to_hash
{ field => attributes_hash }
end
alias_method :name, :field
def attributes
{ name: name, operator: operator, values: values }
end
def field=(field)
@field = field.try :to_sym
def values
@values || []
end
def possible_types_by_operator
@@operators_by_filter_type.select { |_key, operators| operators.include?(operator) }.keys.sort
end
def type
filter_types_by_field[field]
end
def ==(filter)
filter.attributes_hash == attributes_hash
def values=(values)
@values = Array(values).reject(&:blank?).map(&:to_s)
end
protected
def attributes_hash
@@filter_params.inject({}) do |params, param_field|
params.merge(param_field => send(param_field))
end
end
private
def stringify_values
unless values.nil?
values.map!(&:to_s)
def validate_inclusion_of_operator
unless @@operators_by_filter_type[type].include? operator
errors.add(:operator, :inclusion)
end
end
def validate_presence_of_values
errors.add(:values, I18n.t('activerecord.errors.messages.blank')) if values.nil? || values.reject(&:blank?).empty?
if values.nil? || values.reject(&:blank?).empty?
errors.add(:values, I18n.t('activerecord.errors.messages.blank'))
end
end
def validate_filter_values
return true if @@operators_not_requiring_values.include?(operator)
case type
when :integer
errors.add(:values, I18n.t('activerecord.errors.messages.not_an_integer')) unless values.all? { |value| is_integer?(value) }
when :date, :date_past
errors.add(:values, I18n.t('activerecord.errors.messages.not_an_integer')) unless values.all? { |value| is_integer?(value) }
# ...
when :integer, :date, :date_past
validate_values_all_integer
when :list, :list_optional
validate_values_in_allowed_values_list
end
end
def validate_values_all_integer
unless values.all? { |value| integer?(value) }
errors.add(:values, I18n.t('activerecord.errors.messages.not_an_integer'))
end
end
def validate_values_in_allowed_values_list
# TODO: the -1 is a special value that exists for historical reasons
# so one can send the operator '=' and the values ['-1']
# which results in a IS NULL check in the DB
if (values & (allowed_values.map(&:last).map(&:to_s) + ['-1'])) != values
errors.add(:values, :inclusion)
end
end
def is_integer?(str)
true if Integer(str) rescue false
def integer?(str)
true if Integer(str)
rescue
false
end
end

@ -0,0 +1,66 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
class Queries::BaseOrder
include ActiveModel::Validations
validates :direction, inclusion: { in: %i(asc desc) }
class_attribute :model
attr_accessor :direction,
:attribute
def initialize(attribute)
self.attribute = attribute
end
def self.key
raise NotImplementedError
end
def scope
scope = order
scope = scope.joins(joins) if joins
scope
end
def name
attribute
end
private
def order
model.order(name => direction)
end
def joins
nil
end
end

@ -0,0 +1,124 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
class Queries::BaseQuery
class << self
attr_accessor :model, :default_scope
end
include Queries::AvailableFilters
include Queries::AvailableOrders
include ActiveModel::Validations
validate :filters_valid,
:sortation_valid
def initialize
@scope = self.class.default_scope
@filters = []
@orders = []
end
def results
if valid?
filters.each do |filter|
self.scope = scope.merge(filter.scope)
end
orders.each do |order|
self.scope = scope.merge(order.scope)
end
else
empty_scope
end
scope
end
def where(attribute, operator, values)
filter = filter_for(attribute)
filter.operator = operator
filter.values = values
filter.context = context
filters << filter
self
end
def order(hash)
hash.each do |attribute, direction|
order = order_for(attribute)
order.direction = direction
orders << order
end
self
end
protected
attr_accessor :scope,
:filters,
:orders
def filters_valid
filters.each do |filter|
next if filter.valid?
add_error(:filters, filter.human_name, filter)
end
end
def sortation_valid
orders.each do |order|
next if order.valid?
add_error(:orders, order.class.key, order)
end
end
def add_error(local_attribute, attribute_name, object)
messages = object
.errors
.messages
.values
.flatten
.join(" #{I18n.t('support.array.sentence_connector')} ")
errors.add local_attribute, errors.full_message(attribute_name, messages)
end
def empty_scope
self.scope = self.class.model.where(Arel::Nodes::Equality.new(1, 0))
end
def context
nil
end
end

@ -28,12 +28,25 @@
#++
module Queries::FilterSerializer
extend Queries::AvailableFilters
def self.load(serialized_filter_hash)
return [] if serialized_filter_hash.nil?
Queries::WorkPackages::Filter.from_hash(YAML.load(serialized_filter_hash) || {})
(YAML.load(serialized_filter_hash) || {}).each_with_object([]) do |(field, options), array|
options = options.with_indifferent_access
filter = filter_for(field, true)
filter.operator = options['operator']
filter.values = options['values']
array << filter
end
end
def self.dump(filters)
YAML.dump ((filters || []).map(&:to_hash).reduce(:merge) || {}).stringify_keys
end
def self.filter_register
Queries::Register.filters[Query]
end
end

@ -0,0 +1,55 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
class Queries::NotExistingFilter < Queries::BaseFilter
def available?
false
end
def type
:inexistent
end
def self.key
:not_existent
end
def human_name
name
end
validate :always_false
def always_false
errors.add :base, I18n.t(:'activerecord.errors.messages.does_not_exist')
end
# deactivating superclass validation
def validate_inclusion_of_operator; end
end

@ -0,0 +1,42 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
class Queries::NotExistingOrder < Queries::BaseOrder
validate :always_false
def self.key
:inexistent
end
private
def always_false
errors.add :base, I18n.t(:'activerecord.errors.messages.does_not_exist')
end
end

@ -0,0 +1,40 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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 Rails.root.join('config/constants/query_register')
module Queries::Register
class << self
delegate :filter,
:filters,
:order,
:orders,
to: ::Constants::QueryRegister
end
end

@ -0,0 +1,125 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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::SqlForField
private
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +values+ array
def sql_for_field(field, operator, values, db_table, db_field, is_custom_filter = false)
# code expects strings (e.g. for quoting), but ints would work as well: unify them here
values = values.map(&:to_s)
sql = ''
case operator
when '='
if values.present?
if values.include?('-1')
sql = "#{db_table}.#{db_field} IS NULL OR "
end
sql += "#{db_table}.#{db_field} IN (" + values.map { |val| "'#{connection.quote_string(val)}'" }.join(',') + ')'
else
# empty set of allowed values produces no result
sql = '0=1'
end
when '!'
if values.present?
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + values.map { |val| "'#{connection.quote_string(val)}'" }.join(',') + '))'
else
# empty set of forbidden values allows all results
sql = '1=1'
end
when '!*'
sql = "#{db_table}.#{db_field} IS NULL"
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
when '*'
sql = "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when '>='
if is_custom_filter
sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) >= #{values.first.to_f}"
else
sql = "#{db_table}.#{db_field} >= #{values.first.to_f}"
end
when '<='
if is_custom_filter
sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) <= #{values.first.to_f}"
else
sql = "#{db_table}.#{db_field} <= #{values.first.to_f}"
end
when 'o'
sql = "#{Status.table_name}.is_closed=#{connection.quoted_false}" if field == 'status_id'
when 'c'
sql = "#{Status.table_name}.is_closed=#{connection.quoted_true}" if field == 'status_id'
when '>t-'
sql = date_range_clause(db_table, db_field, - values.first.to_i, 0)
when '<t-'
sql = date_range_clause(db_table, db_field, nil, - values.first.to_i)
when 't-'
sql = date_range_clause(db_table, db_field, - values.first.to_i, - values.first.to_i)
when '>t+'
sql = date_range_clause(db_table, db_field, values.first.to_i, nil)
when '<t+'
sql = date_range_clause(db_table, db_field, 0, values.first.to_i)
when 't+'
sql = date_range_clause(db_table, db_field, values.first.to_i, values.first.to_i)
when 't'
sql = date_range_clause(db_table, db_field, 0, 0)
when 'w'
from = l(:general_first_day_of_week) == '7' ?
# week starts on sunday
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
# week starts on monday (Rails default)
Time.now.at_beginning_of_week
sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when '~'
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(values.first.to_s.downcase)}%'"
when '!~'
sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(values.first.to_s.downcase)}%'"
end
sql
end
# Returns a SQL clause for a date or datetime field.
def date_range_clause(table, field, from, to)
s = []
if from
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
end
if to
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
end
s.join(' AND ')
end
def connection
self.class.connection
end
end

@ -0,0 +1,38 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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::Users
Queries::Register.filter Queries::Users::UserQuery, Queries::Users::Filters::NameFilter
Queries::Register.filter Queries::Users::UserQuery, Queries::Users::Filters::GroupFilter
Queries::Register.filter Queries::Users::UserQuery, Queries::Users::Filters::StatusFilter
Queries::Register.order Queries::Users::UserQuery, Queries::Users::Orders::DefaultOrder
Queries::Register.order Queries::Users::UserQuery, Queries::Users::Orders::NameOrder
Queries::Register.order Queries::Users::UserQuery, Queries::Users::Orders::GroupOrder
end

@ -27,43 +27,34 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::BaseFilter
attr_accessor :project
def initialize(project)
self.project = project
end
def [](name)
send(name)
class Queries::Users::Filters::GroupFilter < Queries::Users::Filters::UserFilter
def allowed_values
@allowed_values ||= begin
::Group.pluck(:name, :id).map { |g| [g[0], g[1].to_s] }
end
end
def name
WorkPackage.human_attribute_name(self.class.name)
end
def values
nil
def available?
::Group.exists?
end
def available?
true
def type
:list_optional
end
def key
self.class.key
def human_name
I18n.t('query_fields.member_of_group')
end
def self.key
name.to_sym
:group
end
def self.create(project)
{ key => new(project) }
def joins
:groups
end
private_class_method :new
def self.name
to_s.demodulize.underscore.gsub(/_filter$/, '')
def where
sql_for_field(self.class.key, operator, values, 'groups', 'id')
end
end

@ -0,0 +1,75 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
class Queries::Users::Filters::NameFilter < Queries::Users::Filters::UserFilter
def type
:string
end
def self.key
:name
end
def where
case operator
when '='
["#{sql_concat_name} IN (?)", sql_value]
when '!'
["#{sql_concat_name} NOT IN (?)", sql_value]
when '~'
["#{sql_concat_name} LIKE ?", "%#{sql_value}%"]
when '!~'
["#{sql_concat_name} NOT LIKE ?", "%#{sql_value}%"]
end
end
private
def sql_value
case operator
when '=', '!'
values.map { |val| connection.quote_string(val.downcase) }.join(',')
when '~', '!~'
values.first.downcase
end
end
def sql_concat_name
case Setting.user_format
when :firstname_lastname, :lastname_coma_firstname
"LOWER(CONCAT(firstname, CONCAT(' ', lastname)))"
when :firstname
'LOWER(firstname)'
when :lastname_firstname
"LOWER(CONCAT(lastname, CONCAT(' ', firstname)))"
when :username
"LOWER(login)"
end
end
end

@ -0,0 +1,44 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
class Queries::Users::Filters::StatusFilter < Queries::Users::Filters::UserFilter
def allowed_values
Principal::STATUSES.keys.map do |key|
[I18n.t(:"status_#{key}"), key]
end
end
def type
:list
end
def self.key
:status
end
end

@ -27,10 +27,10 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require Rails.root.join('config/constants/work_package_filter')
class Queries::Users::Filters::UserFilter < Queries::BaseFilter
self.model = User
module Queries::WorkPackages::FilterRegister
class << self
delegate :register, :filters, to: ::Constants::WorkPackageFilter
def human_name
User.human_attribute_name(name)
end
end

@ -27,16 +27,10 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
module Constants
module WorkPackageFilter
class << self
def register(filter)
self.filters ||= []
class Queries::Users::Orders::DefaultOrder < Queries::BaseOrder
self.model = User
self.filters << filter
end
attr_accessor :filters
end
def self.key
/id|lastname|firstname|mail|login/
end
end

@ -0,0 +1,50 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
class Queries::Users::Orders::GroupOrder < Queries::BaseOrder
self.model = User
def self.key
:group
end
private
def order
order_string = "groups_users.lastname"
order_string += " DESC" if direction == :desc
model.order(order_string)
end
def joins
:groups
end
end

@ -0,0 +1,48 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
class Queries::Users::Orders::NameOrder < Queries::BaseOrder
self.model = User
def self.key
:name
end
private
def order
ordered = User.order_by_name
if direction == :desc
ordered = ordered.reverse_order
end
ordered
end
end

@ -26,13 +26,12 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
FactoryGirl.define do
factory :filter, class: Queries::Filter do
field :subject
operator '='
values ['Feature']
class Queries::Users::UserQuery < Queries::BaseQuery
def self.model
User
end
factory :work_packages_filter, class: Queries::WorkPackages::Filter do
end
def self.default_scope
User.not_builtin
end
end

@ -0,0 +1,52 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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::WorkPackages
Queries::Register.filter Query, Queries::WorkPackages::Filter::AssignedToFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::AuthorFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::CategoryFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::CreatedAtFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::CustomFieldFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::DoneRatioFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::DueDateFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::EstimatedHoursFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::GroupFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::PriorityFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::ProjectFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::ResponsibleFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::RoleFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::StartDateFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::StatusFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::SubjectFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::SubprojectFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::TypeFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::UpdatedAtFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::VersionFilter
Queries::Register.filter Query, Queries::WorkPackages::Filter::WatcherFilter
end

@ -27,31 +27,5 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter < Queries::Filter
self.filter_types_by_field = filter_types_by_field.merge(
status_id: :list_status,
type_id: :list,
priority_id: :list,
subject: :text,
start_date: :date,
due_date: :date,
estimated_hours: :integer,
done_ratio: :integer,
project_id: :list,
category_id: :list_optional,
fixed_version_id: :list_optional,
subproject_id: :list_subprojects,
assigned_to_id: :list_optional,
author_id: :list,
member_of_group: :list_optional,
assigned_to_role: :list_optional,
responsible_id: :list_optional,
watcher_id: :list
)
validates :field, inclusion: { in: Proc.new { filter_types_by_field.keys }, message: '%(value) is not a valid filter' }, unless: Proc.new { |filter| filter.field.to_s.starts_with?('cf_') }
def self.add_filter_type_by_field(field, filter_type)
filter_types_by_field[field.to_sym] = filter_type.to_sym
end
module Queries::WorkPackages::Filter
end

@ -29,8 +29,8 @@
class Queries::WorkPackages::Filter::AssignedToFilter <
Queries::WorkPackages::Filter::PrincipalBaseFilter
def values
@values ||= begin
def allowed_values
@allowed_values ||= begin
values = principal_loader.user_values
if Setting.work_package_group_assignment?
@ -49,7 +49,7 @@ class Queries::WorkPackages::Filter::AssignedToFilter <
4
end
def name
def human_name
WorkPackage.human_attribute_name('assigned_to_id')
end

@ -29,7 +29,7 @@
class Queries::WorkPackages::Filter::AuthorFilter <
Queries::WorkPackages::Filter::PrincipalBaseFilter
def values
def allowed_values
@author_values ||= begin
me_value + principal_loader.user_values
end

@ -27,15 +27,11 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::CategoryFilter < Queries::WorkPackages::Filter::BaseFilter
attr_accessor :project
class Queries::WorkPackages::Filter::CategoryFilter <
Queries::WorkPackages::Filter::WorkPackageFilter
def initialize(project)
self.project = project
end
def values
@values ||= begin
def allowed_values
@allowed_values ||= begin
project.categories.map { |s| [s.name, s.id.to_s] }
end
end

@ -27,7 +27,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::CreatedAtFilter < Queries::WorkPackages::Filter::BaseFilter
class Queries::WorkPackages::Filter::CreatedAtFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def type
:date_past
end

@ -27,28 +27,30 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::CustomFieldFilter < Queries::WorkPackages::Filter::BaseFilter
attr_accessor :field
require 'custom_value'
def initialize(field, project)
self.field = field
self.project = project
end
class Queries::WorkPackages::Filter::CustomFieldFilter <
Queries::WorkPackages::Filter::WorkPackageFilter
attr_accessor :custom_field
validate :custom_field_valid
def values
case field.field_format
def allowed_values
case custom_field.field_format
when 'list'
field.possible_values
custom_field.possible_values.map { |value| [value, value] }
when 'bool'
[[I18n.t(:general_text_yes), ActiveRecord::Base.connection.unquoted_true],
[I18n.t(:general_text_no), ActiveRecord::Base.connection.unquoted_false]]
[[I18n.t(:general_text_yes), CustomValue::BoolStrategy::DB_VALUE_TRUE],
[I18n.t(:general_text_no), CustomValue::BoolStrategy::DB_VALUE_FALSE]]
when 'user', 'version'
field.possible_values_options(project)
custom_field.possible_values_options(context)
end
end
def type
case field.field_format
return nil unless custom_field
case custom_field.field_format
when 'int', 'float'
:integer
when 'text'
@ -68,33 +70,73 @@ class Queries::WorkPackages::Filter::CustomFieldFilter < Queries::WorkPackages::
20
end
def key
"cf_#{field.id}".to_sym
def name
:"cf_#{custom_field.id}"
end
def name
field.name
def human_name
custom_field ? custom_field.name : ''
end
def self.create(project)
custom_fields = if project
project
.all_work_package_custom_fields(include: :translations)
else
WorkPackageCustomField.filter
.for_all
.where.not(field_format: ['user', 'version'])
.includes(:translations)
end
custom_fields.each_with_object({}.with_indifferent_access) do |cf, hash|
filter = new(cf, project)
hash[filter.key] = filter
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+/
/cf_(\d+)/
end
def self.all_for(context = nil)
custom_fields(context).map do |cf|
filter = new
filter.custom_field = cf
filter.context = context
filter
end
end
def self.custom_fields(context)
if context
context
.all_work_package_custom_fields
else
WorkPackageCustomField
.filter
.for_all
.where.not(field_format: ['user', 'version'])
end
end
private
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?
context && invalid_custom_field_for_project? ||
!context && invalid_custom_field_globally?
end
def invalid_custom_field_globally?
!self.class.custom_fields(context)
.exists?(custom_field.id)
end
def invalid_custom_field_for_project?
!self.class.custom_fields(context)
.map(&:id).include? custom_field.id
end
end

@ -27,7 +27,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::DoneRatioFilter < Queries::WorkPackages::Filter::BaseFilter
class Queries::WorkPackages::Filter::DoneRatioFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def type
:integer
end

@ -27,7 +27,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::DueDateFilter < Queries::WorkPackages::Filter::BaseFilter
class Queries::WorkPackages::Filter::DueDateFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def type
:date
end

@ -28,7 +28,7 @@
#++
class Queries::WorkPackages::Filter::EstimatedHoursFilter <
Queries::WorkPackages::Filter::BaseFilter
Queries::WorkPackages::Filter::WorkPackageFilter
def type
:integer
end

@ -27,9 +27,9 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::GroupFilter < Queries::WorkPackages::Filter::BaseFilter
def values
@values ||= begin
class Queries::WorkPackages::Filter::GroupFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def allowed_values
@allowed_values ||= begin
::Group.all.map { |g| [g.name, g.id.to_s] }
end
end
@ -46,7 +46,7 @@ class Queries::WorkPackages::Filter::GroupFilter < Queries::WorkPackages::Filter
6
end
def name
def human_name
I18n.t('query_fields.member_of_group')
end

@ -27,20 +27,10 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::PrincipalBaseFilter < Queries::WorkPackages::Filter::BaseFilter
attr_accessor :principal_loader
def initialize(principal_loader)
self.principal_loader = principal_loader
end
class Queries::WorkPackages::Filter::PrincipalBaseFilter <
Queries::WorkPackages::Filter::WorkPackageFilter
def available?
User.current.logged? || values.any?
end
def self.create(project)
principal_loader = ::Queries::WorkPackages::Filter::PrincipalLoader.new(project)
{ key => new(principal_loader) }
User.current.logged? || allowed_values.any?
end
private
@ -50,4 +40,8 @@ class Queries::WorkPackages::Filter::PrincipalBaseFilter < Queries::WorkPackages
values << [I18n.t(:label_me), 'me'] if User.current.logged?
values
end
def principal_loader
@principal_loader ||= ::Queries::WorkPackages::Filter::PrincipalLoader.new(project)
end
end

@ -27,9 +27,9 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::PriorityFilter < Queries::WorkPackages::Filter::BaseFilter
def values
@values ||= begin
class Queries::WorkPackages::Filter::PriorityFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def allowed_values
@allowed_values ||= begin
priorities.map { |s| [s.name, s.id.to_s] }
end
end

@ -27,15 +27,9 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filter::BaseFilter
attr_accessor :project
def initialize(project)
self.project = project
end
def values
@values ||= begin
class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def allowed_values
@allowed_values ||= begin
project_values = []
Project.project_tree(visible_projects) do |p, level|
prefix = (level > 0 ? ('--' * level + ' ') : '')

@ -29,8 +29,8 @@
class Queries::WorkPackages::Filter::ResponsibleFilter <
Queries::WorkPackages::Filter::PrincipalBaseFilter
def values
@values ||= begin
def allowed_values
@allowed_values ||= begin
values = principal_loader.user_values
me_value + values
end

@ -27,9 +27,9 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::RoleFilter < Queries::WorkPackages::Filter::BaseFilter
def values
@values ||= begin
class Queries::WorkPackages::Filter::RoleFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def allowed_values
@allowed_values ||= begin
roles.map { |r| [r.name, r.id.to_s] }
end
end
@ -46,7 +46,7 @@ class Queries::WorkPackages::Filter::RoleFilter < Queries::WorkPackages::Filter:
7
end
def name
def human_name
I18n.t('query_fields.assigned_to_role')
end

@ -27,7 +27,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::StartDateFilter < Queries::WorkPackages::Filter::BaseFilter
class Queries::WorkPackages::Filter::StartDateFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def type
:date
end

@ -27,9 +27,9 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::StatusFilter < Queries::WorkPackages::Filter::BaseFilter
def values
@values ||= begin
class Queries::WorkPackages::Filter::StatusFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def allowed_values
@allowed_values ||= begin
Status.all.map { |s| [s.name, s.id.to_s] }
end
end

@ -27,7 +27,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::SubjectFilter < Queries::WorkPackages::Filter::BaseFilter
class Queries::WorkPackages::Filter::SubjectFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def type
:text
end

@ -27,15 +27,10 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::SubprojectFilter < Queries::WorkPackages::Filter::BaseFilter
attr_accessor :project
def initialize(project)
self.project = project
end
def values
@values ||= begin
class Queries::WorkPackages::Filter::SubprojectFilter <
Queries::WorkPackages::Filter::WorkPackageFilter
def allowed_values
@allowed_values ||= begin
project.descendants.visible.map { |s| [s.name, s.id.to_s] }
end
end
@ -54,7 +49,7 @@ class Queries::WorkPackages::Filter::SubprojectFilter < Queries::WorkPackages::F
13
end
def name
def human_name
I18n.t('query_fields.subproject_id')
end

@ -27,15 +27,10 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::TypeFilter < Queries::WorkPackages::Filter::BaseFilter
attr_accessor :project
def initialize(project)
self.project = project
end
def values
@values ||= begin
class Queries::WorkPackages::Filter::TypeFilter <
Queries::WorkPackages::Filter::WorkPackageFilter
def allowed_values
@allowed_values ||= begin
types.map { |s| [s.name, s.id.to_s] }
end
end

@ -27,7 +27,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::UpdatedAtFilter < Queries::WorkPackages::Filter::BaseFilter
class Queries::WorkPackages::Filter::UpdatedAtFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def type
:date_past
end

@ -27,17 +27,15 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::VersionFilter < Queries::WorkPackages::Filter::BaseFilter
def values
@values ||= begin
class Queries::WorkPackages::Filter::VersionFilter <
Queries::WorkPackages::Filter::WorkPackageFilter
def allowed_values
@allowed_values ||= begin
versions.sort.map { |s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
end
end
def available?
versions.exists?
end
def type
:list_optional
end
@ -46,7 +44,7 @@ class Queries::WorkPackages::Filter::VersionFilter < Queries::WorkPackages::Filt
7
end
def name
def human_name
WorkPackage.human_attribute_name('fixed_version_id')
end

@ -29,8 +29,8 @@
class Queries::WorkPackages::Filter::WatcherFilter <
Queries::WorkPackages::Filter::PrincipalBaseFilter
def values
@values ||= begin
def allowed_values
@allowed_values ||= begin
# populate the watcher list with the same user list as other user filters
# if the user has the :view_work_package_watchers permission
# in at least one project
@ -38,7 +38,7 @@ class Queries::WorkPackages::Filter::WatcherFilter <
# more, e.g. all users could watch issues in public projects,
# but won't necessarily be shown here
values = me_value
if User.current.allowed_to?(:view_work_packages_watchers, project, global: project.nil?)
if User.current.allowed_to?(:view_work_package_watchers, project, global: project.nil?)
values += principal_loader.user_values
end
values

@ -0,0 +1,75 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
class Queries::WorkPackages::Filter::WorkPackageFilter < ::Queries::BaseFilter
include ActiveModel::Serialization
# (de-)serialization
def self.from_hash(filter_hash)
filter_hash.keys.map { |field| new(field, filter_hash[field]) }
end
def to_hash
{ name => attributes_hash }
end
def human_name
WorkPackage.human_attribute_name(name)
end
alias :project :context
alias :project= :context=
def attributes
{ name: name, operator: operator, values: values }
end
def possible_types_by_operator
@@operators_by_filter_type.select { |_key, operators| operators.include?(operator) }.keys.sort
end
def ==(filter)
filter.attributes_hash == attributes_hash
end
protected
def attributes_hash
@@filter_params.inject({}) do |params, param_field|
params.merge(param_field => send(param_field))
end
end
private
def stringify_values
unless values.nil?
values.map!(&:to_s)
end
end
end

@ -28,10 +28,8 @@
#++
class Query < ActiveRecord::Base
include Queries::WorkPackages::AvailableFilterOptions
# referenced in plugin patches - currently there are only work package queries and filters
alias_method :available_filters, :available_work_package_filters
include Queries::AvailableFilters
include Queries::SqlForField
@@user_filters = %w{assigned_to_id author_id watcher_id responsible_id}.freeze
@ -131,7 +129,7 @@ class Query < ActiveRecord::Base
groupable: true),
QueryColumn.new(:created_at,
sortable: "#{WorkPackage.table_name}.created_at",
default_order: 'desc'),
default_order: 'desc')
]
cattr_reader :available_columns
@ -140,21 +138,47 @@ class Query < ActiveRecord::Base
add_default_filter if options[:initialize_with_default_filter]
end
after_initialize :set_context
def set_context
# We need to set the project for each filter if a project
# is present because the information is not available when
# deserializing the filters from the db.
# Allow to use AR's select(...) without
# the filters attribute
return unless respond_to?(:filters)
filters.each do |filter|
filter.context = project
end
end
alias :context :project
def add_default_filter
self.filters = [Queries::WorkPackages::Filter.new('status_id', operator: 'o', values: [''])] if filters.blank?
return unless filters.blank?
add_filter('status_id', 'o', [''])
end
def validate_work_package_filters
filters.each do |filter|
unless filter.valid?
messages = filter.errors.messages.values.flatten.join(" #{I18n.t('support.array.sentence_connector')} ")
cf_id = custom_field_id filter
if cf_id && CustomField.find(cf_id)
attribute_name = CustomField.find(cf_id).name
errors.add :base, attribute_name + I18n.t(default: ' %{message}', message: messages)
messages = filter
.errors
.messages
.values
.flatten
.join(" #{I18n.t('support.array.sentence_connector')} ")
attribute_name = filter.human_name
# TODO: check if this can be handled without the case statment
case filter
when Queries::WorkPackages::Filter::CustomFieldFilter
errors.add :base, attribute_name + I18n.t(default: ' %{message}', message: messages)
else
attribute_name = WorkPackage.human_attribute_name(filter.field)
errors.add :base, errors.full_message(attribute_name, messages)
end
end
@ -170,14 +194,12 @@ class Query < ActiveRecord::Base
end
def add_filter(field, operator, values)
return unless work_package_filter_available?(field)
filter = filter_for(field)
if filter = filter_for(field)
filter.operator = operator
filter.values = values
else
filters << Queries::WorkPackages::Filter.new(field, operator: operator, values: values)
end
filter.operator = operator
filter.values = values
filters << filter
end
def add_short_filter(field, expression)
@ -198,28 +220,15 @@ class Query < ActiveRecord::Base
end
def has_filter?(field)
filters.present? && filters.any? { |filter| filter.field.to_s == field.to_s }
filters.present? && filters.any? { |f| f.field.to_s == field.to_s }
end
def filter_for(field)
(filters || []).detect { |filter| filter.field.to_s == field.to_s }
end
filter = (filters || []).detect { |f| f.field.to_s == field.to_s } || super(field)
# Deprecated
def operator_for(field)
warn '#operator_for is deprecated. Query the filter object directly, instead.'
filter_for(field).try :operator
end
filter.context = project
# Deprecated
def values_for(field)
warn '#values_for is deprecated. Query the filter object directly, instead.'
filter_for(field).try :values
end
def label_for(field)
label = available_work_package_filters[field][:name] if work_package_filter_available?(field)
label ||= field.gsub(/\_id\z/, '')
filter
end
def normalized_name
@ -241,11 +250,11 @@ class Query < ActiveRecord::Base
end
def self.available_columns=(v)
self.available_columns = (v)
self.available_columns = v
end
def self.add_available_column(column)
available_columns << (column) if column.is_a?(QueryColumn)
available_columns << column if column.is_a?(QueryColumn)
end
# Returns an array of columns that can be used to group the results
@ -255,10 +264,13 @@ class Query < ActiveRecord::Base
# Returns a Hash of columns and the key for sorting
def sortable_columns
{ 'id' => "#{WorkPackage.table_name}.id" }.merge(available_columns.inject({}) {|h, column|
h[column.name.to_s] = column.sortable
h
})
column_sortability = available_columns.inject({}) do |h, column|
h[column.name.to_s] = column.sortable
h
end
{ 'id' => "#{WorkPackage.table_name}.id" }
.merge(column_sortability)
end
def columns
@ -307,7 +319,6 @@ class Query < ActiveRecord::Base
end
def sort_criteria=(arg)
c = []
if arg.is_a?(Hash)
arg = arg.keys.sort.map { |k| arg[k] }
end
@ -523,112 +534,14 @@ class Query < ActiveRecord::Base
raise ::Query::StatementInvalid.new(e.message)
end
# Note: Convenience method to allow the angular front end to deal with query menu items in a non implementation-specific way
# Note: Convenience method to allow the angular front end to deal with query
# menu items in a non implementation-specific way
def starred
!!query_menu_item
end
private
def custom_field_id(filter)
matchdata = /cf\_(?<id>\d+)/.match(filter.field.to_s)
matchdata.nil? ? nil : matchdata[:id]
end
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +values+ array
def sql_for_field(field, operator, values, db_table, db_field, is_custom_filter = false)
# code expects strings (e.g. for quoting), but ints would work as well: unify them here
values = values.map(&:to_s)
sql = ''
case operator
when '='
if values.present?
if values.include?('-1')
sql = "#{db_table}.#{db_field} IS NULL OR "
end
sql += "#{db_table}.#{db_field} IN (" + values.map { |val| "'#{connection.quote_string(val)}'" }.join(',') + ')'
else
# empty set of allowed values produces no result
sql = '0=1'
end
when '!'
if values.present?
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + values.map { |val| "'#{connection.quote_string(val)}'" }.join(',') + '))'
else
# empty set of forbidden values allows all results
sql = '1=1'
end
when '!*'
sql = "#{db_table}.#{db_field} IS NULL"
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
when '*'
sql = "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when '>='
if is_custom_filter
sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) >= #{values.first.to_f}"
else
sql = "#{db_table}.#{db_field} >= #{values.first.to_f}"
end
when '<='
if is_custom_filter
sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) <= #{values.first.to_f}"
else
sql = "#{db_table}.#{db_field} <= #{values.first.to_f}"
end
when 'o'
sql = "#{Status.table_name}.is_closed=#{connection.quoted_false}" if field == 'status_id'
when 'c'
sql = "#{Status.table_name}.is_closed=#{connection.quoted_true}" if field == 'status_id'
when '>t-'
sql = date_range_clause(db_table, db_field, - values.first.to_i, 0)
when '<t-'
sql = date_range_clause(db_table, db_field, nil, - values.first.to_i)
when 't-'
sql = date_range_clause(db_table, db_field, - values.first.to_i, - values.first.to_i)
when '>t+'
sql = date_range_clause(db_table, db_field, values.first.to_i, nil)
when '<t+'
sql = date_range_clause(db_table, db_field, 0, values.first.to_i)
when 't+'
sql = date_range_clause(db_table, db_field, values.first.to_i, values.first.to_i)
when 't'
sql = date_range_clause(db_table, db_field, 0, 0)
when 'w'
from = l(:general_first_day_of_week) == '7' ?
# week starts on sunday
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
# week starts on monday (Rails default)
Time.now.at_beginning_of_week
sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when '~'
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(values.first.to_s.downcase)}%'"
when '!~'
sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(values.first.to_s.downcase)}%'"
end
sql
end
# Returns a SQL clause for a date or datetime field.
def date_range_clause(table, field, from, to)
s = []
if from
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
end
if to
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
end
s.join(' AND ')
end
def connection
self.class.connection
end
def for_all?
@for_all ||= project.nil?
end

@ -53,12 +53,44 @@ module DemoData
end
def data
admin = User.admin.first
bug_type = Type.find_by(name: I18n.t('default_type_bug'))
milestone_type = Type.find_by(name: I18n.t('default_type_milestone'))
phase_type = Type.find_by(name: I18n.t('default_type_phase'))
task_type = Type.find_by(name: I18n.t('default_type_task'))
story_type = Type.find_by(name: I18n.t('default_type_user_story'))
[
{ name: "Bugs", filters: [Queries::WorkPackages::Filter.new(:status_id, operator: "o"), Queries::WorkPackages::Filter.new(:type_id, operator: "=", values: ['7'])], user_id: User.admin.first.id, is_public: true, column_names: [:id, :type, :status, :priority, :subject, :assigned_to, :create_at] },
{ name: "Milestones", filters: [Queries::WorkPackages::Filter.new(:status_id, operator: "o"), Queries::WorkPackages::Filter.new(:type_id, operator: "=", values: ['2'])], user_id: User.admin.first.id, is_public: true, column_names: [:id, :type, :status, :subject, :start_date, :due_date] },
{ name: "Phases", filters: [Queries::WorkPackages::Filter.new(:status_id, operator: "o"), Queries::WorkPackages::Filter.new(:type_id, operator: "=", values: ['3'])], user_id: User.admin.first.id, is_public: true, column_names: [:id, :type, :status, :subject, :start_date, :due_date] },
{ name: "Tasks", filters: [Queries::WorkPackages::Filter.new(:status_id, operator: "o"), Queries::WorkPackages::Filter.new(:type_id, operator: "=", values: ['1'])], user_id: User.admin.first.id, is_public: true, column_names: [:id, :type, :status, :priority, :subject, :assigned_to] },
{ name: "User Stories", filters: [Queries::WorkPackages::Filter.new(:status_id, operator: "o"), Queries::WorkPackages::Filter.new(:type_id, operator: "=", values: ['6'])], user_id: User.admin.first.id, is_public: true, column_names: [:id, :type, :status, :priority, :subject, :assigned_to] }
{ name: "Bugs",
filters: [status_id: { operator: "o" },
type_id: { operator: "=", values: [bug_type.id.to_s] }],
user_id: admin.id,
is_public: true,
column_names: [:id, :type, :status, :priority, :subject, :assigned_to, :create_at] },
{ name: "Milestones",
filters: [status_id: { operator: "o" },
type_id: { operator: "=", values: [milestone_type.id.to_s] }],
user_id: admin.id,
is_public: true,
column_names: [:id, :type, :status, :subject, :start_date, :due_date] },
{ name: "Phases",
filters: [status_id: { operator: "o" },
type_id: { operator: "=", values: [phase_type.id.to_s] }],
user_id: admin.id,
is_public: true,
column_names: [:id, :type, :status, :subject, :start_date, :due_date] },
{ name: "Tasks",
filters: [status_id: { operator: "o" },
type_id: { operator: "=", values: [task_type.id.to_s] }],
user_id: admin.id,
is_public: true,
column_names: [:id, :type, :status, :priority, :subject, :assigned_to] },
{ name: "User Stories",
filters: [status_id: { operator: "o" },
type_id: { operator: "=", values: [story_type.id.to_s] }],
user_id: admin.id,
is_public: true,
column_names: [:id, :type, :status, :priority, :subject, :assigned_to] }
]
end
end

@ -0,0 +1,128 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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 API
module V3
class ParamsToQueryService
attr_accessor :model
def initialize(model)
self.model = model
end
def call(params)
query = new_query
query = apply_filters(query, params)
query = apply_order(query, params)
query
end
private
def new_query
model_name = model.name
query_class = Kernel.const_get "::Queries::#{model_name.pluralize}::#{model_name}Query"
query_class.new
end
def apply_filters(query, params)
return query unless params[:filters]
filters = parse_filters_from_json(params[:filters])
filters[:attributes].each do |filter_name|
query = query.where(filter_name,
filters[:operators][filter_name],
filters[:values][filter_name])
end
query
end
def apply_order(query, params)
return query unless params[:sortBy]
sort = parse_sorting_from_json(params[:sortBy])
hash_sort = sort.each_with_object({}) do |(attribute, direction), hash|
hash[attribute.to_sym] = direction.to_sym
end
query.order(hash_sort)
end
# Expected format looks like:
# [
# {
# "filtered_field_name": {
# "operator": "a name for a filter operation",
# "values": ["values", "for the", "operation"]
# }
# },
# { /* more filters if needed */}
# ]
def parse_filters_from_json(json)
filters = JSON.parse(json)
operators = {}
values = {}
filters.each do |filter|
attribute = filter.keys.first # there should only be one attribute per filter
ar_attribute = convert_attribute attribute, append_id: true
operators[ar_attribute] = filter[attribute]['operator']
values[ar_attribute] = filter[attribute]['values']
end
{
attributes: values.keys,
operators: operators,
values: values
}
end
def parse_sorting_from_json(json)
JSON.parse(json).map do |(attribute, order)|
[convert_attribute(attribute), order]
end
end
def convert_attribute(attribute, append_id: false)
::API::Utilities::PropertyNameConverter.to_ar_name(attribute,
context: conversion_model,
refer_to_ids: append_id)
end
def conversion_model
@conversion_model ||= model.new
end
end
end
end

@ -103,13 +103,13 @@ See doc/COPYRIGHT.rdoc for more details.
//]]>
</script>
<ul class="advanced-filters--filters">
<% query.available_work_package_filters.sort { |a, b| a[1][:order]<=>b[1][:order] }.each do |filter| %>
<% field = filter[0]
options = filter[1] %>
<% query.available_filters.sort { |a, b| a[:order]<=>b[:order] }.each do |filter| %>
<% field = filter.name
options = filter %>
<li <%= 'style="display:none;"'.html_safe unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter advanced-filters--filter">
<label class="advanced-filters--filter-name">
<%= check_box_tag 'f[]', field, query.has_filter?(field), onclick: "toggle_filter('#{field}');", id: "cb_#{field}" %>
<%= filter[1][:name] || WorkPackage.human_attribute_name(field) %>
<%= filter[:name] || WorkPackage.human_attribute_name(field) %>
</label>
<div class="advanced-filters--filter-operator">
<%= label_tag "op_#{field}", l(:description_filter), class: "hidden-for-sighted" %>
@ -124,7 +124,7 @@ See doc/COPYRIGHT.rdoc for more details.
</div>
<div id="div_values_<%= field %>" style="display:none;" class="advanced-filters--filter-value">
<% field_values = query.filter_for(field).try(:values) || []
case options[:type]
case filter.type
when :list, :list_optional, :list_status, :list_subprojects %>
<span class="inline-label">
<select <%= "multiple=true" if field_values and field_values.length > 1 %>
@ -133,7 +133,7 @@ See doc/COPYRIGHT.rdoc for more details.
class="form--select -small"
style="vertical-align: top;">
<%= options_for_select options[:values], field_values %>
<%= options_for_select filter.allowed_values, field_values %>
</select>
<%= link_to_function icon_wrapper('icon-context icon-plus', l(:label_enable_multi_select)),

@ -44,8 +44,8 @@ custom_fields.each do |custom_field|
[
['', ''],
[l('timelines.filter.noneElement'), '-1'],
[l('general_text_Yes'), 't' ],
[l('general_text_No'), 'f']
[l('general_text_Yes'), CustomValue::BoolStrategy::DB_VALUE_TRUE],
[l('general_text_No'), CustomValue::BoolStrategy::DB_VALUE_FALSE]
],
timeline.custom_fields_filter[custom_field.id.to_s]),
class: "cf_boolean_select",

@ -0,0 +1,52 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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 Constants
module QueryRegister
class << self
def filter(query, filter)
self.filters ||= Hash.new do |hash, filter_key|
hash[filter_key] = []
end
self.filters[query] << filter
end
def order(query, order)
self.orders ||= Hash.new do |hash, order_key|
hash[order_key] = []
end
self.orders[query] << order
end
attr_accessor :filters, :orders
end
end
end

@ -1,50 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::AssignedToFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::AuthorFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::CategoryFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::CreatedAtFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::CustomFieldFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::DoneRatioFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::DueDateFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::EstimatedHoursFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::GroupFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::PriorityFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::ProjectFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::ResponsibleFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::RoleFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::StartDateFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::StatusFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::SubjectFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::SubprojectFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::TypeFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::UpdatedAtFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::VersionFilter
Queries::WorkPackages::FilterRegister.register Queries::WorkPackages::Filter::WatcherFilter

@ -305,13 +305,13 @@ en:
user:
admin: "Administrator"
auth_source: "Authentication mode"
current_password: "Current password"
force_password_change: "Enforce password change on next login"
language: "Language"
last_login_on: "Last login"
mail_notification: "Email notifications"
current_password: "Current password"
new_password: "New password"
password_confirmation: "Confirmation"
force_password_change: "Enforce password change on next login"
user_preference:
comments_sorting: "Display comments"
hide_mail: "Hide my email address"
@ -329,18 +329,15 @@ en:
parent_title: "Parent page"
redirect_existing_links: "Redirect existing links"
planning_element_type_color:
name: Name
hexcode: Hex code
project_association:
description: Cause
project_b: 'Dependent project'
project_type:
allows_association: Allows association
name: Name
reported_project_statuses: Reported project statuses
position: Position
timeline:
name: Name
options: ""
reporting:
reported_project_status_comment: Status comment
@ -373,6 +370,7 @@ en:
cant_link_a_work_package_with_a_descendant: "A work package cannot be linked to one of its subtasks."
circular_dependency: "This relation would create a circular dependency."
confirmation: "doesn't match %{attribute}."
does_not_exist: "does not exist."
empty: "can't be empty."
even: "must be even."
exclusion: "is reserved."
@ -410,6 +408,11 @@ en:
project_association:
identical_projects: "Dependency cannot be created between one project and itself."
project_association_not_allowed: "does not allow associations."
query:
filters:
custom_fields:
inexistent: "There is no custom field for the filter."
invalid: "The custom field is not valid in the given context."
repository:
not_available: "SCM vendor is not available"
not_whitelisted: "is not allowed by the configuration."
@ -423,7 +426,6 @@ en:
cannot_be_milestone: "cannot be a milestone."
cannot_be_in_another_project: "cannot be in another project."
not_a_valid_parent: "is invalid."
does_not_exist: "does not exist."
start_date:
violates_relationships: "can only be set to %{soonest_start} or later so as not to violate the work package's relationships."
status_id:

@ -404,7 +404,7 @@ Lists users. Only administrators have permission to do this.
+ pageSize (optional, integer, `25`) ... Number of elements to display per page.
+ filters (optional, string, `[{ "status": { "operator": "=", "values": "invited" } }, { "group": { "operator": "=", "values": "Managers" } }, { "name": { "operator": "=", "values": "h.wurst@openproject.com" } }]`) ... JSON specifying filter conditions.
+ filters (optional, string, `[{ "status": { "operator": "=", "values": ["invited"] } }, { "group": { "operator": "=", "values": ["1"] } }, { "name": { "operator": "=", "values": ["h.wurst@openproject.com"] } }]`) ... JSON specifying filter conditions.
Accepts the same format as returned by the [queries](#queries) endpoint. Currently supported filters are:
+ status: Status the user has
+ group: Name of the group in which to-be-listed users are members.

@ -75,7 +75,10 @@ Given(/^the user "([^\"]+)" has the following queries by type in the project "(.
table.hashes.each_with_index do |t, _i|
types = ::Type.where(name: t['type_value']).map { |type| type.id.to_s }
p.queries.create(user_id: u.id, name: t['name'], filters: [Queries::WorkPackages::Filter.new(:type_id, operator: '=', values: types)])
query = p.queries.create(user_id: u.id, name: t['name'])
query.filters.clear
query.add_filter(:type_id, '=', types)
query.save!
end
end

@ -0,0 +1,44 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
# TODO: this is to be removed or rather the UserCollectionRepresenter is
# to be turned into an OffsetPaginatedCollection representer.
# It is not possible to do that right now as we do not have a
# solution for an accessible autocompleter drop down widget. We therefore
# have to fetch all users when we want to present them inside of a drop down.
module API
module V3
module Users
class PaginatedUserCollectionRepresenter < ::API::Decorators::OffsetPaginatedCollection
element_decorator ::API::V3::Users::UserRepresenter
end
end
end
end

@ -27,6 +27,7 @@
#++
require 'api/v3/users/user_representer'
require 'api/v3/users/paginated_user_collection_representer'
module API
module V3
@ -50,6 +51,10 @@ module API
fail ::API::Errors::Unauthorized
end
end
def to_i_or_nil(string)
string ? string.to_i : nil
end
end
resources :users do
@ -60,6 +65,23 @@ module API
create_user(request_body, current_user)
end
get do
allow_only_admin
query = ::API::V3::ParamsToQueryService.new(User).call(params)
if query.valid?
users = query.results.includes(:preference)
PaginatedUserCollectionRepresenter.new(users,
api_v3_paths.users,
page: to_i_or_nil(params[:offset]),
per_page: to_i_or_nil(params[:pageSize]),
current_user: current_user)
else
raise ::API::Errors::InvalidQuery.new(query.errors.full_messages)
end
end
params do
requires :id, desc: 'User\'s id'
end
@ -67,7 +89,7 @@ module API
helpers ::API::V3::Users::UpdateUser
before do
@user = User.find(params[:id])
@user = User.find(params[:id])
end
get do
@ -90,9 +112,7 @@ module API
namespace :lock do
# Authenticate lock transitions
before do
unless current_user.admin?
fail ::API::Errors::Unauthorized
end
allow_only_admin
end
desc 'Set lock on user account'

@ -100,8 +100,11 @@ describe Timeline, 'filtering custom fields', type: :feature, js: true do
shared_examples_for 'filtering by bool custom field' do
it 'filters accordingly' do
FactoryGirl.create(:custom_value, customized: wp1, custom_field: cf, value: 'f')
FactoryGirl.create(:custom_value, customized: wp2, custom_field: cf, value: 't')
wp1.custom_field_values = { cf.id => false }
wp1.save!
wp2.custom_field_values = { cf.id => true }
wp2.save!
wp3
# wp3 has no such custom value

@ -42,22 +42,21 @@ describe 'Query selection', type: :feature do
let(:i18n_filter_1_name) { WorkPackage.human_attribute_name(:assigned_to_id) }
let(:i18n_filter_2_name) { WorkPackage.human_attribute_name(:done_ratio) }
before do
allow(User).to receive(:current).and_return current_user
end
let!(:query) do
FactoryGirl.build(:query, project: project, is_public: true).tap do |query|
query.filters = [
Queries::WorkPackages::Filter.new('assigned_to_id', operator: '=', values: ['me']),
Queries::WorkPackages::Filter.new('done_ratio', operator: '>=', values: [10])
]
query.filters.clear
query.add_filter('assigned_to_id', '=', ['me'])
query.add_filter('done_ratio', '>=', [10])
query.save!
end
end
let(:work_packages_page) { WorkPackagesPage.new(project) }
before do
allow(User).to receive(:current).and_return current_user
end
context 'default view, without a query selected' do
before do
work_packages_page.visit_index

@ -100,14 +100,11 @@ describe ::API::V3::Queries::QueryRepresenter do
end
describe 'with filters' do
let(:query) {
FactoryGirl.build_stubbed(:query,
filters: [
Queries::WorkPackages::Filter.new('status_id',
operator: '=',
values: ['1'])
])
}
let(:query) do
query = FactoryGirl.build_stubbed(:query)
query.add_filter('status_id', '=', ['1'])
query
end
it 'should render the filters' do
expected = [

@ -0,0 +1,69 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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 ::API::V3::Users::PaginatedUserCollectionRepresenter do
let(:self_base_link) { '/api/v3/users' }
let(:collection_inner_type) { 'User' }
let(:total) { 3 }
let(:page) { 1 }
let(:page_size) { 2 }
let(:actual_count) { 3 }
let(:users) {
users = FactoryGirl.build_stubbed_list(:user,
actual_count,
created_on: Time.now,
updated_on: Time.now)
allow(users)
.to receive(:per_page)
.with(page_size)
.and_return(users)
allow(users)
.to receive(:page)
.with(page)
.and_return(users)
users
}
let(:representer) {
described_class.new(users,
'/api/v3/users',
per_page: page_size,
page: page,
current_user: users.first)
}
context 'generation' do
subject(:collection) { representer.to_json }
it_behaves_like 'offset-paginated APIv3 collection'
end
end

@ -331,10 +331,18 @@ describe ::API::V3::Utilities::PathHelper do
end
end
describe '#user' do
subject { helper.user 1 }
describe 'users paths' do
describe '#users' do
subject { helper.users }
it_behaves_like 'api v3 path', '/users/1'
it_behaves_like 'api v3 path', '/users'
end
describe '#user' do
subject { helper.user 1 }
it_behaves_like 'api v3 path', '/users/1'
end
end
describe '#version' do

@ -28,46 +28,57 @@
require 'spec_helper'
describe Queries::WorkPackages::AvailableFilterOptions, type: :model do
let(:project) { FactoryGirl.build_stubbed(:project) }
let(:register) { Queries::WorkPackages::FilterRegister }
describe Queries::AvailableFilters, type: :model do
let(:context) { FactoryGirl.build_stubbed(:project) }
let(:register) { Queries::FilterRegister }
class HelperClass
attr_accessor :project
attr_accessor :context
def initialize(project)
self.project = project
def initialize(context)
self.context = context
end
include Queries::WorkPackages::AvailableFilterOptions
def filter_register
register_class
end
include Queries::AvailableFilters
end
let(:includer) {
includer = HelperClass.new(project)
let(:includer) do
includer = HelperClass.new(context)
allow(includer)
.to receive(:filter_register)
.and_return(register)
.and_return(registered_filters)
includer
}
end
describe '#work_package_filter_available?' do
describe '#filter_for' do
let(:filter_1_available) { true }
let(:filter_2_available) { true }
let(:filter_1_key) { :filter_1 }
let(:filter_2_key) { /f_\d+/ }
let(:filter_1_name) { :filter_1 }
let(:filter_2_name) { :f_1 }
let(:registered_filters) { [filter_1, filter_2] }
let(:filter_1) do
instance = double('filter_1_instance')
let(:filter_1_instance) do
instance = double("filter_1_instance")
allow(instance)
.to receive(:available?)
.and_return(filter_1_available)
.and_return(:filter_1_available)
allow(instance)
.to receive(:name)
.and_return(:filter_1)
allow(instance)
.to receive(:name=)
instance
end
let(:filter_1) do
filter = double('filter_1')
allow(filter)
@ -75,19 +86,35 @@ describe Queries::WorkPackages::AvailableFilterOptions, type: :model do
.and_return(:filter_1)
allow(filter)
.to receive(:create)
.and_return(filter.key => instance)
.to receive(:new)
.and_return(filter_1_instance)
allow(filter)
.to receive(:all_for)
.with(context)
.and_return(filter_1_instance)
filter
end
let(:filter_2) do
instance = double('filter_2_instance')
let(:filter_2_instance) do
instance = double("filter_2_instance")
allow(instance)
.to receive(:available?)
.and_return(filter_2_available)
.and_return(:filter_2_available)
allow(instance)
.to receive(:name)
.and_return(:f_1)
allow(instance)
.to receive(:name=)
instance
end
let(:filter_2) do
filter = double('filter_2')
allow(filter)
@ -95,21 +122,13 @@ describe Queries::WorkPackages::AvailableFilterOptions, type: :model do
.and_return(/f_\d+/)
allow(filter)
.to receive(:create)
.and_return('f_1' => instance)
.to receive(:all_for)
.with(context)
.and_return(filter_2_instance)
filter
end
let(:register) do
register = double('register')
allow(register)
.to receive(:filters)
.and_return(registered_filters)
register
end
context 'for a filter identified by a symbol' do
let(:filter_3_available) { true }
let(:registered_filters) { [filter_3, filter_1, filter_2] }
@ -131,8 +150,9 @@ describe Queries::WorkPackages::AvailableFilterOptions, type: :model do
.and_return(:filter)
allow(filter)
.to receive(:create)
.and_return(filter.key => instance)
.to receive(:all_for)
.with(context)
.and_return(instance)
filter
end
@ -140,12 +160,12 @@ describe Queries::WorkPackages::AvailableFilterOptions, type: :model do
context 'if available' do
let(:filter_3_available) { false }
it 'is true' do
expect(includer.work_package_filter_available?(:filter_1)).to be_truthy
it 'returns an instance of the matching filter' do
expect(includer.filter_for(:filter_1)).to eql filter_1_instance
end
it 'is false if the key is not matched' do
expect(includer.work_package_filter_available?(:not_a_filter_name)).to be_falsey
it 'returns the NotExistingFilter if the name is not matched' do
expect(includer.filter_for(:not_a_filter_name)).to be_a Queries::NotExistingFilter
end
end
@ -153,32 +173,36 @@ describe Queries::WorkPackages::AvailableFilterOptions, type: :model do
let(:filter_1_available) { false }
let(:filter_3_available) { true }
it 'is false' do
expect(includer.work_package_filter_available?(:filter_1)).to be_falsey
it 'returns the NotExistingFilter if the name is not matched' do
expect(includer.filter_for(:not_a_filter_name)).to be_a Queries::NotExistingFilter
end
it 'returns an instance of the matching filter if not caring for availablility' do
expect(includer.filter_for(:filter_1, true)).to eql filter_1_instance
end
end
end
context 'for a filter identified by a regexp' do
context 'is true if if available' do
it 'is true' do
expect(includer.work_package_filter_available?(:f_1)).to be_truthy
context 'if available' do
it 'returns an instance of the matching filter' do
expect(includer.filter_for(:f_1)).to eql filter_2_instance
end
it 'is false if the key is not matched' do
expect(includer.work_package_filter_available?(:f_i1)).to be_falsey
it 'returns the NotExistingFilter if the key is not matched' do
expect(includer.filter_for(:f_i1)).to be_a Queries::NotExistingFilter
end
it 'is false if the regexp matches but the created instance key does not' do
expect(includer.work_package_filter_available?(:f_2)).to be_falsey
it 'returns the NotExistingFilter if the key is matched but the name is not' do
expect(includer.filter_for(:f_2)).to be_a Queries::NotExistingFilter
end
end
context 'is false if if unavailable' do
context 'is false if unavailable' do
let(:filter_2_available) { false }
it 'is false' do
expect(includer.work_package_filter_available?(:f_1)).to be_falsey
it 'returns the NotExistingFilter' do
expect(includer.filter_for(:f_i)).to be_a Queries::NotExistingFilter
end
end
end

@ -0,0 +1,198 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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::BaseFilter, type: :model do
let(:integer_filter) do
filter_class = Class.new(described_class) do
def type
:integer
end
end
filter_class.new
end
let(:date_filter) do
filter_class = Class.new(described_class) do
def type
:date
end
end
filter_class.new
end
let(:date_past_filter) do
filter_class = Class.new(described_class) do
def type
:date_past
end
end
filter_class.new
end
shared_examples_for 'validity checked' do
describe '#valid?' do
context 'when the operator does not require values' do
before do
filter.operator = operator_without_value
end
it 'is valid if no values are given' do
expect(filter).to be_valid
end
end
context 'when the operator requires values' do
before do
filter.operator = valid_operator
end
context 'and no value is given' do
it 'is invalid' do
expect(filter).to be_invalid
end
end
context 'and only an empty string is given as value' do
before do
filter.values = ['']
end
it 'is invalid' do
expect(filter).to be_invalid
end
end
context 'and values are given' do
before do
filter.values = valid_values
end
it 'is valid' do
expect(filter).to be_valid
end
end
end
end
end
shared_examples_for 'date validity checked' do
describe '#valid?' do
context "and the operator is 't' (today)" do
before do
filter.operator = 't'
end
it 'is valid' do
expect(filter).to be_valid
end
end
context "and the operator is 'w' (this week)" do
before do
filter.operator = 'w'
end
it 'is valid' do
expect(filter).to be_valid
end
end
context 'and the operator compares the current day' do
before do
filter.operator = '>t-'
end
context 'and the value is an integer' do
before do
filter.values = ['4']
end
it 'is valid' do
expect(filter).to be_valid
end
end
context 'and the value is not an integer' do
before do
filter.values = ['four']
end
it 'is invalid' do
expect(filter).to be_invalid
end
end
end
end
end
context 'for an integer filter' do
let(:filter) { integer_filter }
let(:valid_values) { [5] }
let(:valid_operator) { '=' }
let(:operator_without_value) { '*' }
it_behaves_like 'validity checked'
describe '#valid?' do
context 'when the filter values is not an integer' do
before do
filter.values == [1, 'asdf']
end
it 'is invalid' do
expect(filter).to be_invalid
end
end
end
end
context 'for a date filter' do
let(:filter) { date_filter }
let(:valid_values) { [5] }
let(:valid_operator) { '<t+' }
let(:operator_without_value) { 't' }
it_behaves_like 'validity checked'
it_behaves_like 'date validity checked'
end
context 'for a date_past filter' do
let(:filter) { date_past_filter }
let(:valid_values) { [5] }
let(:valid_operator) { '<t-' }
let(:operator_without_value) { 't' }
it_behaves_like 'validity checked'
it_behaves_like 'date validity checked'
end
end

@ -0,0 +1,62 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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::Users::Filters::GroupFilter, type: :model do
let(:group1) { FactoryGirl.build_stubbed(:group) }
let(:group2) { FactoryGirl.build_stubbed(:group) }
before do
allow(Group)
.to receive(:pluck)
.and_return([[group1.name, group1.id.to_s], [group2.name, group2.id.to_s]])
end
it_behaves_like 'basic query filter' do
let(:class_key) { :group }
let(:type) { :list_optional }
let(:name) { I18n.t('query_fields.member_of_group') }
describe '#allowed_values' do
it 'is a list of the possible values' do
expected = [[group1.name, group1.id.to_s], [group2.name, group2.id.to_s]]
expect(instance.allowed_values).to match_array(expected)
end
end
end
it_behaves_like 'list_optional query filter' do
let(:attribute) { :id }
let(:model) { User }
let(:joins) { :groups }
let(:valid_values) { [group1.id.to_s] }
end
end

@ -0,0 +1,96 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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::Users::Filters::NameFilter, type: :model do
include_context 'filter tests'
let(:values) { ['A name'] }
let(:model) { User }
it_behaves_like 'basic query filter' do
let(:class_key) { :name }
let(:type) { :string }
let(:model) { User }
describe '#allowed_values' do
it 'is nil' do
expect(instance.allowed_values).to be_nil
end
end
end
describe '#scope' do
before do
allow(Setting)
.to receive(:user_format)
.and_return(:firstname)
end
context 'for "="' do
let(:operator) { '=' }
it 'is the same as handwriting the query' do
expected = model.where("LOWER(firstname) IN ('#{values.first.downcase}')")
expect(instance.scope.to_sql).to eql expected.to_sql
end
end
context 'for "!"' do
let(:operator) { '!' }
it 'is the same as handwriting the query' do
expected = model.where("LOWER(firstname) NOT IN ('#{values.first.downcase}')")
expect(instance.scope.to_sql).to eql expected.to_sql
end
end
context 'for "~"' do
let(:operator) { '~' }
it 'is the same as handwriting the query' do
expected = model.where("LOWER(firstname) LIKE '%#{values.first.downcase}%'")
expect(instance.scope.to_sql).to eql expected.to_sql
end
end
context 'for "!~"' do
let(:operator) { '!~' }
it 'is the same as handwriting the query' do
expected = model.where("LOWER(firstname) NOT LIKE '%#{values.first.downcase}%'")
expect(instance.scope.to_sql).to eql expected.to_sql
end
end
end
end

@ -0,0 +1,53 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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::Users::Filters::StatusFilter, type: :model do
it_behaves_like 'basic query filter' do
let(:class_key) { :status }
let(:type) { :list }
describe '#allowed_values' do
it 'is a list of the possible values' do
expected = Principal::STATUSES.keys.map do |key|
[I18n.t(:"status_#{key}"), key]
end
expect(instance.allowed_values).to match_array(expected)
end
end
end
it_behaves_like 'list query filter' do
let(:attribute) { :status }
let(:model) { User }
let(:valid_values) { [Principal::STATUSES.keys.first] }
end
end

@ -0,0 +1,225 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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'
# prevents test failures where the system user
# is mentioned in the User.not_builtin scope
require 'system_user'
describe Queries::Users::UserQuery, type: :model do
let(:instance) { described_class.new }
let(:base_scope) { User.not_builtin }
context 'without a filter' do
describe '#results' do
it 'is the same as getting all the users' do
expect(instance.results.to_sql).to eql base_scope.to_sql
end
end
end
context 'with a name filter' do
before do
instance.where('name', '~', ['a user'])
end
describe '#results' do
it 'is the same as handwriting the query' do
expected = base_scope
.merge(User
.where(["LOWER(CONCAT(firstname, CONCAT(' ', lastname))) LIKE ?",
"%a user%"]))
expect(instance.results.to_sql).to eql expected.to_sql
end
end
describe '#valid?' do
it 'is true' do
expect(instance).to be_valid
end
it 'is invalid if the filter is invalid' do
instance.where('name', '=', [''])
expect(instance).to be_invalid
end
end
end
context 'with a status filter' do
before do
instance.where('status', '=', ['active'])
end
describe '#results' do
it 'is the same as handwriting the query' do
expected = base_scope.merge(User.where(["users.status IN (?)", "active"]))
expect(instance.results.to_sql).to eql expected.to_sql
end
end
describe '#valid?' do
it 'is true' do
expect(instance).to be_valid
end
it 'is invalid if the filter is invalid' do
instance.where('status', '=', [''])
expect(instance).to be_invalid
end
end
end
context 'with a group filter' do
let(:group_1) { FactoryGirl.build_stubbed(:group) }
before do
allow(Group)
.to receive(:exists?)
.and_return(true)
allow(Group)
.to receive(:all)
.and_return([group_1])
instance.where('group', '=', [group_1.id])
end
describe '#results' do
it 'is the same as handwriting the query' do
expected = base_scope
.merge(User
.joins(:groups)
.where("groups.id IN ('#{group_1.id}')"))
expect(instance.results.to_sql).to eql expected.to_sql
end
end
describe '#valid?' do
it 'is true' do
expect(instance).to be_valid
end
it 'is invalid if the filter is invalid' do
instance.where('group', '=', [''])
expect(instance).to be_invalid
end
end
end
context 'with a non existent filter' do
before do
instance.where('not_supposed_to_exist', '=', ['bogus'])
end
describe '#results' do
it 'returns a query not returning anything' do
expected = User.where(Arel::Nodes::Equality.new(1, 0))
expect(instance.results.to_sql).to eql expected.to_sql
end
end
describe 'valid?' do
it 'is false' do
expect(instance).to be_invalid
end
it 'returns the error on the filter' do
instance.valid?
expect(instance.errors[:filters]).to eql ["Not supposed to exist does not exist."]
end
end
end
context 'with an id sortation' do
before do
instance.order(id: :desc)
end
describe '#results' do
it 'is the same as handwriting the query' do
expected = base_scope.merge(User.order(id: :desc))
expect(instance.results.to_sql).to eql expected.to_sql
end
end
end
context 'with a name sortation' do
before do
instance.order(name: :desc)
end
describe '#results' do
it 'is the same as handwriting the query' do
expected = base_scope.merge(User.order_by_name.reverse_order)
expect(instance.results.to_sql).to eql expected.to_sql
end
end
end
context 'with a group sortation' do
before do
instance.order(group: :desc)
end
describe '#results' do
it 'is the same as handwriting the query' do
expected = base_scope.merge(User.joins(:groups).order("groups_users.lastname DESC"))
expect(instance.results.to_sql).to eql expected.to_sql
end
end
end
context 'with a non existing sortation' do
# this is a field protected from sortation
before do
instance.order(password: :desc)
end
describe '#results' do
it 'returns a query not returning anything' do
expected = User.where(Arel::Nodes::Equality.new(1, 0))
expect(instance.results.to_sql).to eql expected.to_sql
end
end
describe 'valid?' do
it 'is false' do
expect(instance).to be_invalid
end
end
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::AssignedToFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 4 }
let(:type) { :list_optional }
let(:class_key) { :assigned_to_id }
@ -125,7 +124,7 @@ describe Queries::WorkPackages::Filter::AssignedToFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
let(:logged_in) { true }
before do
@ -144,7 +143,7 @@ describe Queries::WorkPackages::Filter::AssignedToFilter, type: :model do
context 'when being logged in' do
it 'returns the me value and the available users and groups' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array([[I18n.t(:label_me), 'me'],
[user_1.name, user_1.id.to_s],
[group_1.name, group_1.id.to_s]])
@ -155,7 +154,7 @@ describe Queries::WorkPackages::Filter::AssignedToFilter, type: :model do
.to receive(:work_package_group_assignment?)
.and_return(false)
expect(instance.values)
expect(instance.allowed_values)
.to match_array([[I18n.t(:label_me), 'me'],
[user_1.name, user_1.id.to_s]])
end
@ -165,7 +164,7 @@ describe Queries::WorkPackages::Filter::AssignedToFilter, type: :model do
let(:logged_in) { false }
it 'returns the available users' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array([[user_1.name, user_1.id.to_s],
[group_1.name, group_1.id.to_s]])
end
@ -175,7 +174,7 @@ describe Queries::WorkPackages::Filter::AssignedToFilter, type: :model do
.to receive(:work_package_group_assignment?)
.and_return(false)
expect(instance.values)
expect(instance.allowed_values)
.to match_array([[user_1.name, user_1.id.to_s]])
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::AuthorFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 5 }
let(:type) { :list }
let(:class_key) { :author_id }
@ -93,7 +92,7 @@ describe Queries::WorkPackages::Filter::AuthorFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
let(:logged_in) { true }
before do
@ -108,7 +107,7 @@ describe Queries::WorkPackages::Filter::AuthorFilter, type: :model do
.to receive(:user_values)
.and_return([[user_1.name, user_1.id.to_s]])
expect(instance.values)
expect(instance.allowed_values)
.to match_array([[I18n.t(:label_me), 'me'],
[user_1.name, user_1.id.to_s]])
end
@ -122,7 +121,7 @@ describe Queries::WorkPackages::Filter::AuthorFilter, type: :model do
.to receive(:user_values)
.and_return([[user_1.name, user_1.id.to_s]])
expect(instance.values)
expect(instance.allowed_values)
.to match_array([[user_1.name, user_1.id.to_s]])
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::CategoryFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 6 }
let(:type) { :list_optional }
let(:class_key) { :category_id }
@ -65,7 +64,7 @@ describe Queries::WorkPackages::Filter::CategoryFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
let(:category) { FactoryGirl.build_stubbed(:category) }
before do
@ -75,7 +74,7 @@ describe Queries::WorkPackages::Filter::CategoryFilter, type: :model do
end
it 'returns an array of type options' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[category.name, category.id.to_s]]
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::CreatedAtFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 9 }
let(:type) { :date_past }
let(:class_key) { :created_at }
@ -41,9 +40,9 @@ describe Queries::WorkPackages::Filter::CreatedAtFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
it 'is nil' do
expect(instance.values).to be_nil
expect(instance.allowed_values).to be_nil
end
end
end

@ -30,7 +30,13 @@ require 'spec_helper'
describe Queries::WorkPackages::Filter::CustomFieldFilter, type: :model do
let(:project) { FactoryGirl.build_stubbed(:project) }
let(:instance) { described_class.create(project)["cf_#{list_wp_custom_field.id}"] }
let(:instance) do
filter = described_class.new
filter.name = "cf_#{custom_field.id}"
filter.operator = '='
filter.context = project
filter
end
let(:instance_key) { nil }
let(:name) { field.name }
@ -43,8 +49,9 @@ describe Queries::WorkPackages::Filter::CustomFieldFilter, type: :model do
let(:version_wp_custom_field) { FactoryGirl.build_stubbed(:version_wp_custom_field) }
let(:date_wp_custom_field) { FactoryGirl.build_stubbed(:date_wp_custom_field) }
let(:string_wp_custom_field) { FactoryGirl.build_stubbed(:string_wp_custom_field) }
let(:custom_field) { list_wp_custom_field }
let(:all_custom_fields) {
let(:all_custom_fields) do
[list_wp_custom_field,
bool_wp_custom_field,
int_wp_custom_field,
@ -54,89 +61,140 @@ describe Queries::WorkPackages::Filter::CustomFieldFilter, type: :model do
version_wp_custom_field,
date_wp_custom_field,
string_wp_custom_field]
}
end
before do
if project
allow(project)
.to receive(:all_work_package_custom_fields)
.with(include: :translations)
.and_return(all_custom_fields)
all_custom_fields.each do |cf|
allow(WorkPackageCustomField)
.to receive(:find_by_id)
.with(cf.id)
.and_return(cf)
end
end
describe '.create' do
describe '.valid?' do
let(:custom_field) { string_wp_custom_field }
before do
instance.values = ['bogus']
end
before do
if project
allow(project)
.to receive_message_chain(:all_work_package_custom_fields, :map, :include?)
.and_return(true)
else
allow(WorkPackageCustomField)
.to receive_message_chain(:filter, :for_all, :where, :not, :exists?)
.and_return(true)
end
end
it 'is invalid without a custom field' do
allow(WorkPackageCustomField)
.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_wp_custom_field }
before do
instance.values = [list_wp_custom_field.possible_values.first.to_s]
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 'within a project' do
it 'returns a hash with a subject key and a filter instance for every custom field' do
expect(described_class.create(project)["cf_#{list_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{bool_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{int_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{float_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{text_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{user_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{version_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{date_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{string_wp_custom_field.id}"])
.to be_a(described_class)
it 'is invalid with a custom field not active in the project' do
scope = double('AR::Scope')
allow(project)
.to receive(:all_work_package_custom_fields)
.and_return(scope)
allow(scope)
.to receive(:map)
.and_return(scope)
allow(scope)
.to receive(:include?)
.with(instance.custom_field.id)
.and_return(false)
expect(instance).to_not be_valid
end
it_behaves_like 'custom field type dependent validity'
end
context 'outside of a project' do
context 'without a project' do
let(:project) { nil }
before do
it 'is invalid with a custom field not valid as a global filter' do
scope = double('AR::Scope')
allow(WorkPackageCustomField)
.to receive_message_chain(:filter, :for_all, :where, :not, :includes)
.and_return([list_wp_custom_field,
bool_wp_custom_field,
int_wp_custom_field,
float_wp_custom_field,
text_wp_custom_field,
date_wp_custom_field,
string_wp_custom_field])
end
.to receive(:filter)
.and_return(scope)
it 'returns a hash with a subject key and a filter instance for every custom field' do
expect(described_class.create(project)["cf_#{list_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{bool_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{int_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{float_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{text_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{user_wp_custom_field.id}"])
.to be_nil
expect(described_class.create(project)["cf_#{version_wp_custom_field.id}"])
.to be_nil
expect(described_class.create(project)["cf_#{date_wp_custom_field.id}"])
.to be_a(described_class)
expect(described_class.create(project)["cf_#{string_wp_custom_field.id}"])
.to be_a(described_class)
allow(scope)
.to receive(:for_all)
.and_return(scope)
allow(scope)
.to receive(:where)
.and_return(scope)
allow(scope)
.to receive(:not)
.with(field_format: ['user', 'version'])
.and_return(scope)
allow(scope)
.to receive(:exists?)
.with(instance.custom_field.id)
.and_return(false)
expect(instance).to_not be_valid
end
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+/)
expect(described_class.key).to eql(/cf_(\d+)/)
end
end
describe '#key' do
describe '#name' do
it 'is the custom fields id prefixed with cf_' do
all_custom_fields.each do |cf|
expect(described_class.create(project)["cf_#{cf.id}"].key).to eql(:"cf_#{cf.id}")
filter = described_class.new
filter.name = "cf_#{cf.id}"
expect(filter.name).to eql(:"cf_#{cf.id}")
end
end
end
@ -144,92 +202,99 @@ describe Queries::WorkPackages::Filter::CustomFieldFilter, type: :model do
describe '#order' do
it 'is 20' do
all_custom_fields.each do |cf|
expect(described_class.create(project)["cf_#{cf.id}"].order).to eql(20)
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
expect(described_class.create(project)["cf_#{int_wp_custom_field.id}"].type)
instance.name = "cf_#{int_wp_custom_field.id}"
expect(instance.type)
.to eql(:integer)
end
it 'is integer for a float' do
expect(described_class.create(project)["cf_#{float_wp_custom_field.id}"].type)
instance.name = "cf_#{float_wp_custom_field.id}"
expect(instance.type)
.to eql(:integer)
end
it 'is text for a text' do
expect(described_class.create(project)["cf_#{text_wp_custom_field.id}"].type)
instance.name = "cf_#{text_wp_custom_field.id}"
expect(instance.type)
.to eql(:text)
end
it 'is list_optional for a list' do
expect(described_class.create(project)["cf_#{list_wp_custom_field.id}"].type)
instance.name = "cf_#{list_wp_custom_field.id}"
expect(instance.type)
.to eql(:list_optional)
end
it 'is list_optional for a user' do
expect(described_class.create(project)["cf_#{user_wp_custom_field.id}"].type)
instance.name = "cf_#{user_wp_custom_field.id}"
expect(instance.type)
.to eql(:list_optional)
end
it 'is list_optional for a version' do
expect(described_class.create(project)["cf_#{version_wp_custom_field.id}"].type)
instance.name = "cf_#{version_wp_custom_field.id}"
expect(instance.type)
.to eql(:list_optional)
end
it 'is date for a date' do
expect(described_class.create(project)["cf_#{date_wp_custom_field.id}"].type)
instance.name = "cf_#{date_wp_custom_field.id}"
expect(instance.type)
.to eql(:date)
end
it 'is list for a bool' do
expect(described_class.create(project)["cf_#{bool_wp_custom_field.id}"].type)
instance.name = "cf_#{bool_wp_custom_field.id}"
expect(instance.type)
.to eql(:list)
end
it 'is string for a string' do
expect(described_class.create(project)["cf_#{string_wp_custom_field.id}"].type)
instance.name = "cf_#{string_wp_custom_field.id}"
expect(instance.type)
.to eql(:string)
end
end
describe '#name' do
describe '#human_name' do
it 'is the field name' do
expect(described_class.create(project)["cf_#{string_wp_custom_field.id}"].name)
.to eql(string_wp_custom_field.name)
end
end
describe '#available' do
it 'is true' do
all_custom_fields.each do |cf|
expect(described_class.create(project)["cf_#{cf.id}"]).to be_available
end
expect(instance.human_name)
.to eql(list_wp_custom_field.name)
end
end
describe '#values' do
describe '#allowed_values' do
it 'is nil for an integer' do
expect(described_class.create(project)["cf_#{int_wp_custom_field.id}"].values)
instance.name = "cf_#{int_wp_custom_field.id}"
expect(instance.allowed_values)
.to be_nil
end
it 'is integer for a float' do
expect(described_class.create(project)["cf_#{float_wp_custom_field.id}"].values)
instance.name = "cf_#{float_wp_custom_field.id}"
expect(instance.allowed_values)
.to be_nil
end
it 'is text for a text' do
expect(described_class.create(project)["cf_#{text_wp_custom_field.id}"].values)
instance.name = "cf_#{text_wp_custom_field.id}"
expect(instance.allowed_values)
.to be_nil
end
it 'is list_optional for a list' do
expect(described_class.create(project)["cf_#{list_wp_custom_field.id}"].values)
.to match_array list_wp_custom_field.possible_values
instance.name = "cf_#{list_wp_custom_field.id}"
expect(instance.allowed_values)
.to match_array list_wp_custom_field.possible_values.map { |value| [value, value] }
end
it 'is list_optional for a user' do
@ -239,7 +304,9 @@ describe Queries::WorkPackages::Filter::CustomFieldFilter, type: :model do
.with(project)
.and_return(bogus_return_value)
expect(described_class.create(project)["cf_#{user_wp_custom_field.id}"].values)
instance.context = project
instance.name = "cf_#{user_wp_custom_field.id}"
expect(instance.allowed_values)
.to match_array bogus_return_value
end
@ -250,24 +317,80 @@ describe Queries::WorkPackages::Filter::CustomFieldFilter, type: :model do
.with(project)
.and_return(bogus_return_value)
expect(described_class.create(project)["cf_#{version_wp_custom_field.id}"].values)
instance.context = project
instance.name = "cf_#{version_wp_custom_field.id}"
expect(instance.allowed_values)
.to match_array bogus_return_value
end
it 'is nil for a date' do
expect(described_class.create(project)["cf_#{date_wp_custom_field.id}"].values)
instance.name = "cf_#{date_wp_custom_field.id}"
expect(instance.allowed_values)
.to be_nil
end
it 'is list for a bool' do
expect(described_class.create(project)["cf_#{bool_wp_custom_field.id}"].values)
.to match_array [[I18n.t(:general_text_yes), ActiveRecord::Base.connection.unquoted_true],
[I18n.t(:general_text_no), ActiveRecord::Base.connection.unquoted_false]]
instance.name = "cf_#{bool_wp_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
expect(described_class.create(project)["cf_#{string_wp_custom_field.id}"].values)
instance.name = "cf_#{string_wp_custom_field.id}"
expect(instance.allowed_values)
.to be_nil
end
end
describe '.all_for' do
context 'within a project' do
before do
allow(project)
.to receive_message_chain(:all_work_package_custom_fields)
.and_return(all_custom_fields)
end
it 'returns a list with a filter for every custom field' do
filters = described_class.all_for(project)
all_custom_fields.each do |cf|
expect(filters.detect { |filter| filter.name == :"cf_#{cf.id}" }).to_not be_nil
end
end
end
context 'without a project' do
before do
allow(WorkPackageCustomField)
.to receive_message_chain(:filter, :for_all, :where, :not)
.and_return([list_wp_custom_field,
bool_wp_custom_field,
int_wp_custom_field,
float_wp_custom_field,
text_wp_custom_field,
date_wp_custom_field,
string_wp_custom_field])
end
it 'returns a list with a filter for every custom field' do
filters = described_class.all_for
[list_wp_custom_field,
bool_wp_custom_field,
int_wp_custom_field,
float_wp_custom_field,
text_wp_custom_field,
date_wp_custom_field,
string_wp_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_wp_custom_field.id}" })
.to be_nil
expect(filters.detect { |filter| filter.name == :"cf_#{user_wp_custom_field.id}" })
.to be_nil
end
end
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::DoneRatioFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 14 }
let(:type) { :integer }
let(:class_key) { :done_ratio }
@ -41,9 +40,9 @@ describe Queries::WorkPackages::Filter::DoneRatioFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
it 'is nil' do
expect(instance.values).to be_nil
expect(instance.allowed_values).to be_nil
end
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::DueDateFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 12 }
let(:type) { :date }
let(:class_key) { :due_date }
@ -41,9 +40,9 @@ describe Queries::WorkPackages::Filter::DueDateFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
it 'is nil' do
expect(instance.values).to be_nil
expect(instance.allowed_values).to be_nil
end
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::EstimatedHoursFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 13 }
let(:type) { :integer }
let(:class_key) { :estimated_hours }
@ -41,9 +40,9 @@ describe Queries::WorkPackages::Filter::EstimatedHoursFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
it 'is nil' do
expect(instance.values).to be_nil
expect(instance.allowed_values).to be_nil
end
end
end

@ -27,12 +27,11 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::GroupFilter, type: :model do
let(:group) { FactoryGirl.build_stubbed(:group) }
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 6 }
let(:type) { :list_optional }
let(:class_key) { :member_of_group }
@ -56,7 +55,7 @@ describe Queries::WorkPackages::Filter::GroupFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
before do
allow(Group)
.to receive(:all)
@ -64,7 +63,7 @@ describe Queries::WorkPackages::Filter::GroupFilter, type: :model do
end
it 'is an array of group values' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[group.name, group.id.to_s]]
end
end

@ -27,7 +27,6 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::PrincipalLoader, type: :model do
let(:user_1) { FactoryGirl.build_stubbed(:user) }

@ -27,12 +27,11 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::PriorityFilter, type: :model do
let(:priority) { FactoryGirl.build_stubbed(:priority) }
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 3 }
let(:type) { :list }
let(:class_key) { :priority_id }
@ -55,7 +54,7 @@ describe Queries::WorkPackages::Filter::PriorityFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
before do
allow(IssuePriority)
.to receive(:active)
@ -63,7 +62,7 @@ describe Queries::WorkPackages::Filter::PriorityFilter, type: :model do
end
it 'is an array of group values' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[priority.name, priority.id.to_s]]
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 1 }
let(:type) { :list }
let(:class_key) { :project_id }
@ -63,7 +62,7 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
let(:project) { nil }
it 'is an array of group values' do
@ -82,7 +81,7 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do
.and_yield(parent, 0)
.and_yield(child, 1)
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[parent.name, parent.id.to_s],
["-- #{child.name}", child.id.to_s]]
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::ResponsibleFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 4 }
let(:type) { :list_optional }
let(:class_key) { :responsible_id }
@ -93,7 +92,7 @@ describe Queries::WorkPackages::Filter::ResponsibleFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
let(:logged_in) { true }
before do
@ -108,7 +107,7 @@ describe Queries::WorkPackages::Filter::ResponsibleFilter, type: :model do
context 'when being logged in' do
it 'returns the me value and the available users' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array([[I18n.t(:label_me), 'me'],
[user_1.name, user_1.id.to_s]])
end
@ -118,7 +117,7 @@ describe Queries::WorkPackages::Filter::ResponsibleFilter, type: :model do
.to receive(:work_package_group_assignment?)
.and_return(false)
expect(instance.values)
expect(instance.allowed_values)
.to match_array([[I18n.t(:label_me), 'me'],
[user_1.name, user_1.id.to_s]])
end
@ -128,7 +127,7 @@ describe Queries::WorkPackages::Filter::ResponsibleFilter, type: :model do
let(:logged_in) { false }
it 'returns the available users' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array([[user_1.name, user_1.id.to_s]])
end
end

@ -27,12 +27,11 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::RoleFilter, type: :model do
let(:role) { FactoryGirl.build_stubbed(:role) }
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 7 }
let(:type) { :list_optional }
let(:class_key) { :assigned_to_role }
@ -56,7 +55,7 @@ describe Queries::WorkPackages::Filter::RoleFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
before do
allow(Role)
.to receive(:givable)
@ -64,7 +63,7 @@ describe Queries::WorkPackages::Filter::RoleFilter, type: :model do
end
it 'is an array of role values' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[role.name, role.id.to_s]]
end
end

@ -1,71 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
shared_examples_for 'work package query filter' do
let(:project) { FactoryGirl.build_stubbed(:project) }
let(:instance) { described_class.create(project)[instance_key || class_key] }
let(:instance_key) { nil }
let(:class_key) { raise "needs to be defined" }
let(:name) { WorkPackage.human_attribute_name(instance_key || class_key) }
describe '.create' do
it 'returns a hash with a subject key and a filter instance' do
expect(described_class.create(project)[instance_key || class_key]).to be_a(described_class)
end
end
describe '.key' do
it 'is the defined key' do
expect(described_class.key).to eql(class_key)
end
end
describe '#key' do
it 'is the defined key' do
expect(instance.key).to eql(instance_key || class_key)
end
end
describe '#order' do
it 'has the defined order' do
expect(instance.order).to eql(order)
end
end
describe '#type' do
it 'is the defined filter type' do
expect(instance.type).to eql(type)
end
end
describe '#name' do
it 'is the l10 name for the filter' do
expect(instance.name).to eql(name)
end
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::StartDateFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 11 }
let(:type) { :date }
let(:class_key) { :start_date }
@ -41,9 +40,9 @@ describe Queries::WorkPackages::Filter::StartDateFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
it 'is nil' do
expect(instance.values).to be_nil
expect(instance.allowed_values).to be_nil
end
end
end

@ -27,12 +27,11 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::StatusFilter, type: :model do
let(:status) { FactoryGirl.build_stubbed(:status) }
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 1 }
let(:type) { :list_status }
let(:class_key) { :status_id }
@ -55,7 +54,7 @@ describe Queries::WorkPackages::Filter::StatusFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
before do
allow(Status)
.to receive(:all)
@ -63,7 +62,7 @@ describe Queries::WorkPackages::Filter::StatusFilter, type: :model do
end
it 'is an array of status values' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[status.name, status.id.to_s]]
end
end

@ -28,10 +28,9 @@
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::SubjectFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 8 }
let(:type) { :text }
let(:class_key) { :subject }
@ -42,9 +41,9 @@ describe Queries::WorkPackages::Filter::SubjectFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
it 'is nil' do
expect(instance.values).to be_nil
expect(instance.allowed_values).to be_nil
end
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::SubprojectFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 13 }
let(:type) { :list_subprojects }
let(:class_key) { :subproject_id }
@ -90,7 +89,7 @@ describe Queries::WorkPackages::Filter::SubprojectFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
let(:subproject1) { FactoryGirl.build_stubbed(:project) }
let(:subproject2) { FactoryGirl.build_stubbed(:project) }
@ -101,7 +100,7 @@ describe Queries::WorkPackages::Filter::SubprojectFilter, type: :model do
end
it 'returns a list of all visible descendants' do
expect(instance.values).to match_array [[subproject1.name, subproject1.id.to_s],
expect(instance.allowed_values).to match_array [[subproject1.name, subproject1.id.to_s],
[subproject2.name, subproject2.id.to_s]]
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::TypeFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 3 }
let(:type) { :list }
let(:class_key) { :type_id }
@ -79,7 +78,7 @@ describe Queries::WorkPackages::Filter::TypeFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
let(:type) { FactoryGirl.build_stubbed(:type) }
context 'within a project' do
before do
@ -89,7 +88,7 @@ describe Queries::WorkPackages::Filter::TypeFilter, type: :model do
end
it 'returns an array of type options' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[type.name, type.id.to_s]]
end
end
@ -104,7 +103,7 @@ describe Queries::WorkPackages::Filter::TypeFilter, type: :model do
end
it 'returns an array of type options' do
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[type.name, type.id.to_s]]
end
end

@ -27,10 +27,9 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::UpdatedAtFilter, type: :model do
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 10 }
let(:type) { :date_past }
let(:class_key) { :updated_at }
@ -41,9 +40,9 @@ describe Queries::WorkPackages::Filter::UpdatedAtFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
it 'is nil' do
expect(instance.values).to be_nil
expect(instance.allowed_values).to be_nil
end
end
end

@ -27,76 +27,74 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::VersionFilter, type: :model do
let(:version) { FactoryGirl.build_stubbed(:version) }
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 7 }
let(:type) { :list_optional }
let(:class_key) { :fixed_version_id }
let(:values) { [version.id.to_s] }
let(:name) { WorkPackage.human_attribute_name('fixed_version_id') }
describe '#available?' do
context 'within a project' do
it 'is true if any version exists' do
allow(project)
.to receive_message_chain(:shared_versions, :exists?)
.and_return true
before do
if project
allow(project)
.to receive_message_chain(:shared_versions)
.and_return [version]
else
allow(Version)
.to receive_message_chain(:visible, :systemwide)
.and_return [version]
end
end
expect(instance).to be_available
describe '#valid?' do
context 'within a project' do
it 'is true if the value exists as a version' do
expect(instance).to be_valid
end
it 'is false if no version exists' do
it 'is false if the value does not exist as a version' do
allow(project)
.to receive_message_chain(:shared_versions, :exists?)
.and_return false
.to receive_message_chain(:shared_versions)
.and_return []
expect(instance).to_not be_available
expect(instance).to_not be_valid
end
end
context 'outside of a project' do
let(:project) { nil }
it 'is true if any version exists' do
allow(Version)
.to receive_message_chain(:visible, :systemwide, :exists?)
.and_return true
expect(instance).to be_available
it 'is true if the value exists as a version' do
expect(instance).to be_valid
end
it 'is false if no version exists' do
it 'is false if the value does not exist as a version' do
allow(Version)
.to receive_message_chain(:visible, :systemwide, :exists?)
.and_return false
.to receive_message_chain(:visible, :systemwide)
.and_return []
expect(instance).to_not be_available
expect(instance).to_not be_valid
end
end
end
describe '#values' do
describe '#allowed_values' do
context 'within a project' do
before do
allow(Version)
.to receive_message_chain(:visible, :systemwide)
.and_return [version]
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[version.name, version.id.to_s]]
end
end
context 'outside of a project' do
before do
allow(Version)
.to receive_message_chain(:visible, :systemwide)
.and_return [version]
let(:project) { nil }
expect(instance.values)
before do
expect(instance.allowed_values)
.to match_array [[version.name, version.id.to_s]]
end
end

@ -27,12 +27,11 @@
#++
require 'spec_helper'
require_relative 'shared'
describe Queries::WorkPackages::Filter::WatcherFilter, type: :model do
let(:user) { FactoryGirl.build_stubbed(:user) }
it_behaves_like 'work package query filter' do
it_behaves_like 'basic query filter' do
let(:order) { 15 }
let(:type) { :list }
let(:class_key) { :watcher_id }
@ -111,14 +110,14 @@ describe Queries::WorkPackages::Filter::WatcherFilter, type: :model do
end
end
describe '#values' do
describe '#allowed_values' do
context 'contains the me value if the user is logged in' do
before do
allow(User)
.to receive_message_chain(:current, :logged?)
.and_return true
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[I18n.t(:label_me), 'me']]
end
end
@ -137,7 +136,7 @@ describe Queries::WorkPackages::Filter::WatcherFilter, type: :model do
.to receive(:user_values)
.and_return([user])
expect(instance.values)
expect(instance.allowed_values)
.to match_array [[I18n.t(:label_me), 'me'],
[user.name, user.id.to_s]]
end

@ -1,157 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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::WorkPackages::Filter, type: :model do
describe '#type' do
describe 'validations' do
subject { filter }
let(:filter) { FactoryGirl.build :work_packages_filter }
context 'when the operator does not require values' do
let(:filter) { FactoryGirl.build :work_packages_filter, field: :status_id, operator: '*', values: [] }
it 'is valid if no values are given' do
expect(filter).to be_valid
end
end
context 'when the operator requires values' do
let(:filter) { FactoryGirl.build :work_packages_filter, field: :done_ratio, operator: '>=', values: [] }
context 'and no value is given' do
it { is_expected.not_to be_valid }
end
context 'and only an empty string is given as value' do
let(:filter) { FactoryGirl.build :work_packages_filter, field: :due_date, operator: 't-', values: [''] }
it { is_expected.not_to be_valid }
end
context 'and values are given' do
before do filter.values = [5] end
it { is_expected.to be_valid }
end
end
context 'when it is of type integer' do
let(:filter) { FactoryGirl.build :work_packages_filter, field: :done_ratio, operator: '>=', values: [] }
before do filter.field = 'done_ratio' end
context 'and the filter values is an integer' do
before do filter.values = [1, '12', 123] end
it { is_expected.to be_valid }
end
context 'and the filter values is not an integer' do
before do filter.values == [1, 'asdf'] end
it { is_expected.not_to be_valid }
context 'and the operator is *' do
before do filter.operator = '*' end
it { is_expected.to be_valid }
end
end
end
context 'when it if of type date or date_past' do
let(:filter) { FactoryGirl.build :work_packages_filter, field: :created_at }
context "and the operator is 't' (today)" do
before do filter.operator = 't' end
it { is_expected.to be_valid }
end
context "and the operator is 'w' (this week)" do
before do filter.operator = 'w' end
it { is_expected.to be_valid }
end
context 'and the operator compares the current day' do
before do filter.operator = '>t-' end
context 'and the value is an integer' do
before do filter.values = ['4'] end
it { is_expected.to be_valid }
end
context 'and the value is not an integer' do
before do filter.values = ['four'] end
it { is_expected.not_to be_valid }
end
end
end
context 'when it is a work package filter' do
let(:filter) { FactoryGirl.build :work_packages_filter }
context 'and the field is whitelisted' do
before do filter.field = :project_id end
it { is_expected.to be_valid }
end
# this context tests the case when a new item is injected in
# the filter_types_by_field hash afterwards
# from within some plugins that patch Queries::WorkPackages::Filter
context 'and the field is whitelisted afterwards' do
before do
filter.field = :some_new_key
filter.class.add_filter_type_by_field('some_new_key', 'list')
end
it { is_expected.to be_valid }
end
context 'and the field is not whitelisted and no custom field key' do
before do filter.field = :any_key end
it { is_expected.not_to be_valid }
end
context 'and the field is a custom field starting with "cf"' do
before do filter.field = :cf_any_key end
it { is_expected.to be_valid }
end
end
end
end
end

@ -77,17 +77,68 @@ describe Query, type: :model do
end
context 'with a missing value for a custom field' do
let(:custom_field) { FactoryGirl.create :text_issue_custom_field }
let(:custom_field) do
FactoryGirl.create :text_issue_custom_field, is_filter: true, is_for_all: true
end
let(:query) { FactoryGirl.build(:query) }
before do
query.filters = [Queries::WorkPackages::Filter.new('cf_' + custom_field.id.to_s, operator: '=', values: [''])]
query.add_filter('cf_' + custom_field.id.to_s, '=', [''])
end
it 'should have the name of the custom field in the error message' do
expect(query.valid?).to be_falsey
expect(query).to_not be_valid
expect(query.errors.messages[:base].to_s).to include(custom_field.name)
end
end
context 'with a filter for a non existing custom field' do
before do
query.add_filter('cf_0', '=', ['1'])
end
it 'is not valid' do
expect(query.valid?).to be_falsey
end
end
end
describe '#filter_for' do
context 'for a status_id filter' do
before do
allow(Status)
.to receive(:exists?)
.and_return(true)
end
subject { query.filter_for('status_id') }
it 'exists' do
is_expected.to_not be_nil
end
it 'has the context set' do
expect(subject.context).to eql query.project
query.project = nil
expect(query.filter_for('status_id').context).to be_nil
end
it 'reuses an existing filter' do
expect(subject.object_id).to eql query.filter_for('status_id').object_id
end
end
end
describe 'filters after deserialization' do
it 'sets the context (project) on deserialization' do
query.save!
query.reload
query.filters.each do |filter|
expect(filter.context).to eql(query.project)
end
end
end
end

@ -92,7 +92,11 @@ describe 'Planning Comparison', type: :model do
end
describe 'filtering work_packages also applies to the history' do
let(:assigned_to_user) { FactoryGirl.create(:user) }
let(:assigned_to_user) do
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: FactoryGirl.build(:role))
end
let (:filter) do
{ f: ['assigned_to_id'],
op: { 'assigned_to_id' => '=' },

@ -38,9 +38,125 @@ describe 'API v3 User resource', type: :request do
let(:model) { ::API::V3::Users::UserModel.new(user) }
let(:representer) { ::API::V3::Users::UserRepresenter.new(model) }
describe '#get' do
subject(:response) { last_response }
subject(:response) { last_response }
describe '#index' do
let(:get_path) { api_v3_paths.users }
before do
user
allow(User).to receive(:current).and_return current_user
get get_path
end
context 'admin user' do
let(:current_user) { FactoryGirl.create(:admin) }
it 'should respond with 200' do
expect(subject.status).to eq(200)
end
# note that the order of the users is depending on the id
# meaning the order in which they where saved
it 'contains the user in the response' do
expect(subject.body)
.to be_json_eql(user.name.to_json)
.at_path('_embedded/elements/0/name')
end
it 'contains the current user in the response' do
expect(subject.body)
.to be_json_eql(current_user.name.to_json)
.at_path('_embedded/elements/1/name')
end
it 'has the users index path for link self href' do
expect(subject.body)
.to be_json_eql((api_v3_paths.users + '?offset=1&pageSize=30').to_json)
.at_path('_links/self/href')
end
context 'if pageSize = 1 and offset = 2' do
let(:get_path) { api_v3_paths.users + '?pageSize=1&offset=2' }
it 'contains the current user in the response' do
expect(subject.body)
.to be_json_eql(current_user.name.to_json)
.at_path('_embedded/elements/0/name')
end
end
context 'on filtering for name' do
let(:get_path) do
filter = [{ 'name' => {
'operator' => '~',
'values' => [user.name]
} }]
"#{api_v3_paths.users}?#{{ filters: filter.to_json }.to_query}"
end
it 'contains the filtered user in the response' do
expect(subject.body)
.to be_json_eql(user.name.to_json)
.at_path('_embedded/elements/0/name')
end
it 'contains no more users' do
expect(subject.body)
.to be_json_eql(1.to_json)
.at_path('total')
end
end
context 'on sorting' do
let(:users_by_name_order) do
User.not_builtin.order_by_name.reverse_order
end
let(:get_path) do
sort = [['name', 'desc']]
"#{api_v3_paths.users}?#{{ sortBy: sort.to_json }.to_query}"
end
it 'contains the first user as the first element' do
expect(subject.body)
.to be_json_eql(users_by_name_order[0].name.to_json)
.at_path('_embedded/elements/0/name')
end
it 'contains the first user as the second element' do
expect(subject.body)
.to be_json_eql(users_by_name_order[1].name.to_json)
.at_path('_embedded/elements/1/name')
end
end
context 'on an invalid filter' do
let(:get_path) do
filter = [{ 'name' => {
'operator' => 'a',
'values' => [user.name]
} }]
"#{api_v3_paths.users}?#{{ filters: filter.to_json }.to_query}"
end
it 'returns an error' do
expect(subject.status).to eql(400)
end
end
end
context 'other user' do
it 'should respond with 403' do
expect(subject.status).to eq(403)
end
end
end
describe '#get' do
context 'logged in user' do
let(:get_path) { api_v3_paths.user user.id }
before do
@ -52,7 +168,7 @@ describe 'API v3 User resource', type: :request do
expect(subject.status).to eq(200)
end
it 'should respond with correct attachment' do
it 'should respond with correct body' do
expect(subject.body).to be_json_eql(user.name.to_json).at_path('name')
end
@ -85,8 +201,6 @@ describe 'API v3 User resource', type: :request do
delete path
end
subject(:response) { last_response }
shared_examples 'deletion through allowed user' do
it 'should respond with 202' do
expect(subject.status).to eq 202

@ -0,0 +1,237 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-2013 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.
#++
shared_context 'filter tests' do
let(:context) { nil }
let(:values) { ['bogus'] }
let(:operator) { '=' }
let(:instance) do
filter = described_class.new
filter.context = context
filter.operator = operator
filter.values = values
filter
end
let(:name) { model.human_attribute_name(instance_key || class_key) }
let(:model) { WorkPackage }
end
shared_examples_for 'basic query filter' do
include_context 'filter tests'
let(:context) { project }
let(:project) { FactoryGirl.build_stubbed(:project) }
let(:instance_key) { nil }
let(:class_key) { raise 'needs to be defined' }
let(:type) { raise 'needs to be defined' }
let(:order) { nil }
describe '.key' do
it 'is the defined key' do
expect(described_class.key).to eql(class_key)
end
end
describe '#name' do
it 'is the defined key' do
expect(instance.name).to eql(instance_key || class_key)
end
end
describe '#order' do
it 'has the defined order' do
if order
expect(instance.order).to eql(order)
end
end
end
describe '#type' do
it 'is the defined filter type' do
expect(instance.type).to eql(type)
end
end
describe '#human_name' do
it 'is the l10 name for the filter' do
expect(instance.human_name).to eql(name)
end
end
end
shared_examples_for 'list query filter' do
include_context 'filter tests'
let(:attribute) { raise "needs to be defined" }
let(:type) { :list }
describe '#scope' do
context 'for "="' do
let(:operator) { '=' }
let(:values) { valid_values }
it 'is the same as handwriting the query' do
expected = model.where(["#{model.table_name}.#{attribute} IN (?)", values])
expect(instance.scope.to_sql).to eql expected.to_sql
end
end
context 'for "!"' do
let(:operator) { '!' }
let(:values) { valid_values }
it 'is the same as handwriting the query' do
sql = "(#{model.table_name}.#{attribute} IS NULL
OR #{model.table_name}.#{attribute} NOT IN (?))".squish
expected = model.where([sql, values])
expect(instance.scope.to_sql).to eql expected.to_sql
end
end
end
describe '#valid?' do
let(:operator) { '=' }
let(:values) { valid_values }
it 'is valid' do
expect(instance).to be_valid
end
context 'for an invalid operator' do
let(:operator) { '*' }
it 'is invalid' do
expect(instance).to be_invalid
end
end
context 'for an invalid value' do
let(:values) { ['inexistent'] }
it 'is invalid' do
expect(instance).to be_invalid
end
end
end
end
shared_examples_for 'list_optional query filter' do
include_context 'filter tests'
let(:attribute) { raise "needs to be defined" }
let(:type) { :list_optional }
let(:joins) { nil }
let(:expected_base_scope) do
if joins
model.joins(joins)
else
model
end
end
let(:expected_table_name) do
if joins
joins
else
model.table_name
end
end
describe '#scope' do
let(:values) { valid_values }
context 'for "="' do
let(:operator) { '=' }
it 'is the same as handwriting the query' do
expected = expected_base_scope
.where(["#{expected_table_name}.#{attribute} IN (?)", values])
expect(instance.scope.to_sql).to eql expected.to_sql
end
end
context 'for "!"' do
let(:operator) { '!' }
it 'is the same as handwriting the query' do
sql = "(#{expected_table_name}.#{attribute} IS NULL
OR #{expected_table_name}.#{attribute} NOT IN (?))".squish
expected = expected_base_scope.where([sql, values])
expect(instance.scope.to_sql).to eql expected.to_sql
end
end
context 'for "*"' do
let(:operator) { '*' }
it 'is the same as handwriting the query' do
sql = "#{expected_table_name}.#{attribute} IS NOT NULL"
expected = expected_base_scope.where([sql])
expect(instance.scope.to_sql).to eql expected.to_sql
end
end
context 'for "!*"' do
let(:operator) { '!*' }
it 'is the same as handwriting the query' do
sql = "#{expected_table_name}.#{attribute} IS NULL"
expected = expected_base_scope.where([sql])
expect(instance.scope.to_sql).to eql expected.to_sql
end
end
end
describe '#valid?' do
let(:operator) { '=' }
let(:values) { valid_values }
it 'is valid' do
expect(instance).to be_valid
end
context 'for an invalid operator' do
let(:operator) { '~' }
it 'is invalid' do
expect(instance).to be_invalid
end
end
context 'for an invalid value' do
let(:values) { ['inexistent'] }
it 'is invalid' do
expect(instance).to be_invalid
end
end
end
end

@ -26,38 +26,31 @@
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'legacy_spec_helper'
require_relative '../legacy_spec_helper'
describe Query, type: :model do
fixtures :all
it 'should custom fields for all projects should be available in global queries' do
query = Query.new(project: nil, name: '_')
assert query.work_package_filter_available?('cf_1')
assert !query.work_package_filter_available?('cf_3')
end
it 'should system shared versions should be available in global queries' do
Version.find(2).update_attribute :sharing, 'system'
query = Query.new(project: nil, name: '_')
assert query.work_package_filter_available?('fixed_version_id')
assert query.available_work_package_filters['fixed_version_id'][:values].detect { |v| v.last == '2' }
assert query.filter_for('fixed_version_id')[:allowed_values].detect { |v| v.last == '2' }
end
it 'should project filter in global queries' do
# User.current should be anonymous here
query = Query.new(project: nil, name: '_')
project_filter = query.available_work_package_filters['project_id']
project_filter = query.filter_for('project_id')
refute_nil project_filter
project_ids = project_filter[:values].map { |p| p[1] }
project_ids = project_filter[:allowed_values].map { |p| p[1] }
assert project_ids.include?('1') # public project
assert !project_ids.include?('2') # private project anonymous user cannot see
end
def find_issues_with_query(query)
WorkPackage.includes(:assigned_to, :status, :type, :project, :priority)
.where(query.statement)
.references(:projects)
.where(query.statement)
.references(:projects)
end
def assert_find_issues_with_query_is_successful(query)
@ -67,7 +60,8 @@ describe Query, type: :model do
end
def assert_query_statement_includes(query, condition)
assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
assert query.statement.include?(condition),
"Query statement condition not found in: #{query.statement}"
end
it 'should query should allow shared versions for a project query' do
@ -220,7 +214,7 @@ describe Query, type: :model do
i3 = FactoryGirl.create(:work_package, project: project, type: project.types.first, assigned_to: Group.find(11))
group.users << user
query = Query.new(name: '_', filters: [Queries::WorkPackages::Filter.new(:assigned_to_id, operator: '=', values: ['me'])])
query = Query.new(name: '_', filters: [{ assigned_to_id: { operator: '=', values: ['me'] } }])
result = query.results.work_packages
assert_equal WorkPackage.visible.where(assigned_to_id: ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
@ -233,7 +227,7 @@ describe Query, type: :model do
it 'should filter watched issues' do
User.current = User.find(1)
query = Query.new(name: '_', filters: [Queries::WorkPackages::Filter.new(:watcher_id, operator: '=', values: ['me'])])
query = Query.new(name: '_', filters: [{ watcher_id: { operator: '=', values: ['me'] } }])
result = find_issues_with_query(query)
refute_nil result
assert !result.empty?
@ -243,11 +237,11 @@ describe Query, type: :model do
it 'should filter unwatched issues' do
User.current = User.find(1)
query = Query.new(name: '_', filters: [Queries::WorkPackages::Filter.new(:watcher_id, operator: '!', values: ['me'])])
query = Query.new(name: '_', filters: [{ watcher_id: { operator: '!', values: ['me'] } }])
result = find_issues_with_query(query)
refute_nil result
assert !result.empty?
assert_equal((WorkPackage.visible - WorkPackage.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
expect((WorkPackage.visible - WorkPackage.watched_by(User.current)).size).to eql result.size
User.current = nil
end
@ -385,11 +379,6 @@ describe Query, type: :model do
assert_equal %w(Fixnum), count_by_group.values.map { |k| k.class.name }.uniq
end
it 'should label for' do
q = Query.new name: '_'
assert_equal WorkPackage.human_attribute_name('assigned_to_id'), q.label_for('assigned_to_id')
end
it 'should editable by' do
admin = User.find(1)
manager = User.find(2)
@ -420,30 +409,30 @@ describe Query, type: :model do
assert !q.editable_by?(developer)
end
context '#available_work_package_filters' do
context '#filter_for' do
before do
@query = Query.new(name: '_')
end
it 'should include users of visible projects in cross-project view' do
users = @query.available_work_package_filters['assigned_to_id']
users = @query.filter_for('assigned_to_id')
refute_nil users
assert users[:values].map { |u| u[1] }.include?('3')
assert users[:allowed_values].map { |u| u[1] }.include?('3')
end
it 'should include visible projects in cross-project view' do
projects = @query.available_work_package_filters['project_id']
projects = @query.filter_for('project_id')
refute_nil projects
assert projects[:values].map { |u| u[1] }.include?('1')
assert projects[:allowed_values].map { |u| u[1] }.include?('1')
end
context "'member_of_group' filter" do
it 'should be present' do
assert @query.available_work_package_filters.keys.include?('member_of_group')
assert @query.filter_for('member_of_group')
end
it 'should be an optional list' do
assert_equal :list_optional, @query.available_work_package_filters['member_of_group'][:type]
assert_equal :list_optional, @query.filter_for('member_of_group')[:type]
end
it 'should have a list of the groups as values' do
@ -455,39 +444,33 @@ describe Query, type: :model do
[group1.name, group1.id.to_s],
[group2.name, group2.id.to_s]
]
assert_equal expected_group_list.sort, @query.available_work_package_filters['member_of_group'][:values].sort
assert_equal expected_group_list.sort, @query.filter_for('member_of_group')[:allowed_values].sort
end
end
context "'assigned_to_role' filter" do
it 'should be present' do
assert @query.available_work_package_filters.keys.include?('assigned_to_role')
assert @query.filter_for('assigned_to_role')
end
it 'should be an optional list' do
assert_equal :list_optional, @query.available_work_package_filters['assigned_to_role'][:type]
assert_equal :list_optional, @query.filter_for('assigned_to_role')[:type]
end
it 'should have a list of the Roles as values' do
assert @query.available_work_package_filters['assigned_to_role'][:values].include?(['Manager', '1'])
assert @query.available_work_package_filters['assigned_to_role'][:values].include?(['Developer', '2'])
assert @query.available_work_package_filters['assigned_to_role'][:values].include?(['Reporter', '3'])
assert @query.filter_for('assigned_to_role')[:allowed_values].include?(['Manager', '1'])
assert @query.filter_for('assigned_to_role')[:allowed_values].include?(['Developer', '2'])
assert @query.filter_for('assigned_to_role')[:allowed_values].include?(['Reporter', '3'])
end
it 'should not include the built in Roles as values' do
assert ! @query.available_work_package_filters['assigned_to_role'][:values].include?(['Non member', '4'])
assert ! @query.available_work_package_filters['assigned_to_role'][:values].include?(['Anonymous', '5'])
it 'should not include the built in Roles as allowed_values' do
assert !@query.filter_for('assigned_to_role')[:allowed_values].include?(['Non member', '4'])
assert !@query.filter_for('assigned_to_role')[:allowed_values].include?(['Anonymous', '5'])
end
end
context "'watcher_id' filter" do
context 'globally' do
context 'for an anonymous user' do
it 'should not be present' do
assert ! @query.available_work_package_filters.keys.include?('watcher_id')
end
end
context 'for a logged in user' do
before do
User.current = User.find 1
@ -498,31 +481,31 @@ describe Query, type: :model do
end
it 'should be present' do
assert @query.available_work_package_filters.keys.include?('watcher_id')
assert @query.filter_for('watcher_id')
end
it 'should be a list' do
assert_equal :list, @query.available_work_package_filters['watcher_id'][:type]
assert_equal :list, @query.filter_for('watcher_id')[:type]
end
it 'should have a list of active users as values' do
assert @query.available_work_package_filters['watcher_id'][:values].include?(['me', 'me'])
assert @query.available_work_package_filters['watcher_id'][:values].include?(['John Smith', '2'])
assert @query.available_work_package_filters['watcher_id'][:values].include?(['Dave Lopper', '3'])
assert @query.available_work_package_filters['watcher_id'][:values].include?(['redMine Admin', '1'])
assert @query.available_work_package_filters['watcher_id'][:values].include?(['User Misc', '8'])
it 'should have a list of active users as allowed_values' do
assert @query.filter_for('watcher_id')[:allowed_values].include?(['me', 'me'])
assert @query.filter_for('watcher_id')[:allowed_values].include?(['John Smith', '2'])
assert @query.filter_for('watcher_id')[:allowed_values].include?(['Dave Lopper', '3'])
assert @query.filter_for('watcher_id')[:allowed_values].include?(['redMine Admin', '1'])
assert @query.filter_for('watcher_id')[:allowed_values].include?(['User Misc', '8'])
end
it 'should not include active users not member of any project' do
assert ! @query.available_work_package_filters['watcher_id'][:values].include?(['Robert Hill', '4'])
assert !@query.filter_for('watcher_id')[:allowed_values].include?(['Robert Hill', '4'])
end
it 'should not include locked users as values' do
assert ! @query.available_work_package_filters['watcher_id'][:values].include?(['Dave2 Lopper2', '5'])
assert !@query.filter_for('watcher_id')[:allowed_values].include?(['Dave2 Lopper2', '5'])
end
it 'should not include the anonymous user as values' do
assert ! @query.available_work_package_filters['watcher_id'][:values].include?(['Anonymous', '6'])
assert !@query.filter_for('watcher_id')[:allowed_values].include?(['Anonymous', '6'])
end
end
end
@ -532,45 +515,35 @@ describe Query, type: :model do
@query.project = Project.find(1)
end
context 'for an anonymous user' do
it 'should not be present' do
assert ! @query.available_work_package_filters.keys.include?('watcher_id')
end
end
context 'for a logged in user' do
before do
User.current = User.find 1
end
after do
User.current = nil
allow(User).to receive(:current).and_return(User.find(1))
end
it 'should be present' do
assert @query.available_work_package_filters.keys.include?('watcher_id')
assert @query.filter_for('watcher_id')
end
it 'should be a list' do
assert_equal :list, @query.available_work_package_filters['watcher_id'][:type]
assert_equal :list, @query.filter_for('watcher_id')[:type]
end
it 'should have a list of the project members as values' do
assert @query.available_work_package_filters['watcher_id'][:values].include?(['me', 'me'])
assert @query.available_work_package_filters['watcher_id'][:values].include?(['John Smith', '2'])
assert @query.available_work_package_filters['watcher_id'][:values].include?(['Dave Lopper', '3'])
it 'should have a list of the project members as allowed_values' do
assert @query.filter_for('watcher_id')[:allowed_values].include?(['me', 'me'])
assert @query.filter_for('watcher_id')[:allowed_values].include?(['John Smith', '2'])
assert @query.filter_for('watcher_id')[:allowed_values].include?(['Dave Lopper', '3'])
end
it 'should not include non-project members as values' do
assert ! @query.available_work_package_filters['watcher_id'][:values].include?(['redMine Admin', '1'])
it 'should not include non-project members as allowed_values' do
assert !@query.filter_for('watcher_id')[:allowed_values].include?(['redMine Admin', '1'])
end
it 'should not include locked project members as values' do
assert ! @query.available_work_package_filters['watcher_id'][:values].include?(['Dave2 Lopper2', '5'])
it 'should not include locked project members as allowed_values' do
assert !@query.filter_for('watcher_id')[:allowed_values].include?(['Dave2 Lopper2', '5'])
end
it 'should not include the anonymous user as values' do
assert ! @query.available_work_package_filters['watcher_id'][:values].include?(['Anonymous', '6'])
it 'should not include the anonymous user as allowed_values' do
assert !@query.filter_for('watcher_id')[:allowed_values].include?(['Anonymous', '6'])
end
end
end

Loading…
Cancel
Save